8000 mini.nvim/lua/mini/diff.lua at main · isti115/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
--- *mini.diff* Work with diff hunks
--- *MiniDiff*
---
--- 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 of reference text used to keep
--- it up to date and define interactions with it.
--- See |MiniDiff-source-specification|. By default uses buffer's file content
--- in Git index. 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':
--- - 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 ~
---
--- * `MiniDiffSignAdd` - "add" hunk lines visualization.
--- * `MiniDiffSignChange` - "change" hunk lines visualization.
--- * `MiniDiffSignDelete` - "delete" hunk lines visualization.
--- * `MiniDiffOverAdd` - added text shown in overlay.
--- * `MiniDiffOverChange` - changed text shown in overlay.
--- * `MiniDiffOverContext` - context of changed text shown in overlay.
--- * `MiniDiffOverDelete` - deleted text shown in overlay.
---
--- To change any highlight group, modify it directly with |:highlight|.
---
--- # 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.
--- # 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|).
---
--- *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 |MiniDiff|) 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.
---
--- # 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 text and highlighted with
--- `MiniDiffOverDelete` highlight group.
---
--- - Changed reference lines are shown as virtual text and highlighted with
--- `MiniDiffOverChange` highlight group.
---
--- "Change" hunks with equal number of buffer and reference lines have special
--- treatment and show "word diff". Reference line is shown next to its buffer
--- counterpart and only changed parts of both lines are highlighted with
--- `MiniDiffOverChange`. The rest of reference line has `MiniDiffOverContext`
--- highlighting.
--- This usually is the case when `config.options.linematch` is enabled.
---
--- 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 source.
--- - `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. 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`
--- in the callback for |MiniDiff-update-event| 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)
-- 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
--- Module config
---
--- Default values:
---@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 usually 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.highlight.priorities| to have
--- higher priority than automated extmarks but not as in user enabled ones.
---
--- *MiniDiff-source-specification*
--- # Source ~
---
--- `config.source` is a table defining how reference text is managed in
--- a particular buffer. It can have the following fields:
--- - <attach> `(function)` - callable which defines how and when reference text
--- should be updated inside a particular buffer. It is called
--- 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 force buffer to not be enabled. If this can not be
--- inferred immediately (for example, due to asynchronous execution), should
--- call |MiniDiff.disable()| later to disable buffer.
---
--- 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: |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.
--- Note: present only in Neovim>=0.9.
--- 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 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 (in Neovim>=0.9)
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)
-- Try attaching source
local attach_output = H.cache[buf_id].source.attach(buf_id)
if attach_output == false then return MiniDiff.disable(buf_id) end
-- 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)
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(buf_cache.source.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.', 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.
--- 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
local lines = vim.api.nvim_buf_get_lines(buf_id, 0, -1, false)
MiniDiff.set_ref_text(buf_id, table.concat(lines, '\n') .. '\n')
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("']& EE35 quot;), 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