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

Skip to content

Latest commit

 

History

History
1830 lines (1589 loc) · 73.1 KB

diff.lua

File metadata and controls

1830 lines (1589 loc) · 73.1 KB
--- 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 gr = vim.api.nvim_create_augroup('MiniDiff', {})
local au = function(event, pattern, callback, desc)
vim.api.nvim_create_autocmd(event, { group = gr, 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')
au('ColorScheme', '*', H.create_default_hl, 'Ensure colors')
end
--stylua: ignore
H.create_default_hl = function()
local hi = function(name, opts)
0