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

Skip to content

Latest commit

 

History

History
1936 lines (1681 loc) · 77.9 KB

diff.lua

File metadata and controls

1936 lines (1681 loc) · 77.9 KB
--- *mini.diff* Work with diff hunks
---
--- MIT License Copyright (c) 2024 Evgeni Chasnovski
--- Features:
---
--- - Visualize difference between buffer text and its configurable reference
--- interactively (updates as you type). This is done per line showing whether
--- it is inside added, changed, or deleted part of difference (called hunk).
--- Visualization can be with customizable colored signs or line numbers.
---
--- - Special toggleable overlay view with more hunk details inside text area.
--- See |MiniDiff.toggle_overlay()|.
---
--- - Completely configurable per buffer source(s) of reference text used to keep
--- it up to date and define interactions with it. Can be array of sources which
--- are attempted to attach in order. See |MiniDiff-source-specification|.
--- By default uses Git source. See |MiniDiff.gen_source.git()|.
---
--- - Configurable mappings to manage diff hunks:
--- - Apply and reset hunks inside region (selected visually or with
--- a dot-repeatable operator).
--- - "Hunk range under cursor" textobject to be used as operator target.
--- - Navigate to first/previous/next/last hunk. See |MiniDiff.goto_hunk()|.
---
--- What it doesn't do:
---
--- - Provide functionality to work directly with Git outside of visualizing
--- and staging (applying) hunks with (default) Git source. In particular,
--- unstaging hunks is not supported. See |MiniDiff.gen_source.git()|.
---
--- Sources with more details:
--- - |MiniDiff-overview|
--- - |MiniDiff-source-specification|
--- - |MiniDiff-hunk-specification|
--- - |MiniDiff-diff-summary|
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.diff').setup({})` (replace
--- `{}` with your `config` table). It will create global Lua table `MiniDiff`
--- which you can use for scripting or manually (with `:lua MiniDiff.*`).
---
--- See |MiniDiff.config| for `config` structure and default values.
---
--- You can override runtime config settings locally to buffer inside
--- `vim.b.minidiff_config` which should have same structure as
--- `MiniDiff.config`. See |mini.nvim-buffer-local-config| for more details.
---
--- # Comparisons ~
---
--- - [lewis6991/gitsigns.nvim](https://github.com/lewis6991/gitsigns.nvim):
--- - Main inspiration for this module, so there are many similarities.
--- - Can display only Git hunks, while this module has extensible design.
--- - Provides more functionality to work with Git outside of hunks.
--- This module does not (by design).
---
--- # Highlight groups ~
--- *MiniDiff-hl-groups*
---
--- - `MiniDiffSignAdd` - "add" hunk lines visualization.
--- - `MiniDiffSignChange` - "change" hunk lines visualization.
--- - `MiniDiffSignDelete` - "delete" hunk lines visualization.
--- - `MiniDiffOverAdd` - added buffer text shown in overlay.
--- - `MiniDiffOverChange` - changed reference text shown in overlay.
--- - `MiniDiffOverChangeBuf` - changed buffer text shown in overlay.
--- - `MiniDiffOverContext` - context of a change shown in reference overlay.
--- - `MiniDiffOverContextBuf` - context of a change shown in buffer overlay.
--- - `MiniDiffOverDelete` - deleted reference text shown in overlay.
---
--- To change any highlight group, set it directly with |nvim_set_hl()|.
---
--- # Disabling ~
---
--- To temporarily disable features without relying on |MiniDiff.disable()|,
--- set `vim.g.minidiff_disable` (globally) or `vim.b.minidiff_disable` (for
--- a buffer) to `true`. Considering high number of different scenarios and
--- customization intentions, writing exact rules for disabling module's
--- functionality is left to user.
--- See |mini.nvim-disabling-recipes| for common recipes.
---@tag MiniDiff
--- # Diffs and hunks ~
---
--- The "diff" (short for "difference") is a result of computing how two text
--- strings differ from one another. This is done on per line basis, i.e. the
--- goal is to compute sequences of lines common to both files, interspersed
--- with groups of differing lines (called "hunks").
---
--- Although computing diff is a general concept (used on its own, in Git, etc.),
--- this module computes difference between current text in a buffer and some
--- reference text which is kept up to date specifically for that buffer.
--- For example, default reference text is computed as file content in Git index.
--- This can be customized in `config.source` (see |MiniDiff-source-specification|).
---
--- # Hunk specification ~
--- *MiniDiff-hunk-specification*
---
--- Hunk describes two sets (one from buffer text, one - from reference) of
--- consecutive lines which are different. In this module hunk is stored as
--- a table with the following fields:
---
--- - <buf_start> `(number)` - start of hunk buffer lines. First line is 1.
--- Can be 0 if first reference lines are deleted.
---
--- - <buf_count> `(number)` - number of consecutive buffer lines. Can be 0 in
--- case reference lines are deleted.
---
--- - <ref_start> `(number)` - start of hunk reference lines. First line is 1.
--- Can be 0 if lines are added before first reference line.
---
--- - <ref_count> `(number)` - number of consecutive reference lines. Can be 0 in
--- case buffer lines are added.
---
--- - <type> `(string)` - hunk type. Can be one of:
--- - "add" - lines are present in buffer but absent in reference.
--- - "change" - lines are present in both buffer and reference.
--- - "delete" - lines are absent in buffer but present in reference.
---
--- # Life cycle ~
---
--- - When entering proper (not already enabled, valid, showing text) buffer,
--- it is attempted to be enabled for diff processing.
--- - During enabling, attempt attaching the source. This should set up how
--- reference text is kept up to date.
--- - On every text change, diff computation is scheduled in debounced fashion
--- after customizable delay (200 ms by default).
--- - After the diff is computed, do the following:
--- - Update visualization based on configurable style: either by placing
--- colored text in sign column or coloring line numbers. Colors for both
--- styles are defined per hunk type in corresponding `MiniDiffSign*`
--- highlight group (see |mini.diff|) and sign text for "sign" style can
--- be configured in `view.signs` of |MiniDiff.config|.
--- - Update overlay view (if it is enabled).
--- - Update `vim.b.minidiff_summary` and `vim.b.minidiff_summary_string`
--- buffer-local variables. These can be used, for example, in statusline.
--- - *MiniDiff-update-event* Trigger `MiniDiffUpdated` `User` event.
--- See |MiniDiff-diff-summary| for example of how to use it.
---
--- Notes:
--- - Use |:edit| to reset (disable and re-enable) current buffer.
--- - To work with BOM bytes, set 'bomb' and have `ucs-bom` in 'fileencodings'.
---
--- # Overlay ~
---
--- Along with basic visualization, there is a special view called "overlay".
--- Although it is meant for temporary overview of diff details and can be
--- manually toggled via |MiniDiff.toggle_overlay()|, text can be changed with
--- overlay reacting accordingly.
---
--- It shows more diff details inside text area:
---
--- - Added buffer lines are highlighted with `MiniDiffOverAdd` highlight group.
---
--- - Deleted reference lines are shown as virtual lines and highlighted with
--- `MiniDiffOverDelete` highlight group.
---
--- - "Change" hunks with equal number of buffer/reference lines show "word diff".
--- This is usually the case when `options.linematch` is enabled (as by default).
--- Reference line is shown next to its buffer counterpart. Changed parts are
--- highlighted with `MiniDiffOverChange` and `MiniDiffOverChangeBuf` in reference
--- and buffer lines. The rest of lines have `MiniDiffOverContext`
--- and `MiniDiffOverContextBuf` highlighting.
---
--- Change with unequal number of buffer/reference lines is shown with reference
--- part as virtual lines highlighted with `MiniDiffOverChange` group.
--- Corresponding buffer lines are treated as context for the change and are
--- highlighted with `MiniDiffOverContextBuf` group.
---
--- Notes:
--- - Word diff has non-zero context width. This means if changed characters
--- are close enough, whole range between them is also colored. This usually
--- reduces visual noise.
--- - Virtual lines above line 1 (like deleted or changed lines) need manual
--- scroll to become visible (with |CTRL-Y|).
---
--- # Mappings ~
---
--- This module provides mappings for common actions with diffs, like:
--- - Apply and reset hunks.
--- - "Hunk range under cursor" textobject.
--- - Go to first/previous/next/last hunk range.
---
--- Examples:
--- - `vip` followed by `gh` / `gH` applies/resets hunks inside current paragraph.
--- Same can be achieved in operator form `ghip` / `gHip`, which has the
--- advantage of being dot-repeatable (see |single-repeat|).
--- - `gh_` / `gH_` applies/resets current line (even if it is not a full hunk).
--- - `ghgh` / `gHgh` applies/resets hunk range under cursor.
--- - `dgh` deletes hunk range under cursor.
--- - `[H` / `[h` / `]h` / `]H` navigate cursor to the first / previous / next / last
--- hunk range of the current buffer.
---
--- Mappings for some functionality are assumed to be done manually.
--- See |MiniDiff.operator()|.
---
--- # Buffer-local variables ~
--- *MiniDiff-diff-summary*
---
--- Each enabled buffer has the following buffer-local variables which can be
--- used in custom statusline to show an overview of hunks in current buffer:
---
--- - `vim.b.minidiff_summary` is a table with the following fields:
--- - `source_name` - name of the active source. This is the only present field
--- if buffer's reference text is not (yet) set.
--- - `n_ranges` - number of hunk ranges (sequences of contiguous hunks).
--- - `add` - number of added lines.
--- - `change` - number of changed lines.
--- - `delete` - number of deleted lines.
---
--- - `vim.b.minidiff_summary_string` is a string representation of summary
--- with a fixed format. Empty string if there is no reference text (yet).
--- It is expected to be used as is. To achieve different formatting, use
--- `vim.b.minidiff_summary` to construct one. The best way to do this is by
--- overriding `vim.b.minidiff_summary_string` inside |MiniDiff-update-event|: >lua
---
--- local format_summary = function(data)
--- local summary = vim.b[data.buf].minidiff_summary
--- local t = {}
--- if summary.add > 0 then table.insert(t, '+' .. summary.add) end
--- if summary.change > 0 then table.insert(t, '~' .. summary.change) end
--- if summary.delete > 0 then table.insert(t, '-' .. summary.delete) end
--- vim.b[data.buf].minidiff_summary_string = table.concat(t, ' ')
--- end
--- local au_opts = { pattern = 'MiniDiffUpdated', callback = format_summary }
--- vim.api.nvim_create_autocmd('User', au_opts)
--- <
---@tag MiniDiff-overview
---@alias __diff_buf_id number Target buffer identifier. Default: 0 for current buffer.
---@diagnostic disable:undefined-field
---@diagnostic disable:discard-returns
---@diagnostic disable:unused-local
---@diagnostic disable:cast-local-type
---@diagnostic disable:undefined-doc-name
---@diagnostic disable:luadoc-miss-type-name
-- Module definition ==========================================================
local MiniDiff = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniDiff.config|.
---
---@usage >lua
--- require('mini.diff').setup() -- use default config
--- -- OR
--- require('mini.diff').setup({}) -- replace {} with your config table
--- <
MiniDiff.setup = function(config)
-- TODO: Remove after Neovim=0.9 support is dropped
if vim.fn.has('nvim-0.10') == 0 then
vim.notify(
'(mini.diff) Neovim<0.10 is soft deprecated (module works but is not supported).'
.. " It will be deprecated after the next 'mini.nvim' release (module might not work)."
.. ' Please update your Neovim version.'
)
end
-- Export module
_G.MiniDiff = MiniDiff
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
-- Define behavior
H.create_autocommands()
for _, buf_id in ipairs(vim.api.nvim_list_bufs()) do
H.auto_enable({ buf = buf_id })
end
-- Create default highlighting
H.create_default_hl()
end
--stylua: ignore
--- Defaults ~
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
---@text # View ~
---
--- `config.view` contains settings for how diff hunks are visualized.
--- Example of using custom signs: >lua
---
--- require('mini.diff').setup({
--- view = {
--- style = 'sign',
--- signs = { add = '+', change = '~', delete = '-' },
--- },
--- })
--- <
--- `view.style` is a string defining visualization style. Can be one of "sign"
--- (as a colored sign in a |sign-column|) or "number" (colored line number).
--- Default: "number" if |'number'| option is enabled, "sign" otherwise.
--- Note: with "sign" style it is better to have |'signcolumn'| always shown.
---
--- `view.signs` is a table with one or two character strings used as signs for
--- corresponding ("add", "change", "delete") hunks.
--- Default: all hunks use "▒" character resulting in a contiguous colored lines.
---
--- `view.priority` is a number with priority used for visualization and
--- overlay |extmarks|.
--- Default: 199 which is one less than `user` in |vim.hl.priorities| (on Neovim<0.11
--- see |vim.hl.priorities|) to have higher priority than automated
--- extmarks but not as in user enabled ones.
---
--- # Source ~
--- *MiniDiff-source-specification*
---
--- `config.source` is a table with single source or array of them. Single source
--- defines how reference text is managed in a particular buffer. Sources in array
--- are attempted to attach in order; call |MiniDiff.disable()| if none attaches.
---
--- A single source table can have the following fields:
---
--- - <attach> `(function)` - callable which defines how and when reference text
--- is updated inside a particular buffer. It is used inside |MiniDiff.enable()|
--- with a buffer identifier as a single argument.
---
--- Should execute logic which results into calling |MiniDiff.set_ref_text()|
--- when reference text for buffer needs to be updated. Like inside callback
--- for an |autocommand| or file watcher (see |watch-file|).
---
--- For example, default Git source watches when ".git/index" file is changed
--- and computes reference text as the one from Git index for current file.
---
--- Can return `false` to indicate that attach has failed. If attach fail can
--- not be inferred immediately (for example, due to asynchronous execution),
--- should explicitly call |MiniDiff.fail_attach()| with appropriate arguments.
--- This is important to properly process array of sources.
---
--- No default value, should be always supplied.
---
--- - <name> `(string|nil)` - source name. String `"unknown"` is used if not supplied.
---
--- - <detach> `(function|nil)` - callable with cleanup action to be done when
--- buffer is disabled. It is called inside |MiniDiff.disable()| with a buffer
--- identifier as a single argument.
---
--- If not supplied, nothing is done during detaching.
---
--- - <apply_hunks> `(function|nil)` - callable which defines how hunks are applied.
--- It is called with buffer identifier as first argument and array of hunks
--- (see |MiniDiff-hunk-specification|) as second. It should eventually update
--- reference text: either by explicitly calling |MiniDiff.set_ref_text()| or
--- performing action triggering its call.
---
--- For example, default Git source computes patch based on the hunks and
--- applies it inside file's git repo.
---
--- If not supplied, applying hunks throws an error.
---
--- Default: a single |MiniDiff.gen_source.git()|.
---
--- # Delay ~
---
--- `config.delay` contains settings for delays in asynchronous processes.
---
--- `delay.text_change` is a number (in ms) defining how long to wait after latest
--- text change (in debounced fashion) before updating diff and visualization.
--- Default: 200.
---
--- # Mappings ~
---
--- `config.mappings` contains keys which are mapped during |MiniDiff.setup()|.
---
--- `mappings.apply` keys can be used to apply hunks inside visual/operator region.
--- What exactly "apply hunks" means depends on the source and its `apply_hunks()`.
--- For example, in default Git source it means stage hunks.
---
--- `mappings.reset` keys can be used to reset hunks inside visual/operator region.
--- Reset means replacing buffer text in region with corresponding reference text.
---
--- `mappings.textobject` keys define "hunk range under cursor" textobject
--- which can be used in Operator-pending mode as target for operator (like
--- |d|, |y|, apply/reset hunks, etc.). It is also set up in Visual mode if
--- keys do not conflict with `mappings.apply` and `mappings.reset`.
--- "Hunk range" is used in a sense that contiguous (back-to-back) hunks are
--- considered as parts of a same hunk range.
---
--- `mappings.goto_first` / `mappings.goto_prev` / `mappings.goto_next` /
--- `mappings.goto_last` keys can be used to navigate to first / previous / next /
--- last hunk range in the current buffer.
---
--- # Options ~
---
--- `config.options` contains various customization options.
---
--- `options.algorithm` is a string defining which diff algorithm to use.
--- Default: "histogram". See |vim.diff()| for possible values.
---
--- `options.indent_heuristic` is a boolean defining whether to use indent
--- heuristic for a (possibly) more naturally aligned hunks.
--- Default: `true`.
---
--- `options.linematch` is a number defining hunk size for which a second
--- stage diff is executed for a better aligned and more granular hunks.
--- Default: 60. See |vim.diff()| and 'diffopt' for more details.
---
--- `options.wrap_goto` is a boolean indicating whether to wrap around edges during
--- hunk navigation (with |MiniDiff.goto_hunk()| or `goto_*` mappings). Like if
--- cursor is after the last hunk, going "next" will put cursor on the first hunk.
--- Default: `false`.
MiniDiff.config = {
-- Options for how hunks are visualized
view = {
-- Visualization style. Possible values are 'sign' and 'number'.
-- Default: 'number' if line numbers are enabled, 'sign' otherwise.
style = vim.go.number and 'number' or 'sign',
-- Signs used for hunks with 'sign' view
signs = { add = '▒', change = '▒', delete = '▒' },
-- Priority of used visualization extmarks
priority = 199,
},
-- Source(s) for how reference text is computed/updated/etc
-- Uses content from Git index by default
source = nil,
-- Delays (in ms) defining asynchronous processes
delay = {
-- How much to wait before update following every text change
text_change = 200,
},
-- Module mappings. Use `''` (empty string) to disable one.
mappings = {
-- Apply hunks inside a visual/operator region
apply = 'gh',
-- Reset hunks inside a visual/operator region
reset = 'gH',
-- Hunk range textobject to be used inside operator
-- Works also in Visual mode if mapping differs from apply and reset
textobject = 'gh',
-- Go to hunk range in corresponding direction
goto_first = '[H',
goto_prev = '[h',
goto_next = ']h',
goto_last = ']H',
},
-- Various options
options = {
-- Diff algorithm. See `:h vim.diff()`.
algorithm = 'histogram',
-- Whether to use "indent heuristic". See `:h vim.diff()`.
indent_heuristic = true,
-- The amount of second-stage diff to align lines
linematch = 60,
-- Whether to wrap around edges during hunk navigation
wrap_goto = false,
},
}
--minidoc_afterlines_end
--- Enable diff processing in buffer
---
---@param buf_id __diff_buf_id
MiniDiff.enable = function(buf_id)
buf_id = H.validate_buf_id(buf_id)
-- Don't enable more than once
if H.is_buf_enabled(buf_id) or H.is_disabled(buf_id) then return end
-- Ensure buffer is loaded (to have up to date lines returned)
H.buf_ensure_loaded(buf_id)
-- Register enabled buffer with cached data for performance
H.update_buf_cache(buf_id)
-- Add buffer watchers
vim.api.nvim_buf_attach(buf_id, false, {
-- Called on every text change (`:h nvim_buf_lines_event`)
on_lines = function(_, _, _, from_line, _, to_line)
local buf_cache = H.cache[buf_id]
-- Properly detach if diffing is disabled
if buf_cache == nil then return true end
H.schedule_diff_update(buf_id, buf_cache.config.delay.text_change)
end,
-- Called when buffer content is changed outside of current session
on_reload = function() H.schedule_diff_update(buf_id, 0) end,
-- Called when buffer is unloaded from memory (`:h nvim_buf_detach_event`),
-- **including** `:edit` command
on_detach = function() MiniDiff.disable(buf_id) end,
})
-- Add buffer autocommands
H.setup_buf_autocommands(buf_id)
-- Try attaching source after all necessary watchers are set up. It is needed
-- to still have them set up if first source of many returned `false`.
local attach_output = H.get_active_source(H.cache[buf_id]).attach(buf_id)
if attach_output == false then MiniDiff.fail_attach(buf_id) end
end
--- Disable diff processing in buffer
---
---@param buf_id __diff_buf_id
MiniDiff.disable = function(buf_id)
buf_id = H.validate_buf_id(buf_id)
local buf_cache = H.cache[buf_id]
if buf_cache == nil then return end
H.cache[buf_id] = nil
pcall(vim.api.nvim_del_augroup_by_id, buf_cache.augroup)
vim.b[buf_id].minidiff_summary, vim.b[buf_id].minidiff_summary_string = nil, nil
H.clear_all_diff(buf_id)
pcall(H.get_active_source(buf_cache).detach, buf_id)
end
--- Toggle diff processing in buffer
---
--- Enable if disabled, disable if enabled.
---
---@param buf_id __diff_buf_id
MiniDiff.toggle = function(buf_id)
buf_id = H.validate_buf_id(buf_id)
if H.is_buf_enabled(buf_id) then return MiniDiff.disable(buf_id) end
return MiniDiff.enable(buf_id)
end
--- Toggle overlay view in buffer
---
---@param buf_id __diff_buf_id
MiniDiff.toggle_overlay = function(buf_id)
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.', 10BC8 buf_id)) end
buf_cache.overlay = not buf_cache.overlay
H.clear_all_diff(buf_id)
H.schedule_diff_update(buf_id, 0)
end
--- Export hunks
---
--- Get and convert hunks from current/all buffers. Example of using it: >lua
---
--- -- Set quickfix list from all available hunks
--- vim.fn.setqflist(MiniDiff.export('qf'))
--- <
---@param format string Output format. Currently only `'qf'` value is supported.
---@param opts table|nil Options. Possible fields:
--- - <scope> `(string)` - scope defining from which buffers to use hunks.
--- One of "all" (all enabled buffers) or "current".
---
---@return table Result of export. Depends on the `format`:
--- - If "qf", an array compatible with |setqflist()| and |setloclist()|.
MiniDiff.export = function(format, opts)
opts = vim.tbl_deep_extend('force', { scope = 'all' }, opts or {})
if format == 'qf' then return H.export_qf(opts) end
H.error('`format` should be one of "qf".')
end
--- Get buffer data
---
---@param buf_id __diff_buf_id
---
---@return table|nil Table with buffer diff data or `nil` if buffer is not enabled.
--- Table has the following fields:
--- - <config> `(table)` - config used for this particular buffer.
--- - <hunks> `(table)` - array of hunks. See |MiniDiff-hunk-specification|.
--- - <overlay> `(boolean)` - whether an overlay view is shown.
--- - <ref_text> `(string|nil)` - current value of reference text. Lines are
--- separated with newline character (`'\n'`). Can be `nil` indicating that
--- reference text was not yet set (for example, if source did not yet react).
--- - <summary> `(table)` - overall diff summary. See |MiniDiff-diff-summary|.
MiniDiff.get_buf_data = function(buf_id)
buf_id = H.validate_buf_id(buf_id)
local buf_cache = H.cache[buf_id]
if buf_cache == nil then return nil end
return vim.deepcopy({
config = buf_cache.config,
hunks = buf_cache.hunks,
overlay = buf_cache.overlay,
ref_text = buf_cache.ref_text,
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.
--- Examples: >lua
---
--- local diff = require('mini.diff')
---
--- -- Single `save` source
--- diff.setup({ source = diff.gen_source.save() })
---
--- -- Multiple sources (attempted to attach in order)
--- diff.setup({ source = { diff.gen_source.git(), 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
-- Claim Git cache to not try to attach to the non-Git path several times.
-- NOTE: Do this after `path==''` check since the same buffer can be reused
-- for opening actual file (`nvim` -> `:edit file`).
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
-- Ensure to clean source cache for `:edit` to force reattach, since regular
-- `disable()` from `on_detach` is not enough as there can be no source
-- attached (like for files not in Git repo).
local augroup = vim.api.nvim_create_augroup('MiniDiffSourceGit', { clear = true })
local clear_buf_cache = function(ev) H.git_cache[ev.buf] = nil end
local au_opts = { group = augroup, callback = clear_buf_cache, desc = 'Clear Git cache' }
vim.api.nvim_create_autocmd({ 'BufUnload' }, au_opts)
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, H.get_buftext(buf_id))
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 H.get_active_source(buf_cache).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
--- Indicate source attach fail
---
--- Try to attach next source; if there is none - call |MiniDiff.disable()|.
---
---@param buf_id integer Buffer identifier for which attach has failed.
MiniDiff.fail_attach = function(buf_id)
buf_id = H.validate_buf_id(buf_id)
-- Do nothing if there was no attempt to enable
local buf_cache = H.cache[buf_id]
if buf_cache == nil then return end
-- If no next source, disable buffer without calling any of `detach`
if buf_cache.source_id >= #buf_cache.source then
H.cache[buf_id].source_id = math.huge
return MiniDiff.disable(buf_id)
end
-- Try attaching next source
buf_cache.source_id = buf_cache.source_id + 1
local attach_output = H.get_active_source(H.cache[buf_id]).attach(buf_id)
if attach_output == false then MiniDiff.fail_attach(buf_id) end
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 for whether to invalidate extmarks
H.extmark_invalidate = vim.fn.has('nvim-0.10') == 1 and true or nil
-- Flag for whether to handle virtual lines overflow
H.extmark_virt_lines_overflow = vim.fn.has('nvim-0.11') == 1 and 'scroll' or nil
-- Permanent `vim.diff()` options
H.vimdiff_opts = { result_type = 'indices', ctxlen = 0, interhunkctxlen = 0 }
-- 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, linematch = 0
}
-- BOM bytes prepended to buffer text if 'bomb' is enabled. See `:h bom-bytes`.
--stylua: ignore
H.bom_bytes = {
['utf-8'] = string.char(0xef, 0xbb, 0xbf),
['utf-16be'] = string.char(0xfe, 0xff),
['utf-16'] = string.char(0xfe, 0xff),
['utf-16le'] = string.char(0xff, 0xfe),
-- In 'fileencoding', 'utf-32' is transformed into 'ucs-4'
['utf-32be'] = string.char(0x00, 0x00, 0xfe, 0xff),
['ucs-4be'] = string.char(0x00, 0x00, 0xfe, 0xff),
['utf-32'] = string.char(0x00, 0x00, 0xfe, 0xff),
['ucs-4'] = string.char(0x00, 0x00, 0xfe, 0xff),
['utf-32le'] = string.char(0xff, 0xfe, 0x00, 0x00),
['ucs-4le'] = string.char(0xff, 0xfe, 0x00, 0x00),
}
-- Helper functionality =======================================================
-- Settings -------------------------------------------------------------------
H.setup_config = function(config)
H.check_type('config', config, 'table', true)
config = vim.tbl_deep_extend('force', vim.deepcopy(H.default_config), config or {})
H.check_type('view', config.view, 'table')
H.check_type('view.style', config.view.style, 'string')
H.check_type('view.signs', config.view.signs, 'table')
H.check_type('view.signs.add', config.view.signs.add, 'string')
H.check_type('view.signs.change', config.view.signs.change, 'string')
H.check_type('view.signs.delete', config.view.signs.delete, 'string')
H.check_type('view.priority', config.view.priority, 'number')
0