8000 mini.nvim/lua/mini/diff.lua at main · augtheo/mini.nvim · GitHub
[go: up one dir, main page]

Skip to content

Latest commit

 

History

History
1806 lines (1567 loc) · 71.8 KB

diff.lua

File metadata and controls

1806 lines (1567 loc) · 71.8 KB
summary = buf_cache.summary,
})
end
--- Set reference text for the buffer
---
--- Note: this will call |MiniDiff.enable()| for target buffer if it is not
--- already enabled.
---
---@param buf_id __diff_buf_id
---@param text string|table New reference text. Either a string with `\n` used to
--- separate lines or array of lines. Use empty table to unset current
--- reference text (results into no hunks shown). Default: `{}`.
--- Note: newline character is appended at the end (if it is not there already)
--- for better diffs.
MiniDiff.set_ref_text = function(buf_id, text)
buf_id = H.validate_buf_id(buf_id)
if not (type(text) == 'table' or type(text) == 'string') then H.error('`text` should be either string or array.') end
if type(text) == 'table' then text = #text > 0 and table.concat(text, '\n') or nil end
-- Enable if not already enabled
if not H.is_buf_enabled(buf_id) then MiniDiff.enable(buf_id) end
if not H.is_buf_enabled(buf_id) then H.error('Can not set reference text for not enabled buffer.') end
-- Appending '\n' makes more intuitive diffs at end-of-file
if text ~= nil and string.sub(text, -1) ~= '\n' then text = text .. '\n' end
if text == nil then
H.clear_all_diff(buf_id)
vim.cmd('redraw')
end
-- Immediately update diff
H.cache[buf_id].ref_text = text
H.schedule_diff_update(buf_id, 0)
end
--- Generate builtin sources
---
--- This is a table with function elements. Call to actually get source.
--- Example of using |MiniDiff.gen_source.save()|: >lua
---
--- local diff = require('mini.diff')
--- diff.setup({ source = diff.gen_source.save() })
--- <
MiniDiff.gen_source = {}
--- Git source
---
--- Default source. Uses file text from Git index as reference. This results in:
--- - "Add" hunks represent text present in current buffer, but not in index.
--- - "Change" hunks represent modified text already present in index.
--- - "Delete" hunks represent text deleted from index.
---
--- Applying hunks means staging, a.k.a adding to index.
--- Notes:
--- - Requires Git version at least 2.38.0.
--- - There is no capability for unstaging hunks. Use full Git client for that.
---
---@return table Source. See |MiniDiff-source-specification|.
MiniDiff.gen_source.git = function()
local attach = function(buf_id)
-- Try attaching to a buffer only once
if H.git_cache[buf_id] ~= nil then return false end
-- - Possibly resolve symlinks to get data from the original repo
local path = H.get_buf_realpath(buf_id)
if path == '' then return false end
H.git_cache[buf_id] = {}
H.git_start_watching_index(buf_id, path)
end
local detach = function(buf_id)
local cache = H.git_cache[buf_id]
H.git_cache[buf_id] = nil
H.git_invalidate_cache(cache)
end
local apply_hunks = function(buf_id, hunks)
local path_data = H.git_get_path_data(H.get_buf_realpath(buf_id))
if path_data == nil or path_data.rel_path == nil then return end
local patch = H.git_format_patch(buf_id, hunks, path_data)
H.git_apply_patch(path_data, patch)
end
return { name = 'git', attach = attach, detach = detach, apply_hunks = apply_hunks }
end
--- "Do nothing" source
---
--- Allows buffers to be enabled while not setting any reference text.
--- Use this if the goal is to rely on manual |MiniDiff.set_ref_text()| calls.
---
---@return table Source. See |MiniDiff-source-specification|.
MiniDiff.gen_source.none = function()
return { name = 'none', attach = function() end }
end
--- Latest save source
---
--- Uses text at latest save as the reference. This results into diff showing
--- difference after the latest save.
---
---@return table Source. See |MiniDiff-source-specification|.
MiniDiff.gen_source.save = function()
local augroups = {}
local attach = function(buf_id)
local augroup = vim.api.nvim_create_augroup('MiniDiffSourceSaveBuffer' .. buf_id, { clear = true })
augroups[buf_id] = augroup
local set_ref = function()
if vim.bo[buf_id].modified then return end
MiniDiff.set_ref_text(buf_id, vim.api.nvim_buf_get_lines(buf_id, 0, -1, false))
end
-- Autocommand are more efficient than file watcher as it doesn't read disk
local au_opts = { group = augroup, buffer = buf_id, callback = set_ref, desc = 'Set reference text after save' }
vim.api.nvim_create_autocmd({ 'BufWritePost', 'FileChangedShellPost' }, au_opts)
set_ref()
end
local detach = function(buf_id) pcall(vim.api.nvim_del_augroup_by_id, augroups[buf_id]) end
return { name = 'save', attach = attach, detach = detach }
end
--- Perform action on hunks in region
---
--- Compute hunks inside a target region (even for hunks only partially inside it)
--- and perform apply/reset/yank operation on them.
---
--- The "yank" action yanks all reference lines of target hunks into
--- a specified register (should be one of |registers|).
---
--- Notes:
--- - Whether hunk is inside a region is computed based on position of its
--- buffer lines.
--- - If "change" or "delete" is only partially inside a target region, all
--- reference lines are used in computed "intersection" hunk.
---
--- Used directly in `config.mappings.apply` and `config.mappings.reset`.
--- Usually there is no need to use this function manually.
--- See |MiniDiff.operator()| for how to set up a mapping for "yank".
---
---@param buf_id __diff_buf_id
---@param action string One of "apply", "reset", "yank".
---@param opts table|nil Options. Possible fields:
--- - <line_start> `(number)` - start line of the region. Default: 1.
--- - <line_end> `(number)` - start line of the region. Default: last buffer line.
--- - <register> `(string)` - register to yank reference lines into.
--- Default: |v:register|.
MiniDiff.do_hunks = function(buf_id, action, opts)
buf_id = H.validate_buf_id(buf_id)
local buf_cache = H.cache[buf_id]
if buf_cache == nil then H.error(string.format('Buffer %d is not enabled.', buf_id)) end
if type(buf_cache.ref_text) ~= 'string' then H.error(string.format('Buffer %d has no reference text.', buf_id)) end
if not (action == 'apply' or action == 'reset' or action == 'yank') then
H.error('`action` should be one of "apply", "reset", "yank".')
end
local default_opts = { line_start = 1, line_end = vim.api.nvim_buf_line_count(buf_id), register = vim.v.register }
opts = vim.tbl_deep_extend('force', default_opts, opts or {})
local line_start, line_end = H.validate_target_lines(buf_id, opts.line_start, opts.line_end)
if type(opts.register) ~= 'string' then H.error('`opts.register` should be string.') end
local hunks = H.get_hunks_in_range(buf_cache.hunks, line_start, line_end)
if #hunks == 0 then return H.notify('No hunks to ' .. action, 'INFO') end
if action == 'apply' then buf_cache.source.apply_hunks(buf_id, hunks) end
if action == 'reset' then H.reset_hunks(buf_id, hunks) end
if action == 'yank' then H.yank_hunks_ref(buf_cache.ref_text, hunks, opts.register) end
end
--- Go to hunk range in current buffer
---
---@param direction string One of "first", "prev", "next", "last".
---@param opts table|nil Options. A table with fields:
--- - <n_times> `(number)` - Number of times to advance. Default: |v:count1|.
--- - <line_start> `(number)` - Line number to start from for directions
--- "prev" and "next". Default: cursor line.
--- - <wrap> `(boolean)` - Whether to wrap around edges.
--- Default: `options.wrap` value of the config.
MiniDiff.goto_hunk = function(direction, opts)
local buf_id = vim.api.nvim_get_current_buf()
local buf_cache = H.cache[buf_id]
if buf_cache == nil then H.error(string.format('Buffer %d is not enabled.', buf_id)) end
if not vim.tbl_contains({ 'first', 'prev', 'next', 'last' }, direction) then
H.error('`direction` should be one of "first", "prev", "next", "last".')
end
local default_wrap = buf_cache.config.options.wrap_goto
local default_opts = { n_times = vim.v.count1, line_start = vim.fn.line('.'), wrap = default_wrap }
opts = vim.tbl_deep_extend('force', default_opts, opts or {})
if not (type(opts.n_times) == 'number' and opts.n_times >= 1) then
H.error('`opts.n_times` should be positive number.')
end
if type(opts.line_start) ~= 'number' then H.error('`opts.line_start` should be number.') end
if type(opts.wrap) ~= 'boolean' then H.error('`opts.wrap` should be boolean.') end
-- Prepare ranges to iterate.
local ranges = H.get_contiguous_hunk_ranges(buf_cache.hunks)
if #ranges == 0 then return H.notify('No hunks to go to', 'INFO') end
-- Iterate
local res_ind, did_wrap = H.iterate_hunk_ranges(ranges, direction, opts)
if res_ind == nil then return H.notify('No hunk ranges in direction ' .. vim.inspect(direction), 'INFO') end
local res_line = ranges[res_ind].from
if did_wrap then H.notify('Wrapped around edge in direction ' .. vim.inspect(direction), 'INFO') end
-- Add to jumplist
vim.cmd([[normal! m']])
-- Jump
local _, col = vim.fn.getline(res_line):find('^%s*')
vim.api.nvim_win_set_cursor(0, { res_line, col })
-- Open just enough folds
vim.cmd('normal! zv')
end
--- Perform action over region
---
--- Perform action over region defined by marks. Used in mappings.
---
--- Example of a mapping to yank reference lines of hunk range under cursor
--- (assuming default 'config.mappings.textobject'): >lua
---
--- local rhs = function() return MiniDiff.operator('yank') .. 'gh' end
--- vim.keymap.set('n', 'ghy', rhs, { expr = true, remap = true })
--- <
---@param mode string One of "apply", "reset", "yank", or the ones used in |g@|.
MiniDiff.operator = function(mode)
local buf_id = vim.api.nvim_get_current_buf()
if H.is_disabled(buf_id) then return '' end
if mode == 'apply' or mode == 'reset' or mode == 'yank' then
H.operator_cache = { action = mode, win_view = vim.fn.winsaveview(), register = vim.v.register }
vim.o.operatorfunc = 'v:lua.MiniDiff.operator'
return 'g@'
end
local cache = H.operator_cache
-- NOTE: Using `[` / `]` marks also works in Visual mode as because it is
-- executed as part of `g@`, which treats visual selection as a result of
-- Operator-pending mode mechanics (for which visual selection is allowed to
-- define motion/textobject). The downside is that it sets 'operatorfunc',
-- but the upside is that it is "dot-repeatable" (for relative selection).
local opts = { line_start = vim.fn.line("'["), line_end = vim.fn.line("']"), register = cache.register }
if opts.line_end < opts.line_start then return H.notify('Not a proper textobject', 'INFO') end
MiniDiff.do_hunks(buf_id, cache.action, opts)
-- Restore window view for "apply" (as buffer text should not have changed)
if cache.action == 'apply' and cache.win_view ~= nil then
vim.fn.winrestview(cache.win_view)
-- NOTE: Restore only once because during dot-repeat it is not up to date
cache.win_view = nil
end
return ''
end
--- Select hunk range textobject
---
--- Selects all contiguous lines adjacent to cursor line which are in any (not
--- necessarily same) hunk (if cursor line itself is in hunk).
--- Used in default mappings.
MiniDiff.textobject = function()
local buf_id = vim.api.nvim_get_current_buf()
local buf_cache = H.cache[buf_id]
if buf_cache == nil or H.is_disabled(buf_id) then H.error('Current buffer is not enabled.') end
-- Get hunk range under cursor
local cur_line = vim.fn.line('.')
local regions, cur_region = H.get_contiguous_hunk_ranges(buf_cache.hunks), nil
for _, r in ipairs(regions) do
if r.from <= cur_line and cur_line <= r.to then cur_region = r end
end
if cur_region == nil then return H.notify('No hunk range under cursor', 'INFO') end
-- Select target region
local is_visual = vim.tbl_contains({ 'v', 'V', '\22' }, vim.fn.mode())
if is_visual then vim.cmd('normal! \27') end
vim.cmd(string.format('normal! %dGV%dG', cur_region.from, cur_region.to))
end
-- Helper data ================================================================
-- Module default config
H.default_config = MiniDiff.config
H.default_source = MiniDiff.gen_source.git()
-- Timers
H.timer_diff_update = vim.loop.new_timer()
-- Namespaces per highlighter name
H.ns_id = {
viz = vim.api.nvim_create_namespace('MiniDiffViz'),
overlay = vim.api.nvim_create_namespace('MiniDiffOverlay'),
}
-- Cache of buffers waiting for debounced diff update
H.bufs_to_update = {}
-- Cache per enabled buffer
H.cache = {}
-- Cache per buffer for attached `git` source
H.git_cache = {}
-- Cache for operator
H.operator_cache = {}
-- Common extmark data for supported styles
--stylua: ignore
H.style_extmark_data = {
sign = { hl_group_prefix = 'MiniDiffSign', field = 'sign_hl_group' },
number = { hl_group_prefix = 'MiniDiffSign', field = 'number_hl_group' },
}
-- Suffix for overlay virtual lines to be highlighted as full line
H.overlay_suffix = string.rep(' ', vim.o.columns)
-- Flag of whether Neovim version supports invalidating extmarks
H.extmark_supports_invalidate = vim.fn.has('nvim-0.10') == 1
-- Permanent `vim.diff()` options
H.vimdiff_opts = { result_type = 'indices', ctxlen = 0, interhunkctxlen = 0 }
H.vimdiff_supports_linematch = vim.fn.has('nvim-0.9') == 1
-- Options for `vim.diff()` during word diff. Use `interhunkctxlen = 4` to
-- reduce noisiness (chosen as slightly less than average English word length)
--stylua: ignore
H.worddiff_opts = { algorithm = 'minimal', result_type = 'indices', ctxlen = 0, interhunkctxlen = 4, indent_heuristic = false }
if H.vimdiff_supports_linematch then H.worddiff_opts.linematch = 0 end
-- Helper functionality =======================================================
-- Settings -------------------------------------------------------------------
H.setup_config = function(config)
-- General idea: if some table elements are not present in user-supplied
-- `config`, take them from default config
vim.validate({ config = { config, 'table', true } })
config = vim.tbl_deep_extend('force', vim.deepcopy(H.default_config), config or {})
vim.validate({
view = { config.view, 'table' },
source = { config.source, 'table', true },
delay = { config.delay, 'table' },
mappings = { config.mappings, 'table' },
options = { config.options, 'table' },
})
vim.validate({
['view.style'] = { config.view.style, 'string' },
['view.signs'] = { config.view.signs, 'table' },
['view.priority'] = { config.view.priority, 'number' },
['delay.text_change'] = { config.delay.text_change, 'number' },
['mappings.apply'] = { config.mappings.apply, 'string' },
['mappings.reset'] = { config.mappings.reset, 'string' },
['mappings.textobject'] = { config.mappings.textobject, 'string' },
['mappings.goto_first'] = { config.mappings.goto_first, 'string' },
['mappings.goto_prev'] = { config.mappings.goto_prev, 'string' },
['mappings.goto_next'] = { config.mappings.goto_next, 'string' },
['mappings.goto_last'] = { config.mappings.goto_last, 'string' },
['options.algorithm'] = { config.options.algorithm, 'string' },
['options.indent_heuristic'] = { config.options.indent_heuristic, 'boolean' },
['options.linematch'] = { config.options.linematch, 'number' },
['options.wrap_goto'] = { config.options.wrap_goto, 'boolean' },
})
vim.validate({
['view.signs.add'] = { config.view.signs.add, 'string' },
['view.signs.change'] = { config.view.signs.change, 'string' },
['view.signs.delete'] = { config.view.signs.delete, 'string' },
})
return config
end
H.apply_config = function(config)
MiniDiff.config = config
-- Make mappings
local mappings = config.mappings
local rhs_apply = function() return MiniDiff.operator('apply') end
H.map({ 'n', 'x' }, mappings.apply, rhs_apply, { expr = true, desc = 'Apply hunks' })
local rhs_reset = function() return MiniDiff.operator('reset') end
H.map({ 'n', 'x' }, mappings.reset, rhs_reset, { expr = true, desc = 'Reset hunks' })
local is_tobj_conflict = mappings.textobject == mappings.apply or mappings.textobject == mappings.reset
local modes = is_tobj_conflict and { 'o' } or { 'x', 'o' }
H.map(modes, mappings.textobject, '<Cmd>lua MiniDiff.textobject()<CR>', { desc = 'Hunk range textobject' })
--stylua: ignore start
H.map({ 'n', 'x' }, mappings.goto_first, "<Cmd>lua MiniDiff.goto_hunk('first')<CR>", { desc = 'First hunk' })
H.map('o', mappings.goto_first, "V<Cmd>lua MiniDiff.goto_hunk('first')<CR>", { desc = 'First hunk' })
H.map({ 'n', 'x' }, mappings.goto_prev, "<Cmd>lua MiniDiff.goto_hunk('prev')<CR>", { desc = 'Previous hunk' })
H.map('o', mappings.goto_prev, "V<Cmd>lua MiniDiff.goto_hunk('prev')<CR>", { desc = 'Previous hunk' })
H.map({ 'n', 'x' }, mappings.goto_next, "<Cmd>lua MiniDiff.goto_hunk('next')<CR>", { desc = 'Next hunk' })
H.map('o', mappings.goto_next, "V<Cmd>lua MiniDiff.goto_hunk('next')<CR>", { desc = 'Next hunk' })
H.map({ 'n', 'x' }, mappings.goto_last, "<Cmd>lua MiniDiff.goto_hunk('last')<CR>", { desc = 'Last hunk' })
H.map('o', mappings.goto_last, "V<Cmd>lua MiniDiff.goto_hunk('last')<CR>", { desc = 'Last hunk' })
--stylua: ignore end
-- Register decoration provider which actually makes visualization
local ns_id_viz, ns_id_overlay = H.ns_id.viz, H.ns_id.overlay
H.set_decoration_provider(ns_id_viz, ns_id_overlay)
end
H.create_autocommands = function()
local augroup = vim.api.nvim_create_augroup('MiniDiff', {})
local au = function(event, pattern, callback, desc)
vim.api.nvim_create_autocmd(event, { group = augroup, pattern = pattern, callback = callback, desc = desc })
end
-- NOTE: Try auto enabling buffer on every `BufEnter` to not have `:edit`
-- disabling buffer, as it calls `on_detach()` from buffer watcher
au('BufEnter', '*', H.auto_enable, 'Enable diff')
au('VimResized', '*', H.on_resize, 'Track Neovim resizing')
end
--stylua: ignore
H.create_default_hl = function()
local hi = function(name, opts)
opts.default = true
vim.api.nvim_set_hl(0, name, opts)
0