Compare commits
No commits in common. "doc/merge-conflicts" and "docs/acknowledgements" have entirely different histories.
doc/merge-
...
docs/ackno
13 changed files with 99 additions and 2339 deletions
20
README.md
20
README.md
|
|
@ -18,9 +18,7 @@ syntax highlighting.
|
|||
- Vim syntax fallback for languages without a treesitter parser
|
||||
- Hunk header context highlighting (`@@ ... @@ function foo()`)
|
||||
- Character-level (intra-line) diff highlighting for changed characters
|
||||
- Inline merge conflict detection, highlighting, and resolution keymaps
|
||||
- Configurable debouncing, max lines, diff prefix concealment, blend alpha, and
|
||||
highlight overrides
|
||||
- Configurable debouncing, max lines, and diff prefix concealment
|
||||
|
||||
## Requirements
|
||||
|
||||
|
|
@ -43,11 +41,12 @@ luarocks install diffs.nvim
|
|||
|
||||
## Known Limitations
|
||||
|
||||
- **Incomplete syntax context**: Treesitter parses each diff hunk in isolation.
|
||||
To improve accuracy, `diffs.nvim` reads lines from disk before and after each
|
||||
hunk for parsing context (`highlights.context`, enabled by default with 25
|
||||
lines). This resolves most boundary issues. Set
|
||||
`highlights.context.enabled = false` to disable.
|
||||
- **Incomplete syntax context**: Treesitter parses each diff hunk in isolation
|
||||
without surrounding code context. When a hunk shows lines added to an existing
|
||||
block (e.g., adding a plugin inside `return { ... }`), the parser doesn't see
|
||||
the `return` statement and may produce incorrect highlighting. This is
|
||||
inherent to parsing code fragments—no diff tooling solves this without
|
||||
significant complexity.
|
||||
|
||||
- **Syntax flashing**: `diffs.nvim` hooks into the `FileType fugitive` event
|
||||
triggered by `vim-fugitive`, at which point the buffer is preliminarily
|
||||
|
|
@ -64,8 +63,7 @@ luarocks install diffs.nvim
|
|||
compatible, but both plugins modifying line highlights may produce
|
||||
unexpected results
|
||||
- [`git-conflict.nvim`](https://github.com/akinsho/git-conflict.nvim) -
|
||||
`diffs.nvim` now includes built-in conflict resolution; disable one or the
|
||||
other to avoid overlap
|
||||
conflict marker highlighting may overlap with `diffs.nvim`
|
||||
|
||||
# Acknowledgements
|
||||
|
||||
|
|
@ -77,4 +75,4 @@ luarocks install diffs.nvim
|
|||
- [`gitsigns.nvim`](https://github.com/lewis6991/gitsigns.nvim)
|
||||
- [`git-conflict.nvim`](https://github.com/akinsho/git-conflict.nvim)
|
||||
- [@phanen](https://github.com/phanen) - diff header highlighting, unknown
|
||||
filetype fix, shebang/modeline detection, treesitter injection support
|
||||
filetype fix, shebang/modeline detection
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ Features: ~
|
|||
- Blended diff background colors that preserve syntax visibility
|
||||
- Optional diff prefix (`+`/`-`/` `) concealment
|
||||
- Gutter (line number) highlighting
|
||||
- Inline merge conflict marker detection, highlighting, and resolution
|
||||
|
||||
==============================================================================
|
||||
REQUIREMENTS *diffs-requirements*
|
||||
|
|
@ -57,43 +56,24 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
|
|||
highlights = {
|
||||
background = true,
|
||||
gutter = true,
|
||||
blend_alpha = 0.6,
|
||||
context = {
|
||||
enabled = true,
|
||||
lines = 25,
|
||||
},
|
||||
treesitter = {
|
||||
enabled = true,
|
||||
max_lines = 500,
|
||||
},
|
||||
vim = {
|
||||
enabled = false,
|
||||
max_lines = 200,
|
||||
max_lines = 500,
|
||||
},
|
||||
intra = {
|
||||
enabled = true,
|
||||
algorithm = 'default',
|
||||
max_lines = 500,
|
||||
},
|
||||
overrides = {},
|
||||
},
|
||||
fugitive = {
|
||||
horizontal = 'du',
|
||||
vertical = 'dU',
|
||||
},
|
||||
conflict = {
|
||||
enabled = true,
|
||||
disable_diagnostics = true,
|
||||
show_virtual_text = true,
|
||||
keymaps = {
|
||||
ours = 'doo',
|
||||
theirs = 'dot',
|
||||
both = 'dob',
|
||||
none = 'don',
|
||||
next = ']x',
|
||||
prev = '[x',
|
||||
},
|
||||
},
|
||||
}
|
||||
<
|
||||
*diffs.Config*
|
||||
|
|
@ -122,10 +102,6 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
|
|||
Fugitive status buffer keymap options.
|
||||
See |diffs.FugitiveConfig| for fields.
|
||||
|
||||
{conflict} (table, default: see below)
|
||||
Inline merge conflict resolution options.
|
||||
See |diffs.ConflictConfig| for fields.
|
||||
|
||||
*diffs.Highlights*
|
||||
Highlights table fields: ~
|
||||
{background} (boolean, default: true)
|
||||
|
|
@ -137,17 +113,6 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
|
|||
Highlight line numbers with matching colors.
|
||||
Only visible if line numbers are enabled.
|
||||
|
||||
{blend_alpha} (number, default: 0.6)
|
||||
Alpha value for character-level blend intensity.
|
||||
Controls how strongly changed characters stand
|
||||
out from the line-level background. Must be
|
||||
between 0 and 1 (inclusive). Higher values
|
||||
produce more vivid character-level highlights.
|
||||
|
||||
{context} (table, default: see below)
|
||||
Syntax parsing context options.
|
||||
See |diffs.ContextConfig| for fields.
|
||||
|
||||
{treesitter} (table, default: see below)
|
||||
Treesitter highlighting options.
|
||||
See |diffs.TreesitterConfig| for fields.
|
||||
|
|
@ -160,27 +125,6 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
|
|||
Character-level (intra-line) diff highlighting.
|
||||
See |diffs.IntraConfig| for fields.
|
||||
|
||||
{overrides} (table, default: {})
|
||||
Map of highlight group names to highlight
|
||||
definitions (see |nvim_set_hl()|). Applied
|
||||
after all computed groups without `default`,
|
||||
so overrides always win over both computed
|
||||
defaults and colorscheme definitions.
|
||||
|
||||
*diffs.ContextConfig*
|
||||
Context config fields: ~
|
||||
{enabled} (boolean, default: true)
|
||||
Read lines from disk before and after each hunk
|
||||
to provide surrounding syntax context. Improves
|
||||
accuracy at hunk boundaries where incomplete
|
||||
constructs (e.g., a function definition with no
|
||||
body) would otherwise confuse the parser.
|
||||
|
||||
{lines} (integer, default: 25)
|
||||
Number of context lines to read in each
|
||||
direction. Lines are read with early exit —
|
||||
cost scales with this value, not file size.
|
||||
|
||||
*diffs.TreesitterConfig*
|
||||
Treesitter config fields: ~
|
||||
{enabled} (boolean, default: true)
|
||||
|
|
@ -244,9 +188,6 @@ COMMANDS *diffs-commands*
|
|||
code language, plus diff header highlighting for `diff --git`, `---`,
|
||||
`+++`, and `@@` lines.
|
||||
|
||||
If a `diffs://` window already exists in the current tabpage, the new
|
||||
diff replaces its buffer instead of creating another split.
|
||||
|
||||
Parameters: ~
|
||||
{revision} (string, optional) Git revision to diff against.
|
||||
Defaults to HEAD.
|
||||
|
|
@ -264,63 +205,6 @@ COMMANDS *diffs-commands*
|
|||
:Ghdiff [revision] *:Ghdiff*
|
||||
Like |:Gdiff| but explicitly opens in a horizontal split.
|
||||
|
||||
==============================================================================
|
||||
MAPPINGS *diffs-mappings*
|
||||
|
||||
*<Plug>(diffs-gdiff)*
|
||||
<Plug>(diffs-gdiff) Show unified diff against HEAD in a horizontal
|
||||
split. Equivalent to |:Gdiff| with no arguments.
|
||||
|
||||
*<Plug>(diffs-gvdiff)*
|
||||
<Plug>(diffs-gvdiff) Show unified diff against HEAD in a vertical
|
||||
split. Equivalent to |:Gvdiff| with no arguments.
|
||||
|
||||
Example configuration: >lua
|
||||
vim.keymap.set('n', '<leader>gd', '<Plug>(diffs-gdiff)')
|
||||
vim.keymap.set('n', '<leader>gD', '<Plug>(diffs-gvdiff)')
|
||||
<
|
||||
|
||||
*<Plug>(diffs-conflict-ours)*
|
||||
<Plug>(diffs-conflict-ours)
|
||||
Accept current (ours) change. Replaces the
|
||||
conflict block with ours content.
|
||||
|
||||
*<Plug>(diffs-conflict-theirs)*
|
||||
<Plug>(diffs-conflict-theirs)
|
||||
Accept incoming (theirs) change. Replaces the
|
||||
conflict block with theirs content.
|
||||
|
||||
*<Plug>(diffs-conflict-both)*
|
||||
<Plug>(diffs-conflict-both)
|
||||
Accept both changes (ours then theirs).
|
||||
|
||||
*<Plug>(diffs-conflict-none)*
|
||||
<Plug>(diffs-conflict-none)
|
||||
Reject both changes (delete entire block).
|
||||
|
||||
*<Plug>(diffs-conflict-next)*
|
||||
<Plug>(diffs-conflict-next)
|
||||
Jump to next conflict marker. Wraps around.
|
||||
|
||||
*<Plug>(diffs-conflict-prev)*
|
||||
<Plug>(diffs-conflict-prev)
|
||||
Jump to previous conflict marker. Wraps around.
|
||||
|
||||
Example configuration: >lua
|
||||
vim.keymap.set('n', 'co', '<Plug>(diffs-conflict-ours)')
|
||||
vim.keymap.set('n', 'ct', '<Plug>(diffs-conflict-theirs)')
|
||||
vim.keymap.set('n', 'cb', '<Plug>(diffs-conflict-both)')
|
||||
vim.keymap.set('n', 'cn', '<Plug>(diffs-conflict-none)')
|
||||
vim.keymap.set('n', ']x', '<Plug>(diffs-conflict-next)')
|
||||
vim.keymap.set('n', '[x', '<Plug>(diffs-conflict-prev)')
|
||||
<
|
||||
|
||||
Diff buffer mappings: ~
|
||||
*diffs-q*
|
||||
q Close the diff window. Available in all `diffs://`
|
||||
buffers created by |:Gdiff|, |:Gvdiff|, |:Ghdiff|,
|
||||
or the fugitive status keymaps.
|
||||
|
||||
==============================================================================
|
||||
FUGITIVE STATUS KEYMAPS *diffs-fugitive*
|
||||
|
||||
|
|
@ -370,94 +254,6 @@ Configuration: ~
|
|||
Keymap for unified diff in vertical split.
|
||||
Set to `false` to disable.
|
||||
|
||||
==============================================================================
|
||||
CONFLICT RESOLUTION *diffs-conflict*
|
||||
|
||||
diffs.nvim detects inline merge conflict markers (`<<<<<<<`/`=======`/
|
||||
`>>>>>>>`) in working files and provides highlighting and resolution keymaps.
|
||||
Both standard and diff3 (`|||||||`) formats are supported.
|
||||
|
||||
Conflict regions are detected automatically on `BufReadPost` and re-scanned
|
||||
on `TextChanged`. When all conflicts in a buffer are resolved, highlighting
|
||||
is removed and diagnostics are re-enabled.
|
||||
|
||||
Configuration: ~
|
||||
*diffs.ConflictConfig*
|
||||
>lua
|
||||
vim.g.diffs = {
|
||||
conflict = {
|
||||
enabled = true,
|
||||
disable_diagnostics = true,
|
||||
show_virtual_text = true,
|
||||
keymaps = {
|
||||
ours = 'doo',
|
||||
theirs = 'dot',
|
||||
both = 'dob',
|
||||
none = 'don',
|
||||
next = ']x',
|
||||
prev = '[x',
|
||||
},
|
||||
},
|
||||
}
|
||||
<
|
||||
Fields: ~
|
||||
{enabled} (boolean, default: true)
|
||||
Enable conflict marker detection and
|
||||
resolution. Set to `false` to disable
|
||||
entirely.
|
||||
|
||||
{disable_diagnostics} (boolean, default: true)
|
||||
Suppress LSP diagnostics on buffers with
|
||||
conflict markers. Markers produce syntax
|
||||
errors that clutter the diagnostic list.
|
||||
Diagnostics are re-enabled when all conflicts
|
||||
are resolved. Set `false` to leave
|
||||
diagnostics alone.
|
||||
|
||||
{show_virtual_text} (boolean, default: true)
|
||||
Show virtual text labels (" current" and
|
||||
" incoming") at the end of `<<<<<<<` and
|
||||
`>>>>>>>` marker lines.
|
||||
|
||||
{keymaps} (table, default: see above)
|
||||
Buffer-local keymaps for conflict resolution
|
||||
and navigation. Each value accepts a string
|
||||
(custom key) or `false` (disabled).
|
||||
|
||||
*diffs.ConflictKeymaps*
|
||||
Keymap fields: ~
|
||||
{ours} (string|false, default: 'doo')
|
||||
Accept current (ours) change.
|
||||
|
||||
{theirs} (string|false, default: 'dot')
|
||||
Accept incoming (theirs) change.
|
||||
|
||||
{both} (string|false, default: 'dob')
|
||||
Accept both changes (ours then theirs).
|
||||
|
||||
{none} (string|false, default: 'don')
|
||||
Reject both changes (delete entire block).
|
||||
|
||||
{next} (string|false, default: ']x')
|
||||
Jump to next conflict marker. Wraps around.
|
||||
|
||||
{prev} (string|false, default: '[x')
|
||||
Jump to previous conflict marker. Wraps
|
||||
around.
|
||||
|
||||
User events: ~
|
||||
*DiffsConflictResolved*
|
||||
DiffsConflictResolved Fired when the last conflict in a buffer is
|
||||
resolved. Useful for triggering custom actions
|
||||
(e.g., auto-staging the file). >lua
|
||||
vim.api.nvim_create_autocmd('User', {
|
||||
pattern = 'DiffsConflictResolved',
|
||||
callback = function()
|
||||
print('all conflicts resolved!')
|
||||
end,
|
||||
})
|
||||
<
|
||||
|
||||
==============================================================================
|
||||
API *diffs-api*
|
||||
|
||||
|
|
@ -509,15 +305,14 @@ KNOWN LIMITATIONS *diffs-limitations*
|
|||
|
||||
Incomplete Syntax Context ~
|
||||
*diffs-syntax-context*
|
||||
Treesitter parses each diff hunk in isolation. To provide surrounding code
|
||||
context, diffs.nvim reads lines from disk before and after each hunk
|
||||
(see |diffs.ContextConfig|, enabled by default). This resolves most boundary
|
||||
issues where incomplete constructs (e.g., a function definition at the edge
|
||||
of a hunk with no body) would confuse the parser.
|
||||
Treesitter parses each diff hunk in isolation without surrounding code
|
||||
context. When a hunk shows lines added to an existing block (e.g., adding a
|
||||
plugin inside `return { ... }`), the parser doesn't see the `return`
|
||||
statement and may produce incorrect or unusual highlighting.
|
||||
|
||||
Set `highlights.context.enabled = false` to disable context padding. In rare
|
||||
cases, context padding may not help if the relevant surrounding code is very
|
||||
far from the hunk boundaries.
|
||||
This is inherent to parsing code fragments. No diff tooling solves this
|
||||
problem without significant complexity—the parser simply doesn't have enough
|
||||
information to understand the full syntactic structure.
|
||||
|
||||
Syntax Highlighting Flash ~
|
||||
*diffs-flash*
|
||||
|
|
@ -555,9 +350,8 @@ conflict:
|
|||
modifying line highlights may produce unexpected results.
|
||||
|
||||
- git-conflict.nvim (akinsho/git-conflict.nvim)
|
||||
Provides conflict marker highlighting and resolution keymaps.
|
||||
diffs.nvim now has built-in conflict resolution (see
|
||||
|diffs-conflict|). Disable one or the other to avoid overlap.
|
||||
Provides conflict marker highlighting that may overlap with
|
||||
diffs.nvim's highlighting in conflict scenarios.
|
||||
|
||||
If you experience visual conflicts, try disabling the conflicting plugin's
|
||||
diff-related features.
|
||||
|
|
@ -565,15 +359,9 @@ diff-related features.
|
|||
==============================================================================
|
||||
HIGHLIGHT GROUPS *diffs-highlights*
|
||||
|
||||
diffs.nvim defines custom highlight groups. All groups use `default = true`,
|
||||
so colorschemes can override them by defining the group before the plugin
|
||||
loads.
|
||||
|
||||
All derived groups are computed by alpha-blending a source color into the
|
||||
`Normal` background. Line-level groups blend at 40% alpha for a subtle tint;
|
||||
character-level groups blend at 60% for more contrast. Line-number groups
|
||||
combine both: background from the line-level blend, foreground from the
|
||||
character-level blend.
|
||||
diffs.nvim defines custom highlight groups. Fugitive unified diff groups use
|
||||
`default = true`, so colorschemes can override them. Diff mode groups are
|
||||
always derived from the corresponding `Diff*` groups.
|
||||
|
||||
Fugitive unified diff highlights: ~
|
||||
*DiffsAdd*
|
||||
|
|
@ -586,54 +374,23 @@ Fugitive unified diff highlights: ~
|
|||
|
||||
*DiffsAddNr*
|
||||
DiffsAddNr Line number for `+` lines. Foreground from
|
||||
`DiffsAddText`, background from `DiffsAdd`.
|
||||
`diffAdded`, background from `DiffsAdd`.
|
||||
|
||||
*DiffsDeleteNr*
|
||||
DiffsDeleteNr Line number for `-` lines. Foreground from
|
||||
`DiffsDeleteText`, background from `DiffsDelete`.
|
||||
`diffRemoved`, background from `DiffsDelete`.
|
||||
|
||||
*DiffsAddText*
|
||||
DiffsAddText Character-level background for changed characters
|
||||
within `+` lines. Derived by blending `diffAdded`
|
||||
foreground with `Normal` background at 60% alpha.
|
||||
foreground with `Normal` background at 70% alpha.
|
||||
Only sets `bg`, so treesitter foreground colors show
|
||||
through.
|
||||
|
||||
*DiffsDeleteText*
|
||||
DiffsDeleteText Character-level background for changed characters
|
||||
within `-` lines. Derived by blending `diffRemoved`
|
||||
foreground with `Normal` background at 60% alpha.
|
||||
|
||||
Conflict highlights: ~
|
||||
*DiffsConflictOurs*
|
||||
DiffsConflictOurs Background for "ours" (current) content lines.
|
||||
Derived by blending `DiffAdd` background with
|
||||
`Normal` at 40% alpha (green tint).
|
||||
|
||||
*DiffsConflictTheirs*
|
||||
DiffsConflictTheirs Background for "theirs" (incoming) content lines.
|
||||
Derived by blending `DiffChange` background with
|
||||
`Normal` at 40% alpha.
|
||||
|
||||
*DiffsConflictBase*
|
||||
DiffsConflictBase Background for base (ancestor) content lines in
|
||||
diff3 conflicts. Derived by blending `DiffText`
|
||||
background with `Normal` at 30% alpha (muted).
|
||||
|
||||
*DiffsConflictMarker*
|
||||
DiffsConflictMarker Dimmed foreground with bold for `<<<<<<<`,
|
||||
`=======`, `>>>>>>>`, and `|||||||` marker lines.
|
||||
|
||||
*DiffsConflictOursNr*
|
||||
DiffsConflictOursNr Line number for "ours" content lines. Foreground
|
||||
from higher-alpha blend, background from line-level
|
||||
blend.
|
||||
|
||||
*DiffsConflictTheirsNr*
|
||||
DiffsConflictTheirsNr Line number for "theirs" content lines.
|
||||
|
||||
*DiffsConflictBaseNr*
|
||||
DiffsConflictBaseNr Line number for base content lines (diff3).
|
||||
foreground with `Normal` background at 70% alpha.
|
||||
|
||||
Diff mode window highlights: ~
|
||||
These are used for |winhighlight| remapping in `&diff` windows.
|
||||
|
|
@ -659,16 +416,6 @@ To customize these in your colorscheme: >lua
|
|||
vim.api.nvim_set_hl(0, 'DiffsAdd', { bg = '#2e4a3a' })
|
||||
vim.api.nvim_set_hl(0, 'DiffsDiffDelete', { link = 'DiffDelete' })
|
||||
<
|
||||
Or via `highlights.overrides` in config: >lua
|
||||
vim.g.diffs = {
|
||||
highlights = {
|
||||
overrides = {
|
||||
DiffsAdd = { bg = '#2e4a3a' },
|
||||
DiffsDiffDelete = { link = 'DiffDelete' },
|
||||
},
|
||||
},
|
||||
}
|
||||
<
|
||||
|
||||
==============================================================================
|
||||
HEALTH CHECK *diffs-health*
|
||||
|
|
@ -686,8 +433,7 @@ ACKNOWLEDGEMENTS *diffs-acknowledgements*
|
|||
- vim-fugitive (https://github.com/tpope/vim-fugitive)
|
||||
- codediff.nvim (https://github.com/esmuellert/codediff.nvim)
|
||||
- diffview.nvim (https://github.com/sindrets/diffview.nvim)
|
||||
- @phanen (https://github.com/phanen) - diff header highlighting,
|
||||
treesitter injection support
|
||||
- @phanen (https://github.com/phanen) - diff header highlighting
|
||||
|
||||
==============================================================================
|
||||
vim:tw=78:ts=8:ft=help:norl:
|
||||
|
|
|
|||
|
|
@ -3,27 +3,6 @@ local M = {}
|
|||
local git = require('diffs.git')
|
||||
local dbg = require('diffs.log').dbg
|
||||
|
||||
---@return integer?
|
||||
function M.find_diffs_window()
|
||||
local tabpage = vim.api.nvim_get_current_tabpage()
|
||||
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(tabpage)) do
|
||||
if vim.api.nvim_win_is_valid(win) then
|
||||
local buf = vim.api.nvim_win_get_buf(win)
|
||||
local name = vim.api.nvim_buf_get_name(buf)
|
||||
if name:match('^diffs://') then
|
||||
return win
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
function M.setup_diff_buf(bufnr)
|
||||
vim.diagnostic.enable(false, { bufnr = bufnr })
|
||||
vim.keymap.set('n', 'q', '<cmd>close<CR>', { buffer = bufnr })
|
||||
end
|
||||
|
||||
---@param diff_lines string[]
|
||||
---@param hunk_position { hunk_header: string, offset: integer }
|
||||
---@return integer?
|
||||
|
|
@ -117,16 +96,9 @@ function M.gdiff(revision, vertical)
|
|||
vim.api.nvim_buf_set_var(diff_buf, 'diffs_repo_root', repo_root)
|
||||
end
|
||||
|
||||
local existing_win = M.find_diffs_window()
|
||||
if existing_win then
|
||||
vim.api.nvim_set_current_win(existing_win)
|
||||
vim.api.nvim_win_set_buf(existing_win, diff_buf)
|
||||
else
|
||||
vim.cmd(vertical and 'vsplit' or 'split')
|
||||
vim.api.nvim_win_set_buf(0, diff_buf)
|
||||
end
|
||||
vim.cmd(vertical and 'vsplit' or 'split')
|
||||
vim.api.nvim_win_set_buf(0, diff_buf)
|
||||
|
||||
M.setup_diff_buf(diff_buf)
|
||||
dbg('opened diff buffer %d for %s against %s', diff_buf, rel_path, revision)
|
||||
|
||||
vim.schedule(function()
|
||||
|
|
@ -218,14 +190,8 @@ function M.gdiff_file(filepath, opts)
|
|||
vim.api.nvim_buf_set_var(diff_buf, 'diffs_old_filepath', old_rel_path)
|
||||
end
|
||||
|
||||
local existing_win = M.find_diffs_window()
|
||||
if existing_win then
|
||||
vim.api.nvim_set_current_win(existing_win)
|
||||
vim.api.nvim_win_set_buf(existing_win, diff_buf)
|
||||
else
|
||||
vim.cmd(opts.vertical and 'vsplit' or 'split')
|
||||
vim.api.nvim_win_set_buf(0, diff_buf)
|
||||
end
|
||||
vim.cmd(opts.vertical and 'vsplit' or 'split')
|
||||
vim.api.nvim_win_set_buf(0, diff_buf)
|
||||
|
||||
if opts.hunk_position then
|
||||
local target_line = M.find_hunk_line(diff_lines, opts.hunk_position)
|
||||
|
|
@ -235,7 +201,6 @@ function M.gdiff_file(filepath, opts)
|
|||
end
|
||||
end
|
||||
|
||||
M.setup_diff_buf(diff_buf)
|
||||
dbg('opened diff buffer %d for %s (%s)', diff_buf, rel_path, diff_label)
|
||||
|
||||
vim.schedule(function()
|
||||
|
|
@ -279,16 +244,9 @@ function M.gdiff_section(repo_root, opts)
|
|||
vim.api.nvim_buf_set_name(diff_buf, 'diffs://' .. diff_label .. ':all')
|
||||
vim.api.nvim_buf_set_var(diff_buf, 'diffs_repo_root', repo_root)
|
||||
|
||||
local existing_win = M.find_diffs_window()
|
||||
if existing_win then
|
||||
vim.api.nvim_set_current_win(existing_win)
|
||||
vim.api.nvim_win_set_buf(existing_win, diff_buf)
|
||||
else
|
||||
vim.cmd(opts.vertical and 'vsplit' or 'split')
|
||||
vim.api.nvim_win_set_buf(0, diff_buf)
|
||||
end
|
||||
vim.cmd(opts.vertical and 'vsplit' or 'split')
|
||||
vim.api.nvim_win_set_buf(0, diff_buf)
|
||||
|
||||
M.setup_diff_buf(diff_buf)
|
||||
dbg('opened section diff buffer %d (%s)', diff_buf, diff_label)
|
||||
|
||||
vim.schedule(function()
|
||||
|
|
|
|||
|
|
@ -1,438 +0,0 @@
|
|||
---@class diffs.ConflictRegion
|
||||
---@field marker_ours integer
|
||||
---@field ours_start integer
|
||||
---@field ours_end integer
|
||||
---@field marker_base integer?
|
||||
---@field base_start integer?
|
||||
---@field base_end integer?
|
||||
---@field marker_sep integer
|
||||
---@field theirs_start integer
|
||||
---@field theirs_end integer
|
||||
---@field marker_theirs integer
|
||||
|
||||
local M = {}
|
||||
|
||||
local ns = vim.api.nvim_create_namespace('diffs-conflict')
|
||||
|
||||
---@type table<integer, true>
|
||||
local attached_buffers = {}
|
||||
|
||||
---@type table<integer, boolean>
|
||||
local diagnostics_suppressed = {}
|
||||
|
||||
local PRIORITY_LINE_BG = 200
|
||||
|
||||
---@param lines string[]
|
||||
---@return diffs.ConflictRegion[]
|
||||
function M.parse(lines)
|
||||
local regions = {}
|
||||
local state = 'idle'
|
||||
---@type table?
|
||||
local current = nil
|
||||
|
||||
for i, line in ipairs(lines) do
|
||||
local idx = i - 1
|
||||
|
||||
if state == 'idle' then
|
||||
if line:match('^<<<<<<<') then
|
||||
current = { marker_ours = idx, ours_start = idx + 1 }
|
||||
state = 'in_ours'
|
||||
end
|
||||
elseif state == 'in_ours' then
|
||||
if line:match('^|||||||') then
|
||||
current.ours_end = idx
|
||||
current.marker_base = idx
|
||||
current.base_start = idx + 1
|
||||
state = 'in_base'
|
||||
elseif line:match('^=======') then
|
||||
current.ours_end = idx
|
||||
current.marker_sep = idx
|
||||
current.theirs_start = idx + 1
|
||||
state = 'in_theirs'
|
||||
elseif line:match('^<<<<<<<') then
|
||||
current = { marker_ours = idx, ours_start = idx + 1 }
|
||||
elseif line:match('^>>>>>>>') then
|
||||
current = nil
|
||||
state = 'idle'
|
||||
end
|
||||
elseif state == 'in_base' then
|
||||
if line:match('^=======') then
|
||||
current.base_end = idx
|
||||
current.marker_sep = idx
|
||||
current.theirs_start = idx + 1
|
||||
state = 'in_theirs'
|
||||
elseif line:match('^<<<<<<<') then
|
||||
current = { marker_ours = idx, ours_start = idx + 1 }
|
||||
state = 'in_ours'
|
||||
elseif line:match('^>>>>>>>') then
|
||||
current = nil
|
||||
state = 'idle'
|
||||
end
|
||||
elseif state == 'in_theirs' then
|
||||
if line:match('^>>>>>>>') then
|
||||
current.theirs_end = idx
|
||||
current.marker_theirs = idx
|
||||
table.insert(regions, current)
|
||||
current = nil
|
||||
state = 'idle'
|
||||
elseif line:match('^<<<<<<<') then
|
||||
current = { marker_ours = idx, ours_start = idx + 1 }
|
||||
state = 'in_ours'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return regions
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@return diffs.ConflictRegion[]
|
||||
local function parse_buffer(bufnr)
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
return M.parse(lines)
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param regions diffs.ConflictRegion[]
|
||||
---@param config diffs.ConflictConfig
|
||||
local function apply_highlights(bufnr, regions, config)
|
||||
vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
|
||||
|
||||
for _, region in ipairs(regions) do
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, region.marker_ours, 0, {
|
||||
end_row = region.marker_ours + 1,
|
||||
hl_group = 'DiffsConflictMarker',
|
||||
hl_eol = true,
|
||||
priority = PRIORITY_LINE_BG,
|
||||
})
|
||||
|
||||
if config.show_virtual_text then
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, region.marker_ours, 0, {
|
||||
virt_text = { { ' (current)', 'DiffsConflictMarker' } },
|
||||
virt_text_pos = 'eol',
|
||||
})
|
||||
end
|
||||
|
||||
for line = region.ours_start, region.ours_end - 1 do
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, line, 0, {
|
||||
end_row = line + 1,
|
||||
hl_group = 'DiffsConflictOurs',
|
||||
hl_eol = true,
|
||||
priority = PRIORITY_LINE_BG,
|
||||
})
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, line, 0, {
|
||||
number_hl_group = 'DiffsConflictOursNr',
|
||||
priority = PRIORITY_LINE_BG,
|
||||
})
|
||||
end
|
||||
|
||||
if region.marker_base then
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, region.marker_base, 0, {
|
||||
end_row = region.marker_base + 1,
|
||||
hl_group = 'DiffsConflictMarker',
|
||||
hl_eol = true,
|
||||
priority = PRIORITY_LINE_BG,
|
||||
})
|
||||
|
||||
for line = region.base_start, region.base_end - 1 do
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, line, 0, {
|
||||
end_row = line + 1,
|
||||
hl_group = 'DiffsConflictBase',
|
||||
hl_eol = true,
|
||||
priority = PRIORITY_LINE_BG,
|
||||
})
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, line, 0, {
|
||||
number_hl_group = 'DiffsConflictBaseNr',
|
||||
priority = PRIORITY_LINE_BG,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, region.marker_sep, 0, {
|
||||
end_row = region.marker_sep + 1,
|
||||
hl_group = 'DiffsConflictMarker',
|
||||
hl_eol = true,
|
||||
priority = PRIORITY_LINE_BG,
|
||||
})
|
||||
|
||||
for line = region.theirs_start, region.theirs_end - 1 do
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, line, 0, {
|
||||
end_row = line + 1,
|
||||
hl_group = 'DiffsConflictTheirs',
|
||||
hl_eol = true,
|
||||
priority = PRIORITY_LINE_BG,
|
||||
})
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, line, 0, {
|
||||
number_hl_group = 'DiffsConflictTheirsNr',
|
||||
priority = PRIORITY_LINE_BG,
|
||||
})
|
||||
end
|
||||
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, region.marker_theirs, 0, {
|
||||
end_row = region.marker_theirs + 1,
|
||||
hl_group = 'DiffsConflictMarker',
|
||||
hl_eol = true,
|
||||
priority = PRIORITY_LINE_BG,
|
||||
})
|
||||
|
||||
if config.show_virtual_text then
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, region.marker_theirs, 0, {
|
||||
virt_text = { { ' (incoming)', 'DiffsConflictMarker' } },
|
||||
virt_text_pos = 'eol',
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param cursor_line integer
|
||||
---@param regions diffs.ConflictRegion[]
|
||||
---@return diffs.ConflictRegion?
|
||||
local function find_conflict_at_cursor(cursor_line, regions)
|
||||
for _, region in ipairs(regions) do
|
||||
if cursor_line >= region.marker_ours and cursor_line <= region.marker_theirs then
|
||||
return region
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param region diffs.ConflictRegion
|
||||
---@param replacement string[]
|
||||
local function replace_region(bufnr, region, replacement)
|
||||
vim.api.nvim_buf_set_lines(
|
||||
bufnr,
|
||||
region.marker_ours,
|
||||
region.marker_theirs + 1,
|
||||
false,
|
||||
replacement
|
||||
)
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param config diffs.ConflictConfig
|
||||
local function refresh(bufnr, config)
|
||||
local regions = parse_buffer(bufnr)
|
||||
if #regions == 0 then
|
||||
vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
|
||||
if diagnostics_suppressed[bufnr] then
|
||||
pcall(vim.diagnostic.reset, nil, bufnr)
|
||||
pcall(vim.diagnostic.enable, true, { bufnr = bufnr })
|
||||
diagnostics_suppressed[bufnr] = nil
|
||||
end
|
||||
vim.api.nvim_exec_autocmds('User', { pattern = 'DiffsConflictResolved' })
|
||||
return
|
||||
end
|
||||
apply_highlights(bufnr, regions, config)
|
||||
if config.disable_diagnostics and not diagnostics_suppressed[bufnr] then
|
||||
pcall(vim.diagnostic.enable, false, { bufnr = bufnr })
|
||||
diagnostics_suppressed[bufnr] = true
|
||||
end
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param config diffs.ConflictConfig
|
||||
function M.resolve_ours(bufnr, config)
|
||||
if not vim.api.nvim_get_option_value('modifiable', { buf = bufnr }) then
|
||||
vim.notify('[diffs.nvim]: buffer is not modifiable', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
local regions = parse_buffer(bufnr)
|
||||
local cursor = vim.api.nvim_win_get_cursor(0)
|
||||
local region = find_conflict_at_cursor(cursor[1] - 1, regions)
|
||||
if not region then
|
||||
return
|
||||
end
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, region.ours_start, region.ours_end, false)
|
||||
replace_region(bufnr, region, lines)
|
||||
refresh(bufnr, config)
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param config diffs.ConflictConfig
|
||||
function M.resolve_theirs(bufnr, config)
|
||||
if not vim.api.nvim_get_option_value('modifiable', { buf = bufnr }) then
|
||||
vim.notify('[diffs.nvim]: buffer is not modifiable', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
local regions = parse_buffer(bufnr)
|
||||
local cursor = vim.api.nvim_win_get_cursor(0)
|
||||
local region = find_conflict_at_cursor(cursor[1] - 1, regions)
|
||||
if not region then
|
||||
return
|
||||
end
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, region.theirs_start, region.theirs_end, false)
|
||||
replace_region(bufnr, region, lines)
|
||||
refresh(bufnr, config)
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param config diffs.ConflictConfig
|
||||
function M.resolve_both(bufnr, config)
|
||||
if not vim.api.nvim_get_option_value('modifiable', { buf = bufnr }) then
|
||||
vim.notify('[diffs.nvim]: buffer is not modifiable', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
local regions = parse_buffer(bufnr)
|
||||
local cursor = vim.api.nvim_win_get_cursor(0)
|
||||
local region = find_conflict_at_cursor(cursor[1] - 1, regions)
|
||||
if not region then
|
||||
return
|
||||
end
|
||||
local ours = vim.api.nvim_buf_get_lines(bufnr, region.ours_start, region.ours_end, false)
|
||||
local theirs = vim.api.nvim_buf_get_lines(bufnr, region.theirs_start, region.theirs_end, false)
|
||||
local combined = {}
|
||||
for _, l in ipairs(ours) do
|
||||
table.insert(combined, l)
|
||||
end
|
||||
for _, l in ipairs(theirs) do
|
||||
table.insert(combined, l)
|
||||
end
|
||||
replace_region(bufnr, region, combined)
|
||||
refresh(bufnr, config)
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param config diffs.ConflictConfig
|
||||
function M.resolve_none(bufnr, config)
|
||||
if not vim.api.nvim_get_option_value('modifiable', { buf = bufnr }) then
|
||||
vim.notify('[diffs.nvim]: buffer is not modifiable', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
local regions = parse_buffer(bufnr)
|
||||
local cursor = vim.api.nvim_win_get_cursor(0)
|
||||
local region = find_conflict_at_cursor(cursor[1] - 1, regions)
|
||||
if not region then
|
||||
return
|
||||
end
|
||||
replace_region(bufnr, region, {})
|
||||
refresh(bufnr, config)
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
function M.goto_next(bufnr)
|
||||
local regions = parse_buffer(bufnr)
|
||||
if #regions == 0 then
|
||||
return
|
||||
end
|
||||
local cursor = vim.api.nvim_win_get_cursor(0)
|
||||
local cursor_line = cursor[1] - 1
|
||||
for _, region in ipairs(regions) do
|
||||
if region.marker_ours > cursor_line then
|
||||
vim.api.nvim_win_set_cursor(0, { region.marker_ours + 1, 0 })
|
||||
return
|
||||
end
|
||||
end
|
||||
vim.api.nvim_win_set_cursor(0, { regions[1].marker_ours + 1, 0 })
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
function M.goto_prev(bufnr)
|
||||
local regions = parse_buffer(bufnr)
|
||||
if #regions == 0 then
|
||||
return
|
||||
end
|
||||
local cursor = vim.api.nvim_win_get_cursor(0)
|
||||
local cursor_line = cursor[1] - 1
|
||||
for i = #regions, 1, -1 do
|
||||
if regions[i].marker_ours < cursor_line then
|
||||
vim.api.nvim_win_set_cursor(0, { regions[i].marker_ours + 1, 0 })
|
||||
return
|
||||
end
|
||||
end
|
||||
vim.api.nvim_win_set_cursor(0, { regions[#regions].marker_ours + 1, 0 })
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param config diffs.ConflictConfig
|
||||
local function setup_keymaps(bufnr, config)
|
||||
local km = config.keymaps
|
||||
|
||||
local maps = {
|
||||
{ km.ours, '<Plug>(diffs-conflict-ours)' },
|
||||
{ km.theirs, '<Plug>(diffs-conflict-theirs)' },
|
||||
{ km.both, '<Plug>(diffs-conflict-both)' },
|
||||
{ km.none, '<Plug>(diffs-conflict-none)' },
|
||||
{ km.next, '<Plug>(diffs-conflict-next)' },
|
||||
{ km.prev, '<Plug>(diffs-conflict-prev)' },
|
||||
}
|
||||
|
||||
for _, map in ipairs(maps) do
|
||||
if map[1] then
|
||||
vim.keymap.set('n', map[1], map[2], { buffer = bufnr })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
function M.detach(bufnr)
|
||||
vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
|
||||
attached_buffers[bufnr] = nil
|
||||
|
||||
if diagnostics_suppressed[bufnr] then
|
||||
pcall(vim.diagnostic.reset, nil, bufnr)
|
||||
pcall(vim.diagnostic.enable, true, { bufnr = bufnr })
|
||||
diagnostics_suppressed[bufnr] = nil
|
||||
end
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param config diffs.ConflictConfig
|
||||
function M.attach(bufnr, config)
|
||||
if attached_buffers[bufnr] then
|
||||
return
|
||||
end
|
||||
|
||||
local buftype = vim.api.nvim_get_option_value('buftype', { buf = bufnr })
|
||||
if buftype ~= '' then
|
||||
return
|
||||
end
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
local has_marker = false
|
||||
for _, line in ipairs(lines) do
|
||||
if line:match('^<<<<<<<') then
|
||||
has_marker = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not has_marker then
|
||||
return
|
||||
end
|
||||
|
||||
attached_buffers[bufnr] = true
|
||||
|
||||
local regions = M.parse(lines)
|
||||
apply_highlights(bufnr, regions, config)
|
||||
setup_keymaps(bufnr, config)
|
||||
|
||||
if config.disable_diagnostics then
|
||||
pcall(vim.diagnostic.enable, false, { bufnr = bufnr })
|
||||
diagnostics_suppressed[bufnr] = true
|
||||
end
|
||||
|
||||
vim.api.nvim_create_autocmd({ 'TextChanged', 'TextChangedI' }, {
|
||||
buffer = bufnr,
|
||||
callback = function()
|
||||
if not attached_buffers[bufnr] then
|
||||
return true
|
||||
end
|
||||
refresh(bufnr, config)
|
||||
end,
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd('BufWipeout', {
|
||||
buffer = bufnr,
|
||||
callback = function()
|
||||
attached_buffers[bufnr] = nil
|
||||
diagnostics_suppressed[bufnr] = nil
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
---@return integer
|
||||
function M.get_namespace()
|
||||
return ns
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -3,33 +3,6 @@ local M = {}
|
|||
local dbg = require('diffs.log').dbg
|
||||
local diff = require('diffs.diff')
|
||||
|
||||
---@param filepath string
|
||||
---@param from_line integer
|
||||
---@param count integer
|
||||
---@return string[]
|
||||
local function read_line_range(filepath, from_line, count)
|
||||
if count <= 0 then
|
||||
return {}
|
||||
end
|
||||
local f = io.open(filepath, 'r')
|
||||
if not f then
|
||||
return {}
|
||||
end
|
||||
local result = {}
|
||||
local line_num = 0
|
||||
for line in f:lines() do
|
||||
line_num = line_num + 1
|
||||
if line_num >= from_line then
|
||||
table.insert(result, line)
|
||||
if #result >= count then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
f:close()
|
||||
return result
|
||||
end
|
||||
|
||||
local PRIORITY_CLEAR = 198
|
||||
local PRIORITY_SYNTAX = 199
|
||||
local PRIORITY_LINE_BG = 200
|
||||
|
|
@ -41,15 +14,9 @@ local PRIORITY_CHAR_BG = 201
|
|||
---@param col_offset integer
|
||||
---@param text string
|
||||
---@param lang string
|
||||
---@param context_lines? string[]
|
||||
---@return integer
|
||||
local function highlight_text(bufnr, ns, hunk, col_offset, text, lang, context_lines)
|
||||
local parse_text = text
|
||||
if context_lines and #context_lines > 0 then
|
||||
parse_text = text .. '\n' .. table.concat(context_lines, '\n')
|
||||
end
|
||||
|
||||
local ok, parser_obj = pcall(vim.treesitter.get_string_parser, parse_text, lang)
|
||||
local function highlight_text(bufnr, ns, hunk, col_offset, text, lang)
|
||||
local ok, parser_obj = pcall(vim.treesitter.get_string_parser, text, lang)
|
||||
if not ok or not parser_obj then
|
||||
return 0
|
||||
end
|
||||
|
|
@ -67,26 +34,24 @@ local function highlight_text(bufnr, ns, hunk, col_offset, text, lang, context_l
|
|||
local extmark_count = 0
|
||||
local header_line = hunk.start_line - 1
|
||||
|
||||
for id, node, metadata in query:iter_captures(trees[1]:root(), parse_text) do
|
||||
local sr, sc, _, ec = node:range()
|
||||
if sr == 0 then
|
||||
local capture_name = '@' .. query.captures[id] .. '.' .. lang
|
||||
for id, node, metadata in query:iter_captures(trees[1]:root(), text) do
|
||||
local capture_name = '@' .. query.captures[id] .. '.' .. lang
|
||||
local sr, sc, er, ec = node:range()
|
||||
|
||||
local buf_sr = header_line
|
||||
local buf_er = header_line
|
||||
local buf_sc = col_offset + sc
|
||||
local buf_ec = col_offset + ec
|
||||
local buf_sr = header_line + sr
|
||||
local buf_er = header_line + er
|
||||
local buf_sc = col_offset + sc
|
||||
local buf_ec = col_offset + ec
|
||||
|
||||
local priority = lang == 'diff' and (tonumber(metadata.priority) or 100) or PRIORITY_SYNTAX
|
||||
local priority = lang == 'diff' and (tonumber(metadata.priority) or 100) or PRIORITY_SYNTAX
|
||||
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_sr, buf_sc, {
|
||||
end_row = buf_er,
|
||||
end_col = buf_ec,
|
||||
hl_group = capture_name,
|
||||
priority = priority,
|
||||
})
|
||||
extmark_count = extmark_count + 1
|
||||
end
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_sr, buf_sc, {
|
||||
end_row = buf_er,
|
||||
end_col = buf_ec,
|
||||
hl_group = capture_name,
|
||||
priority = priority,
|
||||
})
|
||||
extmark_count = extmark_count + 1
|
||||
end
|
||||
|
||||
return extmark_count
|
||||
|
|
@ -124,50 +89,44 @@ local function highlight_treesitter(
|
|||
return 0
|
||||
end
|
||||
|
||||
local trees = parser_obj:parse(true)
|
||||
local trees = parser_obj:parse()
|
||||
if not trees or #trees == 0 then
|
||||
dbg('parse returned no trees for lang: %s', lang)
|
||||
return 0
|
||||
end
|
||||
|
||||
local query = vim.treesitter.query.get(lang, 'highlights')
|
||||
if not query then
|
||||
dbg('no highlights query for lang: %s', lang)
|
||||
return 0
|
||||
end
|
||||
|
||||
local extmark_count = 0
|
||||
parser_obj:for_each_tree(function(tree, ltree)
|
||||
local tree_lang = ltree:lang()
|
||||
local query = vim.treesitter.query.get(tree_lang, 'highlights')
|
||||
if not query then
|
||||
return
|
||||
end
|
||||
for id, node, metadata in query:iter_captures(trees[1]:root(), code) do
|
||||
local capture_name = '@' .. query.captures[id] .. '.' .. lang
|
||||
local sr, sc, er, ec = node:range()
|
||||
|
||||
for id, node, metadata in query:iter_captures(tree:root(), code) do
|
||||
local capture = query.captures[id]
|
||||
if capture ~= 'spell' and capture ~= 'nospell' then
|
||||
local capture_name = '@' .. capture .. '.' .. tree_lang
|
||||
local sr, sc, er, ec = node:range()
|
||||
local buf_sr = line_map[sr]
|
||||
if buf_sr then
|
||||
local buf_er = line_map[er] or buf_sr
|
||||
|
||||
local buf_sr = line_map[sr]
|
||||
if buf_sr then
|
||||
local buf_er = line_map[er] or buf_sr
|
||||
local buf_sc = sc + col_offset
|
||||
local buf_ec = ec + col_offset
|
||||
|
||||
local buf_sc = sc + col_offset
|
||||
local buf_ec = ec + col_offset
|
||||
local priority = lang == 'diff' and (tonumber(metadata.priority) or 100) or PRIORITY_SYNTAX
|
||||
|
||||
local priority = tree_lang == 'diff' and (tonumber(metadata.priority) or 100)
|
||||
or PRIORITY_SYNTAX
|
||||
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_sr, buf_sc, {
|
||||
end_row = buf_er,
|
||||
end_col = buf_ec,
|
||||
hl_group = capture_name,
|
||||
priority = priority,
|
||||
})
|
||||
extmark_count = extmark_count + 1
|
||||
if covered_lines then
|
||||
covered_lines[buf_sr] = true
|
||||
end
|
||||
end
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_sr, buf_sc, {
|
||||
end_row = buf_er,
|
||||
end_col = buf_ec,
|
||||
hl_group = capture_name,
|
||||
priority = priority,
|
||||
})
|
||||
extmark_count = extmark_count + 1
|
||||
if covered_lines then
|
||||
covered_lines[buf_sr] = true
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return extmark_count
|
||||
end
|
||||
|
|
@ -218,9 +177,8 @@ end
|
|||
---@param hunk diffs.Hunk
|
||||
---@param code_lines string[]
|
||||
---@param covered_lines? table<integer, true>
|
||||
---@param leading_offset? integer
|
||||
---@return integer
|
||||
local function highlight_vim_syntax(bufnr, ns, hunk, code_lines, covered_lines, leading_offset)
|
||||
local function highlight_vim_syntax(bufnr, ns, hunk, code_lines, covered_lines)
|
||||
local ft = hunk.ft
|
||||
if not ft then
|
||||
return 0
|
||||
|
|
@ -230,8 +188,6 @@ local function highlight_vim_syntax(bufnr, ns, hunk, code_lines, covered_lines,
|
|||
return 0
|
||||
end
|
||||
|
||||
leading_offset = leading_offset or 0
|
||||
|
||||
local scratch = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_lines(scratch, 0, -1, false, code_lines)
|
||||
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = scratch })
|
||||
|
|
@ -258,21 +214,17 @@ local function highlight_vim_syntax(bufnr, ns, hunk, code_lines, covered_lines,
|
|||
|
||||
vim.api.nvim_buf_delete(scratch, { force = true })
|
||||
|
||||
local hunk_line_count = #hunk.lines
|
||||
local extmark_count = 0
|
||||
for _, span in ipairs(spans) do
|
||||
local adj = span.line - leading_offset
|
||||
if adj >= 1 and adj <= hunk_line_count then
|
||||
local buf_line = hunk.start_line + adj - 1
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, span.col_start, {
|
||||
end_col = span.col_end,
|
||||
hl_group = span.hl_name,
|
||||
priority = PRIORITY_SYNTAX,
|
||||
})
|
||||
extmark_count = extmark_count + 1
|
||||
if covered_lines then
|
||||
covered_lines[buf_line] = true
|
||||
end
|
||||
local buf_line = hunk.start_line + span.line - 1
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, span.col_start, {
|
||||
end_col = span.col_end,
|
||||
hl_group = span.hl_name,
|
||||
priority = PRIORITY_SYNTAX,
|
||||
})
|
||||
extmark_count = extmark_count + 1
|
||||
if covered_lines then
|
||||
covered_lines[buf_line] = true
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -303,21 +255,6 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
|
|||
---@type table<integer, true>
|
||||
local covered_lines = {}
|
||||
|
||||
local ctx_cfg = opts.highlights.context
|
||||
local context = (ctx_cfg and ctx_cfg.enabled) and ctx_cfg.lines or 0
|
||||
local leading = {}
|
||||
local trailing = {}
|
||||
if (use_ts or use_vim) and context > 0 and hunk.file_new_start and hunk.repo_root then
|
||||
local filepath = vim.fs.joinpath(hunk.repo_root, hunk.filename)
|
||||
local lead_from = math.max(1, hunk.file_new_start - context)
|
||||
local lead_count = hunk.file_new_start - lead_from
|
||||
if lead_count > 0 then
|
||||
leading = read_line_range(filepath, lead_from, lead_count)
|
||||
end
|
||||
local trail_from = hunk.file_new_start + (hunk.file_new_count or 0)
|
||||
trailing = read_line_range(filepath, trail_from, context)
|
||||
end
|
||||
|
||||
local extmark_count = 0
|
||||
if use_ts then
|
||||
---@type string[]
|
||||
|
|
@ -329,11 +266,6 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
|
|||
---@type table<integer, integer>
|
||||
local old_map = {}
|
||||
|
||||
for _, pad_line in ipairs(leading) do
|
||||
table.insert(new_code, pad_line)
|
||||
table.insert(old_code, pad_line)
|
||||
end
|
||||
|
||||
for i, line in ipairs(hunk.lines) do
|
||||
local prefix = line:sub(1, 1)
|
||||
local stripped = line:sub(2)
|
||||
|
|
@ -352,11 +284,6 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
|
|||
end
|
||||
end
|
||||
|
||||
for _, pad_line in ipairs(trailing) do
|
||||
table.insert(new_code, pad_line)
|
||||
table.insert(old_code, pad_line)
|
||||
end
|
||||
|
||||
extmark_count = highlight_treesitter(bufnr, ns, new_code, hunk.lang, new_map, 1, covered_lines)
|
||||
extmark_count = extmark_count
|
||||
+ highlight_treesitter(bufnr, ns, old_code, hunk.lang, old_map, 1, covered_lines)
|
||||
|
|
@ -368,15 +295,8 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
|
|||
hl_group = 'DiffsClear',
|
||||
priority = PRIORITY_CLEAR,
|
||||
})
|
||||
local header_extmarks = highlight_text(
|
||||
bufnr,
|
||||
ns,
|
||||
hunk,
|
||||
hunk.header_context_col,
|
||||
hunk.header_context,
|
||||
hunk.lang,
|
||||
new_code
|
||||
)
|
||||
local header_extmarks =
|
||||
highlight_text(bufnr, ns, hunk, hunk.header_context_col, hunk.header_context, hunk.lang)
|
||||
if header_extmarks > 0 then
|
||||
dbg('header %s:%d applied %d extmarks', hunk.filename, hunk.start_line, header_extmarks)
|
||||
end
|
||||
|
|
@ -385,16 +305,10 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
|
|||
elseif use_vim then
|
||||
---@type string[]
|
||||
local code_lines = {}
|
||||
for _, pad_line in ipairs(leading) do
|
||||
table.insert(code_lines, pad_line)
|
||||
end
|
||||
for _, line in ipairs(hunk.lines) do
|
||||
table.insert(code_lines, line:sub(2))
|
||||
end
|
||||
for _, pad_line in ipairs(trailing) do
|
||||
table.insert(code_lines, pad_line)
|
||||
end
|
||||
extmark_count = highlight_vim_syntax(bufnr, ns, hunk, code_lines, covered_lines, #leading)
|
||||
extmark_count = highlight_vim_syntax(bufnr, ns, hunk, code_lines, covered_lines)
|
||||
end
|
||||
|
||||
if
|
||||
|
|
@ -473,17 +387,12 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
|
|||
|
||||
if opts.highlights.background and is_diff_line then
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, {
|
||||
end_row = buf_line + 1,
|
||||
end_col = line_len,
|
||||
hl_group = line_hl,
|
||||
hl_eol = true,
|
||||
number_hl_group = opts.highlights.gutter and number_hl or nil,
|
||||
priority = PRIORITY_LINE_BG,
|
||||
})
|
||||
if opts.highlights.gutter then
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, {
|
||||
number_hl_group = number_hl,
|
||||
priority = PRIORITY_LINE_BG,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
if char_spans_by_line[i] then
|
||||
|
|
|
|||
|
|
@ -11,16 +11,9 @@
|
|||
---@field algorithm string
|
||||
---@field max_lines integer
|
||||
|
||||
---@class diffs.ContextConfig
|
||||
---@field enabled boolean
|
||||
---@field lines integer
|
||||
|
||||
---@class diffs.Highlights
|
||||
---@field background boolean
|
||||
---@field gutter boolean
|
||||
---@field blend_alpha? number
|
||||
---@field overrides? table<string, table>
|
||||
---@field context diffs.ContextConfig
|
||||
---@field treesitter diffs.TreesitterConfig
|
||||
---@field vim diffs.VimConfig
|
||||
---@field intra diffs.IntraConfig
|
||||
|
|
@ -29,27 +22,12 @@
|
|||
---@field horizontal string|false
|
||||
---@field vertical string|false
|
||||
|
||||
---@class diffs.ConflictKeymaps
|
||||
---@field ours string|false
|
||||
---@field theirs string|false
|
||||
---@field both string|false
|
||||
---@field none string|false
|
||||
---@field next string|false
|
||||
---@field prev string|false
|
||||
|
||||
---@class diffs.ConflictConfig
|
||||
---@field enabled boolean
|
||||
---@field disable_diagnostics boolean
|
||||
---@field show_virtual_text boolean
|
||||
---@field keymaps diffs.ConflictKeymaps
|
||||
|
||||
---@class diffs.Config
|
||||
---@field debug boolean
|
||||
---@field debounce_ms integer
|
||||
---@field hide_prefix boolean
|
||||
---@field highlights diffs.Highlights
|
||||
---@field fugitive diffs.FugitiveConfig
|
||||
---@field conflict diffs.ConflictConfig
|
||||
|
||||
---@class diffs
|
||||
---@field attach fun(bufnr?: integer)
|
||||
|
|
@ -102,10 +80,6 @@ local default_config = {
|
|||
highlights = {
|
||||
background = true,
|
||||
gutter = true,
|
||||
context = {
|
||||
enabled = true,
|
||||
lines = 25,
|
||||
},
|
||||
treesitter = {
|
||||
enabled = true,
|
||||
max_lines = 500,
|
||||
|
|
@ -124,19 +98,6 @@ local default_config = {
|
|||
horizontal = 'du',
|
||||
vertical = 'dU',
|
||||
},
|
||||
conflict = {
|
||||
enabled = true,
|
||||
disable_diagnostics = true,
|
||||
show_virtual_text = true,
|
||||
keymaps = {
|
||||
ours = 'doo',
|
||||
theirs = 'dot',
|
||||
both = 'dob',
|
||||
none = 'don',
|
||||
next = ']x',
|
||||
prev = '[x',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
---@type diffs.Config
|
||||
|
|
@ -222,19 +183,14 @@ local function compute_highlight_groups()
|
|||
local blended_add = blend_color(add_bg, bg, 0.4)
|
||||
local blended_del = blend_color(del_bg, bg, 0.4)
|
||||
|
||||
local alpha = config.highlights.blend_alpha or 0.6
|
||||
local blended_add_text = blend_color(add_fg, bg, alpha)
|
||||
local blended_del_text = blend_color(del_fg, bg, alpha)
|
||||
local blended_add_text = blend_color(add_fg, bg, 0.7)
|
||||
local blended_del_text = blend_color(del_fg, bg, 0.7)
|
||||
|
||||
vim.api.nvim_set_hl(0, 'DiffsClear', { default = true, fg = normal.fg or 0xc0c0c0 })
|
||||
vim.api.nvim_set_hl(0, 'DiffsAdd', { default = true, bg = blended_add })
|
||||
vim.api.nvim_set_hl(0, 'DiffsDelete', { default = true, bg = blended_del })
|
||||
vim.api.nvim_set_hl(0, 'DiffsAddNr', { default = true, fg = blended_add_text, bg = blended_add })
|
||||
vim.api.nvim_set_hl(
|
||||
0,
|
||||
'DiffsDeleteNr',
|
||||
{ default = true, fg = blended_del_text, bg = blended_del }
|
||||
)
|
||||
vim.api.nvim_set_hl(0, 'DiffsAddNr', { default = true, fg = add_fg, bg = blended_add })
|
||||
vim.api.nvim_set_hl(0, 'DiffsDeleteNr', { default = true, fg = del_fg, bg = blended_del })
|
||||
vim.api.nvim_set_hl(0, 'DiffsAddText', { default = true, bg = blended_add_text })
|
||||
vim.api.nvim_set_hl(0, 'DiffsDeleteText', { default = true, bg = blended_del_text })
|
||||
|
||||
|
|
@ -250,51 +206,10 @@ local function compute_highlight_groups()
|
|||
local diff_change = resolve_hl('DiffChange')
|
||||
local diff_text = resolve_hl('DiffText')
|
||||
|
||||
vim.api.nvim_set_hl(0, 'DiffsDiffAdd', { default = true, bg = diff_add.bg })
|
||||
vim.api.nvim_set_hl(
|
||||
0,
|
||||
'DiffsDiffDelete',
|
||||
{ default = true, fg = diff_delete.fg, bg = diff_delete.bg }
|
||||
)
|
||||
vim.api.nvim_set_hl(0, 'DiffsDiffChange', { default = true, bg = diff_change.bg })
|
||||
vim.api.nvim_set_hl(0, 'DiffsDiffText', { default = true, bg = diff_text.bg })
|
||||
|
||||
local change_bg = diff_change.bg or 0x3a3a4a
|
||||
local text_bg = diff_text.bg or 0x4a4a5a
|
||||
local change_fg = diff_change.fg or diff_text.fg or 0x80a0c0
|
||||
|
||||
local blended_ours = blend_color(add_bg, bg, 0.4)
|
||||
local blended_theirs = blend_color(change_bg, bg, 0.4)
|
||||
local blended_base = blend_color(text_bg, bg, 0.3)
|
||||
local blended_ours_nr = blend_color(add_fg, bg, alpha)
|
||||
local blended_theirs_nr = blend_color(change_fg, bg, alpha)
|
||||
local blended_base_nr = blend_color(change_fg, bg, 0.4)
|
||||
|
||||
vim.api.nvim_set_hl(0, 'DiffsConflictOurs', { default = true, bg = blended_ours })
|
||||
vim.api.nvim_set_hl(0, 'DiffsConflictTheirs', { default = true, bg = blended_theirs })
|
||||
vim.api.nvim_set_hl(0, 'DiffsConflictBase', { default = true, bg = blended_base })
|
||||
vim.api.nvim_set_hl(0, 'DiffsConflictMarker', { default = true, fg = 0x808080, bold = true })
|
||||
vim.api.nvim_set_hl(
|
||||
0,
|
||||
'DiffsConflictOursNr',
|
||||
{ default = true, fg = blended_ours_nr, bg = blended_ours }
|
||||
)
|
||||
vim.api.nvim_set_hl(
|
||||
0,
|
||||
'DiffsConflictTheirsNr',
|
||||
{ default = true, fg = blended_theirs_nr, bg = blended_theirs }
|
||||
)
|
||||
vim.api.nvim_set_hl(
|
||||
0,
|
||||
'DiffsConflictBaseNr',
|
||||
{ default = true, fg = blended_base_nr, bg = blended_base }
|
||||
)
|
||||
|
||||
if config.highlights.overrides then
|
||||
for group, hl in pairs(config.highlights.overrides) do
|
||||
vim.api.nvim_set_hl(0, group, hl)
|
||||
end
|
||||
end
|
||||
vim.api.nvim_set_hl(0, 'DiffsDiffAdd', { bg = diff_add.bg })
|
||||
vim.api.nvim_set_hl(0, 'DiffsDiffDelete', { fg = diff_delete.fg, bg = diff_delete.bg })
|
||||
vim.api.nvim_set_hl(0, 'DiffsDiffChange', { bg = diff_change.bg })
|
||||
vim.api.nvim_set_hl(0, 'DiffsDiffText', { bg = diff_text.bg })
|
||||
end
|
||||
|
||||
local function init()
|
||||
|
|
@ -316,21 +231,11 @@ local function init()
|
|||
vim.validate({
|
||||
['highlights.background'] = { opts.highlights.background, 'boolean', true },
|
||||
['highlights.gutter'] = { opts.highlights.gutter, 'boolean', true },
|
||||
['highlights.blend_alpha'] = { opts.highlights.blend_alpha, 'number', true },
|
||||
['highlights.overrides'] = { opts.highlights.overrides, 'table', true },
|
||||
['highlights.context'] = { opts.highlights.context, 'table', true },
|
||||
['highlights.treesitter'] = { opts.highlights.treesitter, 'table', true },
|
||||
['highlights.vim'] = { opts.highlights.vim, 'table', true },
|
||||
['highlights.intra'] = { opts.highlights.intra, 'table', true },
|
||||
})
|
||||
|
||||
if opts.highlights.context then
|
||||
vim.validate({
|
||||
['highlights.context.enabled'] = { opts.highlights.context.enabled, 'boolean', true },
|
||||
['highlights.context.lines'] = { opts.highlights.context.lines, 'number', true },
|
||||
})
|
||||
end
|
||||
|
||||
if opts.highlights.treesitter then
|
||||
vim.validate({
|
||||
['highlights.treesitter.enabled'] = { opts.highlights.treesitter.enabled, 'boolean', true },
|
||||
|
|
@ -383,41 +288,9 @@ local function init()
|
|||
})
|
||||
end
|
||||
|
||||
if opts.conflict then
|
||||
vim.validate({
|
||||
['conflict.enabled'] = { opts.conflict.enabled, 'boolean', true },
|
||||
['conflict.disable_diagnostics'] = { opts.conflict.disable_diagnostics, 'boolean', true },
|
||||
['conflict.show_virtual_text'] = { opts.conflict.show_virtual_text, 'boolean', true },
|
||||
['conflict.keymaps'] = { opts.conflict.keymaps, 'table', true },
|
||||
})
|
||||
|
||||
if opts.conflict.keymaps then
|
||||
local keymap_validator = function(v)
|
||||
return v == false or type(v) == 'string'
|
||||
end
|
||||
for _, key in ipairs({ 'ours', 'theirs', 'both', 'none', 'next', 'prev' }) do
|
||||
vim.validate({
|
||||
['conflict.keymaps.' .. key] = {
|
||||
opts.conflict.keymaps[key],
|
||||
keymap_validator,
|
||||
'string or false',
|
||||
},
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if opts.debounce_ms and opts.debounce_ms < 0 then
|
||||
error('diffs: debounce_ms must be >= 0')
|
||||
end
|
||||
if
|
||||
opts.highlights
|
||||
and opts.highlights.context
|
||||
and opts.highlights.context.lines
|
||||
and opts.highlights.context.lines < 0
|
||||
then
|
||||
error('diffs: highlights.context.lines must be >= 0')
|
||||
end
|
||||
if
|
||||
opts.highlights
|
||||
and opts.highlights.treesitter
|
||||
|
|
@ -442,13 +315,6 @@ local function init()
|
|||
then
|
||||
error('diffs: highlights.intra.max_lines must be >= 1')
|
||||
end
|
||||
if
|
||||
opts.highlights
|
||||
and opts.highlights.blend_alpha
|
||||
and (opts.highlights.blend_alpha < 0 or opts.highlights.blend_alpha > 1)
|
||||
then
|
||||
error('diffs: highlights.blend_alpha must be >= 0 and <= 1')
|
||||
end
|
||||
|
||||
config = vim.tbl_deep_extend('force', default_config, opts)
|
||||
log.set_enabled(config.debug)
|
||||
|
|
@ -571,10 +437,4 @@ function M.get_fugitive_config()
|
|||
return config.fugitive
|
||||
end
|
||||
|
||||
---@return diffs.ConflictConfig
|
||||
function M.get_conflict_config()
|
||||
init()
|
||||
return config.conflict
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -8,11 +8,6 @@
|
|||
---@field lines string[]
|
||||
---@field header_start_line integer?
|
||||
---@field header_lines string[]?
|
||||
---@field file_old_start integer?
|
||||
---@field file_old_count integer?
|
||||
---@field file_new_start integer?
|
||||
---@field file_new_count integer?
|
||||
---@field repo_root string?
|
||||
|
||||
local M = {}
|
||||
|
||||
|
|
@ -137,14 +132,6 @@ function M.parse_buffer(bufnr)
|
|||
local header_start = nil
|
||||
---@type string[]
|
||||
local header_lines = {}
|
||||
---@type integer?
|
||||
local file_old_start = nil
|
||||
---@type integer?
|
||||
local file_old_count = nil
|
||||
---@type integer?
|
||||
local file_new_start = nil
|
||||
---@type integer?
|
||||
local file_new_count = nil
|
||||
|
||||
local function flush_hunk()
|
||||
if hunk_start and #hunk_lines > 0 then
|
||||
|
|
@ -156,11 +143,6 @@ function M.parse_buffer(bufnr)
|
|||
header_context = hunk_header_context,
|
||||
header_context_col = hunk_header_context_col,
|
||||
lines = hunk_lines,
|
||||
file_old_start = file_old_start,
|
||||
file_old_count = file_old_count,
|
||||
file_new_start = file_new_start,
|
||||
file_new_count = file_new_count,
|
||||
repo_root = repo_root,
|
||||
}
|
||||
if hunk_count == 1 and header_start and #header_lines > 0 then
|
||||
hunk.header_start_line = header_start
|
||||
|
|
@ -172,10 +154,6 @@ function M.parse_buffer(bufnr)
|
|||
hunk_header_context = nil
|
||||
hunk_header_context_col = nil
|
||||
hunk_lines = {}
|
||||
file_old_start = nil
|
||||
file_old_count = nil
|
||||
file_new_start = nil
|
||||
file_new_count = nil
|
||||
end
|
||||
|
||||
for i, line in ipairs(lines) do
|
||||
|
|
@ -196,13 +174,6 @@ function M.parse_buffer(bufnr)
|
|||
elseif line:match('^@@.-@@') then
|
||||
flush_hunk()
|
||||
hunk_start = i
|
||||
local hs, hc, hs2, hc2 = line:match('^@@ %-(%d+),?(%d*) %+(%d+),?(%d*) @@')
|
||||
if hs then
|
||||
file_old_start = tonumber(hs)
|
||||
file_old_count = tonumber(hc) or 1
|
||||
file_new_start = tonumber(hs2)
|
||||
file_new_count = tonumber(hc2) or 1
|
||||
end
|
||||
local prefix, context = line:match('^(@@.-@@%s*)(.*)')
|
||||
if context and context ~= '' then
|
||||
hunk_header_context = context
|
||||
|
|
|
|||
|
|
@ -30,15 +30,6 @@ vim.api.nvim_create_autocmd('BufReadCmd', {
|
|||
end,
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd('BufReadPost', {
|
||||
callback = function(args)
|
||||
local conflict_config = require('diffs').get_conflict_config()
|
||||
if conflict_config.enabled then
|
||||
require('diffs.conflict').attach(args.buf, conflict_config)
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd('OptionSet', {
|
||||
pattern = 'diff',
|
||||
callback = function()
|
||||
|
|
@ -49,36 +40,3 @@ vim.api.nvim_create_autocmd('OptionSet', {
|
|||
end
|
||||
end,
|
||||
})
|
||||
|
||||
local cmds = require('diffs.commands')
|
||||
vim.keymap.set('n', '<Plug>(diffs-gdiff)', function()
|
||||
cmds.gdiff(nil, false)
|
||||
end, { desc = 'Unified diff (horizontal)' })
|
||||
vim.keymap.set('n', '<Plug>(diffs-gvdiff)', function()
|
||||
cmds.gdiff(nil, true)
|
||||
end, { desc = 'Unified diff (vertical)' })
|
||||
|
||||
local function conflict_action(fn)
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local config = require('diffs').get_conflict_config()
|
||||
fn(bufnr, config)
|
||||
end
|
||||
|
||||
vim.keymap.set('n', '<Plug>(diffs-conflict-ours)', function()
|
||||
conflict_action(require('diffs.conflict').resolve_ours)
|
||||
end, { desc = 'Accept current (ours) change' })
|
||||
vim.keymap.set('n', '<Plug>(diffs-conflict-theirs)', function()
|
||||
conflict_action(require('diffs.conflict').resolve_theirs)
|
||||
end, { desc = 'Accept incoming (theirs) change' })
|
||||
vim.keymap.set('n', '<Plug>(diffs-conflict-both)', function()
|
||||
conflict_action(require('diffs.conflict').resolve_both)
|
||||
end, { desc = 'Accept both changes' })
|
||||
vim.keymap.set('n', '<Plug>(diffs-conflict-none)', function()
|
||||
conflict_action(require('diffs.conflict').resolve_none)
|
||||
end, { desc = 'Reject both changes' })
|
||||
vim.keymap.set('n', '<Plug>(diffs-conflict-next)', function()
|
||||
require('diffs.conflict').goto_next(vim.api.nvim_get_current_buf())
|
||||
end, { desc = 'Jump to next conflict' })
|
||||
vim.keymap.set('n', '<Plug>(diffs-conflict-prev)', function()
|
||||
require('diffs.conflict').goto_prev(vim.api.nvim_get_current_buf())
|
||||
end, { desc = 'Jump to previous conflict' })
|
||||
|
|
|
|||
|
|
@ -1,688 +0,0 @@
|
|||
local conflict = require('diffs.conflict')
|
||||
local helpers = require('spec.helpers')
|
||||
|
||||
local function default_config(overrides)
|
||||
local cfg = {
|
||||
enabled = true,
|
||||
disable_diagnostics = false,
|
||||
show_virtual_text = true,
|
||||
keymaps = {
|
||||
ours = 'doo',
|
||||
theirs = 'dot',
|
||||
both = 'dob',
|
||||
none = 'don',
|
||||
next = ']x',
|
||||
prev = '[x',
|
||||
},
|
||||
}
|
||||
if overrides then
|
||||
cfg = vim.tbl_deep_extend('force', cfg, overrides)
|
||||
end
|
||||
return cfg
|
||||
end
|
||||
|
||||
local function create_file_buffer(lines)
|
||||
local bufnr = vim.api.nvim_create_buf(false, false)
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines or {})
|
||||
return bufnr
|
||||
end
|
||||
|
||||
local function get_extmarks(bufnr)
|
||||
return vim.api.nvim_buf_get_extmarks(bufnr, conflict.get_namespace(), 0, -1, { details = true })
|
||||
end
|
||||
|
||||
describe('conflict', function()
|
||||
describe('parse', function()
|
||||
it('parses a single conflict', function()
|
||||
local lines = {
|
||||
'<<<<<<< HEAD',
|
||||
'local x = 1',
|
||||
'=======',
|
||||
'local x = 2',
|
||||
'>>>>>>> feature',
|
||||
}
|
||||
local regions = conflict.parse(lines)
|
||||
assert.are.equal(1, #regions)
|
||||
assert.are.equal(0, regions[1].marker_ours)
|
||||
assert.are.equal(1, regions[1].ours_start)
|
||||
assert.are.equal(2, regions[1].ours_end)
|
||||
assert.are.equal(2, regions[1].marker_sep)
|
||||
assert.are.equal(3, regions[1].theirs_start)
|
||||
assert.are.equal(4, regions[1].theirs_end)
|
||||
assert.are.equal(4, regions[1].marker_theirs)
|
||||
end)
|
||||
|
||||
it('parses multiple conflicts', function()
|
||||
local lines = {
|
||||
'<<<<<<< HEAD',
|
||||
'a',
|
||||
'=======',
|
||||
'b',
|
||||
'>>>>>>> feat',
|
||||
'normal line',
|
||||
'<<<<<<< HEAD',
|
||||
'c',
|
||||
'=======',
|
||||
'd',
|
||||
'>>>>>>> feat',
|
||||
}
|
||||
local regions = conflict.parse(lines)
|
||||
assert.are.equal(2, #regions)
|
||||
assert.are.equal(0, regions[1].marker_ours)
|
||||
assert.are.equal(6, regions[2].marker_ours)
|
||||
end)
|
||||
|
||||
it('parses diff3 format', function()
|
||||
local lines = {
|
||||
'<<<<<<< HEAD',
|
||||
'local x = 1',
|
||||
'||||||| base',
|
||||
'local x = 0',
|
||||
'=======',
|
||||
'local x = 2',
|
||||
'>>>>>>> feature',
|
||||
}
|
||||
local regions = conflict.parse(lines)
|
||||
assert.are.equal(1, #regions)
|
||||
assert.are.equal(2, regions[1].marker_base)
|
||||
assert.are.equal(3, regions[1].base_start)
|
||||
assert.are.equal(4, regions[1].base_end)
|
||||
end)
|
||||
|
||||
it('handles empty ours section', function()
|
||||
local lines = {
|
||||
'<<<<<<< HEAD',
|
||||
'=======',
|
||||
'local x = 2',
|
||||
'>>>>>>> feature',
|
||||
}
|
||||
local regions = conflict.parse(lines)
|
||||
assert.are.equal(1, #regions)
|
||||
assert.are.equal(1, regions[1].ours_start)
|
||||
assert.are.equal(1, regions[1].ours_end)
|
||||
end)
|
||||
|
||||
it('handles empty theirs section', function()
|
||||
local lines = {
|
||||
'<<<<<<< HEAD',
|
||||
'local x = 1',
|
||||
'=======',
|
||||
'>>>>>>> feature',
|
||||
}
|
||||
local regions = conflict.parse(lines)
|
||||
assert.are.equal(1, #regions)
|
||||
assert.are.equal(3, regions[1].theirs_start)
|
||||
assert.are.equal(3, regions[1].theirs_end)
|
||||
end)
|
||||
|
||||
it('returns empty for no markers', function()
|
||||
local lines = { 'local x = 1', 'local y = 2' }
|
||||
local regions = conflict.parse(lines)
|
||||
assert.are.equal(0, #regions)
|
||||
end)
|
||||
|
||||
it('discards malformed markers (no separator)', function()
|
||||
local lines = {
|
||||
'<<<<<<< HEAD',
|
||||
'local x = 1',
|
||||
'>>>>>>> feature',
|
||||
}
|
||||
local regions = conflict.parse(lines)
|
||||
assert.are.equal(0, #regions)
|
||||
end)
|
||||
|
||||
it('discards malformed markers (no end)', function()
|
||||
local lines = {
|
||||
'<<<<<<< HEAD',
|
||||
'local x = 1',
|
||||
'=======',
|
||||
'local x = 2',
|
||||
}
|
||||
local regions = conflict.parse(lines)
|
||||
assert.are.equal(0, #regions)
|
||||
end)
|
||||
|
||||
it('handles trailing text on marker lines', function()
|
||||
local lines = {
|
||||
'<<<<<<< HEAD (some text)',
|
||||
'local x = 1',
|
||||
'======= extra',
|
||||
'local x = 2',
|
||||
'>>>>>>> feature-branch/some-thing',
|
||||
}
|
||||
local regions = conflict.parse(lines)
|
||||
assert.are.equal(1, #regions)
|
||||
end)
|
||||
|
||||
it('handles empty base in diff3', function()
|
||||
local lines = {
|
||||
'<<<<<<< HEAD',
|
||||
'local x = 1',
|
||||
'||||||| base',
|
||||
'=======',
|
||||
'local x = 2',
|
||||
'>>>>>>> feature',
|
||||
}
|
||||
local regions = conflict.parse(lines)
|
||||
assert.are.equal(1, #regions)
|
||||
assert.are.equal(3, regions[1].base_start)
|
||||
assert.are.equal(3, regions[1].base_end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('highlighting', function()
|
||||
after_each(function()
|
||||
conflict.detach(vim.api.nvim_get_current_buf())
|
||||
end)
|
||||
|
||||
it('applies extmarks for conflict regions', function()
|
||||
local bufnr = create_file_buffer({
|
||||
'<<<<<<< HEAD',
|
||||
'local x = 1',
|
||||
'=======',
|
||||
'local x = 2',
|
||||
'>>>>>>> feature',
|
||||
})
|
||||
|
||||
conflict.attach(bufnr, default_config())
|
||||
|
||||
local extmarks = get_extmarks(bufnr)
|
||||
assert.is_true(#extmarks > 0)
|
||||
|
||||
local has_ours = false
|
||||
local has_theirs = false
|
||||
local has_marker = false
|
||||
for _, mark in ipairs(extmarks) do
|
||||
local hl = mark[4] and mark[4].hl_group
|
||||
if hl == 'DiffsConflictOurs' then
|
||||
has_ours = true
|
||||
end
|
||||
if hl == 'DiffsConflictTheirs' then
|
||||
has_theirs = true
|
||||
end
|
||||
if hl == 'DiffsConflictMarker' then
|
||||
has_marker = true
|
||||
end
|
||||
end
|
||||
assert.is_true(has_ours)
|
||||
assert.is_true(has_theirs)
|
||||
assert.is_true(has_marker)
|
||||
|
||||
helpers.delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('applies virtual text when enabled', function()
|
||||
local bufnr = create_file_buffer({
|
||||
'<<<<<<< HEAD',
|
||||
'local x = 1',
|
||||
'=======',
|
||||
'local x = 2',
|
||||
'>>>>>>> feature',
|
||||
})
|
||||
|
||||
conflict.attach(bufnr, default_config({ show_virtual_text = true }))
|
||||
|
||||
local extmarks = get_extmarks(bufnr)
|
||||
local virt_text_count = 0
|
||||
for _, mark in ipairs(extmarks) do
|
||||
if mark[4] and mark[4].virt_text then
|
||||
virt_text_count = virt_text_count + 1
|
||||
end
|
||||
end
|
||||
assert.are.equal(2, virt_text_count)
|
||||
|
||||
helpers.delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('does not apply virtual text when disabled', function()
|
||||
local bufnr = create_file_buffer({
|
||||
'<<<<<<< HEAD',
|
||||
'local x = 1',
|
||||
'=======',
|
||||
'local x = 2',
|
||||
'>>>>>>> feature',
|
||||
})
|
||||
|
||||
conflict.attach(bufnr, default_config({ show_virtual_text = false }))
|
||||
|
||||
local extmarks = get_extmarks(bufnr)
|
||||
local virt_text_count = 0
|
||||
for _, mark in ipairs(extmarks) do
|
||||
if mark[4] and mark[4].virt_text then
|
||||
virt_text_count = virt_text_count + 1
|
||||
end
|
||||
end
|
||||
assert.are.equal(0, virt_text_count)
|
||||
|
||||
helpers.delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('applies number_hl_group to content lines', function()
|
||||
local bufnr = create_file_buffer({
|
||||
'<<<<<<< HEAD',
|
||||
'local x = 1',
|
||||
'=======',
|
||||
'local x = 2',
|
||||
'>>>>>>> feature',
|
||||
})
|
||||
|
||||
conflict.attach(bufnr, default_config())
|
||||
|
||||
local extmarks = get_extmarks(bufnr)
|
||||
local has_ours_nr = false
|
||||
local has_theirs_nr = false
|
||||
for _, mark in ipairs(extmarks) do
|
||||
local nr = mark[4] and mark[4].number_hl_group
|
||||
if nr == 'DiffsConflictOursNr' then
|
||||
has_ours_nr = true
|
||||
end
|
||||
if nr == 'DiffsConflictTheirsNr' then
|
||||
has_theirs_nr = true
|
||||
end
|
||||
end
|
||||
assert.is_true(has_ours_nr)
|
||||
assert.is_true(has_theirs_nr)
|
||||
|
||||
helpers.delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('highlights base region in diff3', function()
|
||||
local bufnr = create_file_buffer({
|
||||
'<<<<<<< HEAD',
|
||||
'local x = 1',
|
||||
'||||||| base',
|
||||
'local x = 0',
|
||||
'=======',
|
||||
'local x = 2',
|
||||
'>>>>>>> feature',
|
||||
})
|
||||
|
||||
conflict.attach(bufnr, default_config())
|
||||
|
||||
local extmarks = get_extmarks(bufnr)
|
||||
local has_base = false
|
||||
for _, mark in ipairs(extmarks) do
|
||||
if mark[4] and mark[4].hl_group == 'DiffsConflictBase' then
|
||||
has_base = true
|
||||
break
|
||||
end
|
||||
end
|
||||
assert.is_true(has_base)
|
||||
|
||||
helpers.delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('clears extmarks on detach', function()
|
||||
local bufnr = create_file_buffer({
|
||||
'<<<<<<< HEAD',
|
||||
'local x = 1',
|
||||
'=======',
|
||||
'local x = 2',
|
||||
'>>>>>>> feature',
|
||||
})
|
||||
|
||||
conflict.attach(bufnr, default_config())
|
||||
assert.is_true(#get_extmarks(bufnr) > 0)
|
||||
|
||||
conflict.detach(bufnr)
|
||||
assert.are.equal(0, #get_extmarks(bufnr))
|
||||
|
||||
helpers.delete_buffer(bufnr)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('resolution', function()
|
||||
local function make_conflict_buffer()
|
||||
local bufnr = create_file_buffer({
|
||||
'<<<<<<< HEAD',
|
||||
'local x = 1',
|
||||
'=======',
|
||||
'local x = 2',
|
||||
'>>>>>>> feature',
|
||||
})
|
||||
vim.api.nvim_set_current_buf(bufnr)
|
||||
return bufnr
|
||||
end
|
||||
|
||||
it('resolve_ours keeps ours content', function()
|
||||
local bufnr = make_conflict_buffer()
|
||||
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||
|
||||
conflict.resolve_ours(bufnr, default_config())
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
assert.are.equal(1, #lines)
|
||||
assert.are.equal('local x = 1', lines[1])
|
||||
|
||||
helpers.delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('resolve_theirs keeps theirs content', function()
|
||||
local bufnr = make_conflict_buffer()
|
||||
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||
|
||||
conflict.resolve_theirs(bufnr, default_config())
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
assert.are.equal(1, #lines)
|
||||
assert.are.equal('local x = 2', lines[1])
|
||||
|
||||
helpers.delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('resolve_both keeps ours then theirs', function()
|
||||
local bufnr = make_conflict_buffer()
|
||||
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||
|
||||
conflict.resolve_both(bufnr, default_config())
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
assert.are.equal(2, #lines)
|
||||
assert.are.equal('local x = 1', lines[1])
|
||||
assert.are.equal('local x = 2', lines[2])
|
||||
|
||||
helpers.delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('resolve_none removes entire block', function()
|
||||
local bufnr = make_conflict_buffer()
|
||||
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||
|
||||
conflict.resolve_none(bufnr, default_config())
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
assert.are.equal(1, #lines)
|
||||
assert.are.equal('', lines[1])
|
||||
|
||||
helpers.delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('does nothing when cursor is outside conflict', function()
|
||||
local bufnr = create_file_buffer({
|
||||
'normal line',
|
||||
'<<<<<<< HEAD',
|
||||
'local x = 1',
|
||||
'=======',
|
||||
'local x = 2',
|
||||
'>>>>>>> feature',
|
||||
})
|
||||
vim.api.nvim_set_current_buf(bufnr)
|
||||
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||
|
||||
conflict.resolve_ours(bufnr, default_config())
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
assert.are.equal(6, #lines)
|
||||
|
||||
helpers.delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('resolves one conflict among multiple', function()
|
||||
local bufnr = create_file_buffer({
|
||||
'<<<<<<< HEAD',
|
||||
'a',
|
||||
'=======',
|
||||
'b',
|
||||
'>>>>>>> feat',
|
||||
'middle',
|
||||
'<<<<<<< HEAD',
|
||||
'c',
|
||||
'=======',
|
||||
'd',
|
||||
'>>>>>>> feat',
|
||||
})
|
||||
vim.api.nvim_set_current_buf(bufnr)
|
||||
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||
|
||||
conflict.resolve_ours(bufnr, default_config())
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
assert.are.equal('a', lines[1])
|
||||
assert.are.equal('middle', lines[2])
|
||||
assert.are.equal('<<<<<<< HEAD', lines[3])
|
||||
|
||||
helpers.delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('resolve_ours with empty ours section', function()
|
||||
local bufnr = create_file_buffer({
|
||||
'<<<<<<< HEAD',
|
||||
'=======',
|
||||
'local x = 2',
|
||||
'>>>>>>> feature',
|
||||
})
|
||||
vim.api.nvim_set_current_buf(bufnr)
|
||||
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||
|
||||
conflict.resolve_ours(bufnr, default_config())
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
assert.are.equal(1, #lines)
|
||||
assert.are.equal('', lines[1])
|
||||
|
||||
helpers.delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('handles diff3 resolution (ignores base)', function()
|
||||
local bufnr = create_file_buffer({
|
||||
'<<<<<<< HEAD',
|
||||
'local x = 1',
|
||||
'||||||| base',
|
||||
'local x = 0',
|
||||
'=======',
|
||||
'local x = 2',
|
||||
'>>>>>>> feature',
|
||||
})
|
||||
vim.api.nvim_set_current_buf(bufnr)
|
||||
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||
|
||||
conflict.resolve_theirs(bufnr, default_config())
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
assert.are.equal(1, #lines)
|
||||
assert.are.equal('local x = 2', lines[1])
|
||||
|
||||
helpers.delete_buffer(bufnr)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('navigation', function()
|
||||
it('goto_next jumps to next conflict', function()
|
||||
local bufnr = create_file_buffer({
|
||||
'normal',
|
||||
'<<<<<<< HEAD',
|
||||
'a',
|
||||
'=======',
|
||||
'b',
|
||||
'>>>>>>> feat',
|
||||
'middle',
|
||||
'<<<<<<< HEAD',
|
||||
'c',
|
||||
'=======',
|
||||
'd',
|
||||
'>>>>>>> feat',
|
||||
})
|
||||
vim.api.nvim_set_current_buf(bufnr)
|
||||
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||
|
||||
conflict.goto_next(bufnr)
|
||||
assert.are.equal(2, vim.api.nvim_win_get_cursor(0)[1])
|
||||
|
||||
conflict.goto_next(bufnr)
|
||||
assert.are.equal(8, vim.api.nvim_win_get_cursor(0)[1])
|
||||
|
||||
helpers.delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('goto_next wraps to first conflict', function()
|
||||
local bufnr = create_file_buffer({
|
||||
'<<<<<<< HEAD',
|
||||
'a',
|
||||
'=======',
|
||||
'b',
|
||||
'>>>>>>> feat',
|
||||
})
|
||||
vim.api.nvim_set_current_buf(bufnr)
|
||||
vim.api.nvim_win_set_cursor(0, { 5, 0 })
|
||||
|
||||
conflict.goto_next(bufnr)
|
||||
assert.are.equal(1, vim.api.nvim_win_get_cursor(0)[1])
|
||||
|
||||
helpers.delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('goto_prev jumps to previous conflict', function()
|
||||
local bufnr = create_file_buffer({
|
||||
'<<<<<<< HEAD',
|
||||
'a',
|
||||
'=======',
|
||||
'b',
|
||||
'>>>>>>> feat',
|
||||
'middle',
|
||||
'<<<<<<< HEAD',
|
||||
'c',
|
||||
'=======',
|
||||
'd',
|
||||
'>>>>>>> feat',
|
||||
'end',
|
||||
})
|
||||
vim.api.nvim_set_current_buf(bufnr)
|
||||
vim.api.nvim_win_set_cursor(0, { 12, 0 })
|
||||
|
||||
conflict.goto_prev(bufnr)
|
||||
assert.are.equal(7, vim.api.nvim_win_get_cursor(0)[1])
|
||||
|
||||
conflict.goto_prev(bufnr)
|
||||
assert.are.equal(1, vim.api.nvim_win_get_cursor(0)[1])
|
||||
|
||||
helpers.delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('goto_prev wraps to last conflict', function()
|
||||
local bufnr = create_file_buffer({
|
||||
'<<<<<<< HEAD',
|
||||
'a',
|
||||
'=======',
|
||||
'b',
|
||||
'>>>>>>> feat',
|
||||
})
|
||||
vim.api.nvim_set_current_buf(bufnr)
|
||||
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||
|
||||
conflict.goto_prev(bufnr)
|
||||
assert.are.equal(1, vim.api.nvim_win_get_cursor(0)[1])
|
||||
|
||||
helpers.delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('goto_next does nothing with no conflicts', function()
|
||||
local bufnr = create_file_buffer({ 'normal line' })
|
||||
vim.api.nvim_set_current_buf(bufnr)
|
||||
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||
|
||||
conflict.goto_next(bufnr)
|
||||
assert.are.equal(1, vim.api.nvim_win_get_cursor(0)[1])
|
||||
|
||||
helpers.delete_buffer(bufnr)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('lifecycle', function()
|
||||
it('attach is idempotent', function()
|
||||
local bufnr = create_file_buffer({
|
||||
'<<<<<<< HEAD',
|
||||
'a',
|
||||
'=======',
|
||||
'b',
|
||||
'>>>>>>> feat',
|
||||
})
|
||||
local cfg = default_config()
|
||||
conflict.attach(bufnr, cfg)
|
||||
local count1 = #get_extmarks(bufnr)
|
||||
conflict.attach(bufnr, cfg)
|
||||
local count2 = #get_extmarks(bufnr)
|
||||
assert.are.equal(count1, count2)
|
||||
conflict.detach(bufnr)
|
||||
helpers.delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('skips non-file buffers', function()
|
||||
local bufnr = helpers.create_buffer({
|
||||
'<<<<<<< HEAD',
|
||||
'a',
|
||||
'=======',
|
||||
'b',
|
||||
'>>>>>>> feat',
|
||||
})
|
||||
vim.api.nvim_set_option_value('buftype', 'nofile', { buf = bufnr })
|
||||
|
||||
conflict.attach(bufnr, default_config())
|
||||
assert.are.equal(0, #get_extmarks(bufnr))
|
||||
|
||||
helpers.delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('skips buffers without conflict markers', function()
|
||||
local bufnr = create_file_buffer({ 'local x = 1', 'local y = 2' })
|
||||
|
||||
conflict.attach(bufnr, default_config())
|
||||
assert.are.equal(0, #get_extmarks(bufnr))
|
||||
|
||||
helpers.delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('re-highlights when markers return after resolution', function()
|
||||
local bufnr = create_file_buffer({
|
||||
'<<<<<<< HEAD',
|
||||
'local x = 1',
|
||||
'=======',
|
||||
'local x = 2',
|
||||
'>>>>>>> feature',
|
||||
})
|
||||
vim.api.nvim_set_current_buf(bufnr)
|
||||
local cfg = default_config()
|
||||
conflict.attach(bufnr, cfg)
|
||||
|
||||
assert.is_true(#get_extmarks(bufnr) > 0)
|
||||
|
||||
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||
conflict.resolve_ours(bufnr, cfg)
|
||||
assert.are.equal(0, #get_extmarks(bufnr))
|
||||
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
|
||||
'<<<<<<< HEAD',
|
||||
'local x = 1',
|
||||
'=======',
|
||||
'local x = 2',
|
||||
'>>>>>>> feature',
|
||||
})
|
||||
vim.api.nvim_exec_autocmds('TextChanged', { buffer = bufnr })
|
||||
|
||||
assert.is_true(#get_extmarks(bufnr) > 0)
|
||||
|
||||
conflict.detach(bufnr)
|
||||
helpers.delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('detaches after last conflict resolved', function()
|
||||
local bufnr = create_file_buffer({
|
||||
'<<<<<<< HEAD',
|
||||
'local x = 1',
|
||||
'=======',
|
||||
'local x = 2',
|
||||
'>>>>>>> feature',
|
||||
})
|
||||
vim.api.nvim_set_current_buf(bufnr)
|
||||
conflict.attach(bufnr, default_config())
|
||||
|
||||
assert.is_true(#get_extmarks(bufnr) > 0)
|
||||
|
||||
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||
conflict.resolve_ours(bufnr, default_config())
|
||||
|
||||
assert.are.equal(0, #get_extmarks(bufnr))
|
||||
|
||||
helpers.delete_buffer(bufnr)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
|
@ -12,7 +12,6 @@ local function ensure_parser(lang)
|
|||
end
|
||||
|
||||
ensure_parser('lua')
|
||||
ensure_parser('vim')
|
||||
|
||||
local M = {}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ describe('highlight', function()
|
|||
highlights = {
|
||||
background = false,
|
||||
gutter = false,
|
||||
context = { enabled = false, lines = 0 },
|
||||
treesitter = {
|
||||
enabled = true,
|
||||
max_lines = 500,
|
||||
|
|
@ -220,40 +219,6 @@ describe('highlight', function()
|
|||
delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('highlights function keyword in header context', function()
|
||||
local bufnr = create_buffer({
|
||||
'@@ -5,3 +5,4 @@ function M.setup()',
|
||||
' local x = 1',
|
||||
'+local y = 2',
|
||||
' return x',
|
||||
})
|
||||
|
||||
local hunk = {
|
||||
filename = 'test.lua',
|
||||
lang = 'lua',
|
||||
start_line = 1,
|
||||
header_context = 'function M.setup()',
|
||||
header_context_col = 18,
|
||||
lines = { ' local x = 1', '+local y = 2', ' return x' },
|
||||
}
|
||||
|
||||
highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
|
||||
|
||||
local extmarks = get_extmarks(bufnr)
|
||||
local has_keyword_function = false
|
||||
for _, mark in ipairs(extmarks) do
|
||||
if mark[2] == 0 and mark[4] and mark[4].hl_group then
|
||||
local hl = mark[4].hl_group
|
||||
if hl == '@keyword.function.lua' or hl == '@keyword.lua' then
|
||||
has_keyword_function = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
assert.is_true(has_keyword_function)
|
||||
delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('does not highlight header when no header_context', function()
|
||||
local bufnr = create_buffer({
|
||||
'@@ -10,3 +10,4 @@',
|
||||
|
|
@ -834,72 +799,6 @@ describe('highlight', function()
|
|||
delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('hl_eol background extmarks are multiline so hl_eol takes effect', function()
|
||||
local bufnr = create_buffer({
|
||||
'@@ -1,2 +1,1 @@',
|
||||
'-local x = 1',
|
||||
'+local y = 2',
|
||||
})
|
||||
|
||||
local hunk = {
|
||||
filename = 'test.lua',
|
||||
lang = 'lua',
|
||||
start_line = 1,
|
||||
lines = { '-local x = 1', '+local y = 2' },
|
||||
}
|
||||
|
||||
highlight.highlight_hunk(
|
||||
bufnr,
|
||||
ns,
|
||||
hunk,
|
||||
default_opts({ highlights = { background = true } })
|
||||
)
|
||||
|
||||
local extmarks = get_extmarks(bufnr)
|
||||
for _, mark in ipairs(extmarks) do
|
||||
local d = mark[4]
|
||||
if d and (d.hl_group == 'DiffsAdd' or d.hl_group == 'DiffsDelete') then
|
||||
assert.is_true(d.end_row > mark[2])
|
||||
end
|
||||
end
|
||||
delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('number_hl_group does not bleed to adjacent lines', function()
|
||||
local bufnr = create_buffer({
|
||||
'@@ -1,3 +1,3 @@',
|
||||
' local a = 0',
|
||||
'-local x = 1',
|
||||
'+local y = 2',
|
||||
' local b = 3',
|
||||
})
|
||||
|
||||
local hunk = {
|
||||
filename = 'test.lua',
|
||||
lang = 'lua',
|
||||
start_line = 1,
|
||||
lines = { ' local a = 0', '-local x = 1', '+local y = 2', ' local b = 3' },
|
||||
}
|
||||
|
||||
highlight.highlight_hunk(
|
||||
bufnr,
|
||||
ns,
|
||||
hunk,
|
||||
default_opts({ highlights = { background = true, gutter = true } })
|
||||
)
|
||||
|
||||
local extmarks = get_extmarks(bufnr)
|
||||
for _, mark in ipairs(extmarks) do
|
||||
local d = mark[4]
|
||||
if d and d.number_hl_group then
|
||||
local start_row = mark[2]
|
||||
local end_row = d.end_row or start_row
|
||||
assert.are.equal(start_row, end_row)
|
||||
end
|
||||
end
|
||||
delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('line bg priority > DiffsClear priority', function()
|
||||
local bufnr = create_buffer({
|
||||
'@@ -1,2 +1,1 @@',
|
||||
|
|
@ -1156,202 +1055,6 @@ describe('highlight', function()
|
|||
assert.is_true(min_line_bg < min_char_bg)
|
||||
delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('context padding produces no extmarks on padding lines', function()
|
||||
local repo_root = '/tmp/diffs-test-context'
|
||||
vim.fn.mkdir(repo_root, 'p')
|
||||
|
||||
local f = io.open(repo_root .. '/test.lua', 'w')
|
||||
f:write('local M = {}\n')
|
||||
f:write('function M.hello()\n')
|
||||
f:write(' return "hi"\n')
|
||||
f:write('end\n')
|
||||
f:write('return M\n')
|
||||
f:close()
|
||||
|
||||
local bufnr = create_buffer({
|
||||
'@@ -3,1 +3,2 @@',
|
||||
' return "hi"',
|
||||
'+"bye"',
|
||||
})
|
||||
|
||||
local hunk = {
|
||||
filename = 'test.lua',
|
||||
lang = 'lua',
|
||||
start_line = 1,
|
||||
lines = { ' return "hi"', '+"bye"' },
|
||||
file_old_start = 3,
|
||||
file_old_count = 1,
|
||||
file_new_start = 3,
|
||||
file_new_count = 2,
|
||||
repo_root = repo_root,
|
||||
}
|
||||
|
||||
highlight.highlight_hunk(
|
||||
bufnr,
|
||||
ns,
|
||||
hunk,
|
||||
default_opts({ highlights = { context = { enabled = true, lines = 25 } } })
|
||||
)
|
||||
|
||||
local extmarks = get_extmarks(bufnr)
|
||||
for _, mark in ipairs(extmarks) do
|
||||
local row = mark[2]
|
||||
assert.is_true(row >= 1 and row <= 2)
|
||||
end
|
||||
|
||||
delete_buffer(bufnr)
|
||||
os.remove(repo_root .. '/test.lua')
|
||||
vim.fn.delete(repo_root, 'rf')
|
||||
end)
|
||||
|
||||
it('context disabled matches behavior without padding', function()
|
||||
local bufnr = create_buffer({
|
||||
'@@ -1,1 +1,2 @@',
|
||||
' local x = 1',
|
||||
'+local y = 2',
|
||||
})
|
||||
|
||||
local hunk = {
|
||||
filename = 'test.lua',
|
||||
lang = 'lua',
|
||||
start_line = 1,
|
||||
lines = { ' local x = 1', '+local y = 2' },
|
||||
file_new_start = 1,
|
||||
file_new_count = 2,
|
||||
repo_root = '/nonexistent',
|
||||
}
|
||||
|
||||
highlight.highlight_hunk(
|
||||
bufnr,
|
||||
ns,
|
||||
hunk,
|
||||
default_opts({ highlights = { context = { enabled = false, lines = 0 } } })
|
||||
)
|
||||
|
||||
local extmarks = get_extmarks(bufnr)
|
||||
assert.is_true(#extmarks > 0)
|
||||
delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('gracefully handles missing file for context padding', function()
|
||||
local bufnr = create_buffer({
|
||||
'@@ -1,1 +1,2 @@',
|
||||
' local x = 1',
|
||||
'+local y = 2',
|
||||
})
|
||||
|
||||
local hunk = {
|
||||
filename = 'test.lua',
|
||||
lang = 'lua',
|
||||
start_line = 1,
|
||||
lines = { ' local x = 1', '+local y = 2' },
|
||||
file_new_start = 1,
|
||||
file_new_count = 2,
|
||||
repo_root = '/nonexistent/path',
|
||||
}
|
||||
|
||||
assert.has_no.errors(function()
|
||||
highlight.highlight_hunk(
|
||||
bufnr,
|
||||
ns,
|
||||
hunk,
|
||||
default_opts({ highlights = { context = { enabled = true, lines = 25 } } })
|
||||
)
|
||||
end)
|
||||
|
||||
local extmarks = get_extmarks(bufnr)
|
||||
assert.is_true(#extmarks > 0)
|
||||
delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('highlights treesitter injections', function()
|
||||
local bufnr = create_buffer({
|
||||
'@@ -1,1 +1,2 @@',
|
||||
' local x = 1',
|
||||
'+vim.cmd([[ echo 1 ]])',
|
||||
})
|
||||
|
||||
local hunk = {
|
||||
filename = 'test.lua',
|
||||
lang = 'lua',
|
||||
start_line = 1,
|
||||
lines = { ' local x = 1', '+vim.cmd([[ echo 1 ]])' },
|
||||
}
|
||||
|
||||
highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
|
||||
|
||||
local extmarks = get_extmarks(bufnr)
|
||||
local has_vim_capture = false
|
||||
for _, mark in ipairs(extmarks) do
|
||||
if mark[4] and mark[4].hl_group and mark[4].hl_group:match('^@.*%.vim$') then
|
||||
has_vim_capture = true
|
||||
break
|
||||
end
|
||||
end
|
||||
assert.is_true(has_vim_capture)
|
||||
delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('includes captures from both base and injected languages', function()
|
||||
local bufnr = create_buffer({
|
||||
'@@ -1,1 +1,2 @@',
|
||||
' local x = 1',
|
||||
'+vim.cmd([[ echo 1 ]])',
|
||||
})
|
||||
|
||||
local hunk = {
|
||||
filename = 'test.lua',
|
||||
lang = 'lua',
|
||||
start_line = 1,
|
||||
lines = { ' local x = 1', '+vim.cmd([[ echo 1 ]])' },
|
||||
}
|
||||
|
||||
highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
|
||||
|
||||
local extmarks = get_extmarks(bufnr)
|
||||
local has_lua = false
|
||||
local has_vim = false
|
||||
for _, mark in ipairs(extmarks) do
|
||||
if mark[4] and mark[4].hl_group then
|
||||
if mark[4].hl_group:match('^@.*%.lua$') then
|
||||
has_lua = true
|
||||
end
|
||||
if mark[4].hl_group:match('^@.*%.vim$') then
|
||||
has_vim = true
|
||||
end
|
||||
end
|
||||
end
|
||||
assert.is_true(has_lua)
|
||||
assert.is_true(has_vim)
|
||||
delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('filters @spell and @nospell captures from injections', function()
|
||||
local bufnr = create_buffer({
|
||||
'@@ -1,1 +1,2 @@',
|
||||
' local x = 1',
|
||||
'+vim.cmd([[ echo 1 ]])',
|
||||
})
|
||||
|
||||
local hunk = {
|
||||
filename = 'test.lua',
|
||||
lang = 'lua',
|
||||
start_line = 1,
|
||||
lines = { ' local x = 1', '+vim.cmd([[ echo 1 ]])' },
|
||||
}
|
||||
|
||||
highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
|
||||
|
||||
local extmarks = get_extmarks(bufnr)
|
||||
for _, mark in ipairs(extmarks) do
|
||||
if mark[4] and mark[4].hl_group then
|
||||
assert.is_falsy(mark[4].hl_group:match('@spell'))
|
||||
assert.is_falsy(mark[4].hl_group:match('@nospell'))
|
||||
end
|
||||
end
|
||||
delete_buffer(bufnr)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('diff header highlighting', function()
|
||||
|
|
@ -1383,7 +1086,6 @@ describe('highlight', function()
|
|||
highlights = {
|
||||
background = false,
|
||||
gutter = false,
|
||||
context = { enabled = false, lines = 0 },
|
||||
treesitter = { enabled = true, max_lines = 500 },
|
||||
vim = { enabled = false, max_lines = 200 },
|
||||
},
|
||||
|
|
@ -1540,7 +1242,6 @@ describe('highlight', function()
|
|||
highlights = {
|
||||
background = false,
|
||||
gutter = false,
|
||||
context = { enabled = false, lines = 0 },
|
||||
treesitter = { enabled = true, max_lines = 500 },
|
||||
vim = { enabled = false, max_lines = 200 },
|
||||
},
|
||||
|
|
|
|||
|
|
@ -421,84 +421,5 @@ describe('parser', function()
|
|||
os.remove(file_path)
|
||||
vim.fn.delete(repo_root, 'rf')
|
||||
end)
|
||||
|
||||
it('extracts file line numbers from @@ header', function()
|
||||
local bufnr = create_buffer({
|
||||
'M lua/test.lua',
|
||||
'@@ -1,3 +1,4 @@',
|
||||
' local M = {}',
|
||||
'+local new = true',
|
||||
' return M',
|
||||
})
|
||||
local hunks = parser.parse_buffer(bufnr)
|
||||
|
||||
assert.are.equal(1, #hunks)
|
||||
assert.are.equal(1, hunks[1].file_old_start)
|
||||
assert.are.equal(3, hunks[1].file_old_count)
|
||||
assert.are.equal(1, hunks[1].file_new_start)
|
||||
assert.are.equal(4, hunks[1].file_new_count)
|
||||
delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('extracts large line numbers from @@ header', function()
|
||||
local bufnr = create_buffer({
|
||||
'M lua/test.lua',
|
||||
'@@ -100,20 +200,30 @@',
|
||||
' local M = {}',
|
||||
})
|
||||
local hunks = parser.parse_buffer(bufnr)
|
||||
|
||||
assert.are.equal(1, #hunks)
|
||||
assert.are.equal(100, hunks[1].file_old_start)
|
||||
assert.are.equal(20, hunks[1].file_old_count)
|
||||
assert.are.equal(200, hunks[1].file_new_start)
|
||||
assert.are.equal(30, hunks[1].file_new_count)
|
||||
delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('defaults count to 1 when omitted in @@ header', function()
|
||||
local bufnr = create_buffer({
|
||||
'M lua/test.lua',
|
||||
'@@ -1 +1 @@',
|
||||
' local M = {}',
|
||||
})
|
||||
local hunks = parser.parse_buffer(bufnr)
|
||||
|
||||
assert.are.equal(1, #hunks)
|
||||
assert.are.equal(1, hunks[1].file_old_start)
|
||||
assert.are.equal(1, hunks[1].file_old_count)
|
||||
assert.are.equal(1, hunks[1].file_new_start)
|
||||
assert.are.equal(1, hunks[1].file_new_count)
|
||||
delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('stores repo_root on hunk when available', function()
|
||||
local bufnr = create_buffer({
|
||||
'M lua/test.lua',
|
||||
'@@ -1,3 +1,4 @@',
|
||||
' local M = {}',
|
||||
'+local new = true',
|
||||
' return M',
|
||||
})
|
||||
vim.api.nvim_buf_set_var(bufnr, 'diffs_repo_root', '/tmp/test-repo')
|
||||
local hunks = parser.parse_buffer(bufnr)
|
||||
|
||||
assert.are.equal(1, #hunks)
|
||||
assert.are.equal('/tmp/test-repo', hunks[1].repo_root)
|
||||
delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('repo_root is nil when not available', function()
|
||||
local bufnr = create_buffer({
|
||||
'M lua/test.lua',
|
||||
'@@ -1,3 +1,4 @@',
|
||||
' local M = {}',
|
||||
})
|
||||
local hunks = parser.parse_buffer(bufnr)
|
||||
|
||||
assert.are.equal(1, #hunks)
|
||||
assert.is_nil(hunks[1].repo_root)
|
||||
delete_buffer(bufnr)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
|
|
|||
135
spec/ux_spec.lua
135
spec/ux_spec.lua
|
|
@ -1,135 +0,0 @@
|
|||
local commands = require('diffs.commands')
|
||||
local helpers = require('spec.helpers')
|
||||
|
||||
local counter = 0
|
||||
|
||||
local function create_diffs_buffer(name)
|
||||
counter = counter + 1
|
||||
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
|
||||
'diff --git a/file.lua b/file.lua',
|
||||
'--- a/file.lua',
|
||||
'+++ b/file.lua',
|
||||
'@@ -1,1 +1,2 @@',
|
||||
' local x = 1',
|
||||
'+local y = 2',
|
||||
})
|
||||
vim.api.nvim_set_option_value('buftype', 'nowrite', { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('swapfile', false, { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('filetype', 'diff', { buf = bufnr })
|
||||
vim.api.nvim_buf_set_name(bufnr, name or ('diffs://unstaged:file_' .. counter .. '.lua'))
|
||||
return bufnr
|
||||
end
|
||||
|
||||
describe('ux', function()
|
||||
describe('diagnostics', function()
|
||||
it('disables diagnostics on diff buffers', function()
|
||||
local bufnr = create_diffs_buffer()
|
||||
commands.setup_diff_buf(bufnr)
|
||||
|
||||
assert.is_false(vim.diagnostic.is_enabled({ bufnr = bufnr }))
|
||||
vim.api.nvim_buf_delete(bufnr, { force = true })
|
||||
end)
|
||||
|
||||
it('does not affect other buffers', function()
|
||||
local diff_buf = create_diffs_buffer()
|
||||
local normal_buf = helpers.create_buffer({ 'hello' })
|
||||
|
||||
commands.setup_diff_buf(diff_buf)
|
||||
|
||||
assert.is_true(vim.diagnostic.is_enabled({ bufnr = normal_buf }))
|
||||
vim.api.nvim_buf_delete(diff_buf, { force = true })
|
||||
helpers.delete_buffer(normal_buf)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('q keymap', function()
|
||||
it('sets q keymap on diff buffer', function()
|
||||
local bufnr = create_diffs_buffer()
|
||||
commands.setup_diff_buf(bufnr)
|
||||
|
||||
local keymaps = vim.api.nvim_buf_get_keymap(bufnr, 'n')
|
||||
local has_q = false
|
||||
for _, km in ipairs(keymaps) do
|
||||
if km.lhs == 'q' then
|
||||
has_q = true
|
||||
break
|
||||
end
|
||||
end
|
||||
assert.is_true(has_q)
|
||||
vim.api.nvim_buf_delete(bufnr, { force = true })
|
||||
end)
|
||||
|
||||
it('q closes the window', function()
|
||||
local bufnr = create_diffs_buffer()
|
||||
commands.setup_diff_buf(bufnr)
|
||||
|
||||
vim.cmd('split')
|
||||
local win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(win, bufnr)
|
||||
|
||||
local win_count_before = #vim.api.nvim_tabpage_list_wins(0)
|
||||
|
||||
vim.api.nvim_buf_call(bufnr, function()
|
||||
vim.cmd('normal q')
|
||||
end)
|
||||
|
||||
local win_count_after = #vim.api.nvim_tabpage_list_wins(0)
|
||||
assert.equals(win_count_before - 1, win_count_after)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('window reuse', function()
|
||||
it('returns nil when no diffs window exists', function()
|
||||
local win = commands.find_diffs_window()
|
||||
assert.is_nil(win)
|
||||
end)
|
||||
|
||||
it('finds existing diffs:// window', function()
|
||||
local bufnr = create_diffs_buffer()
|
||||
vim.cmd('split')
|
||||
local expected_win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(expected_win, bufnr)
|
||||
|
||||
local found = commands.find_diffs_window()
|
||||
assert.equals(expected_win, found)
|
||||
|
||||
vim.api.nvim_win_close(expected_win, true)
|
||||
end)
|
||||
|
||||
it('ignores non-diffs buffers', function()
|
||||
local normal_buf = helpers.create_buffer({ 'hello' })
|
||||
vim.cmd('split')
|
||||
local win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(win, normal_buf)
|
||||
|
||||
local found = commands.find_diffs_window()
|
||||
assert.is_nil(found)
|
||||
|
||||
vim.api.nvim_win_close(win, true)
|
||||
helpers.delete_buffer(normal_buf)
|
||||
end)
|
||||
|
||||
it('returns first diffs window when multiple exist', function()
|
||||
local buf1 = create_diffs_buffer()
|
||||
local buf2 = create_diffs_buffer()
|
||||
|
||||
vim.cmd('split')
|
||||
local win1 = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(win1, buf1)
|
||||
|
||||
vim.cmd('split')
|
||||
local win2 = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(win2, buf2)
|
||||
|
||||
local found = commands.find_diffs_window()
|
||||
assert.is_not_nil(found)
|
||||
assert.is_true(found == win1 or found == win2)
|
||||
|
||||
vim.api.nvim_win_close(win1, true)
|
||||
vim.api.nvim_win_close(win2, true)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
Loading…
Add table
Add a link
Reference in a new issue