Compare commits
No commits in common. "doc/merge-conflicts" and "fix/known-limitation-syntax" have entirely different histories.
doc/merge-
...
fix/known-
28 changed files with 212 additions and 6220 deletions
6
.github/workflows/test.yaml
vendored
6
.github/workflows/test.yaml
vendored
|
|
@ -20,9 +20,3 @@ jobs:
|
||||||
- uses: nvim-neorocks/nvim-busted-action@v1
|
- uses: nvim-neorocks/nvim-busted-action@v1
|
||||||
with:
|
with:
|
||||||
nvim_version: ${{ matrix.nvim }}
|
nvim_version: ${{ matrix.nvim }}
|
||||||
before: |
|
|
||||||
git clone --depth 1 https://github.com/the-mikedavis/tree-sitter-diff /tmp/ts-diff
|
|
||||||
cd /tmp/ts-diff && cc -shared -fPIC -o diff.so -I./src src/parser.c
|
|
||||||
mkdir -p ~/.local/share/nvim/site/parser ~/.local/share/nvim/site/queries/diff
|
|
||||||
cp diff.so ~/.local/share/nvim/site/parser/
|
|
||||||
cp queries/*.scm ~/.local/share/nvim/site/queries/diff/
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"runtime.version": "Lua 5.1",
|
"runtime.version": "Lua 5.1",
|
||||||
"runtime.path": ["lua/?.lua", "lua/?/init.lua"],
|
"runtime.path": ["lua/?.lua", "lua/?/init.lua"],
|
||||||
"diagnostics.globals": ["vim", "jit"],
|
"diagnostics.globals": ["vim"],
|
||||||
"workspace.library": ["$VIMRUNTIME/lua", "${3rd}/luv/library"],
|
"workspace.library": ["$VIMRUNTIME/lua", "${3rd}/luv/library"],
|
||||||
"workspace.checkThirdParty": false,
|
"workspace.checkThirdParty": false,
|
||||||
"completion.callSnippet": "Replace"
|
"completion.callSnippet": "Replace"
|
||||||
|
|
|
||||||
28
README.md
28
README.md
|
|
@ -10,17 +10,11 @@ syntax highlighting.
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Treesitter syntax highlighting in `:Git` diffs and commit views
|
- Treesitter syntax highlighting in `:Git` diffs and commit views
|
||||||
- Diff header highlighting (`diff --git`, `index`, `---`, `+++`)
|
|
||||||
- `:Gdiffsplit` / `:Gvdiffsplit` syntax through diff backgrounds
|
- `:Gdiffsplit` / `:Gvdiffsplit` syntax through diff backgrounds
|
||||||
- `:Gdiff` unified diff against any git revision with syntax highlighting
|
|
||||||
- Fugitive status buffer keymaps (`du`/`dU`) for unified diffs
|
|
||||||
- Background-only diff colors for any `&diff` buffer (`:diffthis`, `vimdiff`)
|
- Background-only diff colors for any `&diff` buffer (`:diffthis`, `vimdiff`)
|
||||||
- Vim syntax fallback for languages without a treesitter parser
|
- Vim syntax fallback for languages without a treesitter parser
|
||||||
- Hunk header context highlighting (`@@ ... @@ function foo()`)
|
- Hunk header context highlighting (`@@ ... @@ function foo()`)
|
||||||
- Character-level (intra-line) diff highlighting for changed characters
|
- Configurable debouncing, max lines, and diff prefix concealment
|
||||||
- Inline merge conflict detection, highlighting, and resolution keymaps
|
|
||||||
- Configurable debouncing, max lines, diff prefix concealment, blend alpha, and
|
|
||||||
highlight overrides
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
|
@ -43,11 +37,12 @@ luarocks install diffs.nvim
|
||||||
|
|
||||||
## Known Limitations
|
## Known Limitations
|
||||||
|
|
||||||
- **Incomplete syntax context**: Treesitter parses each diff hunk in isolation.
|
- **Incomplete syntax context**: Treesitter parses each diff hunk in isolation
|
||||||
To improve accuracy, `diffs.nvim` reads lines from disk before and after each
|
without surrounding code context. When a hunk shows lines added to an existing
|
||||||
hunk for parsing context (`highlights.context`, enabled by default with 25
|
block (e.g., adding a plugin inside `return { ... }`), the parser doesn't see
|
||||||
lines). This resolves most boundary issues. Set
|
the `return` statement and may produce incorrect highlighting. This is
|
||||||
`highlights.context.enabled = false` to disable.
|
inherent to parsing code fragments—no diff tooling solves this without
|
||||||
|
significant complexity.
|
||||||
|
|
||||||
- **Syntax flashing**: `diffs.nvim` hooks into the `FileType fugitive` event
|
- **Syntax flashing**: `diffs.nvim` hooks into the `FileType fugitive` event
|
||||||
triggered by `vim-fugitive`, at which point the buffer is preliminarily
|
triggered by `vim-fugitive`, at which point the buffer is preliminarily
|
||||||
|
|
@ -64,17 +59,10 @@ luarocks install diffs.nvim
|
||||||
compatible, but both plugins modifying line highlights may produce
|
compatible, but both plugins modifying line highlights may produce
|
||||||
unexpected results
|
unexpected results
|
||||||
- [`git-conflict.nvim`](https://github.com/akinsho/git-conflict.nvim) -
|
- [`git-conflict.nvim`](https://github.com/akinsho/git-conflict.nvim) -
|
||||||
`diffs.nvim` now includes built-in conflict resolution; disable one or the
|
conflict marker highlighting may overlap with `diffs.nvim`
|
||||||
other to avoid overlap
|
|
||||||
|
|
||||||
# Acknowledgements
|
# Acknowledgements
|
||||||
|
|
||||||
- [`vim-fugitive`](https://github.com/tpope/vim-fugitive)
|
- [`vim-fugitive`](https://github.com/tpope/vim-fugitive)
|
||||||
- [`codediff.nvim`](https://github.com/esmuellert/codediff.nvim)
|
- [`codediff.nvim`](https://github.com/esmuellert/codediff.nvim)
|
||||||
- [`diffview.nvim`](https://github.com/sindrets/diffview.nvim)
|
- [`diffview.nvim`](https://github.com/sindrets/diffview.nvim)
|
||||||
- [`difftastic`](https://github.com/Wilfred/difftastic)
|
|
||||||
- [`mini.diff`](https://github.com/echasnovski/mini.diff)
|
|
||||||
- [`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
|
|
||||||
|
|
|
||||||
|
|
@ -12,15 +12,12 @@ built-in diff mode.
|
||||||
|
|
||||||
Features: ~
|
Features: ~
|
||||||
- Syntax highlighting in |:Git| summary diffs and commit detail views
|
- Syntax highlighting in |:Git| summary diffs and commit detail views
|
||||||
- Diff header highlighting (`diff --git`, `index`, `---`, `+++`)
|
|
||||||
- Syntax highlighting in |:Gdiffsplit| / |:Gvdiffsplit| side-by-side diffs
|
- Syntax highlighting in |:Gdiffsplit| / |:Gvdiffsplit| side-by-side diffs
|
||||||
- |:Gdiff| command for unified diff against any git revision
|
|
||||||
- Background-only diff colors for any `&diff` buffer (vimdiff, diffthis, etc.)
|
- Background-only diff colors for any `&diff` buffer (vimdiff, diffthis, etc.)
|
||||||
- Vim syntax fallback for languages without a treesitter parser
|
- Vim syntax fallback for languages without a treesitter parser
|
||||||
- Blended diff background colors that preserve syntax visibility
|
- Blended diff background colors that preserve syntax visibility
|
||||||
- Optional diff prefix (`+`/`-`/` `) concealment
|
- Optional diff prefix (`+`/`-`/` `) concealment
|
||||||
- Gutter (line number) highlighting
|
- Gutter (line number) highlighting
|
||||||
- Inline merge conflict marker detection, highlighting, and resolution
|
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
REQUIREMENTS *diffs-requirements*
|
REQUIREMENTS *diffs-requirements*
|
||||||
|
|
@ -57,11 +54,6 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
|
||||||
highlights = {
|
highlights = {
|
||||||
background = true,
|
background = true,
|
||||||
gutter = true,
|
gutter = true,
|
||||||
blend_alpha = 0.6,
|
|
||||||
context = {
|
|
||||||
enabled = true,
|
|
||||||
lines = 25,
|
|
||||||
},
|
|
||||||
treesitter = {
|
treesitter = {
|
||||||
enabled = true,
|
enabled = true,
|
||||||
max_lines = 500,
|
max_lines = 500,
|
||||||
|
|
@ -70,29 +62,6 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
|
||||||
enabled = false,
|
enabled = false,
|
||||||
max_lines = 200,
|
max_lines = 200,
|
||||||
},
|
},
|
||||||
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',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
<
|
<
|
||||||
|
|
@ -118,14 +87,6 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
|
||||||
Controls which highlight features are enabled.
|
Controls which highlight features are enabled.
|
||||||
See |diffs.Highlights| for fields.
|
See |diffs.Highlights| for fields.
|
||||||
|
|
||||||
{fugitive} (table, default: see below)
|
|
||||||
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*
|
*diffs.Highlights*
|
||||||
Highlights table fields: ~
|
Highlights table fields: ~
|
||||||
{background} (boolean, default: true)
|
{background} (boolean, default: true)
|
||||||
|
|
@ -137,17 +98,6 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
|
||||||
Highlight line numbers with matching colors.
|
Highlight line numbers with matching colors.
|
||||||
Only visible if line numbers are enabled.
|
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} (table, default: see below)
|
||||||
Treesitter highlighting options.
|
Treesitter highlighting options.
|
||||||
See |diffs.TreesitterConfig| for fields.
|
See |diffs.TreesitterConfig| for fields.
|
||||||
|
|
@ -156,31 +106,6 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
|
||||||
Vim syntax highlighting options (experimental).
|
Vim syntax highlighting options (experimental).
|
||||||
See |diffs.VimConfig| for fields.
|
See |diffs.VimConfig| for fields.
|
||||||
|
|
||||||
{intra} (table, default: see below)
|
|
||||||
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*
|
*diffs.TreesitterConfig*
|
||||||
Treesitter config fields: ~
|
Treesitter config fields: ~
|
||||||
{enabled} (boolean, default: true)
|
{enabled} (boolean, default: true)
|
||||||
|
|
@ -205,26 +130,6 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
|
||||||
this many lines. Lower than the treesitter default
|
this many lines. Lower than the treesitter default
|
||||||
due to the per-character cost of |synID()|.
|
due to the per-character cost of |synID()|.
|
||||||
|
|
||||||
*diffs.IntraConfig*
|
|
||||||
Intra config fields: ~
|
|
||||||
{enabled} (boolean, default: true)
|
|
||||||
Enable character-level diff highlighting within
|
|
||||||
changed lines. When a line changes from `local x = 1`
|
|
||||||
to `local x = 2`, only the `1`/`2` characters get
|
|
||||||
an intense background overlay while the rest of the
|
|
||||||
line keeps the softer line-level background.
|
|
||||||
|
|
||||||
{algorithm} (string, default: 'default')
|
|
||||||
Diff algorithm for character-level analysis.
|
|
||||||
`'default'`: use |vim.diff()| with settings
|
|
||||||
inherited from |'diffopt'| (`algorithm` and
|
|
||||||
`linematch`). `'vscode'`: use libvscodediff FFI
|
|
||||||
(falls back to default if not available).
|
|
||||||
|
|
||||||
{max_lines} (integer, default: 500)
|
|
||||||
Skip character-level highlighting for hunks larger
|
|
||||||
than this many lines.
|
|
||||||
|
|
||||||
Note: Header context (e.g., `@@ -10,3 +10,4 @@ func()`) is always
|
Note: Header context (e.g., `@@ -10,3 +10,4 @@ func()`) is always
|
||||||
highlighted with treesitter when a parser is available.
|
highlighted with treesitter when a parser is available.
|
||||||
|
|
||||||
|
|
@ -233,231 +138,6 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
|
||||||
or register treesitter parsers for custom filetypes, use
|
or register treesitter parsers for custom filetypes, use
|
||||||
|vim.filetype.add()| and |vim.treesitter.language.register()|.
|
|vim.filetype.add()| and |vim.treesitter.language.register()|.
|
||||||
|
|
||||||
==============================================================================
|
|
||||||
COMMANDS *diffs-commands*
|
|
||||||
|
|
||||||
:Gdiff [revision] *:Gdiff*
|
|
||||||
Open a unified diff of the current file against a git revision. Displays
|
|
||||||
in a horizontal split below the current window.
|
|
||||||
|
|
||||||
The diff buffer shows `+`/`-` lines with full syntax highlighting for the
|
|
||||||
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.
|
|
||||||
|
|
||||||
Examples: >vim
|
|
||||||
:Gdiff " diff against HEAD
|
|
||||||
:Gdiff main " diff against main branch
|
|
||||||
:Gdiff HEAD~3 " diff against 3 commits ago
|
|
||||||
:Gdiff abc123 " diff against specific commit
|
|
||||||
<
|
|
||||||
|
|
||||||
:Gvdiff [revision] *:Gvdiff*
|
|
||||||
Like |:Gdiff| but opens in a vertical split.
|
|
||||||
|
|
||||||
: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*
|
|
||||||
|
|
||||||
When inside a vim-fugitive |:Git| status buffer, diffs.nvim provides keymaps
|
|
||||||
to open unified diffs for files or entire sections.
|
|
||||||
|
|
||||||
Keymaps: ~
|
|
||||||
*diffs-du* *diffs-dU*
|
|
||||||
du Open unified diff in a horizontal split.
|
|
||||||
dU Open unified diff in a vertical split.
|
|
||||||
|
|
||||||
These keymaps work on:
|
|
||||||
- File lines (e.g., `M src/foo.lua`) - opens diff for that file
|
|
||||||
- Section headers (e.g., `Staged (3)`) - opens diff for all files in section
|
|
||||||
- Hunk/context lines below a file - opens diff for the parent file
|
|
||||||
|
|
||||||
Behavior by file status: ~
|
|
||||||
|
|
||||||
Status Section Base Current Result ~
|
|
||||||
M Unstaged index working tree unstaged changes
|
|
||||||
M Staged HEAD index staged changes
|
|
||||||
A Staged (empty) index file as all-added
|
|
||||||
D Staged HEAD (empty) file as all-removed
|
|
||||||
R Staged HEAD:oldname index:newname content diff
|
|
||||||
? Untracked (empty) working tree file as all-added
|
|
||||||
|
|
||||||
On section headers, the keymap runs `git diff` (or `git diff --cached` for
|
|
||||||
staged) and displays all changes in that section as a single unified diff.
|
|
||||||
Untracked section headers show a warning since there is no meaningful diff.
|
|
||||||
|
|
||||||
Configuration: ~
|
|
||||||
*diffs.FugitiveConfig*
|
|
||||||
>lua
|
|
||||||
vim.g.diffs = {
|
|
||||||
fugitive = {
|
|
||||||
horizontal = 'du', -- keymap for horizontal split, false to disable
|
|
||||||
vertical = 'dU', -- keymap for vertical split, false to disable
|
|
||||||
},
|
|
||||||
}
|
|
||||||
<
|
|
||||||
Fields: ~
|
|
||||||
{horizontal} (string|false, default: 'du')
|
|
||||||
Keymap for unified diff in horizontal split.
|
|
||||||
Set to `false` to disable.
|
|
||||||
|
|
||||||
{vertical} (string|false, default: 'dU')
|
|
||||||
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*
|
API *diffs-api*
|
||||||
|
|
||||||
|
|
@ -489,11 +169,9 @@ Summary / commit detail views: ~
|
||||||
- Code is parsed with |vim.treesitter.get_string_parser()|
|
- Code is parsed with |vim.treesitter.get_string_parser()|
|
||||||
- If no treesitter parser and `vim.enabled`: vim syntax fallback via
|
- If no treesitter parser and `vim.enabled`: vim syntax fallback via
|
||||||
scratch buffer and |synID()|
|
scratch buffer and |synID()|
|
||||||
- `Normal` extmarks at priority 198 clear underlying diff foreground
|
- Background extmarks (`DiffsAdd`/`DiffsDelete`) at priority 198
|
||||||
- Background extmarks (`DiffsAdd`/`DiffsDelete`) at priority 199
|
- `Normal` extmarks at priority 199 clear underlying diff foreground
|
||||||
- Syntax highlights are applied as extmarks at priority 200
|
- Syntax highlights are applied as extmarks at priority 200
|
||||||
- Character-level diff extmarks (`DiffsAddText`/`DiffsDeleteText`) at
|
|
||||||
priority 201 overlay changed characters with an intense background
|
|
||||||
- Conceal extmarks hide diff prefixes when `hide_prefix` is enabled
|
- Conceal extmarks hide diff prefixes when `hide_prefix` is enabled
|
||||||
4. Re-highlighting occurs on `TextChanged` (debounced) and `Syntax` events
|
4. Re-highlighting occurs on `TextChanged` (debounced) and `Syntax` events
|
||||||
|
|
||||||
|
|
@ -509,15 +187,14 @@ KNOWN LIMITATIONS *diffs-limitations*
|
||||||
|
|
||||||
Incomplete Syntax Context ~
|
Incomplete Syntax Context ~
|
||||||
*diffs-syntax-context*
|
*diffs-syntax-context*
|
||||||
Treesitter parses each diff hunk in isolation. To provide surrounding code
|
Treesitter parses each diff hunk in isolation without surrounding code
|
||||||
context, diffs.nvim reads lines from disk before and after each hunk
|
context. When a hunk shows lines added to an existing block (e.g., adding a
|
||||||
(see |diffs.ContextConfig|, enabled by default). This resolves most boundary
|
plugin inside `return { ... }`), the parser doesn't see the `return`
|
||||||
issues where incomplete constructs (e.g., a function definition at the edge
|
statement and may produce incorrect or unusual highlighting.
|
||||||
of a hunk with no body) would confuse the parser.
|
|
||||||
|
|
||||||
Set `highlights.context.enabled = false` to disable context padding. In rare
|
This is inherent to parsing code fragments. No diff tooling solves this
|
||||||
cases, context padding may not help if the relevant surrounding code is very
|
problem without significant complexity—the parser simply doesn't have enough
|
||||||
far from the hunk boundaries.
|
information to understand the full syntactic structure.
|
||||||
|
|
||||||
Syntax Highlighting Flash ~
|
Syntax Highlighting Flash ~
|
||||||
*diffs-flash*
|
*diffs-flash*
|
||||||
|
|
@ -555,9 +232,8 @@ conflict:
|
||||||
modifying line highlights may produce unexpected results.
|
modifying line highlights may produce unexpected results.
|
||||||
|
|
||||||
- git-conflict.nvim (akinsho/git-conflict.nvim)
|
- git-conflict.nvim (akinsho/git-conflict.nvim)
|
||||||
Provides conflict marker highlighting and resolution keymaps.
|
Provides conflict marker highlighting that may overlap with
|
||||||
diffs.nvim now has built-in conflict resolution (see
|
diffs.nvim's highlighting in conflict scenarios.
|
||||||
|diffs-conflict|). Disable one or the other to avoid overlap.
|
|
||||||
|
|
||||||
If you experience visual conflicts, try disabling the conflicting plugin's
|
If you experience visual conflicts, try disabling the conflicting plugin's
|
||||||
diff-related features.
|
diff-related features.
|
||||||
|
|
@ -565,15 +241,9 @@ diff-related features.
|
||||||
==============================================================================
|
==============================================================================
|
||||||
HIGHLIGHT GROUPS *diffs-highlights*
|
HIGHLIGHT GROUPS *diffs-highlights*
|
||||||
|
|
||||||
diffs.nvim defines custom highlight groups. All groups use `default = true`,
|
diffs.nvim defines custom highlight groups. Fugitive unified diff groups use
|
||||||
so colorschemes can override them by defining the group before the plugin
|
`default = true`, so colorschemes can override them. Diff mode groups are
|
||||||
loads.
|
always derived from the corresponding `Diff*` groups.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
Fugitive unified diff highlights: ~
|
Fugitive unified diff highlights: ~
|
||||||
*DiffsAdd*
|
*DiffsAdd*
|
||||||
|
|
@ -586,54 +256,11 @@ Fugitive unified diff highlights: ~
|
||||||
|
|
||||||
*DiffsAddNr*
|
*DiffsAddNr*
|
||||||
DiffsAddNr Line number for `+` lines. Foreground from
|
DiffsAddNr Line number for `+` lines. Foreground from
|
||||||
`DiffsAddText`, background from `DiffsAdd`.
|
`diffAdded`, background from `DiffsAdd`.
|
||||||
|
|
||||||
*DiffsDeleteNr*
|
*DiffsDeleteNr*
|
||||||
DiffsDeleteNr Line number for `-` lines. Foreground from
|
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.
|
|
||||||
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).
|
|
||||||
|
|
||||||
Diff mode window highlights: ~
|
Diff mode window highlights: ~
|
||||||
These are used for |winhighlight| remapping in `&diff` windows.
|
These are used for |winhighlight| remapping in `&diff` windows.
|
||||||
|
|
@ -659,16 +286,6 @@ To customize these in your colorscheme: >lua
|
||||||
vim.api.nvim_set_hl(0, 'DiffsAdd', { bg = '#2e4a3a' })
|
vim.api.nvim_set_hl(0, 'DiffsAdd', { bg = '#2e4a3a' })
|
||||||
vim.api.nvim_set_hl(0, 'DiffsDiffDelete', { link = 'DiffDelete' })
|
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*
|
HEALTH CHECK *diffs-health*
|
||||||
|
|
@ -678,16 +295,6 @@ Run |:checkhealth| diffs to verify your setup.
|
||||||
Checks performed:
|
Checks performed:
|
||||||
- Neovim version >= 0.9.0
|
- Neovim version >= 0.9.0
|
||||||
- vim-fugitive is installed (optional)
|
- vim-fugitive is installed (optional)
|
||||||
- libvscode_diff shared library is available (optional)
|
|
||||||
|
|
||||||
==============================================================================
|
|
||||||
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
|
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
vim:tw=78:ts=8:ft=help:norl:
|
vim:tw=78:ts=8:ft=help:norl:
|
||||||
|
|
|
||||||
|
|
@ -1,393 +0,0 @@
|
||||||
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?
|
|
||||||
function M.find_hunk_line(diff_lines, hunk_position)
|
|
||||||
for i, line in ipairs(diff_lines) do
|
|
||||||
if line == hunk_position.hunk_header then
|
|
||||||
return i + hunk_position.offset
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param old_lines string[]
|
|
||||||
---@param new_lines string[]
|
|
||||||
---@param old_name string
|
|
||||||
---@param new_name string
|
|
||||||
---@return string[]
|
|
||||||
local function generate_unified_diff(old_lines, new_lines, old_name, new_name)
|
|
||||||
local old_content = table.concat(old_lines, '\n')
|
|
||||||
local new_content = table.concat(new_lines, '\n')
|
|
||||||
|
|
||||||
local diff_fn = vim.text and vim.text.diff or vim.diff
|
|
||||||
local diff_output = diff_fn(old_content, new_content, {
|
|
||||||
result_type = 'unified',
|
|
||||||
ctxlen = 3,
|
|
||||||
})
|
|
||||||
|
|
||||||
if not diff_output or diff_output == '' then
|
|
||||||
return {}
|
|
||||||
end
|
|
||||||
|
|
||||||
local diff_lines = vim.split(diff_output, '\n', { plain = true })
|
|
||||||
|
|
||||||
local result = {
|
|
||||||
'diff --git a/' .. old_name .. ' b/' .. new_name,
|
|
||||||
'--- a/' .. old_name,
|
|
||||||
'+++ b/' .. new_name,
|
|
||||||
}
|
|
||||||
for _, line in ipairs(diff_lines) do
|
|
||||||
table.insert(result, line)
|
|
||||||
end
|
|
||||||
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param revision? string
|
|
||||||
---@param vertical? boolean
|
|
||||||
function M.gdiff(revision, vertical)
|
|
||||||
revision = revision or 'HEAD'
|
|
||||||
|
|
||||||
local bufnr = vim.api.nvim_get_current_buf()
|
|
||||||
local filepath = vim.api.nvim_buf_get_name(bufnr)
|
|
||||||
|
|
||||||
if filepath == '' then
|
|
||||||
vim.notify('[diffs.nvim]: cannot diff unnamed buffer', vim.log.levels.ERROR)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local rel_path = git.get_relative_path(filepath)
|
|
||||||
if not rel_path then
|
|
||||||
vim.notify('[diffs.nvim]: not in a git repository', vim.log.levels.ERROR)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local old_lines, err = git.get_file_content(revision, filepath)
|
|
||||||
if not old_lines then
|
|
||||||
vim.notify('[diffs.nvim]: ' .. (err or 'unknown error'), vim.log.levels.ERROR)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local new_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
|
||||||
|
|
||||||
local diff_lines = generate_unified_diff(old_lines, new_lines, rel_path, rel_path)
|
|
||||||
|
|
||||||
if #diff_lines == 0 then
|
|
||||||
vim.notify('[diffs.nvim]: no diff against ' .. revision, vim.log.levels.INFO)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local repo_root = git.get_repo_root(filepath)
|
|
||||||
|
|
||||||
local diff_buf = vim.api.nvim_create_buf(false, true)
|
|
||||||
vim.api.nvim_buf_set_lines(diff_buf, 0, -1, false, diff_lines)
|
|
||||||
vim.api.nvim_set_option_value('buftype', 'nowrite', { buf = diff_buf })
|
|
||||||
vim.api.nvim_set_option_value('bufhidden', 'delete', { buf = diff_buf })
|
|
||||||
vim.api.nvim_set_option_value('swapfile', false, { buf = diff_buf })
|
|
||||||
vim.api.nvim_set_option_value('modifiable', false, { buf = diff_buf })
|
|
||||||
vim.api.nvim_set_option_value('filetype', 'diff', { buf = diff_buf })
|
|
||||||
vim.api.nvim_buf_set_name(diff_buf, 'diffs://' .. revision .. ':' .. rel_path)
|
|
||||||
if repo_root then
|
|
||||||
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
|
|
||||||
|
|
||||||
M.setup_diff_buf(diff_buf)
|
|
||||||
dbg('opened diff buffer %d for %s against %s', diff_buf, rel_path, revision)
|
|
||||||
|
|
||||||
vim.schedule(function()
|
|
||||||
require('diffs').attach(diff_buf)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@class diffs.GdiffFileOpts
|
|
||||||
---@field vertical? boolean
|
|
||||||
---@field staged? boolean
|
|
||||||
---@field untracked? boolean
|
|
||||||
---@field old_filepath? string
|
|
||||||
---@field hunk_position? { hunk_header: string, offset: integer }
|
|
||||||
|
|
||||||
---@param filepath string
|
|
||||||
---@param opts? diffs.GdiffFileOpts
|
|
||||||
function M.gdiff_file(filepath, opts)
|
|
||||||
opts = opts or {}
|
|
||||||
|
|
||||||
local rel_path = git.get_relative_path(filepath)
|
|
||||||
if not rel_path then
|
|
||||||
vim.notify('[diffs.nvim]: not in a git repository', vim.log.levels.ERROR)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local old_rel_path = opts.old_filepath and git.get_relative_path(opts.old_filepath) or rel_path
|
|
||||||
|
|
||||||
local old_lines, new_lines, err
|
|
||||||
local diff_label
|
|
||||||
|
|
||||||
if opts.untracked then
|
|
||||||
old_lines = {}
|
|
||||||
new_lines, err = git.get_working_content(filepath)
|
|
||||||
if not new_lines then
|
|
||||||
vim.notify('[diffs.nvim]: ' .. (err or 'cannot read file'), vim.log.levels.ERROR)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
diff_label = 'untracked'
|
|
||||||
elseif opts.staged then
|
|
||||||
old_lines, err = git.get_file_content('HEAD', opts.old_filepath or filepath)
|
|
||||||
if not old_lines then
|
|
||||||
old_lines = {}
|
|
||||||
end
|
|
||||||
new_lines, err = git.get_index_content(filepath)
|
|
||||||
if not new_lines then
|
|
||||||
new_lines = {}
|
|
||||||
end
|
|
||||||
diff_label = 'staged'
|
|
||||||
else
|
|
||||||
old_lines, err = git.get_index_content(opts.old_filepath or filepath)
|
|
||||||
if not old_lines then
|
|
||||||
old_lines, err = git.get_file_content('HEAD', opts.old_filepath or filepath)
|
|
||||||
if not old_lines then
|
|
||||||
old_lines = {}
|
|
||||||
diff_label = 'untracked'
|
|
||||||
else
|
|
||||||
diff_label = 'unstaged'
|
|
||||||
end
|
|
||||||
else
|
|
||||||
diff_label = 'unstaged'
|
|
||||||
end
|
|
||||||
new_lines, err = git.get_working_content(filepath)
|
|
||||||
if not new_lines then
|
|
||||||
new_lines = {}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local diff_lines = generate_unified_diff(old_lines, new_lines, old_rel_path, rel_path)
|
|
||||||
|
|
||||||
if #diff_lines == 0 then
|
|
||||||
vim.notify('[diffs.nvim]: no changes', vim.log.levels.INFO)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local repo_root = git.get_repo_root(filepath)
|
|
||||||
|
|
||||||
local diff_buf = vim.api.nvim_create_buf(false, true)
|
|
||||||
vim.api.nvim_buf_set_lines(diff_buf, 0, -1, false, diff_lines)
|
|
||||||
vim.api.nvim_set_option_value('buftype', 'nowrite', { buf = diff_buf })
|
|
||||||
vim.api.nvim_set_option_value('bufhidden', 'delete', { buf = diff_buf })
|
|
||||||
vim.api.nvim_set_option_value('swapfile', false, { buf = diff_buf })
|
|
||||||
vim.api.nvim_set_option_value('modifiable', false, { buf = diff_buf })
|
|
||||||
vim.api.nvim_set_option_value('filetype', 'diff', { buf = diff_buf })
|
|
||||||
vim.api.nvim_buf_set_name(diff_buf, 'diffs://' .. diff_label .. ':' .. rel_path)
|
|
||||||
if repo_root then
|
|
||||||
vim.api.nvim_buf_set_var(diff_buf, 'diffs_repo_root', repo_root)
|
|
||||||
end
|
|
||||||
if old_rel_path ~= rel_path then
|
|
||||||
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
|
|
||||||
|
|
||||||
if opts.hunk_position then
|
|
||||||
local target_line = M.find_hunk_line(diff_lines, opts.hunk_position)
|
|
||||||
if target_line then
|
|
||||||
vim.api.nvim_win_set_cursor(0, { target_line, 0 })
|
|
||||||
dbg('jumped to line %d for hunk', target_line)
|
|
||||||
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()
|
|
||||||
require('diffs').attach(diff_buf)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@class diffs.GdiffSectionOpts
|
|
||||||
---@field vertical? boolean
|
|
||||||
---@field staged? boolean
|
|
||||||
|
|
||||||
---@param repo_root string
|
|
||||||
---@param opts? diffs.GdiffSectionOpts
|
|
||||||
function M.gdiff_section(repo_root, opts)
|
|
||||||
opts = opts or {}
|
|
||||||
|
|
||||||
local cmd = { 'git', '-C', repo_root, 'diff', '--no-ext-diff', '--no-color' }
|
|
||||||
if opts.staged then
|
|
||||||
table.insert(cmd, '--cached')
|
|
||||||
end
|
|
||||||
|
|
||||||
local result = vim.fn.systemlist(cmd)
|
|
||||||
if vim.v.shell_error ~= 0 then
|
|
||||||
vim.notify('[diffs.nvim]: git diff failed', vim.log.levels.ERROR)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if #result == 0 then
|
|
||||||
vim.notify('[diffs.nvim]: no changes in section', vim.log.levels.INFO)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local diff_label = opts.staged and 'staged' or 'unstaged'
|
|
||||||
local diff_buf = vim.api.nvim_create_buf(false, true)
|
|
||||||
vim.api.nvim_buf_set_lines(diff_buf, 0, -1, false, result)
|
|
||||||
vim.api.nvim_set_option_value('buftype', 'nowrite', { buf = diff_buf })
|
|
||||||
vim.api.nvim_set_option_value('bufhidden', 'delete', { buf = diff_buf })
|
|
||||||
vim.api.nvim_set_option_value('swapfile', false, { buf = diff_buf })
|
|
||||||
vim.api.nvim_set_option_value('modifiable', false, { buf = diff_buf })
|
|
||||||
vim.api.nvim_set_option_value('filetype', 'diff', { buf = diff_buf })
|
|
||||||
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
|
|
||||||
|
|
||||||
M.setup_diff_buf(diff_buf)
|
|
||||||
dbg('opened section diff buffer %d (%s)', diff_buf, diff_label)
|
|
||||||
|
|
||||||
vim.schedule(function()
|
|
||||||
require('diffs').attach(diff_buf)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param bufnr integer
|
|
||||||
function M.read_buffer(bufnr)
|
|
||||||
local name = vim.api.nvim_buf_get_name(bufnr)
|
|
||||||
local url_body = name:match('^diffs://(.+)$')
|
|
||||||
if not url_body then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local label, path = url_body:match('^([^:]+):(.+)$')
|
|
||||||
if not label or not path then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local ok, repo_root = pcall(vim.api.nvim_buf_get_var, bufnr, 'diffs_repo_root')
|
|
||||||
if not ok or not repo_root then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local diff_lines
|
|
||||||
|
|
||||||
if path == 'all' then
|
|
||||||
local cmd = { 'git', '-C', repo_root, 'diff', '--no-ext-diff', '--no-color' }
|
|
||||||
if label == 'staged' then
|
|
||||||
table.insert(cmd, '--cached')
|
|
||||||
end
|
|
||||||
diff_lines = vim.fn.systemlist(cmd)
|
|
||||||
if vim.v.shell_error ~= 0 then
|
|
||||||
diff_lines = {}
|
|
||||||
end
|
|
||||||
else
|
|
||||||
local abs_path = repo_root .. '/' .. path
|
|
||||||
|
|
||||||
local old_ok, old_rel_path = pcall(vim.api.nvim_buf_get_var, bufnr, 'diffs_old_filepath')
|
|
||||||
local old_abs_path = old_ok and old_rel_path and (repo_root .. '/' .. old_rel_path) or abs_path
|
|
||||||
local old_name = old_ok and old_rel_path or path
|
|
||||||
|
|
||||||
local old_lines, new_lines
|
|
||||||
|
|
||||||
if label == 'untracked' then
|
|
||||||
old_lines = {}
|
|
||||||
new_lines = git.get_working_content(abs_path) or {}
|
|
||||||
elseif label == 'staged' then
|
|
||||||
old_lines = git.get_file_content('HEAD', old_abs_path) or {}
|
|
||||||
new_lines = git.get_index_content(abs_path) or {}
|
|
||||||
elseif label == 'unstaged' then
|
|
||||||
old_lines = git.get_index_content(old_abs_path)
|
|
||||||
if not old_lines then
|
|
||||||
old_lines = git.get_file_content('HEAD', old_abs_path) or {}
|
|
||||||
end
|
|
||||||
new_lines = git.get_working_content(abs_path) or {}
|
|
||||||
else
|
|
||||||
old_lines = git.get_file_content(label, abs_path) or {}
|
|
||||||
new_lines = git.get_working_content(abs_path) or {}
|
|
||||||
end
|
|
||||||
|
|
||||||
diff_lines = generate_unified_diff(old_lines, new_lines, old_name, path)
|
|
||||||
end
|
|
||||||
|
|
||||||
vim.api.nvim_set_option_value('modifiable', true, { buf = bufnr })
|
|
||||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, diff_lines)
|
|
||||||
vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr })
|
|
||||||
vim.api.nvim_set_option_value('buftype', 'nowrite', { buf = bufnr })
|
|
||||||
vim.api.nvim_set_option_value('bufhidden', 'delete', { buf = bufnr })
|
|
||||||
vim.api.nvim_set_option_value('swapfile', false, { buf = bufnr })
|
|
||||||
vim.api.nvim_set_option_value('filetype', 'diff', { buf = bufnr })
|
|
||||||
|
|
||||||
dbg('reloaded diff buffer %d (%s:%s)', bufnr, label, path)
|
|
||||||
|
|
||||||
require('diffs').attach(bufnr)
|
|
||||||
end
|
|
||||||
|
|
||||||
function M.setup()
|
|
||||||
vim.api.nvim_create_user_command('Gdiff', function(opts)
|
|
||||||
M.gdiff(opts.args ~= '' and opts.args or nil, false)
|
|
||||||
end, {
|
|
||||||
nargs = '?',
|
|
||||||
desc = 'Show unified diff against git revision (default: HEAD)',
|
|
||||||
})
|
|
||||||
|
|
||||||
vim.api.nvim_create_user_command('Gvdiff', function(opts)
|
|
||||||
M.gdiff(opts.args ~= '' and opts.args or nil, true)
|
|
||||||
end, {
|
|
||||||
nargs = '?',
|
|
||||||
desc = 'Show unified diff against git revision in vertical split',
|
|
||||||
})
|
|
||||||
|
|
||||||
vim.api.nvim_create_user_command('Ghdiff', function(opts)
|
|
||||||
M.gdiff(opts.args ~= '' and opts.args or nil, false)
|
|
||||||
end, {
|
|
||||||
nargs = '?',
|
|
||||||
desc = 'Show unified diff against git revision in horizontal split',
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
return M
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
local M = {}
|
|
||||||
|
|
||||||
local ns = vim.api.nvim_create_namespace('diffs')
|
|
||||||
|
|
||||||
function M.dump()
|
|
||||||
local bufnr = vim.api.nvim_get_current_buf()
|
|
||||||
local marks = vim.api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, { details = true })
|
|
||||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
|
||||||
|
|
||||||
local by_line = {}
|
|
||||||
for _, mark in ipairs(marks) do
|
|
||||||
local id, row, col, details = mark[1], mark[2], mark[3], mark[4]
|
|
||||||
local entry = {
|
|
||||||
id = id,
|
|
||||||
row = row,
|
|
||||||
col = col,
|
|
||||||
end_row = details.end_row,
|
|
||||||
end_col = details.end_col,
|
|
||||||
hl_group = details.hl_group,
|
|
||||||
priority = details.priority,
|
|
||||||
hl_eol = details.hl_eol,
|
|
||||||
line_hl_group = details.line_hl_group,
|
|
||||||
number_hl_group = details.number_hl_group,
|
|
||||||
virt_text = details.virt_text,
|
|
||||||
}
|
|
||||||
local key = tostring(row)
|
|
||||||
if not by_line[key] then
|
|
||||||
by_line[key] = { text = lines[row + 1] or '', marks = {} }
|
|
||||||
end
|
|
||||||
table.insert(by_line[key].marks, entry)
|
|
||||||
end
|
|
||||||
|
|
||||||
local all_ns_marks = vim.api.nvim_buf_get_extmarks(bufnr, -1, 0, -1, { details = true })
|
|
||||||
local non_diffs = {}
|
|
||||||
for _, mark in ipairs(all_ns_marks) do
|
|
||||||
local details = mark[4]
|
|
||||||
if details.ns_id ~= ns then
|
|
||||||
table.insert(non_diffs, {
|
|
||||||
ns_id = details.ns_id,
|
|
||||||
row = mark[2],
|
|
||||||
col = mark[3],
|
|
||||||
end_row = details.end_row,
|
|
||||||
end_col = details.end_col,
|
|
||||||
hl_group = details.hl_group,
|
|
||||||
priority = details.priority,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local result = {
|
|
||||||
bufnr = bufnr,
|
|
||||||
buf_name = vim.api.nvim_buf_get_name(bufnr),
|
|
||||||
ns_id = ns,
|
|
||||||
total_diffs_marks = #marks,
|
|
||||||
total_all_marks = #all_ns_marks,
|
|
||||||
non_diffs_marks = non_diffs,
|
|
||||||
lines = by_line,
|
|
||||||
}
|
|
||||||
|
|
||||||
local state_dir = vim.fn.stdpath('state')
|
|
||||||
local path = state_dir .. '/diffs_debug.json'
|
|
||||||
local f = io.open(path, 'w')
|
|
||||||
if f then
|
|
||||||
f:write(vim.json.encode(result))
|
|
||||||
f:close()
|
|
||||||
vim.notify('[diffs.nvim] debug dump: ' .. path, vim.log.levels.INFO)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return M
|
|
||||||
|
|
@ -1,401 +0,0 @@
|
||||||
---@class diffs.CharSpan
|
|
||||||
---@field line integer
|
|
||||||
---@field col_start integer
|
|
||||||
---@field col_end integer
|
|
||||||
|
|
||||||
---@class diffs.IntraChanges
|
|
||||||
---@field add_spans diffs.CharSpan[]
|
|
||||||
---@field del_spans diffs.CharSpan[]
|
|
||||||
|
|
||||||
---@class diffs.ChangeGroup
|
|
||||||
---@field del_lines {idx: integer, text: string}[]
|
|
||||||
---@field add_lines {idx: integer, text: string}[]
|
|
||||||
|
|
||||||
---@class diffs.DiffOpts
|
|
||||||
---@field algorithm? string
|
|
||||||
---@field linematch? integer
|
|
||||||
|
|
||||||
local M = {}
|
|
||||||
|
|
||||||
local dbg = require('diffs.log').dbg
|
|
||||||
|
|
||||||
---@param hunk_lines string[]
|
|
||||||
---@return diffs.ChangeGroup[]
|
|
||||||
function M.extract_change_groups(hunk_lines)
|
|
||||||
---@type diffs.ChangeGroup[]
|
|
||||||
local groups = {}
|
|
||||||
---@type {idx: integer, text: string}[]
|
|
||||||
local del_buf = {}
|
|
||||||
---@type {idx: integer, text: string}[]
|
|
||||||
local add_buf = {}
|
|
||||||
|
|
||||||
---@type boolean
|
|
||||||
local in_del = false
|
|
||||||
|
|
||||||
for i, line in ipairs(hunk_lines) do
|
|
||||||
local prefix = line:sub(1, 1)
|
|
||||||
if prefix == '-' then
|
|
||||||
if not in_del and #add_buf > 0 then
|
|
||||||
if #del_buf > 0 then
|
|
||||||
table.insert(groups, { del_lines = del_buf, add_lines = add_buf })
|
|
||||||
end
|
|
||||||
del_buf = {}
|
|
||||||
add_buf = {}
|
|
||||||
end
|
|
||||||
in_del = true
|
|
||||||
table.insert(del_buf, { idx = i, text = line:sub(2) })
|
|
||||||
elseif prefix == '+' then
|
|
||||||
in_del = false
|
|
||||||
table.insert(add_buf, { idx = i, text = line:sub(2) })
|
|
||||||
else
|
|
||||||
if #del_buf > 0 and #add_buf > 0 then
|
|
||||||
table.insert(groups, { del_lines = del_buf, add_lines = add_buf })
|
|
||||||
end
|
|
||||||
del_buf = {}
|
|
||||||
add_buf = {}
|
|
||||||
in_del = false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if #del_buf > 0 and #add_buf > 0 then
|
|
||||||
table.insert(groups, { del_lines = del_buf, add_lines = add_buf })
|
|
||||||
end
|
|
||||||
|
|
||||||
return groups
|
|
||||||
end
|
|
||||||
|
|
||||||
---@return diffs.DiffOpts
|
|
||||||
local function parse_diffopt()
|
|
||||||
local opts = {}
|
|
||||||
for _, item in ipairs(vim.split(vim.o.diffopt, ',')) do
|
|
||||||
local key, val = item:match('^(%w+):(.+)$')
|
|
||||||
if key == 'algorithm' then
|
|
||||||
opts.algorithm = val
|
|
||||||
elseif key == 'linematch' then
|
|
||||||
opts.linematch = tonumber(val)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return opts
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param old_text string
|
|
||||||
---@param new_text string
|
|
||||||
---@param diff_opts? diffs.DiffOpts
|
|
||||||
---@return {old_start: integer, old_count: integer, new_start: integer, new_count: integer}[]
|
|
||||||
local function byte_diff(old_text, new_text, diff_opts)
|
|
||||||
local vim_opts = { result_type = 'indices' }
|
|
||||||
if diff_opts then
|
|
||||||
if diff_opts.algorithm then
|
|
||||||
vim_opts.algorithm = diff_opts.algorithm
|
|
||||||
end
|
|
||||||
if diff_opts.linematch then
|
|
||||||
vim_opts.linematch = diff_opts.linematch
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local ok, result = pcall(vim.diff, old_text, new_text, vim_opts)
|
|
||||||
if not ok or not result then
|
|
||||||
return {}
|
|
||||||
end
|
|
||||||
---@type {old_start: integer, old_count: integer, new_start: integer, new_count: integer}[]
|
|
||||||
local hunks = {}
|
|
||||||
for _, h in ipairs(result) do
|
|
||||||
table.insert(hunks, {
|
|
||||||
old_start = h[1],
|
|
||||||
old_count = h[2],
|
|
||||||
new_start = h[3],
|
|
||||||
new_count = h[4],
|
|
||||||
})
|
|
||||||
end
|
|
||||||
return hunks
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param s string
|
|
||||||
---@return string[]
|
|
||||||
local function split_bytes(s)
|
|
||||||
local bytes = {}
|
|
||||||
for i = 1, #s do
|
|
||||||
table.insert(bytes, s:sub(i, i))
|
|
||||||
end
|
|
||||||
return bytes
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param old_line string
|
|
||||||
---@param new_line string
|
|
||||||
---@param del_idx integer
|
|
||||||
---@param add_idx integer
|
|
||||||
---@param diff_opts? diffs.DiffOpts
|
|
||||||
---@return diffs.CharSpan[], diffs.CharSpan[]
|
|
||||||
local function char_diff_pair(old_line, new_line, del_idx, add_idx, diff_opts)
|
|
||||||
---@type diffs.CharSpan[]
|
|
||||||
local del_spans = {}
|
|
||||||
---@type diffs.CharSpan[]
|
|
||||||
local add_spans = {}
|
|
||||||
|
|
||||||
local old_bytes = split_bytes(old_line)
|
|
||||||
local new_bytes = split_bytes(new_line)
|
|
||||||
|
|
||||||
local old_text = table.concat(old_bytes, '\n') .. '\n'
|
|
||||||
local new_text = table.concat(new_bytes, '\n') .. '\n'
|
|
||||||
|
|
||||||
local char_opts = diff_opts
|
|
||||||
if diff_opts and diff_opts.linematch then
|
|
||||||
char_opts = { algorithm = diff_opts.algorithm }
|
|
||||||
end
|
|
||||||
|
|
||||||
local char_hunks = byte_diff(old_text, new_text, char_opts)
|
|
||||||
|
|
||||||
for _, ch in ipairs(char_hunks) do
|
|
||||||
if ch.old_count > 0 then
|
|
||||||
table.insert(del_spans, {
|
|
||||||
line = del_idx,
|
|
||||||
col_start = ch.old_start,
|
|
||||||
col_end = ch.old_start + ch.old_count,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
if ch.new_count > 0 then
|
|
||||||
table.insert(add_spans, {
|
|
||||||
line = add_idx,
|
|
||||||
col_start = ch.new_start,
|
|
||||||
col_end = ch.new_start + ch.new_count,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return del_spans, add_spans
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param group diffs.ChangeGroup
|
|
||||||
---@param diff_opts? diffs.DiffOpts
|
|
||||||
---@return diffs.CharSpan[], diffs.CharSpan[]
|
|
||||||
local function diff_group_native(group, diff_opts)
|
|
||||||
---@type diffs.CharSpan[]
|
|
||||||
local all_del = {}
|
|
||||||
---@type diffs.CharSpan[]
|
|
||||||
local all_add = {}
|
|
||||||
|
|
||||||
local del_count = #group.del_lines
|
|
||||||
local add_count = #group.add_lines
|
|
||||||
|
|
||||||
if del_count == 1 and add_count == 1 then
|
|
||||||
local ds, as = char_diff_pair(
|
|
||||||
group.del_lines[1].text,
|
|
||||||
group.add_lines[1].text,
|
|
||||||
group.del_lines[1].idx,
|
|
||||||
group.add_lines[1].idx,
|
|
||||||
diff_opts
|
|
||||||
)
|
|
||||||
vim.list_extend(all_del, ds)
|
|
||||||
vim.list_extend(all_add, as)
|
|
||||||
return all_del, all_add
|
|
||||||
end
|
|
||||||
|
|
||||||
local old_texts = {}
|
|
||||||
for _, l in ipairs(group.del_lines) do
|
|
||||||
table.insert(old_texts, l.text)
|
|
||||||
end
|
|
||||||
local new_texts = {}
|
|
||||||
for _, l in ipairs(group.add_lines) do
|
|
||||||
table.insert(new_texts, l.text)
|
|
||||||
end
|
|
||||||
|
|
||||||
local old_block = table.concat(old_texts, '\n') .. '\n'
|
|
||||||
local new_block = table.concat(new_texts, '\n') .. '\n'
|
|
||||||
|
|
||||||
local line_hunks = byte_diff(old_block, new_block, diff_opts)
|
|
||||||
|
|
||||||
---@type table<integer, integer>
|
|
||||||
local old_to_new = {}
|
|
||||||
for _, lh in ipairs(line_hunks) do
|
|
||||||
if lh.old_count == lh.new_count then
|
|
||||||
for k = 0, lh.old_count - 1 do
|
|
||||||
old_to_new[lh.old_start + k] = lh.new_start + k
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
for old_i, new_i in pairs(old_to_new) do
|
|
||||||
if group.del_lines[old_i] and group.add_lines[new_i] then
|
|
||||||
local ds, as = char_diff_pair(
|
|
||||||
group.del_lines[old_i].text,
|
|
||||||
group.add_lines[new_i].text,
|
|
||||||
group.del_lines[old_i].idx,
|
|
||||||
group.add_lines[new_i].idx,
|
|
||||||
diff_opts
|
|
||||||
)
|
|
||||||
vim.list_extend(all_del, ds)
|
|
||||||
vim.list_extend(all_add, as)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, lh in ipairs(line_hunks) do
|
|
||||||
if lh.old_count ~= lh.new_count then
|
|
||||||
local pairs_count = math.min(lh.old_count, lh.new_count)
|
|
||||||
for k = 0, pairs_count - 1 do
|
|
||||||
local oi = lh.old_start + k
|
|
||||||
local ni = lh.new_start + k
|
|
||||||
if group.del_lines[oi] and group.add_lines[ni] then
|
|
||||||
local ds, as = char_diff_pair(
|
|
||||||
group.del_lines[oi].text,
|
|
||||||
group.add_lines[ni].text,
|
|
||||||
group.del_lines[oi].idx,
|
|
||||||
group.add_lines[ni].idx,
|
|
||||||
diff_opts
|
|
||||||
)
|
|
||||||
vim.list_extend(all_del, ds)
|
|
||||||
vim.list_extend(all_add, as)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return all_del, all_add
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param group diffs.ChangeGroup
|
|
||||||
---@param handle table
|
|
||||||
---@return diffs.CharSpan[], diffs.CharSpan[]
|
|
||||||
local function diff_group_vscode(group, handle)
|
|
||||||
---@type diffs.CharSpan[]
|
|
||||||
local all_del = {}
|
|
||||||
---@type diffs.CharSpan[]
|
|
||||||
local all_add = {}
|
|
||||||
|
|
||||||
local ffi = require('ffi')
|
|
||||||
|
|
||||||
local old_texts = {}
|
|
||||||
for _, l in ipairs(group.del_lines) do
|
|
||||||
table.insert(old_texts, l.text)
|
|
||||||
end
|
|
||||||
local new_texts = {}
|
|
||||||
for _, l in ipairs(group.add_lines) do
|
|
||||||
table.insert(new_texts, l.text)
|
|
||||||
end
|
|
||||||
|
|
||||||
local orig_arr = ffi.new('const char*[?]', #old_texts)
|
|
||||||
for i, t in ipairs(old_texts) do
|
|
||||||
orig_arr[i - 1] = t
|
|
||||||
end
|
|
||||||
|
|
||||||
local mod_arr = ffi.new('const char*[?]', #new_texts)
|
|
||||||
for i, t in ipairs(new_texts) do
|
|
||||||
mod_arr[i - 1] = t
|
|
||||||
end
|
|
||||||
|
|
||||||
local opts = ffi.new('DiffsDiffOptions', {
|
|
||||||
ignore_trim_whitespace = false,
|
|
||||||
max_computation_time_ms = 1000,
|
|
||||||
compute_moves = false,
|
|
||||||
extend_to_subwords = false,
|
|
||||||
})
|
|
||||||
|
|
||||||
local result = handle.compute_diff(orig_arr, #old_texts, mod_arr, #new_texts, opts)
|
|
||||||
if result == nil then
|
|
||||||
return all_del, all_add
|
|
||||||
end
|
|
||||||
|
|
||||||
for ci = 0, result.changes.count - 1 do
|
|
||||||
local mapping = result.changes.mappings[ci]
|
|
||||||
for ii = 0, mapping.inner_change_count - 1 do
|
|
||||||
local inner = mapping.inner_changes[ii]
|
|
||||||
|
|
||||||
local orig_line = inner.original.start_line
|
|
||||||
if group.del_lines[orig_line] then
|
|
||||||
table.insert(all_del, {
|
|
||||||
line = group.del_lines[orig_line].idx,
|
|
||||||
col_start = inner.original.start_col,
|
|
||||||
col_end = inner.original.end_col,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
local mod_line = inner.modified.start_line
|
|
||||||
if group.add_lines[mod_line] then
|
|
||||||
table.insert(all_add, {
|
|
||||||
line = group.add_lines[mod_line].idx,
|
|
||||||
col_start = inner.modified.start_col,
|
|
||||||
col_end = inner.modified.end_col,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
handle.free_lines_diff(result)
|
|
||||||
|
|
||||||
return all_del, all_add
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param hunk_lines string[]
|
|
||||||
---@param algorithm? string
|
|
||||||
---@return diffs.IntraChanges?
|
|
||||||
function M.compute_intra_hunks(hunk_lines, algorithm)
|
|
||||||
local groups = M.extract_change_groups(hunk_lines)
|
|
||||||
if #groups == 0 then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
algorithm = algorithm or 'default'
|
|
||||||
|
|
||||||
local vscode_handle = nil
|
|
||||||
if algorithm == 'vscode' then
|
|
||||||
vscode_handle = require('diffs.lib').load()
|
|
||||||
if not vscode_handle then
|
|
||||||
dbg('vscode algorithm requested but library not available, falling back to default')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@type diffs.DiffOpts?
|
|
||||||
local diff_opts = nil
|
|
||||||
if not vscode_handle then
|
|
||||||
diff_opts = parse_diffopt()
|
|
||||||
if diff_opts.algorithm then
|
|
||||||
dbg('diffopt algorithm: %s', diff_opts.algorithm)
|
|
||||||
end
|
|
||||||
if diff_opts.linematch then
|
|
||||||
dbg('diffopt linematch: %d', diff_opts.linematch)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@type diffs.CharSpan[]
|
|
||||||
local all_add = {}
|
|
||||||
---@type diffs.CharSpan[]
|
|
||||||
local all_del = {}
|
|
||||||
|
|
||||||
dbg(
|
|
||||||
'intra: %d change groups, algorithm=%s, vscode=%s',
|
|
||||||
#groups,
|
|
||||||
algorithm,
|
|
||||||
vscode_handle and 'yes' or 'no'
|
|
||||||
)
|
|
||||||
|
|
||||||
for gi, group in ipairs(groups) do
|
|
||||||
dbg('group %d: %d del lines, %d add lines', gi, #group.del_lines, #group.add_lines)
|
|
||||||
local ds, as
|
|
||||||
if vscode_handle then
|
|
||||||
ds, as = diff_group_vscode(group, vscode_handle)
|
|
||||||
else
|
|
||||||
ds, as = diff_group_native(group, diff_opts)
|
|
||||||
end
|
|
||||||
dbg('group %d result: %d del spans, %d add spans', gi, #ds, #as)
|
|
||||||
for _, s in ipairs(ds) do
|
|
||||||
dbg(' del span: line=%d col=%d..%d', s.line, s.col_start, s.col_end)
|
|
||||||
end
|
|
||||||
for _, s in ipairs(as) do
|
|
||||||
dbg(' add span: line=%d col=%d..%d', s.line, s.col_start, s.col_end)
|
|
||||||
end
|
|
||||||
vim.list_extend(all_del, ds)
|
|
||||||
vim.list_extend(all_add, as)
|
|
||||||
end
|
|
||||||
|
|
||||||
if #all_add == 0 and #all_del == 0 then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
return { add_spans = all_add, del_spans = all_del }
|
|
||||||
end
|
|
||||||
|
|
||||||
---@return boolean
|
|
||||||
function M.has_vscode()
|
|
||||||
return require('diffs.lib').has_lib()
|
|
||||||
end
|
|
||||||
|
|
||||||
return M
|
|
||||||
|
|
@ -1,218 +0,0 @@
|
||||||
local M = {}
|
|
||||||
|
|
||||||
local commands = require('diffs.commands')
|
|
||||||
local git = require('diffs.git')
|
|
||||||
local dbg = require('diffs.log').dbg
|
|
||||||
|
|
||||||
---@alias diffs.FugitiveSection 'staged' | 'unstaged' | 'untracked' | nil
|
|
||||||
|
|
||||||
---@param bufnr integer
|
|
||||||
---@param lnum integer
|
|
||||||
---@return diffs.FugitiveSection
|
|
||||||
function M.get_section_at_line(bufnr, lnum)
|
|
||||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, lnum, false)
|
|
||||||
|
|
||||||
for i = #lines, 1, -1 do
|
|
||||||
local line = lines[i]
|
|
||||||
if line:match('^Staged ') then
|
|
||||||
return 'staged'
|
|
||||||
elseif line:match('^Unstaged ') then
|
|
||||||
return 'unstaged'
|
|
||||||
elseif line:match('^Untracked ') then
|
|
||||||
return 'untracked'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param line string
|
|
||||||
---@return string?, string?
|
|
||||||
local function parse_file_line(line)
|
|
||||||
local old, new = line:match('^R%d*%s+(.-)%s+->%s+(.+)$')
|
|
||||||
if old and new then
|
|
||||||
return vim.trim(new), vim.trim(old)
|
|
||||||
end
|
|
||||||
|
|
||||||
local filename = line:match('^[MADRCU?][MADRCU%s]*%s+(.+)$')
|
|
||||||
if filename then
|
|
||||||
return vim.trim(filename), nil
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param line string
|
|
||||||
---@return diffs.FugitiveSection?
|
|
||||||
local function parse_section_header(line)
|
|
||||||
if line:match('^Staged %(%d') then
|
|
||||||
return 'staged'
|
|
||||||
elseif line:match('^Unstaged %(%d') then
|
|
||||||
return 'unstaged'
|
|
||||||
elseif line:match('^Untracked %(%d') then
|
|
||||||
return 'untracked'
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param bufnr integer
|
|
||||||
---@param lnum integer
|
|
||||||
---@return string?, diffs.FugitiveSection, boolean, string?
|
|
||||||
function M.get_file_at_line(bufnr, lnum)
|
|
||||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
|
||||||
local current_line = lines[lnum]
|
|
||||||
|
|
||||||
if not current_line then
|
|
||||||
return nil, nil, false, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local section_header = parse_section_header(current_line)
|
|
||||||
if section_header then
|
|
||||||
return nil, section_header, true, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local filename, old_filename = parse_file_line(current_line)
|
|
||||||
if filename then
|
|
||||||
local section = M.get_section_at_line(bufnr, lnum)
|
|
||||||
return filename, section, false, old_filename
|
|
||||||
end
|
|
||||||
|
|
||||||
local prefix = current_line:sub(1, 1)
|
|
||||||
if prefix == '+' or prefix == '-' or prefix == ' ' then
|
|
||||||
for i = lnum - 1, 1, -1 do
|
|
||||||
local prev_line = lines[i]
|
|
||||||
filename, old_filename = parse_file_line(prev_line)
|
|
||||||
if filename then
|
|
||||||
local section = M.get_section_at_line(bufnr, i)
|
|
||||||
return filename, section, false, old_filename
|
|
||||||
end
|
|
||||||
if prev_line:match('^%w+ %(') or prev_line == '' then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil, nil, false, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
---@class diffs.HunkPosition
|
|
||||||
---@field hunk_header string
|
|
||||||
---@field offset integer
|
|
||||||
|
|
||||||
---@param bufnr integer
|
|
||||||
---@param lnum integer
|
|
||||||
---@return diffs.HunkPosition?
|
|
||||||
function M.get_hunk_position(bufnr, lnum)
|
|
||||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, lnum, false)
|
|
||||||
local current = lines[lnum]
|
|
||||||
|
|
||||||
if not current then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local prefix = current:sub(1, 1)
|
|
||||||
if prefix ~= '+' and prefix ~= '-' and prefix ~= ' ' then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
for i = lnum - 1, 1, -1 do
|
|
||||||
local line = lines[i]
|
|
||||||
if line:match('^@@.-@@') then
|
|
||||||
return {
|
|
||||||
hunk_header = line,
|
|
||||||
offset = lnum - i,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
if line:match('^[MADRCU?!]%s') or line:match('^%w+ %(') then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param bufnr integer
|
|
||||||
---@return string?
|
|
||||||
local function get_repo_root_from_fugitive(bufnr)
|
|
||||||
local bufname = vim.api.nvim_buf_get_name(bufnr)
|
|
||||||
local fugitive_path = bufname:match('^fugitive://(.+)///')
|
|
||||||
if fugitive_path then
|
|
||||||
return fugitive_path
|
|
||||||
end
|
|
||||||
|
|
||||||
local cwd = vim.fn.getcwd()
|
|
||||||
local root = git.get_repo_root(cwd .. '/.')
|
|
||||||
return root
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param vertical boolean
|
|
||||||
function M.diff_file_under_cursor(vertical)
|
|
||||||
local bufnr = vim.api.nvim_get_current_buf()
|
|
||||||
local lnum = vim.api.nvim_win_get_cursor(0)[1]
|
|
||||||
|
|
||||||
local filename, section, is_header, old_filename = M.get_file_at_line(bufnr, lnum)
|
|
||||||
|
|
||||||
local repo_root = get_repo_root_from_fugitive(bufnr)
|
|
||||||
if not repo_root then
|
|
||||||
vim.notify('[diffs.nvim]: could not determine repository root', vim.log.levels.ERROR)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if is_header then
|
|
||||||
dbg('diff_section: %s', section or 'unknown')
|
|
||||||
if section == 'untracked' then
|
|
||||||
vim.notify('[diffs.nvim]: cannot diff untracked section', vim.log.levels.WARN)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
commands.gdiff_section(repo_root, {
|
|
||||||
vertical = vertical,
|
|
||||||
staged = section == 'staged',
|
|
||||||
})
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if not filename then
|
|
||||||
vim.notify('[diffs.nvim]: no file under cursor', vim.log.levels.WARN)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local filepath = repo_root .. '/' .. filename
|
|
||||||
local old_filepath = old_filename and (repo_root .. '/' .. old_filename) or nil
|
|
||||||
local hunk_position = M.get_hunk_position(bufnr, lnum)
|
|
||||||
|
|
||||||
dbg(
|
|
||||||
'diff_file_under_cursor: %s (section: %s, old: %s, hunk_offset: %s)',
|
|
||||||
filename,
|
|
||||||
section or 'unknown',
|
|
||||||
old_filename or 'none',
|
|
||||||
hunk_position and tostring(hunk_position.offset) or 'none'
|
|
||||||
)
|
|
||||||
|
|
||||||
commands.gdiff_file(filepath, {
|
|
||||||
vertical = vertical,
|
|
||||||
staged = section == 'staged',
|
|
||||||
untracked = section == 'untracked',
|
|
||||||
old_filepath = old_filepath,
|
|
||||||
hunk_position = hunk_position,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param bufnr integer
|
|
||||||
---@param config { horizontal: string|false, vertical: string|false }
|
|
||||||
function M.setup_keymaps(bufnr, config)
|
|
||||||
if config.horizontal and config.horizontal ~= '' then
|
|
||||||
vim.keymap.set('n', config.horizontal, function()
|
|
||||||
M.diff_file_under_cursor(false)
|
|
||||||
end, { buffer = bufnr, desc = 'Unified diff (horizontal)' })
|
|
||||||
dbg('set keymap %s for buffer %d', config.horizontal, bufnr)
|
|
||||||
end
|
|
||||||
|
|
||||||
if config.vertical and config.vertical ~= '' then
|
|
||||||
vim.keymap.set('n', config.vertical, function()
|
|
||||||
M.diff_file_under_cursor(true)
|
|
||||||
end, { buffer = bufnr, desc = 'Unified diff (vertical)' })
|
|
||||||
dbg('set keymap %s for buffer %d', config.vertical, bufnr)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return M
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
local M = {}
|
|
||||||
|
|
||||||
---@param filepath string
|
|
||||||
---@return string?
|
|
||||||
function M.get_repo_root(filepath)
|
|
||||||
local dir = vim.fn.fnamemodify(filepath, ':h')
|
|
||||||
local result = vim.fn.systemlist({ 'git', '-C', dir, 'rev-parse', '--show-toplevel' })
|
|
||||||
if vim.v.shell_error ~= 0 then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
return result[1]
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param revision string
|
|
||||||
---@param filepath string
|
|
||||||
---@return string[]?, string?
|
|
||||||
function M.get_file_content(revision, filepath)
|
|
||||||
local repo_root = M.get_repo_root(filepath)
|
|
||||||
if not repo_root then
|
|
||||||
return nil, 'not in a git repository'
|
|
||||||
end
|
|
||||||
|
|
||||||
local rel_path = vim.fn.fnamemodify(filepath, ':.')
|
|
||||||
if vim.startswith(filepath, repo_root) then
|
|
||||||
rel_path = filepath:sub(#repo_root + 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
local result = vim.fn.systemlist({ 'git', '-C', repo_root, 'show', revision .. ':' .. rel_path })
|
|
||||||
if vim.v.shell_error ~= 0 then
|
|
||||||
return nil, 'failed to get file at revision: ' .. revision
|
|
||||||
end
|
|
||||||
return result, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param filepath string
|
|
||||||
---@return string?
|
|
||||||
function M.get_relative_path(filepath)
|
|
||||||
local repo_root = M.get_repo_root(filepath)
|
|
||||||
if not repo_root then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
if vim.startswith(filepath, repo_root) then
|
|
||||||
return filepath:sub(#repo_root + 2)
|
|
||||||
end
|
|
||||||
return vim.fn.fnamemodify(filepath, ':.')
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param filepath string
|
|
||||||
---@return string[]?, string?
|
|
||||||
function M.get_index_content(filepath)
|
|
||||||
local repo_root = M.get_repo_root(filepath)
|
|
||||||
if not repo_root then
|
|
||||||
return nil, 'not in a git repository'
|
|
||||||
end
|
|
||||||
|
|
||||||
local rel_path = M.get_relative_path(filepath)
|
|
||||||
if not rel_path then
|
|
||||||
return nil, 'could not determine relative path'
|
|
||||||
end
|
|
||||||
|
|
||||||
local result = vim.fn.systemlist({ 'git', '-C', repo_root, 'show', ':0:' .. rel_path })
|
|
||||||
if vim.v.shell_error ~= 0 then
|
|
||||||
return nil, 'file not in index'
|
|
||||||
end
|
|
||||||
return result, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param filepath string
|
|
||||||
---@return string[]?, string?
|
|
||||||
function M.get_working_content(filepath)
|
|
||||||
if vim.fn.filereadable(filepath) ~= 1 then
|
|
||||||
return nil, 'file not readable'
|
|
||||||
end
|
|
||||||
local lines = vim.fn.readfile(filepath)
|
|
||||||
return lines, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param filepath string
|
|
||||||
---@return boolean
|
|
||||||
function M.file_exists_in_index(filepath)
|
|
||||||
local repo_root = M.get_repo_root(filepath)
|
|
||||||
if not repo_root then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
local rel_path = M.get_relative_path(filepath)
|
|
||||||
if not rel_path then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
vim.fn.system({ 'git', '-C', repo_root, 'ls-files', '--stage', '--', rel_path })
|
|
||||||
return vim.v.shell_error == 0
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param revision string
|
|
||||||
---@param filepath string
|
|
||||||
---@return boolean
|
|
||||||
function M.file_exists_at_revision(revision, filepath)
|
|
||||||
local repo_root = M.get_repo_root(filepath)
|
|
||||||
if not repo_root then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
local rel_path = M.get_relative_path(filepath)
|
|
||||||
if not rel_path then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
vim.fn.system({ 'git', '-C', repo_root, 'cat-file', '-e', revision .. ':' .. rel_path })
|
|
||||||
return vim.v.shell_error == 0
|
|
||||||
end
|
|
||||||
|
|
||||||
return M
|
|
||||||
|
|
@ -15,13 +15,6 @@ function M.check()
|
||||||
else
|
else
|
||||||
vim.health.warn('vim-fugitive not detected (required for unified diff highlighting)')
|
vim.health.warn('vim-fugitive not detected (required for unified diff highlighting)')
|
||||||
end
|
end
|
||||||
|
|
||||||
local lib = require('diffs.lib')
|
|
||||||
if lib.has_lib() then
|
|
||||||
vim.health.ok('libvscode_diff found at ' .. lib.lib_path())
|
|
||||||
else
|
|
||||||
vim.health.info('libvscode_diff not found (optional, using native vim.diff fallback)')
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,6 @@
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
local dbg = require('diffs.log').dbg
|
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
|
|
||||||
local PRIORITY_CHAR_BG = 201
|
|
||||||
|
|
||||||
---@param bufnr integer
|
---@param bufnr integer
|
||||||
---@param ns integer
|
---@param ns integer
|
||||||
|
|
@ -41,15 +8,9 @@ local PRIORITY_CHAR_BG = 201
|
||||||
---@param col_offset integer
|
---@param col_offset integer
|
||||||
---@param text string
|
---@param text string
|
||||||
---@param lang string
|
---@param lang string
|
||||||
---@param context_lines? string[]
|
|
||||||
---@return integer
|
---@return integer
|
||||||
local function highlight_text(bufnr, ns, hunk, col_offset, text, lang, context_lines)
|
local function highlight_text(bufnr, ns, hunk, col_offset, text, lang)
|
||||||
local parse_text = text
|
local ok, parser_obj = pcall(vim.treesitter.get_string_parser, text, lang)
|
||||||
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)
|
|
||||||
if not ok or not parser_obj then
|
if not ok or not parser_obj then
|
||||||
return 0
|
return 0
|
||||||
end
|
end
|
||||||
|
|
@ -67,26 +28,22 @@ local function highlight_text(bufnr, ns, hunk, col_offset, text, lang, context_l
|
||||||
local extmark_count = 0
|
local extmark_count = 0
|
||||||
local header_line = hunk.start_line - 1
|
local header_line = hunk.start_line - 1
|
||||||
|
|
||||||
for id, node, metadata in query:iter_captures(trees[1]:root(), parse_text) do
|
for id, node, _ in query:iter_captures(trees[1]:root(), text) do
|
||||||
local sr, sc, _, ec = node:range()
|
local capture_name = '@' .. query.captures[id]
|
||||||
if sr == 0 then
|
local sr, sc, er, ec = node:range()
|
||||||
local capture_name = '@' .. query.captures[id] .. '.' .. lang
|
|
||||||
|
|
||||||
local buf_sr = header_line
|
local buf_sr = header_line + sr
|
||||||
local buf_er = header_line
|
local buf_er = header_line + er
|
||||||
local buf_sc = col_offset + sc
|
local buf_sc = col_offset + sc
|
||||||
local buf_ec = col_offset + ec
|
local buf_ec = col_offset + ec
|
||||||
|
|
||||||
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,
|
||||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_sr, buf_sc, {
|
end_col = buf_ec,
|
||||||
end_row = buf_er,
|
hl_group = capture_name,
|
||||||
end_col = buf_ec,
|
priority = 200,
|
||||||
hl_group = capture_name,
|
})
|
||||||
priority = priority,
|
extmark_count = extmark_count + 1
|
||||||
})
|
|
||||||
extmark_count = extmark_count + 1
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return extmark_count
|
return extmark_count
|
||||||
|
|
@ -98,21 +55,15 @@ end
|
||||||
|
|
||||||
---@param bufnr integer
|
---@param bufnr integer
|
||||||
---@param ns integer
|
---@param ns integer
|
||||||
|
---@param hunk diffs.Hunk
|
||||||
---@param code_lines string[]
|
---@param code_lines string[]
|
||||||
---@param lang string
|
|
||||||
---@param line_map table<integer, integer>
|
|
||||||
---@param col_offset integer
|
|
||||||
---@param covered_lines? table<integer, true>
|
|
||||||
---@return integer
|
---@return integer
|
||||||
local function highlight_treesitter(
|
local function highlight_treesitter(bufnr, ns, hunk, code_lines)
|
||||||
bufnr,
|
local lang = hunk.lang
|
||||||
ns,
|
if not lang then
|
||||||
code_lines,
|
return 0
|
||||||
lang,
|
end
|
||||||
line_map,
|
|
||||||
col_offset,
|
|
||||||
covered_lines
|
|
||||||
)
|
|
||||||
local code = table.concat(code_lines, '\n')
|
local code = table.concat(code_lines, '\n')
|
||||||
if code == '' then
|
if code == '' then
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -124,50 +75,50 @@ local function highlight_treesitter(
|
||||||
return 0
|
return 0
|
||||||
end
|
end
|
||||||
|
|
||||||
local trees = parser_obj:parse(true)
|
local trees = parser_obj:parse()
|
||||||
if not trees or #trees == 0 then
|
if not trees or #trees == 0 then
|
||||||
dbg('parse returned no trees for lang: %s', lang)
|
dbg('parse returned no trees for lang: %s', lang)
|
||||||
return 0
|
return 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local query = vim.treesitter.query.get(lang, 'highlights')
|
||||||
|
if not query then
|
||||||
|
dbg('no highlights query for lang: %s', lang)
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
if hunk.header_context and hunk.header_context_col then
|
||||||
|
local header_line = hunk.start_line - 1
|
||||||
|
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, header_line, hunk.header_context_col, {
|
||||||
|
end_col = hunk.header_context_col + #hunk.header_context,
|
||||||
|
hl_group = 'Normal',
|
||||||
|
priority = 199,
|
||||||
|
})
|
||||||
|
local header_extmarks =
|
||||||
|
highlight_text(bufnr, ns, hunk, hunk.header_context_col, hunk.header_context, lang)
|
||||||
|
if header_extmarks > 0 then
|
||||||
|
dbg('header %s:%d applied %d extmarks', hunk.filename, hunk.start_line, header_extmarks)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local extmark_count = 0
|
local extmark_count = 0
|
||||||
parser_obj:for_each_tree(function(tree, ltree)
|
for id, node, _ in query:iter_captures(trees[1]:root(), code) do
|
||||||
local tree_lang = ltree:lang()
|
local capture_name = '@' .. query.captures[id]
|
||||||
local query = vim.treesitter.query.get(tree_lang, 'highlights')
|
local sr, sc, er, ec = node:range()
|
||||||
if not query then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
for id, node, metadata in query:iter_captures(tree:root(), code) do
|
local buf_sr = hunk.start_line + sr
|
||||||
local capture = query.captures[id]
|
local buf_er = hunk.start_line + er
|
||||||
if capture ~= 'spell' and capture ~= 'nospell' then
|
local buf_sc = sc + 1
|
||||||
local capture_name = '@' .. capture .. '.' .. tree_lang
|
local buf_ec = ec + 1
|
||||||
local sr, sc, er, ec = node:range()
|
|
||||||
|
|
||||||
local buf_sr = line_map[sr]
|
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_sr, buf_sc, {
|
||||||
if buf_sr then
|
end_row = buf_er,
|
||||||
local buf_er = line_map[er] or buf_sr
|
end_col = buf_ec,
|
||||||
|
hl_group = capture_name,
|
||||||
local buf_sc = sc + col_offset
|
priority = 200,
|
||||||
local buf_ec = ec + col_offset
|
})
|
||||||
|
extmark_count = extmark_count + 1
|
||||||
local priority = tree_lang == 'diff' and (tonumber(metadata.priority) or 100)
|
end
|
||||||
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
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
return extmark_count
|
return extmark_count
|
||||||
end
|
end
|
||||||
|
|
@ -217,10 +168,8 @@ end
|
||||||
---@param ns integer
|
---@param ns integer
|
||||||
---@param hunk diffs.Hunk
|
---@param hunk diffs.Hunk
|
||||||
---@param code_lines string[]
|
---@param code_lines string[]
|
||||||
---@param covered_lines? table<integer, true>
|
|
||||||
---@param leading_offset? integer
|
|
||||||
---@return 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)
|
||||||
local ft = hunk.ft
|
local ft = hunk.ft
|
||||||
if not ft then
|
if not ft then
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -230,8 +179,6 @@ local function highlight_vim_syntax(bufnr, ns, hunk, code_lines, covered_lines,
|
||||||
return 0
|
return 0
|
||||||
end
|
end
|
||||||
|
|
||||||
leading_offset = leading_offset or 0
|
|
||||||
|
|
||||||
local scratch = vim.api.nvim_create_buf(false, true)
|
local scratch = vim.api.nvim_create_buf(false, true)
|
||||||
vim.api.nvim_buf_set_lines(scratch, 0, -1, false, code_lines)
|
vim.api.nvim_buf_set_lines(scratch, 0, -1, false, code_lines)
|
||||||
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = scratch })
|
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = scratch })
|
||||||
|
|
@ -258,22 +205,15 @@ local function highlight_vim_syntax(bufnr, ns, hunk, code_lines, covered_lines,
|
||||||
|
|
||||||
vim.api.nvim_buf_delete(scratch, { force = true })
|
vim.api.nvim_buf_delete(scratch, { force = true })
|
||||||
|
|
||||||
local hunk_line_count = #hunk.lines
|
|
||||||
local extmark_count = 0
|
local extmark_count = 0
|
||||||
for _, span in ipairs(spans) do
|
for _, span in ipairs(spans) do
|
||||||
local adj = span.line - leading_offset
|
local buf_line = hunk.start_line + span.line - 1
|
||||||
if adj >= 1 and adj <= hunk_line_count then
|
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, span.col_start, {
|
||||||
local buf_line = hunk.start_line + adj - 1
|
end_col = span.col_end,
|
||||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, span.col_start, {
|
hl_group = span.hl_name,
|
||||||
end_col = span.col_end,
|
priority = 200,
|
||||||
hl_group = span.hl_name,
|
})
|
||||||
priority = PRIORITY_SYNTAX,
|
extmark_count = extmark_count + 1
|
||||||
})
|
|
||||||
extmark_count = extmark_count + 1
|
|
||||||
if covered_lines then
|
|
||||||
covered_lines[buf_line] = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return extmark_count
|
return extmark_count
|
||||||
|
|
@ -300,151 +240,24 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
|
||||||
use_vim = false
|
use_vim = false
|
||||||
end
|
end
|
||||||
|
|
||||||
---@type table<integer, true>
|
local apply_syntax = use_ts or use_vim
|
||||||
local covered_lines = {}
|
|
||||||
|
|
||||||
local ctx_cfg = opts.highlights.context
|
---@type string[]
|
||||||
local context = (ctx_cfg and ctx_cfg.enabled) and ctx_cfg.lines or 0
|
local code_lines = {}
|
||||||
local leading = {}
|
if apply_syntax then
|
||||||
local trailing = {}
|
for _, line in ipairs(hunk.lines) do
|
||||||
if (use_ts or use_vim) and context > 0 and hunk.file_new_start and hunk.repo_root then
|
table.insert(code_lines, line:sub(2))
|
||||||
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
|
end
|
||||||
local trail_from = hunk.file_new_start + (hunk.file_new_count or 0)
|
|
||||||
trailing = read_line_range(filepath, trail_from, context)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local extmark_count = 0
|
local extmark_count = 0
|
||||||
if use_ts then
|
if use_ts then
|
||||||
---@type string[]
|
extmark_count = highlight_treesitter(bufnr, ns, hunk, code_lines)
|
||||||
local new_code = {}
|
|
||||||
---@type table<integer, integer>
|
|
||||||
local new_map = {}
|
|
||||||
---@type string[]
|
|
||||||
local old_code = {}
|
|
||||||
---@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)
|
|
||||||
local buf_line = hunk.start_line + i - 1
|
|
||||||
|
|
||||||
if prefix == '+' then
|
|
||||||
new_map[#new_code] = buf_line
|
|
||||||
table.insert(new_code, stripped)
|
|
||||||
elseif prefix == '-' then
|
|
||||||
old_map[#old_code] = buf_line
|
|
||||||
table.insert(old_code, stripped)
|
|
||||||
else
|
|
||||||
new_map[#new_code] = buf_line
|
|
||||||
table.insert(new_code, stripped)
|
|
||||||
table.insert(old_code, stripped)
|
|
||||||
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)
|
|
||||||
|
|
||||||
if hunk.header_context and hunk.header_context_col then
|
|
||||||
local header_line = hunk.start_line - 1
|
|
||||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, header_line, hunk.header_context_col, {
|
|
||||||
end_col = hunk.header_context_col + #hunk.header_context,
|
|
||||||
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
|
|
||||||
)
|
|
||||||
if header_extmarks > 0 then
|
|
||||||
dbg('header %s:%d applied %d extmarks', hunk.filename, hunk.start_line, header_extmarks)
|
|
||||||
end
|
|
||||||
extmark_count = extmark_count + header_extmarks
|
|
||||||
end
|
|
||||||
elseif use_vim then
|
elseif use_vim then
|
||||||
---@type string[]
|
extmark_count = highlight_vim_syntax(bufnr, ns, hunk, code_lines)
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if
|
local syntax_applied = extmark_count > 0
|
||||||
hunk.header_start_line
|
|
||||||
and hunk.header_lines
|
|
||||||
and #hunk.header_lines > 0
|
|
||||||
and opts.highlights.treesitter.enabled
|
|
||||||
then
|
|
||||||
---@type table<integer, integer>
|
|
||||||
local header_map = {}
|
|
||||||
for i = 0, #hunk.header_lines - 1 do
|
|
||||||
header_map[i] = hunk.header_start_line - 1 + i
|
|
||||||
end
|
|
||||||
extmark_count = extmark_count
|
|
||||||
+ highlight_treesitter(bufnr, ns, hunk.header_lines, 'diff', header_map, 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@type diffs.IntraChanges?
|
|
||||||
local intra = nil
|
|
||||||
local intra_cfg = opts.highlights.intra
|
|
||||||
if intra_cfg and intra_cfg.enabled and #hunk.lines <= intra_cfg.max_lines then
|
|
||||||
dbg('computing intra for hunk %s:%d (%d lines)', hunk.filename, hunk.start_line, #hunk.lines)
|
|
||||||
intra = diff.compute_intra_hunks(hunk.lines, intra_cfg.algorithm)
|
|
||||||
if intra then
|
|
||||||
dbg('intra result: %d add spans, %d del spans', #intra.add_spans, #intra.del_spans)
|
|
||||||
else
|
|
||||||
dbg('intra result: nil (no change groups)')
|
|
||||||
end
|
|
||||||
elseif intra_cfg and not intra_cfg.enabled then
|
|
||||||
dbg('intra disabled by config')
|
|
||||||
elseif intra_cfg and #hunk.lines > intra_cfg.max_lines then
|
|
||||||
dbg('intra skipped: %d lines > %d max', #hunk.lines, intra_cfg.max_lines)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@type table<integer, diffs.CharSpan[]>
|
|
||||||
local char_spans_by_line = {}
|
|
||||||
if intra then
|
|
||||||
for _, span in ipairs(intra.add_spans) do
|
|
||||||
if not char_spans_by_line[span.line] then
|
|
||||||
char_spans_by_line[span.line] = {}
|
|
||||||
end
|
|
||||||
table.insert(char_spans_by_line[span.line], span)
|
|
||||||
end
|
|
||||||
for _, span in ipairs(intra.del_spans) do
|
|
||||||
if not char_spans_by_line[span.line] then
|
|
||||||
char_spans_by_line[span.line] = {}
|
|
||||||
end
|
|
||||||
table.insert(char_spans_by_line[span.line], span)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
for i, line in ipairs(hunk.lines) do
|
for i, line in ipairs(hunk.lines) do
|
||||||
local buf_line = hunk.start_line + i - 1
|
local buf_line = hunk.start_line + i - 1
|
||||||
|
|
@ -463,52 +276,24 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
if line_len > 1 and covered_lines[buf_line] then
|
if opts.highlights.background and is_diff_line then
|
||||||
|
local extmark_opts = {
|
||||||
|
line_hl_group = line_hl,
|
||||||
|
priority = 198,
|
||||||
|
}
|
||||||
|
if opts.highlights.gutter then
|
||||||
|
extmark_opts.number_hl_group = number_hl
|
||||||
|
end
|
||||||
|
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, extmark_opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
if line_len > 1 and syntax_applied then
|
||||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 1, {
|
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 1, {
|
||||||
end_col = line_len,
|
end_col = line_len,
|
||||||
hl_group = 'DiffsClear',
|
hl_group = 'Normal',
|
||||||
priority = PRIORITY_CLEAR,
|
priority = 199,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
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,
|
|
||||||
hl_group = line_hl,
|
|
||||||
hl_eol = true,
|
|
||||||
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
|
|
||||||
local char_hl = prefix == '+' and 'DiffsAddText' or 'DiffsDeleteText'
|
|
||||||
for _, span in ipairs(char_spans_by_line[i]) do
|
|
||||||
dbg(
|
|
||||||
'char extmark: line=%d buf_line=%d col=%d..%d hl=%s text="%s"',
|
|
||||||
i,
|
|
||||||
buf_line,
|
|
||||||
span.col_start,
|
|
||||||
span.col_end,
|
|
||||||
char_hl,
|
|
||||||
line:sub(span.col_start + 1, span.col_end)
|
|
||||||
)
|
|
||||||
local ok, err = pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, span.col_start, {
|
|
||||||
end_col = span.col_end,
|
|
||||||
hl_group = char_hl,
|
|
||||||
priority = PRIORITY_CHAR_BG,
|
|
||||||
})
|
|
||||||
if not ok then
|
|
||||||
dbg('char extmark FAILED: %s', err)
|
|
||||||
end
|
|
||||||
extmark_count = extmark_count + 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
dbg('hunk %s:%d applied %d extmarks', hunk.filename, hunk.start_line, extmark_count)
|
dbg('hunk %s:%d applied %d extmarks', hunk.filename, hunk.start_line, extmark_count)
|
||||||
|
|
|
||||||
|
|
@ -6,50 +6,17 @@
|
||||||
---@field enabled boolean
|
---@field enabled boolean
|
||||||
---@field max_lines integer
|
---@field max_lines integer
|
||||||
|
|
||||||
---@class diffs.IntraConfig
|
|
||||||
---@field enabled boolean
|
|
||||||
---@field algorithm string
|
|
||||||
---@field max_lines integer
|
|
||||||
|
|
||||||
---@class diffs.ContextConfig
|
|
||||||
---@field enabled boolean
|
|
||||||
---@field lines integer
|
|
||||||
|
|
||||||
---@class diffs.Highlights
|
---@class diffs.Highlights
|
||||||
---@field background boolean
|
---@field background boolean
|
||||||
---@field gutter boolean
|
---@field gutter boolean
|
||||||
---@field blend_alpha? number
|
|
||||||
---@field overrides? table<string, table>
|
|
||||||
---@field context diffs.ContextConfig
|
|
||||||
---@field treesitter diffs.TreesitterConfig
|
---@field treesitter diffs.TreesitterConfig
|
||||||
---@field vim diffs.VimConfig
|
---@field vim diffs.VimConfig
|
||||||
---@field intra diffs.IntraConfig
|
|
||||||
|
|
||||||
---@class diffs.FugitiveConfig
|
|
||||||
---@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
|
---@class diffs.Config
|
||||||
---@field debug boolean
|
---@field debug boolean
|
||||||
---@field debounce_ms integer
|
---@field debounce_ms integer
|
||||||
---@field hide_prefix boolean
|
---@field hide_prefix boolean
|
||||||
---@field highlights diffs.Highlights
|
---@field highlights diffs.Highlights
|
||||||
---@field fugitive diffs.FugitiveConfig
|
|
||||||
---@field conflict diffs.ConflictConfig
|
|
||||||
|
|
||||||
---@class diffs
|
---@class diffs
|
||||||
---@field attach fun(bufnr?: integer)
|
---@field attach fun(bufnr?: integer)
|
||||||
|
|
@ -102,10 +69,6 @@ local default_config = {
|
||||||
highlights = {
|
highlights = {
|
||||||
background = true,
|
background = true,
|
||||||
gutter = true,
|
gutter = true,
|
||||||
context = {
|
|
||||||
enabled = true,
|
|
||||||
lines = 25,
|
|
||||||
},
|
|
||||||
treesitter = {
|
treesitter = {
|
||||||
enabled = true,
|
enabled = true,
|
||||||
max_lines = 500,
|
max_lines = 500,
|
||||||
|
|
@ -114,28 +77,6 @@ local default_config = {
|
||||||
enabled = false,
|
enabled = false,
|
||||||
max_lines = 200,
|
max_lines = 200,
|
||||||
},
|
},
|
||||||
intra = {
|
|
||||||
enabled = true,
|
|
||||||
algorithm = 'default',
|
|
||||||
max_lines = 500,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,79 +163,18 @@ local function compute_highlight_groups()
|
||||||
local blended_add = blend_color(add_bg, bg, 0.4)
|
local blended_add = blend_color(add_bg, bg, 0.4)
|
||||||
local blended_del = blend_color(del_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)
|
|
||||||
|
|
||||||
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, 'DiffsAdd', { default = true, bg = blended_add })
|
||||||
vim.api.nvim_set_hl(0, 'DiffsDelete', { default = true, bg = blended_del })
|
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, 'DiffsAddNr', { default = true, fg = add_fg, bg = blended_add })
|
||||||
vim.api.nvim_set_hl(
|
vim.api.nvim_set_hl(0, 'DiffsDeleteNr', { default = true, fg = del_fg, bg = blended_del })
|
||||||
0,
|
|
||||||
'DiffsDeleteNr',
|
|
||||||
{ default = true, fg = blended_del_text, 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 })
|
|
||||||
|
|
||||||
dbg('highlight groups: Normal.bg=#%06x DiffAdd.bg=#%06x diffAdded.fg=#%06x', bg, add_bg, add_fg)
|
|
||||||
dbg(
|
|
||||||
'DiffsAdd.bg=#%06x DiffsAddText.bg=#%06x DiffsAddNr.fg=#%06x',
|
|
||||||
blended_add,
|
|
||||||
blended_add_text,
|
|
||||||
add_fg
|
|
||||||
)
|
|
||||||
dbg('DiffsDelete.bg=#%06x DiffsDeleteText.bg=#%06x', blended_del, blended_del_text)
|
|
||||||
|
|
||||||
local diff_change = resolve_hl('DiffChange')
|
local diff_change = resolve_hl('DiffChange')
|
||||||
local diff_text = resolve_hl('DiffText')
|
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, 'DiffsDiffAdd', { bg = diff_add.bg })
|
||||||
vim.api.nvim_set_hl(
|
vim.api.nvim_set_hl(0, 'DiffsDiffDelete', { fg = diff_delete.fg, bg = diff_delete.bg })
|
||||||
0,
|
vim.api.nvim_set_hl(0, 'DiffsDiffChange', { bg = diff_change.bg })
|
||||||
'DiffsDiffDelete',
|
vim.api.nvim_set_hl(0, 'DiffsDiffText', { bg = diff_text.bg })
|
||||||
{ 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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function init()
|
local function init()
|
||||||
|
|
@ -316,21 +196,10 @@ local function init()
|
||||||
vim.validate({
|
vim.validate({
|
||||||
['highlights.background'] = { opts.highlights.background, 'boolean', true },
|
['highlights.background'] = { opts.highlights.background, 'boolean', true },
|
||||||
['highlights.gutter'] = { opts.highlights.gutter, '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.treesitter'] = { opts.highlights.treesitter, 'table', true },
|
||||||
['highlights.vim'] = { opts.highlights.vim, '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
|
if opts.highlights.treesitter then
|
||||||
vim.validate({
|
vim.validate({
|
||||||
['highlights.treesitter.enabled'] = { opts.highlights.treesitter.enabled, 'boolean', true },
|
['highlights.treesitter.enabled'] = { opts.highlights.treesitter.enabled, 'boolean', true },
|
||||||
|
|
@ -348,76 +217,11 @@ local function init()
|
||||||
['highlights.vim.max_lines'] = { opts.highlights.vim.max_lines, 'number', true },
|
['highlights.vim.max_lines'] = { opts.highlights.vim.max_lines, 'number', true },
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
if opts.highlights.intra then
|
|
||||||
vim.validate({
|
|
||||||
['highlights.intra.enabled'] = { opts.highlights.intra.enabled, 'boolean', true },
|
|
||||||
['highlights.intra.algorithm'] = {
|
|
||||||
opts.highlights.intra.algorithm,
|
|
||||||
function(v)
|
|
||||||
return v == nil or v == 'default' or v == 'vscode'
|
|
||||||
end,
|
|
||||||
"'default' or 'vscode'",
|
|
||||||
},
|
|
||||||
['highlights.intra.max_lines'] = { opts.highlights.intra.max_lines, 'number', true },
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if opts.fugitive then
|
|
||||||
vim.validate({
|
|
||||||
['fugitive.horizontal'] = {
|
|
||||||
opts.fugitive.horizontal,
|
|
||||||
function(v)
|
|
||||||
return v == false or type(v) == 'string'
|
|
||||||
end,
|
|
||||||
'string or false',
|
|
||||||
},
|
|
||||||
['fugitive.vertical'] = {
|
|
||||||
opts.fugitive.vertical,
|
|
||||||
function(v)
|
|
||||||
return v == false or type(v) == 'string'
|
|
||||||
end,
|
|
||||||
'string or false',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
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
|
end
|
||||||
|
|
||||||
if opts.debounce_ms and opts.debounce_ms < 0 then
|
if opts.debounce_ms and opts.debounce_ms < 0 then
|
||||||
error('diffs: debounce_ms must be >= 0')
|
error('diffs: debounce_ms must be >= 0')
|
||||||
end
|
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
|
if
|
||||||
opts.highlights
|
opts.highlights
|
||||||
and opts.highlights.treesitter
|
and opts.highlights.treesitter
|
||||||
|
|
@ -434,21 +238,6 @@ local function init()
|
||||||
then
|
then
|
||||||
error('diffs: highlights.vim.max_lines must be >= 1')
|
error('diffs: highlights.vim.max_lines must be >= 1')
|
||||||
end
|
end
|
||||||
if
|
|
||||||
opts.highlights
|
|
||||||
and opts.highlights.intra
|
|
||||||
and opts.highlights.intra.max_lines
|
|
||||||
and opts.highlights.intra.max_lines < 1
|
|
||||||
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)
|
config = vim.tbl_deep_extend('force', default_config, opts)
|
||||||
log.set_enabled(config.debug)
|
log.set_enabled(config.debug)
|
||||||
|
|
@ -565,16 +354,4 @@ function M.detach_diff()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
---@return diffs.FugitiveConfig
|
|
||||||
function M.get_fugitive_config()
|
|
||||||
init()
|
|
||||||
return config.fugitive
|
|
||||||
end
|
|
||||||
|
|
||||||
---@return diffs.ConflictConfig
|
|
||||||
function M.get_conflict_config()
|
|
||||||
init()
|
|
||||||
return config.conflict
|
|
||||||
end
|
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|
|
||||||
|
|
@ -1,214 +0,0 @@
|
||||||
local M = {}
|
|
||||||
|
|
||||||
local dbg = require('diffs.log').dbg
|
|
||||||
|
|
||||||
---@type table?
|
|
||||||
local cached_handle = nil
|
|
||||||
|
|
||||||
---@type boolean
|
|
||||||
local download_in_progress = false
|
|
||||||
|
|
||||||
---@return string
|
|
||||||
local function get_os()
|
|
||||||
local os_name = jit.os:lower()
|
|
||||||
if os_name == 'osx' then
|
|
||||||
return 'macos'
|
|
||||||
end
|
|
||||||
return os_name
|
|
||||||
end
|
|
||||||
|
|
||||||
---@return string
|
|
||||||
local function get_arch()
|
|
||||||
return jit.arch:lower()
|
|
||||||
end
|
|
||||||
|
|
||||||
---@return string
|
|
||||||
local function get_ext()
|
|
||||||
local os_name = jit.os:lower()
|
|
||||||
if os_name == 'windows' then
|
|
||||||
return 'dll'
|
|
||||||
elseif os_name == 'osx' then
|
|
||||||
return 'dylib'
|
|
||||||
end
|
|
||||||
return 'so'
|
|
||||||
end
|
|
||||||
|
|
||||||
---@return string
|
|
||||||
local function lib_dir()
|
|
||||||
return vim.fn.stdpath('data') .. '/diffs/lib'
|
|
||||||
end
|
|
||||||
|
|
||||||
---@return string
|
|
||||||
local function lib_path()
|
|
||||||
return lib_dir() .. '/libvscode_diff.' .. get_ext()
|
|
||||||
end
|
|
||||||
|
|
||||||
---@return string
|
|
||||||
local function version_path()
|
|
||||||
return lib_dir() .. '/version'
|
|
||||||
end
|
|
||||||
|
|
||||||
local EXPECTED_VERSION = '2.18.0'
|
|
||||||
|
|
||||||
---@return boolean
|
|
||||||
function M.has_lib()
|
|
||||||
if cached_handle then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
return vim.fn.filereadable(lib_path()) == 1
|
|
||||||
end
|
|
||||||
|
|
||||||
---@return string
|
|
||||||
function M.lib_path()
|
|
||||||
return lib_path()
|
|
||||||
end
|
|
||||||
|
|
||||||
---@return table?
|
|
||||||
function M.load()
|
|
||||||
if cached_handle then
|
|
||||||
return cached_handle
|
|
||||||
end
|
|
||||||
|
|
||||||
local path = lib_path()
|
|
||||||
if vim.fn.filereadable(path) ~= 1 then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local ffi = require('ffi')
|
|
||||||
|
|
||||||
ffi.cdef([[
|
|
||||||
typedef struct {
|
|
||||||
int start_line;
|
|
||||||
int end_line;
|
|
||||||
} DiffsLineRange;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
int start_line;
|
|
||||||
int start_col;
|
|
||||||
int end_line;
|
|
||||||
int end_col;
|
|
||||||
} DiffsCharRange;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
DiffsCharRange original;
|
|
||||||
DiffsCharRange modified;
|
|
||||||
} DiffsRangeMapping;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
DiffsLineRange original;
|
|
||||||
DiffsLineRange modified;
|
|
||||||
DiffsRangeMapping* inner_changes;
|
|
||||||
int inner_change_count;
|
|
||||||
} DiffsDetailedMapping;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
DiffsDetailedMapping* mappings;
|
|
||||||
int count;
|
|
||||||
int capacity;
|
|
||||||
} DiffsDetailedMappingArray;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
DiffsLineRange original;
|
|
||||||
DiffsLineRange modified;
|
|
||||||
} DiffsMovedText;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
DiffsMovedText* moves;
|
|
||||||
int count;
|
|
||||||
int capacity;
|
|
||||||
} DiffsMovedTextArray;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
DiffsDetailedMappingArray changes;
|
|
||||||
DiffsMovedTextArray moves;
|
|
||||||
bool hit_timeout;
|
|
||||||
} DiffsLinesDiff;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
bool ignore_trim_whitespace;
|
|
||||||
int max_computation_time_ms;
|
|
||||||
bool compute_moves;
|
|
||||||
bool extend_to_subwords;
|
|
||||||
} DiffsDiffOptions;
|
|
||||||
|
|
||||||
DiffsLinesDiff* compute_diff(
|
|
||||||
const char** original_lines,
|
|
||||||
int original_count,
|
|
||||||
const char** modified_lines,
|
|
||||||
int modified_count,
|
|
||||||
const DiffsDiffOptions* options
|
|
||||||
);
|
|
||||||
|
|
||||||
void free_lines_diff(DiffsLinesDiff* diff);
|
|
||||||
]])
|
|
||||||
|
|
||||||
local ok, handle = pcall(ffi.load, path)
|
|
||||||
if not ok then
|
|
||||||
dbg('failed to load libvscode_diff: %s', handle)
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
cached_handle = handle
|
|
||||||
return handle
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param callback fun(handle: table?)
|
|
||||||
function M.ensure(callback)
|
|
||||||
if cached_handle then
|
|
||||||
callback(cached_handle)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if M.has_lib() then
|
|
||||||
callback(M.load())
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if download_in_progress then
|
|
||||||
dbg('download already in progress')
|
|
||||||
callback(nil)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
download_in_progress = true
|
|
||||||
|
|
||||||
local dir = lib_dir()
|
|
||||||
vim.fn.mkdir(dir, 'p')
|
|
||||||
|
|
||||||
local os_name = get_os()
|
|
||||||
local arch = get_arch()
|
|
||||||
local ext = get_ext()
|
|
||||||
local filename = ('libvscode_diff_%s_%s_%s.%s'):format(os_name, arch, EXPECTED_VERSION, ext)
|
|
||||||
local url = ('https://github.com/esmuellert/vscode-diff.nvim/releases/download/v%s/%s'):format(
|
|
||||||
EXPECTED_VERSION,
|
|
||||||
filename
|
|
||||||
)
|
|
||||||
|
|
||||||
local dest = lib_path()
|
|
||||||
vim.notify('[diffs] downloading libvscode_diff...', vim.log.levels.INFO)
|
|
||||||
|
|
||||||
local cmd = { 'curl', '-fSL', '-o', dest, url }
|
|
||||||
|
|
||||||
vim.system(cmd, {}, function(result)
|
|
||||||
download_in_progress = false
|
|
||||||
vim.schedule(function()
|
|
||||||
if result.code ~= 0 then
|
|
||||||
vim.notify('[diffs] failed to download libvscode_diff', vim.log.levels.WARN)
|
|
||||||
dbg('curl failed: %s', result.stderr or '')
|
|
||||||
callback(nil)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local f = io.open(version_path(), 'w')
|
|
||||||
if f then
|
|
||||||
f:write(EXPECTED_VERSION)
|
|
||||||
f:close()
|
|
||||||
end
|
|
||||||
|
|
||||||
vim.notify('[diffs] libvscode_diff downloaded', vim.log.levels.INFO)
|
|
||||||
callback(M.load())
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
return M
|
|
||||||
|
|
@ -13,7 +13,7 @@ function M.dbg(msg, ...)
|
||||||
if not enabled then
|
if not enabled then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
vim.notify('[diffs.nvim]: ' .. string.format(msg, ...), vim.log.levels.DEBUG)
|
vim.notify('[diffs] ' .. string.format(msg, ...), vim.log.levels.DEBUG)
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|
|
||||||
|
|
@ -6,75 +6,19 @@
|
||||||
---@field header_context string?
|
---@field header_context string?
|
||||||
---@field header_context_col integer?
|
---@field header_context_col integer?
|
||||||
---@field lines string[]
|
---@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 = {}
|
local M = {}
|
||||||
|
|
||||||
local dbg = require('diffs.log').dbg
|
local dbg = require('diffs.log').dbg
|
||||||
|
|
||||||
---@param filepath string
|
|
||||||
---@param n integer
|
|
||||||
---@return string[]?
|
|
||||||
local function read_first_lines(filepath, n)
|
|
||||||
local f = io.open(filepath, 'r')
|
|
||||||
if not f then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
local lines = {}
|
|
||||||
for _ = 1, n do
|
|
||||||
local line = f:read('*l')
|
|
||||||
if not line then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
table.insert(lines, line)
|
|
||||||
end
|
|
||||||
f:close()
|
|
||||||
return #lines > 0 and lines or nil
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param filename string
|
---@param filename string
|
||||||
---@param repo_root string?
|
|
||||||
---@return string?
|
---@return string?
|
||||||
local function get_ft_from_filename(filename, repo_root)
|
local function get_ft_from_filename(filename)
|
||||||
if repo_root then
|
|
||||||
local full_path = vim.fs.joinpath(repo_root, filename)
|
|
||||||
|
|
||||||
local buf = vim.fn.bufnr(full_path)
|
|
||||||
if buf ~= -1 then
|
|
||||||
local ft = vim.api.nvim_get_option_value('filetype', { buf = buf })
|
|
||||||
if ft and ft ~= '' then
|
|
||||||
dbg('filetype from existing buffer %d: %s', buf, ft)
|
|
||||||
return ft
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local ft = vim.filetype.match({ filename = filename })
|
local ft = vim.filetype.match({ filename = filename })
|
||||||
if ft then
|
if not ft then
|
||||||
dbg('filetype from filename: %s', ft)
|
dbg('no filetype for: %s', filename)
|
||||||
return ft
|
|
||||||
end
|
end
|
||||||
|
return ft
|
||||||
if repo_root then
|
|
||||||
local full_path = vim.fs.joinpath(repo_root, filename)
|
|
||||||
local contents = read_first_lines(full_path, 10)
|
|
||||||
if contents then
|
|
||||||
ft = vim.filetype.match({ filename = filename, contents = contents })
|
|
||||||
if ft then
|
|
||||||
dbg('filetype from file content: %s', ft)
|
|
||||||
return ft
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
dbg('no filetype for: %s', filename)
|
|
||||||
return nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param ft string
|
---@param ft string
|
||||||
|
|
@ -93,27 +37,10 @@ local function get_lang_from_ft(ft)
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param bufnr integer
|
|
||||||
---@return string?
|
|
||||||
local function get_repo_root(bufnr)
|
|
||||||
local ok, repo_root = pcall(vim.api.nvim_buf_get_var, bufnr, 'diffs_repo_root')
|
|
||||||
if ok and repo_root then
|
|
||||||
return repo_root
|
|
||||||
end
|
|
||||||
|
|
||||||
local ok2, git_dir = pcall(vim.api.nvim_buf_get_var, bufnr, 'git_dir')
|
|
||||||
if ok2 and git_dir then
|
|
||||||
return vim.fn.fnamemodify(git_dir, ':h')
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param bufnr integer
|
---@param bufnr integer
|
||||||
---@return diffs.Hunk[]
|
---@return diffs.Hunk[]
|
||||||
function M.parse_buffer(bufnr)
|
function M.parse_buffer(bufnr)
|
||||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||||
local repo_root = get_repo_root(bufnr)
|
|
||||||
---@type diffs.Hunk[]
|
---@type diffs.Hunk[]
|
||||||
local hunks = {}
|
local hunks = {}
|
||||||
|
|
||||||
|
|
@ -131,24 +58,10 @@ function M.parse_buffer(bufnr)
|
||||||
local hunk_header_context_col = nil
|
local hunk_header_context_col = nil
|
||||||
---@type string[]
|
---@type string[]
|
||||||
local hunk_lines = {}
|
local hunk_lines = {}
|
||||||
---@type integer?
|
|
||||||
local hunk_count = nil
|
|
||||||
---@type integer?
|
|
||||||
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()
|
local function flush_hunk()
|
||||||
if hunk_start and #hunk_lines > 0 then
|
if hunk_start and #hunk_lines > 0 and (current_lang or current_ft) then
|
||||||
local hunk = {
|
table.insert(hunks, {
|
||||||
filename = current_filename,
|
filename = current_filename,
|
||||||
ft = current_ft,
|
ft = current_ft,
|
||||||
lang = current_lang,
|
lang = current_lang,
|
||||||
|
|
@ -156,26 +69,12 @@ function M.parse_buffer(bufnr)
|
||||||
header_context = hunk_header_context,
|
header_context = hunk_header_context,
|
||||||
header_context_col = hunk_header_context_col,
|
header_context_col = hunk_header_context_col,
|
||||||
lines = hunk_lines,
|
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
|
|
||||||
hunk.header_lines = header_lines
|
|
||||||
end
|
|
||||||
table.insert(hunks, hunk)
|
|
||||||
end
|
end
|
||||||
hunk_start = nil
|
hunk_start = nil
|
||||||
hunk_header_context = nil
|
hunk_header_context = nil
|
||||||
hunk_header_context_col = nil
|
hunk_header_context_col = nil
|
||||||
hunk_lines = {}
|
hunk_lines = {}
|
||||||
file_old_start = nil
|
|
||||||
file_old_count = nil
|
|
||||||
file_new_start = nil
|
|
||||||
file_new_count = nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
for i, line in ipairs(lines) do
|
for i, line in ipairs(lines) do
|
||||||
|
|
@ -183,34 +82,21 @@ function M.parse_buffer(bufnr)
|
||||||
if filename then
|
if filename then
|
||||||
flush_hunk()
|
flush_hunk()
|
||||||
current_filename = filename
|
current_filename = filename
|
||||||
current_ft = get_ft_from_filename(filename, repo_root)
|
current_ft = get_ft_from_filename(filename)
|
||||||
current_lang = current_ft and get_lang_from_ft(current_ft) or nil
|
current_lang = current_ft and get_lang_from_ft(current_ft) or nil
|
||||||
if current_lang then
|
if current_lang then
|
||||||
dbg('file: %s -> lang: %s', filename, current_lang)
|
dbg('file: %s -> lang: %s', filename, current_lang)
|
||||||
elseif current_ft then
|
elseif current_ft then
|
||||||
dbg('file: %s -> ft: %s (no ts parser)', filename, current_ft)
|
dbg('file: %s -> ft: %s (no ts parser)', filename, current_ft)
|
||||||
end
|
end
|
||||||
hunk_count = 0
|
|
||||||
header_start = i
|
|
||||||
header_lines = {}
|
|
||||||
elseif line:match('^@@.-@@') then
|
elseif line:match('^@@.-@@') then
|
||||||
flush_hunk()
|
flush_hunk()
|
||||||
hunk_start = i
|
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*)(.*)')
|
local prefix, context = line:match('^(@@.-@@%s*)(.*)')
|
||||||
if context and context ~= '' then
|
if context and context ~= '' then
|
||||||
hunk_header_context = context
|
hunk_header_context = context
|
||||||
hunk_header_context_col = #prefix
|
hunk_header_context_col = #prefix
|
||||||
end
|
end
|
||||||
if hunk_count then
|
|
||||||
hunk_count = hunk_count + 1
|
|
||||||
end
|
|
||||||
elseif hunk_start then
|
elseif hunk_start then
|
||||||
local prefix = line:sub(1, 1)
|
local prefix = line:sub(1, 1)
|
||||||
if prefix == ' ' or prefix == '+' or prefix == '-' then
|
if prefix == ' ' or prefix == '+' or prefix == '-' then
|
||||||
|
|
@ -226,12 +112,8 @@ function M.parse_buffer(bufnr)
|
||||||
current_filename = nil
|
current_filename = nil
|
||||||
current_ft = nil
|
current_ft = nil
|
||||||
current_lang = nil
|
current_lang = nil
|
||||||
header_start = nil
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if header_start and not hunk_start then
|
|
||||||
table.insert(header_lines, line)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
flush_hunk()
|
flush_hunk()
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@ if vim.g.loaded_diffs then
|
||||||
end
|
end
|
||||||
vim.g.loaded_diffs = 1
|
vim.g.loaded_diffs = 1
|
||||||
|
|
||||||
require('diffs.commands').setup()
|
|
||||||
|
|
||||||
vim.api.nvim_create_autocmd('FileType', {
|
vim.api.nvim_create_autocmd('FileType', {
|
||||||
pattern = { 'fugitive', 'git' },
|
pattern = { 'fugitive', 'git' },
|
||||||
callback = function(args)
|
callback = function(args)
|
||||||
|
|
@ -13,29 +11,6 @@ vim.api.nvim_create_autocmd('FileType', {
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
diffs.attach(args.buf)
|
diffs.attach(args.buf)
|
||||||
|
|
||||||
if args.match == 'fugitive' then
|
|
||||||
local fugitive_config = diffs.get_fugitive_config()
|
|
||||||
if fugitive_config.horizontal or fugitive_config.vertical then
|
|
||||||
require('diffs.fugitive').setup_keymaps(args.buf, fugitive_config)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
vim.api.nvim_create_autocmd('BufReadCmd', {
|
|
||||||
pattern = 'diffs://*',
|
|
||||||
callback = function(args)
|
|
||||||
require('diffs.commands').read_buffer(args.buf)
|
|
||||||
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,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -49,36 +24,3 @@ vim.api.nvim_create_autocmd('OptionSet', {
|
||||||
end
|
end
|
||||||
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' })
|
|
||||||
|
|
|
||||||
65
scripts/ci.sh
Executable file
65
scripts/ci.sh
Executable file
|
|
@ -0,0 +1,65 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
RESET='\033[0m'
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
tmpdir=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$tmpdir"' EXIT
|
||||||
|
|
||||||
|
run_job() {
|
||||||
|
local name=$1
|
||||||
|
shift
|
||||||
|
local log="$tmpdir/$name.log"
|
||||||
|
if "$@" >"$log" 2>&1; then
|
||||||
|
echo -e "${GREEN}✓${RESET} $name"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗${RESET} $name"
|
||||||
|
cat "$log"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo -e "${BOLD}Running CI jobs in parallel...${RESET}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
pids=()
|
||||||
|
jobs_names=()
|
||||||
|
|
||||||
|
run_job "stylua" stylua --check . &
|
||||||
|
pids+=($!); jobs_names+=("stylua")
|
||||||
|
|
||||||
|
run_job "selene" selene --display-style quiet . &
|
||||||
|
pids+=($!); jobs_names+=("selene")
|
||||||
|
|
||||||
|
run_job "prettier" prettier --check . &
|
||||||
|
pids+=($!); jobs_names+=("prettier")
|
||||||
|
|
||||||
|
run_job "busted" env \
|
||||||
|
LUA_PATH="/usr/share/lua/5.1/?.lua;/usr/share/lua/5.1/?/init.lua;/usr/lib/lua/5.1/?.lua;/usr/lib/lua/5.1/?/init.lua;;" \
|
||||||
|
LUA_CPATH="/usr/lib/lua/5.1/?.so;;" \
|
||||||
|
nvim -l /usr/lib/luarocks/rocks-5.1/busted/2.3.0-1/bin/busted --verbose spec/ &
|
||||||
|
pids+=($!); jobs_names+=("busted")
|
||||||
|
|
||||||
|
failed=0
|
||||||
|
for i in "${!pids[@]}"; do
|
||||||
|
if ! wait "${pids[$i]}"; then
|
||||||
|
failed=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
if [ "$failed" -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}${BOLD}All jobs passed.${RESET}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}${BOLD}Some jobs failed.${RESET}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
require('spec.helpers')
|
|
||||||
|
|
||||||
local commands = require('diffs.commands')
|
|
||||||
|
|
||||||
describe('commands', function()
|
|
||||||
describe('setup', function()
|
|
||||||
it('registers Gdiff, Gvdiff, and Ghdiff commands', function()
|
|
||||||
commands.setup()
|
|
||||||
local cmds = vim.api.nvim_get_commands({})
|
|
||||||
assert.is_not_nil(cmds.Gdiff)
|
|
||||||
assert.is_not_nil(cmds.Gvdiff)
|
|
||||||
assert.is_not_nil(cmds.Ghdiff)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe('unified diff generation', function()
|
|
||||||
local old_lines = { 'local M = {}', 'return M' }
|
|
||||||
local new_lines = { 'local M = {}', 'local x = 1', 'return M' }
|
|
||||||
local diff_fn = vim.text and vim.text.diff or vim.diff
|
|
||||||
|
|
||||||
it('generates valid unified diff', function()
|
|
||||||
local old_content = table.concat(old_lines, '\n')
|
|
||||||
local new_content = table.concat(new_lines, '\n')
|
|
||||||
local diff_output = diff_fn(old_content, new_content, {
|
|
||||||
result_type = 'unified',
|
|
||||||
ctxlen = 3,
|
|
||||||
})
|
|
||||||
assert.is_not_nil(diff_output)
|
|
||||||
assert.is_true(diff_output:find('@@ ') ~= nil)
|
|
||||||
assert.is_true(diff_output:find('+local x = 1') ~= nil)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('returns empty for identical content', function()
|
|
||||||
local content = table.concat(old_lines, '\n')
|
|
||||||
local diff_output = diff_fn(content, content, {
|
|
||||||
result_type = 'unified',
|
|
||||||
ctxlen = 3,
|
|
||||||
})
|
|
||||||
assert.are.equal('', diff_output)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe('find_hunk_line', function()
|
|
||||||
it('finds matching @@ header and returns target line', function()
|
|
||||||
local diff_lines = {
|
|
||||||
'diff --git a/file.lua b/file.lua',
|
|
||||||
'--- a/file.lua',
|
|
||||||
'+++ b/file.lua',
|
|
||||||
'@@ -1,3 +1,4 @@',
|
|
||||||
' local M = {}',
|
|
||||||
'+local new = true',
|
|
||||||
' return M',
|
|
||||||
}
|
|
||||||
local hunk_position = {
|
|
||||||
hunk_header = '@@ -1,3 +1,4 @@',
|
|
||||||
offset = 2,
|
|
||||||
}
|
|
||||||
local target_line = commands.find_hunk_line(diff_lines, hunk_position)
|
|
||||||
assert.equals(6, target_line)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('returns nil when hunk header not found', function()
|
|
||||||
local diff_lines = {
|
|
||||||
'diff --git a/file.lua b/file.lua',
|
|
||||||
'@@ -1,3 +1,4 @@',
|
|
||||||
' local M = {}',
|
|
||||||
}
|
|
||||||
local hunk_position = {
|
|
||||||
hunk_header = '@@ -99,3 +99,4 @@',
|
|
||||||
offset = 1,
|
|
||||||
}
|
|
||||||
local target_line = commands.find_hunk_line(diff_lines, hunk_position)
|
|
||||||
assert.is_nil(target_line)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('handles multiple hunks and finds correct one', function()
|
|
||||||
local diff_lines = {
|
|
||||||
'diff --git a/file.lua b/file.lua',
|
|
||||||
'--- a/file.lua',
|
|
||||||
'+++ b/file.lua',
|
|
||||||
'@@ -1,3 +1,4 @@',
|
|
||||||
' local M = {}',
|
|
||||||
'+local x = 1',
|
|
||||||
' ',
|
|
||||||
'@@ -10,3 +11,4 @@',
|
|
||||||
' function M.foo()',
|
|
||||||
'+ print("hello")',
|
|
||||||
' end',
|
|
||||||
}
|
|
||||||
local hunk_position = {
|
|
||||||
hunk_header = '@@ -10,3 +11,4 @@',
|
|
||||||
offset = 2,
|
|
||||||
}
|
|
||||||
local target_line = commands.find_hunk_line(diff_lines, hunk_position)
|
|
||||||
assert.equals(10, target_line)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -1,163 +0,0 @@
|
||||||
require('spec.helpers')
|
|
||||||
local diff = require('diffs.diff')
|
|
||||||
|
|
||||||
describe('diff', function()
|
|
||||||
describe('extract_change_groups', function()
|
|
||||||
it('returns empty for all context lines', function()
|
|
||||||
local groups = diff.extract_change_groups({ ' line1', ' line2', ' line3' })
|
|
||||||
assert.are.equal(0, #groups)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('returns empty for pure additions', function()
|
|
||||||
local groups = diff.extract_change_groups({ '+line1', '+line2' })
|
|
||||||
assert.are.equal(0, #groups)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('returns empty for pure deletions', function()
|
|
||||||
local groups = diff.extract_change_groups({ '-line1', '-line2' })
|
|
||||||
assert.are.equal(0, #groups)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('extracts single change group', function()
|
|
||||||
local groups = diff.extract_change_groups({
|
|
||||||
' context',
|
|
||||||
'-old line',
|
|
||||||
'+new line',
|
|
||||||
' context',
|
|
||||||
})
|
|
||||||
assert.are.equal(1, #groups)
|
|
||||||
assert.are.equal(1, #groups[1].del_lines)
|
|
||||||
assert.are.equal(1, #groups[1].add_lines)
|
|
||||||
assert.are.equal('old line', groups[1].del_lines[1].text)
|
|
||||||
assert.are.equal('new line', groups[1].add_lines[1].text)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('extracts multiple change groups separated by context', function()
|
|
||||||
local groups = diff.extract_change_groups({
|
|
||||||
'-old1',
|
|
||||||
'+new1',
|
|
||||||
' context',
|
|
||||||
'-old2',
|
|
||||||
'+new2',
|
|
||||||
})
|
|
||||||
assert.are.equal(2, #groups)
|
|
||||||
assert.are.equal('old1', groups[1].del_lines[1].text)
|
|
||||||
assert.are.equal('new1', groups[1].add_lines[1].text)
|
|
||||||
assert.are.equal('old2', groups[2].del_lines[1].text)
|
|
||||||
assert.are.equal('new2', groups[2].add_lines[1].text)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('tracks correct line indices', function()
|
|
||||||
local groups = diff.extract_change_groups({
|
|
||||||
' context',
|
|
||||||
'-deleted',
|
|
||||||
'+added',
|
|
||||||
})
|
|
||||||
assert.are.equal(2, groups[1].del_lines[1].idx)
|
|
||||||
assert.are.equal(3, groups[1].add_lines[1].idx)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('handles multiple del lines followed by multiple add lines', function()
|
|
||||||
local groups = diff.extract_change_groups({
|
|
||||||
'-del1',
|
|
||||||
'-del2',
|
|
||||||
'+add1',
|
|
||||||
'+add2',
|
|
||||||
'+add3',
|
|
||||||
})
|
|
||||||
assert.are.equal(1, #groups)
|
|
||||||
assert.are.equal(2, #groups[1].del_lines)
|
|
||||||
assert.are.equal(3, #groups[1].add_lines)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe('compute_intra_hunks', function()
|
|
||||||
it('returns nil for all-addition hunks', function()
|
|
||||||
local result = diff.compute_intra_hunks({ '+line1', '+line2' }, 'default')
|
|
||||||
assert.is_nil(result)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('returns nil for all-deletion hunks', function()
|
|
||||||
local result = diff.compute_intra_hunks({ '-line1', '-line2' }, 'default')
|
|
||||||
assert.is_nil(result)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('returns nil for context-only hunks', function()
|
|
||||||
local result = diff.compute_intra_hunks({ ' line1', ' line2' }, 'default')
|
|
||||||
assert.is_nil(result)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('returns spans for single word change', function()
|
|
||||||
local result = diff.compute_intra_hunks({
|
|
||||||
'-local x = 1',
|
|
||||||
'+local x = 2',
|
|
||||||
}, 'default')
|
|
||||||
assert.is_not_nil(result)
|
|
||||||
assert.is_true(#result.del_spans > 0)
|
|
||||||
assert.is_true(#result.add_spans > 0)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('identifies correct byte offsets for word change', function()
|
|
||||||
local result = diff.compute_intra_hunks({
|
|
||||||
'-local x = 1',
|
|
||||||
'+local x = 2',
|
|
||||||
}, 'default')
|
|
||||||
assert.is_not_nil(result)
|
|
||||||
|
|
||||||
assert.are.equal(1, #result.del_spans)
|
|
||||||
assert.are.equal(1, #result.add_spans)
|
|
||||||
local del_span = result.del_spans[1]
|
|
||||||
local add_span = result.add_spans[1]
|
|
||||||
local del_text = ('local x = 1'):sub(del_span.col_start, del_span.col_end - 1)
|
|
||||||
local add_text = ('local x = 2'):sub(add_span.col_start, add_span.col_end - 1)
|
|
||||||
assert.are.equal('1', del_text)
|
|
||||||
assert.are.equal('2', add_text)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('handles multiple change groups separated by context', function()
|
|
||||||
local result = diff.compute_intra_hunks({
|
|
||||||
'-local a = 1',
|
|
||||||
'+local a = 2',
|
|
||||||
' local b = 3',
|
|
||||||
'-local c = 4',
|
|
||||||
'+local c = 5',
|
|
||||||
}, 'default')
|
|
||||||
assert.is_not_nil(result)
|
|
||||||
assert.is_true(#result.del_spans >= 2)
|
|
||||||
assert.is_true(#result.add_spans >= 2)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('handles uneven line counts (2 old, 1 new)', function()
|
|
||||||
local result = diff.compute_intra_hunks({
|
|
||||||
'-line one',
|
|
||||||
'-line two',
|
|
||||||
'+line combined',
|
|
||||||
}, 'default')
|
|
||||||
assert.is_not_nil(result)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('handles multi-byte UTF-8 content', function()
|
|
||||||
local result = diff.compute_intra_hunks({
|
|
||||||
'-local x = "héllo"',
|
|
||||||
'+local x = "wörld"',
|
|
||||||
}, 'default')
|
|
||||||
assert.is_not_nil(result)
|
|
||||||
assert.is_true(#result.del_spans > 0)
|
|
||||||
assert.is_true(#result.add_spans > 0)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('returns nil when del and add are identical', function()
|
|
||||||
local result = diff.compute_intra_hunks({
|
|
||||||
'-local x = 1',
|
|
||||||
'+local x = 1',
|
|
||||||
}, 'default')
|
|
||||||
assert.is_nil(result)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe('has_vscode', function()
|
|
||||||
it('returns false in test environment', function()
|
|
||||||
assert.is_false(diff.has_vscode())
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
@ -1,480 +0,0 @@
|
||||||
require('spec.helpers')
|
|
||||||
|
|
||||||
local fugitive = require('diffs.fugitive')
|
|
||||||
|
|
||||||
describe('fugitive', function()
|
|
||||||
describe('get_section_at_line', function()
|
|
||||||
local function create_status_buffer(lines)
|
|
||||||
local buf = vim.api.nvim_create_buf(false, true)
|
|
||||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
|
||||||
return buf
|
|
||||||
end
|
|
||||||
|
|
||||||
it('returns staged for lines in Staged section', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Head: main',
|
|
||||||
'',
|
|
||||||
'Staged (2)',
|
|
||||||
'M file1.lua',
|
|
||||||
'A file2.lua',
|
|
||||||
'',
|
|
||||||
'Unstaged (1)',
|
|
||||||
'M file3.lua',
|
|
||||||
})
|
|
||||||
assert.equals('staged', fugitive.get_section_at_line(buf, 4))
|
|
||||||
assert.equals('staged', fugitive.get_section_at_line(buf, 5))
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('returns unstaged for lines in Unstaged section', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Head: main',
|
|
||||||
'',
|
|
||||||
'Staged (1)',
|
|
||||||
'M file1.lua',
|
|
||||||
'',
|
|
||||||
'Unstaged (2)',
|
|
||||||
'M file2.lua',
|
|
||||||
'M file3.lua',
|
|
||||||
})
|
|
||||||
assert.equals('unstaged', fugitive.get_section_at_line(buf, 7))
|
|
||||||
assert.equals('unstaged', fugitive.get_section_at_line(buf, 8))
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('returns untracked for lines in Untracked section', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Head: main',
|
|
||||||
'',
|
|
||||||
'Untracked (2)',
|
|
||||||
'? newfile.lua',
|
|
||||||
'? another.lua',
|
|
||||||
})
|
|
||||||
assert.equals('untracked', fugitive.get_section_at_line(buf, 4))
|
|
||||||
assert.equals('untracked', fugitive.get_section_at_line(buf, 5))
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('returns nil for lines before any section', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Head: main',
|
|
||||||
'Push: origin/main',
|
|
||||||
'',
|
|
||||||
'Staged (1)',
|
|
||||||
'M file1.lua',
|
|
||||||
})
|
|
||||||
assert.is_nil(fugitive.get_section_at_line(buf, 1))
|
|
||||||
assert.is_nil(fugitive.get_section_at_line(buf, 2))
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe('get_file_at_line', function()
|
|
||||||
local function create_status_buffer(lines)
|
|
||||||
local buf = vim.api.nvim_create_buf(false, true)
|
|
||||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
|
||||||
return buf
|
|
||||||
end
|
|
||||||
|
|
||||||
it('parses simple modified file', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Unstaged (1)',
|
|
||||||
'M src/foo.lua',
|
|
||||||
})
|
|
||||||
local filename, section = fugitive.get_file_at_line(buf, 2)
|
|
||||||
assert.equals('src/foo.lua', filename)
|
|
||||||
assert.equals('unstaged', section)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('parses added file', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Staged (1)',
|
|
||||||
'A newfile.lua',
|
|
||||||
})
|
|
||||||
local filename, section = fugitive.get_file_at_line(buf, 2)
|
|
||||||
assert.equals('newfile.lua', filename)
|
|
||||||
assert.equals('staged', section)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('parses deleted file', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Staged (1)',
|
|
||||||
'D oldfile.lua',
|
|
||||||
})
|
|
||||||
local filename, section = fugitive.get_file_at_line(buf, 2)
|
|
||||||
assert.equals('oldfile.lua', filename)
|
|
||||||
assert.equals('staged', section)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('parses renamed file and returns both names', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Staged (1)',
|
|
||||||
'R oldname.lua -> newname.lua',
|
|
||||||
})
|
|
||||||
local filename, section, is_header, old_filename = fugitive.get_file_at_line(buf, 2)
|
|
||||||
assert.equals('newname.lua', filename)
|
|
||||||
assert.equals('staged', section)
|
|
||||||
assert.is_false(is_header)
|
|
||||||
assert.equals('oldname.lua', old_filename)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('parses renamed file with similarity index', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Staged (1)',
|
|
||||||
'R100 old.lua -> new.lua',
|
|
||||||
})
|
|
||||||
local filename, section, _, old_filename = fugitive.get_file_at_line(buf, 2)
|
|
||||||
assert.equals('new.lua', filename)
|
|
||||||
assert.equals('staged', section)
|
|
||||||
assert.equals('old.lua', old_filename)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('returns nil old_filename for non-renames', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Staged (1)',
|
|
||||||
'M modified.lua',
|
|
||||||
})
|
|
||||||
local filename, section, _, old_filename = fugitive.get_file_at_line(buf, 2)
|
|
||||||
assert.equals('modified.lua', filename)
|
|
||||||
assert.equals('staged', section)
|
|
||||||
assert.is_nil(old_filename)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('handles renamed file with spaces in name', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Staged (1)',
|
|
||||||
'R old file.lua -> new file.lua',
|
|
||||||
})
|
|
||||||
local filename, _, _, old_filename = fugitive.get_file_at_line(buf, 2)
|
|
||||||
assert.equals('new file.lua', filename)
|
|
||||||
assert.equals('old file.lua', old_filename)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('handles renamed file in subdirectory', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Staged (1)',
|
|
||||||
'R src/old.lua -> src/new.lua',
|
|
||||||
})
|
|
||||||
local filename, _, _, old_filename = fugitive.get_file_at_line(buf, 2)
|
|
||||||
assert.equals('src/new.lua', filename)
|
|
||||||
assert.equals('src/old.lua', old_filename)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('handles renamed file moved to different directory', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Staged (1)',
|
|
||||||
'R old/file.lua -> new/file.lua',
|
|
||||||
})
|
|
||||||
local filename, _, _, old_filename = fugitive.get_file_at_line(buf, 2)
|
|
||||||
assert.equals('new/file.lua', filename)
|
|
||||||
assert.equals('old/file.lua', old_filename)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('KNOWN LIMITATION: filename containing arrow parsed incorrectly', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Staged (1)',
|
|
||||||
'R a -> b.lua -> c.lua',
|
|
||||||
})
|
|
||||||
local filename, _, _, old_filename = fugitive.get_file_at_line(buf, 2)
|
|
||||||
assert.equals('b.lua -> c.lua', filename)
|
|
||||||
assert.equals('a', old_filename)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('handles double extensions', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Staged (1)',
|
|
||||||
'M test.spec.lua',
|
|
||||||
})
|
|
||||||
local filename, _, _, old_filename = fugitive.get_file_at_line(buf, 2)
|
|
||||||
assert.equals('test.spec.lua', filename)
|
|
||||||
assert.is_nil(old_filename)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('handles hyphenated filenames', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Unstaged (1)',
|
|
||||||
'M my-component-test.lua',
|
|
||||||
})
|
|
||||||
local filename, section = fugitive.get_file_at_line(buf, 2)
|
|
||||||
assert.equals('my-component-test.lua', filename)
|
|
||||||
assert.equals('unstaged', section)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('handles underscores and numbers', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Staged (1)',
|
|
||||||
'A test_file_123.lua',
|
|
||||||
})
|
|
||||||
local filename = fugitive.get_file_at_line(buf, 2)
|
|
||||||
assert.equals('test_file_123.lua', filename)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('handles dotfiles', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Unstaged (1)',
|
|
||||||
'M .gitignore',
|
|
||||||
})
|
|
||||||
local filename = fugitive.get_file_at_line(buf, 2)
|
|
||||||
assert.equals('.gitignore', filename)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('handles renamed with complex names', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Staged (1)',
|
|
||||||
'R src/old-file.spec.lua -> src/new-file.spec.lua',
|
|
||||||
})
|
|
||||||
local filename, _, _, old_filename = fugitive.get_file_at_line(buf, 2)
|
|
||||||
assert.equals('src/new-file.spec.lua', filename)
|
|
||||||
assert.equals('src/old-file.spec.lua', old_filename)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('handles deeply nested paths', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Unstaged (1)',
|
|
||||||
'M lua/diffs/ui/components/diff-view.lua',
|
|
||||||
})
|
|
||||||
local filename = fugitive.get_file_at_line(buf, 2)
|
|
||||||
assert.equals('lua/diffs/ui/components/diff-view.lua', filename)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('parses untracked file', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Untracked (1)',
|
|
||||||
'? untracked.lua',
|
|
||||||
})
|
|
||||||
local filename, section = fugitive.get_file_at_line(buf, 2)
|
|
||||||
assert.equals('untracked.lua', filename)
|
|
||||||
assert.equals('untracked', section)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('returns nil for section header', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Unstaged (1)',
|
|
||||||
'M file.lua',
|
|
||||||
})
|
|
||||||
local filename = fugitive.get_file_at_line(buf, 1)
|
|
||||||
assert.is_nil(filename)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('walks back from hunk line to find file', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Unstaged (1)',
|
|
||||||
'M file.lua',
|
|
||||||
'@@ -1,3 +1,4 @@',
|
|
||||||
' local M = {}',
|
|
||||||
'+local new = true',
|
|
||||||
' return M',
|
|
||||||
})
|
|
||||||
local filename, section = fugitive.get_file_at_line(buf, 5)
|
|
||||||
assert.equals('file.lua', filename)
|
|
||||||
assert.equals('unstaged', section)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('handles file with both staged and unstaged indicator', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Staged (1)',
|
|
||||||
'M both.lua',
|
|
||||||
'',
|
|
||||||
'Unstaged (1)',
|
|
||||||
'M both.lua',
|
|
||||||
})
|
|
||||||
local filename1, section1 = fugitive.get_file_at_line(buf, 2)
|
|
||||||
assert.equals('both.lua', filename1)
|
|
||||||
assert.equals('staged', section1)
|
|
||||||
|
|
||||||
local filename2, section2 = fugitive.get_file_at_line(buf, 5)
|
|
||||||
assert.equals('both.lua', filename2)
|
|
||||||
assert.equals('unstaged', section2)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('detects section header for Staged', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Head: main',
|
|
||||||
'',
|
|
||||||
'Staged (2)',
|
|
||||||
'M file1.lua',
|
|
||||||
})
|
|
||||||
local filename, section, is_header = fugitive.get_file_at_line(buf, 3)
|
|
||||||
assert.is_nil(filename)
|
|
||||||
assert.equals('staged', section)
|
|
||||||
assert.is_true(is_header)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('detects section header for Unstaged', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Unstaged (3)',
|
|
||||||
'M file1.lua',
|
|
||||||
})
|
|
||||||
local filename, section, is_header = fugitive.get_file_at_line(buf, 1)
|
|
||||||
assert.is_nil(filename)
|
|
||||||
assert.equals('unstaged', section)
|
|
||||||
assert.is_true(is_header)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('detects section header for Untracked', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Untracked (1)',
|
|
||||||
'? newfile.lua',
|
|
||||||
})
|
|
||||||
local filename, section, is_header = fugitive.get_file_at_line(buf, 1)
|
|
||||||
assert.is_nil(filename)
|
|
||||||
assert.equals('untracked', section)
|
|
||||||
assert.is_true(is_header)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('returns is_header=false for file lines', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Staged (1)',
|
|
||||||
'M file.lua',
|
|
||||||
})
|
|
||||||
local filename, section, is_header = fugitive.get_file_at_line(buf, 2)
|
|
||||||
assert.equals('file.lua', filename)
|
|
||||||
assert.equals('staged', section)
|
|
||||||
assert.is_false(is_header)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe('get_hunk_position', function()
|
|
||||||
local function create_status_buffer(lines)
|
|
||||||
local buf = vim.api.nvim_create_buf(false, true)
|
|
||||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
|
||||||
return buf
|
|
||||||
end
|
|
||||||
|
|
||||||
it('returns nil when on file header line', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Unstaged (1)',
|
|
||||||
'M file.lua',
|
|
||||||
'@@ -1,3 +1,4 @@',
|
|
||||||
' local M = {}',
|
|
||||||
'+local new = true',
|
|
||||||
})
|
|
||||||
local pos = fugitive.get_hunk_position(buf, 2)
|
|
||||||
assert.is_nil(pos)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('returns nil when on @@ header line', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Unstaged (1)',
|
|
||||||
'M file.lua',
|
|
||||||
'@@ -1,3 +1,4 @@',
|
|
||||||
' local M = {}',
|
|
||||||
})
|
|
||||||
local pos = fugitive.get_hunk_position(buf, 3)
|
|
||||||
assert.is_nil(pos)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('returns hunk header and offset for + line', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Unstaged (1)',
|
|
||||||
'M file.lua',
|
|
||||||
'@@ -1,3 +1,4 @@',
|
|
||||||
' local M = {}',
|
|
||||||
'+local new = true',
|
|
||||||
' return M',
|
|
||||||
})
|
|
||||||
local pos = fugitive.get_hunk_position(buf, 5)
|
|
||||||
assert.is_not_nil(pos)
|
|
||||||
assert.equals('@@ -1,3 +1,4 @@', pos.hunk_header)
|
|
||||||
assert.equals(2, pos.offset)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('returns hunk header and offset for - line', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Unstaged (1)',
|
|
||||||
'M file.lua',
|
|
||||||
'@@ -1,3 +1,3 @@',
|
|
||||||
' local M = {}',
|
|
||||||
'-local old = false',
|
|
||||||
' return M',
|
|
||||||
})
|
|
||||||
local pos = fugitive.get_hunk_position(buf, 5)
|
|
||||||
assert.is_not_nil(pos)
|
|
||||||
assert.equals('@@ -1,3 +1,3 @@', pos.hunk_header)
|
|
||||||
assert.equals(2, pos.offset)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('returns hunk header and offset for context line', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Unstaged (1)',
|
|
||||||
'M file.lua',
|
|
||||||
'@@ -1,3 +1,4 @@',
|
|
||||||
' local M = {}',
|
|
||||||
'+local new = true',
|
|
||||||
' return M',
|
|
||||||
})
|
|
||||||
local pos = fugitive.get_hunk_position(buf, 6)
|
|
||||||
assert.is_not_nil(pos)
|
|
||||||
assert.equals('@@ -1,3 +1,4 @@', pos.hunk_header)
|
|
||||||
assert.equals(3, pos.offset)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('returns correct offset for first line after @@', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Unstaged (1)',
|
|
||||||
'M file.lua',
|
|
||||||
'@@ -1,3 +1,4 @@',
|
|
||||||
' local M = {}',
|
|
||||||
})
|
|
||||||
local pos = fugitive.get_hunk_position(buf, 4)
|
|
||||||
assert.is_not_nil(pos)
|
|
||||||
assert.equals(1, pos.offset)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('handles @@ header with context text', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Unstaged (1)',
|
|
||||||
'M file.lua',
|
|
||||||
'@@ -10,3 +10,4 @@ function M.hello()',
|
|
||||||
' print("hi")',
|
|
||||||
'+ print("world")',
|
|
||||||
})
|
|
||||||
local pos = fugitive.get_hunk_position(buf, 5)
|
|
||||||
assert.is_not_nil(pos)
|
|
||||||
assert.equals('@@ -10,3 +10,4 @@ function M.hello()', pos.hunk_header)
|
|
||||||
assert.equals(2, pos.offset)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('returns nil when section header interrupts search', function()
|
|
||||||
local buf = create_status_buffer({
|
|
||||||
'Unstaged (1)',
|
|
||||||
'M file.lua',
|
|
||||||
' some orphan line',
|
|
||||||
})
|
|
||||||
local pos = fugitive.get_hunk_position(buf, 3)
|
|
||||||
assert.is_nil(pos)
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
require('spec.helpers')
|
|
||||||
local git = require('diffs.git')
|
|
||||||
|
|
||||||
describe('git', function()
|
|
||||||
describe('get_repo_root', function()
|
|
||||||
it('returns repo root for current repo', function()
|
|
||||||
local cwd = vim.fn.getcwd()
|
|
||||||
local root = git.get_repo_root(cwd .. '/lua/diffs/init.lua')
|
|
||||||
assert.is_not_nil(root)
|
|
||||||
assert.are.equal(cwd, root)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('returns nil for non-git directory', function()
|
|
||||||
local root = git.get_repo_root('/tmp')
|
|
||||||
assert.is_nil(root)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe('get_file_content', function()
|
|
||||||
it('returns file content at HEAD', function()
|
|
||||||
local cwd = vim.fn.getcwd()
|
|
||||||
local content, err = git.get_file_content('HEAD', cwd .. '/lua/diffs/init.lua')
|
|
||||||
assert.is_nil(err)
|
|
||||||
assert.is_not_nil(content)
|
|
||||||
assert.is_true(#content > 0)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('returns error for non-existent file', function()
|
|
||||||
local cwd = vim.fn.getcwd()
|
|
||||||
local content, err = git.get_file_content('HEAD', cwd .. '/does_not_exist.lua')
|
|
||||||
assert.is_nil(content)
|
|
||||||
assert.is_not_nil(err)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('returns error for non-git directory', function()
|
|
||||||
local content, err = git.get_file_content('HEAD', '/tmp/some_file.txt')
|
|
||||||
assert.is_nil(content)
|
|
||||||
assert.is_not_nil(err)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe('get_relative_path', function()
|
|
||||||
it('returns relative path within repo', function()
|
|
||||||
local cwd = vim.fn.getcwd()
|
|
||||||
local rel = git.get_relative_path(cwd .. '/lua/diffs/init.lua')
|
|
||||||
assert.are.equal('lua/diffs/init.lua', rel)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('returns nil for non-git directory', function()
|
|
||||||
local rel = git.get_relative_path('/tmp/some_file.txt')
|
|
||||||
assert.is_nil(rel)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
@ -2,8 +2,6 @@ local plugin_dir = vim.fn.getcwd()
|
||||||
vim.opt.runtimepath:prepend(plugin_dir)
|
vim.opt.runtimepath:prepend(plugin_dir)
|
||||||
vim.opt.packpath = {}
|
vim.opt.packpath = {}
|
||||||
|
|
||||||
vim.cmd('filetype on')
|
|
||||||
|
|
||||||
local function ensure_parser(lang)
|
local function ensure_parser(lang)
|
||||||
local ok = pcall(vim.treesitter.language.inspect, lang)
|
local ok = pcall(vim.treesitter.language.inspect, lang)
|
||||||
if not ok then
|
if not ok then
|
||||||
|
|
@ -12,7 +10,6 @@ local function ensure_parser(lang)
|
||||||
end
|
end
|
||||||
|
|
||||||
ensure_parser('lua')
|
ensure_parser('lua')
|
||||||
ensure_parser('vim')
|
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -215,290 +215,5 @@ describe('parser', function()
|
||||||
assert.are.equal(1, #hunks[2].lines)
|
assert.are.equal(1, #hunks[2].lines)
|
||||||
delete_buffer(bufnr)
|
delete_buffer(bufnr)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('attaches header_lines to first hunk only', function()
|
|
||||||
local bufnr = create_buffer({
|
|
||||||
'diff --git a/parser.lua b/parser.lua',
|
|
||||||
'index 3e8afa0..018159c 100644',
|
|
||||||
'--- a/parser.lua',
|
|
||||||
'+++ b/parser.lua',
|
|
||||||
'@@ -1,2 +1,3 @@',
|
|
||||||
' local M = {}',
|
|
||||||
'+local x = 1',
|
|
||||||
'@@ -10,2 +11,3 @@',
|
|
||||||
' function M.foo()',
|
|
||||||
'+ return true',
|
|
||||||
' end',
|
|
||||||
})
|
|
||||||
local hunks = parser.parse_buffer(bufnr)
|
|
||||||
|
|
||||||
assert.are.equal(2, #hunks)
|
|
||||||
assert.is_not_nil(hunks[1].header_start_line)
|
|
||||||
assert.is_not_nil(hunks[1].header_lines)
|
|
||||||
assert.are.equal(1, hunks[1].header_start_line)
|
|
||||||
assert.is_nil(hunks[2].header_start_line)
|
|
||||||
assert.is_nil(hunks[2].header_lines)
|
|
||||||
delete_buffer(bufnr)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('header_lines contains only diff metadata, not hunk content', function()
|
|
||||||
local bufnr = create_buffer({
|
|
||||||
'diff --git a/parser.lua b/parser.lua',
|
|
||||||
'index 3e8afa0..018159c 100644',
|
|
||||||
'--- a/parser.lua',
|
|
||||||
'+++ b/parser.lua',
|
|
||||||
'@@ -1,2 +1,3 @@',
|
|
||||||
' local M = {}',
|
|
||||||
'+local x = 1',
|
|
||||||
})
|
|
||||||
local hunks = parser.parse_buffer(bufnr)
|
|
||||||
|
|
||||||
assert.are.equal(1, #hunks)
|
|
||||||
assert.are.equal(4, #hunks[1].header_lines)
|
|
||||||
assert.are.equal('diff --git a/parser.lua b/parser.lua', hunks[1].header_lines[1])
|
|
||||||
assert.are.equal('index 3e8afa0..018159c 100644', hunks[1].header_lines[2])
|
|
||||||
assert.are.equal('--- a/parser.lua', hunks[1].header_lines[3])
|
|
||||||
assert.are.equal('+++ b/parser.lua', hunks[1].header_lines[4])
|
|
||||||
delete_buffer(bufnr)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('handles fugitive status format with diff headers', function()
|
|
||||||
local bufnr = create_buffer({
|
|
||||||
'Head: main',
|
|
||||||
'Push: origin/main',
|
|
||||||
'',
|
|
||||||
'Unstaged (1)',
|
|
||||||
'M parser.lua',
|
|
||||||
'diff --git a/parser.lua b/parser.lua',
|
|
||||||
'index 3e8afa0..018159c 100644',
|
|
||||||
'--- a/parser.lua',
|
|
||||||
'+++ b/parser.lua',
|
|
||||||
'@@ -1,2 +1,3 @@',
|
|
||||||
' local M = {}',
|
|
||||||
'+local x = 1',
|
|
||||||
})
|
|
||||||
local hunks = parser.parse_buffer(bufnr)
|
|
||||||
|
|
||||||
assert.are.equal(1, #hunks)
|
|
||||||
assert.are.equal(6, hunks[1].header_start_line)
|
|
||||||
assert.are.equal(4, #hunks[1].header_lines)
|
|
||||||
assert.are.equal('diff --git a/parser.lua b/parser.lua', hunks[1].header_lines[1])
|
|
||||||
delete_buffer(bufnr)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('emits hunk for files with unknown filetype', function()
|
|
||||||
local bufnr = create_buffer({
|
|
||||||
'M config.obscuretype',
|
|
||||||
'@@ -1,2 +1,3 @@',
|
|
||||||
' setting1 = value1',
|
|
||||||
'-setting2 = value2',
|
|
||||||
'+setting2 = MODIFIED',
|
|
||||||
'+setting4 = newvalue',
|
|
||||||
})
|
|
||||||
local hunks = parser.parse_buffer(bufnr)
|
|
||||||
|
|
||||||
assert.are.equal(1, #hunks)
|
|
||||||
assert.are.equal('config.obscuretype', hunks[1].filename)
|
|
||||||
assert.is_nil(hunks[1].ft)
|
|
||||||
assert.is_nil(hunks[1].lang)
|
|
||||||
assert.are.equal(4, #hunks[1].lines)
|
|
||||||
delete_buffer(bufnr)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('uses filetype from existing buffer when available', function()
|
|
||||||
local repo_root = '/tmp/test-repo'
|
|
||||||
local file_path = repo_root .. '/build'
|
|
||||||
|
|
||||||
local file_buf = vim.api.nvim_create_buf(false, true)
|
|
||||||
vim.api.nvim_buf_set_name(file_buf, file_path)
|
|
||||||
vim.api.nvim_set_option_value('filetype', 'bash', { buf = file_buf })
|
|
||||||
|
|
||||||
local diff_buf = create_buffer({
|
|
||||||
'M build',
|
|
||||||
'@@ -1,2 +1,3 @@',
|
|
||||||
' echo "hello"',
|
|
||||||
'+set -e',
|
|
||||||
' echo "done"',
|
|
||||||
})
|
|
||||||
vim.api.nvim_buf_set_var(diff_buf, 'diffs_repo_root', repo_root)
|
|
||||||
|
|
||||||
local hunks = parser.parse_buffer(diff_buf)
|
|
||||||
|
|
||||||
assert.are.equal(1, #hunks)
|
|
||||||
assert.are.equal('build', hunks[1].filename)
|
|
||||||
assert.are.equal('bash', hunks[1].ft)
|
|
||||||
|
|
||||||
delete_buffer(file_buf)
|
|
||||||
delete_buffer(diff_buf)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('uses filetype from existing buffer via git_dir', function()
|
|
||||||
local git_dir = '/tmp/test-repo/.git'
|
|
||||||
local repo_root = '/tmp/test-repo'
|
|
||||||
local file_path = repo_root .. '/script'
|
|
||||||
|
|
||||||
local file_buf = vim.api.nvim_create_buf(false, true)
|
|
||||||
vim.api.nvim_buf_set_name(file_buf, file_path)
|
|
||||||
vim.api.nvim_set_option_value('filetype', 'python', { buf = file_buf })
|
|
||||||
|
|
||||||
local diff_buf = create_buffer({
|
|
||||||
'M script',
|
|
||||||
'@@ -1,2 +1,3 @@',
|
|
||||||
' def main():',
|
|
||||||
'+ print("hi")',
|
|
||||||
' pass',
|
|
||||||
})
|
|
||||||
vim.api.nvim_buf_set_var(diff_buf, 'git_dir', git_dir)
|
|
||||||
|
|
||||||
local hunks = parser.parse_buffer(diff_buf)
|
|
||||||
|
|
||||||
assert.are.equal(1, #hunks)
|
|
||||||
assert.are.equal('script', hunks[1].filename)
|
|
||||||
assert.are.equal('python', hunks[1].ft)
|
|
||||||
|
|
||||||
delete_buffer(file_buf)
|
|
||||||
delete_buffer(diff_buf)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('detects filetype from file content shebang without open buffer', function()
|
|
||||||
local repo_root = '/tmp/diffs-test-shebang'
|
|
||||||
vim.fn.mkdir(repo_root, 'p')
|
|
||||||
|
|
||||||
local file_path = repo_root .. '/build'
|
|
||||||
local f = io.open(file_path, 'w')
|
|
||||||
f:write('#!/bin/bash\n')
|
|
||||||
f:write('set -e\n')
|
|
||||||
f:write('echo "hello"\n')
|
|
||||||
f:close()
|
|
||||||
|
|
||||||
local diff_buf = create_buffer({
|
|
||||||
'M build',
|
|
||||||
'@@ -1,2 +1,3 @@',
|
|
||||||
' #!/bin/bash',
|
|
||||||
'+set -e',
|
|
||||||
' echo "hello"',
|
|
||||||
})
|
|
||||||
vim.api.nvim_buf_set_var(diff_buf, 'diffs_repo_root', repo_root)
|
|
||||||
|
|
||||||
local hunks = parser.parse_buffer(diff_buf)
|
|
||||||
|
|
||||||
assert.are.equal(1, #hunks)
|
|
||||||
assert.are.equal('build', hunks[1].filename)
|
|
||||||
assert.are.equal('sh', hunks[1].ft)
|
|
||||||
|
|
||||||
delete_buffer(diff_buf)
|
|
||||||
os.remove(file_path)
|
|
||||||
vim.fn.delete(repo_root, 'rf')
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('detects python from shebang without open buffer', function()
|
|
||||||
local repo_root = '/tmp/diffs-test-shebang-py'
|
|
||||||
vim.fn.mkdir(repo_root, 'p')
|
|
||||||
|
|
||||||
local file_path = repo_root .. '/deploy'
|
|
||||||
local f = io.open(file_path, 'w')
|
|
||||||
f:write('#!/usr/bin/env python3\n')
|
|
||||||
f:write('import sys\n')
|
|
||||||
f:write('print("hi")\n')
|
|
||||||
f:close()
|
|
||||||
|
|
||||||
local diff_buf = create_buffer({
|
|
||||||
'M deploy',
|
|
||||||
'@@ -1,2 +1,3 @@',
|
|
||||||
' #!/usr/bin/env python3',
|
|
||||||
'+import sys',
|
|
||||||
' print("hi")',
|
|
||||||
})
|
|
||||||
vim.api.nvim_buf_set_var(diff_buf, 'diffs_repo_root', repo_root)
|
|
||||||
|
|
||||||
local hunks = parser.parse_buffer(diff_buf)
|
|
||||||
|
|
||||||
assert.are.equal(1, #hunks)
|
|
||||||
assert.are.equal('deploy', hunks[1].filename)
|
|
||||||
assert.are.equal('python', hunks[1].ft)
|
|
||||||
|
|
||||||
delete_buffer(diff_buf)
|
|
||||||
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)
|
||||||
end)
|
end)
|
||||||
|
|
|
||||||
|
|
@ -1,400 +0,0 @@
|
||||||
require('spec.helpers')
|
|
||||||
|
|
||||||
local commands = require('diffs.commands')
|
|
||||||
local diffs = require('diffs')
|
|
||||||
local git = require('diffs.git')
|
|
||||||
|
|
||||||
local saved_git = {}
|
|
||||||
local saved_systemlist
|
|
||||||
local test_buffers = {}
|
|
||||||
|
|
||||||
local function mock_git(overrides)
|
|
||||||
overrides = overrides or {}
|
|
||||||
saved_git.get_file_content = git.get_file_content
|
|
||||||
saved_git.get_index_content = git.get_index_content
|
|
||||||
saved_git.get_working_content = git.get_working_content
|
|
||||||
|
|
||||||
git.get_file_content = overrides.get_file_content
|
|
||||||
or function()
|
|
||||||
return { 'local M = {}', 'return M' }
|
|
||||||
end
|
|
||||||
git.get_index_content = overrides.get_index_content
|
|
||||||
or function()
|
|
||||||
return { 'local M = {}', 'return M' }
|
|
||||||
end
|
|
||||||
git.get_working_content = overrides.get_working_content
|
|
||||||
or function()
|
|
||||||
return { 'local M = {}', 'local x = 1', 'return M' }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function mock_systemlist(fn)
|
|
||||||
saved_systemlist = vim.fn.systemlist
|
|
||||||
vim.fn.systemlist = function(cmd)
|
|
||||||
local result = fn(cmd)
|
|
||||||
saved_systemlist({ 'true' })
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function restore_mocks()
|
|
||||||
for k, v in pairs(saved_git) do
|
|
||||||
git[k] = v
|
|
||||||
end
|
|
||||||
saved_git = {}
|
|
||||||
if saved_systemlist then
|
|
||||||
vim.fn.systemlist = saved_systemlist
|
|
||||||
saved_systemlist = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param name string
|
|
||||||
---@param vars? table<string, any>
|
|
||||||
---@return integer
|
|
||||||
local function create_diffs_buffer(name, vars)
|
|
||||||
local existing = vim.fn.bufnr(name)
|
|
||||||
if existing ~= -1 then
|
|
||||||
vim.api.nvim_buf_delete(existing, { force = true })
|
|
||||||
end
|
|
||||||
local bufnr = vim.api.nvim_create_buf(false, true)
|
|
||||||
vim.api.nvim_buf_set_name(bufnr, name)
|
|
||||||
vars = vars or {}
|
|
||||||
for k, v in pairs(vars) do
|
|
||||||
vim.api.nvim_buf_set_var(bufnr, k, v)
|
|
||||||
end
|
|
||||||
table.insert(test_buffers, bufnr)
|
|
||||||
return bufnr
|
|
||||||
end
|
|
||||||
|
|
||||||
local function cleanup_buffers()
|
|
||||||
for _, bufnr in ipairs(test_buffers) do
|
|
||||||
if vim.api.nvim_buf_is_valid(bufnr) then
|
|
||||||
vim.api.nvim_buf_delete(bufnr, { force = true })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
test_buffers = {}
|
|
||||||
end
|
|
||||||
|
|
||||||
describe('read_buffer', function()
|
|
||||||
after_each(function()
|
|
||||||
restore_mocks()
|
|
||||||
cleanup_buffers()
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe('early returns', function()
|
|
||||||
it('does nothing on non-diffs:// buffer', function()
|
|
||||||
local bufnr = vim.api.nvim_create_buf(false, true)
|
|
||||||
table.insert(test_buffers, bufnr)
|
|
||||||
assert.has_no.errors(function()
|
|
||||||
commands.read_buffer(bufnr)
|
|
||||||
end)
|
|
||||||
assert.are.same({ '' }, vim.api.nvim_buf_get_lines(bufnr, 0, -1, false))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('does nothing on malformed url without colon separator', function()
|
|
||||||
local bufnr = create_diffs_buffer('diffs://nocolonseparator')
|
|
||||||
vim.api.nvim_buf_set_var(bufnr, 'diffs_repo_root', '/tmp')
|
|
||||||
local lines_before = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
|
||||||
assert.has_no.errors(function()
|
|
||||||
commands.read_buffer(bufnr)
|
|
||||||
end)
|
|
||||||
assert.are.same(lines_before, vim.api.nvim_buf_get_lines(bufnr, 0, -1, false))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('does nothing when diffs_repo_root is missing', function()
|
|
||||||
local bufnr = create_diffs_buffer('diffs://staged:missing_root.lua')
|
|
||||||
assert.has_no.errors(function()
|
|
||||||
commands.read_buffer(bufnr)
|
|
||||||
end)
|
|
||||||
assert.are.same({ '' }, vim.api.nvim_buf_get_lines(bufnr, 0, -1, false))
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe('buffer options', function()
|
|
||||||
it('sets buftype, bufhidden, swapfile, modifiable, filetype', function()
|
|
||||||
mock_git()
|
|
||||||
local bufnr = create_diffs_buffer('diffs://staged:options_test.lua', {
|
|
||||||
diffs_repo_root = '/tmp',
|
|
||||||
})
|
|
||||||
|
|
||||||
commands.read_buffer(bufnr)
|
|
||||||
|
|
||||||
assert.are.equal('nowrite', vim.api.nvim_get_option_value('buftype', { buf = bufnr }))
|
|
||||||
assert.are.equal('delete', vim.api.nvim_get_option_value('bufhidden', { buf = bufnr }))
|
|
||||||
assert.is_false(vim.api.nvim_get_option_value('swapfile', { buf = bufnr }))
|
|
||||||
assert.is_false(vim.api.nvim_get_option_value('modifiable', { buf = bufnr }))
|
|
||||||
assert.are.equal('diff', vim.api.nvim_get_option_value('filetype', { buf = bufnr }))
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe('dispatch', function()
|
|
||||||
it('calls get_file_content + get_index_content for staged label', function()
|
|
||||||
local called_get_file = false
|
|
||||||
local called_get_index = false
|
|
||||||
mock_git({
|
|
||||||
get_file_content = function()
|
|
||||||
called_get_file = true
|
|
||||||
return { 'old' }
|
|
||||||
end,
|
|
||||||
get_index_content = function()
|
|
||||||
called_get_index = true
|
|
||||||
return { 'new' }
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
local bufnr = create_diffs_buffer('diffs://staged:dispatch_staged.lua', {
|
|
||||||
diffs_repo_root = '/tmp',
|
|
||||||
})
|
|
||||||
commands.read_buffer(bufnr)
|
|
||||||
|
|
||||||
assert.is_true(called_get_file)
|
|
||||||
assert.is_true(called_get_index)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('calls get_index_content + get_working_content for unstaged label', function()
|
|
||||||
local called_get_index = false
|
|
||||||
local called_get_working = false
|
|
||||||
mock_git({
|
|
||||||
get_index_content = function()
|
|
||||||
called_get_index = true
|
|
||||||
return { 'index' }
|
|
||||||
end,
|
|
||||||
get_working_content = function()
|
|
||||||
called_get_working = true
|
|
||||||
return { 'working' }
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
local bufnr = create_diffs_buffer('diffs://unstaged:dispatch_unstaged.lua', {
|
|
||||||
diffs_repo_root = '/tmp',
|
|
||||||
})
|
|
||||||
commands.read_buffer(bufnr)
|
|
||||||
|
|
||||||
assert.is_true(called_get_index)
|
|
||||||
assert.is_true(called_get_working)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('calls only get_working_content for untracked label', function()
|
|
||||||
local called_get_file = false
|
|
||||||
local called_get_working = false
|
|
||||||
mock_git({
|
|
||||||
get_file_content = function()
|
|
||||||
called_get_file = true
|
|
||||||
return {}
|
|
||||||
end,
|
|
||||||
get_working_content = function()
|
|
||||||
called_get_working = true
|
|
||||||
return { 'new file' }
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
local bufnr = create_diffs_buffer('diffs://untracked:dispatch_untracked.lua', {
|
|
||||||
diffs_repo_root = '/tmp',
|
|
||||||
})
|
|
||||||
commands.read_buffer(bufnr)
|
|
||||||
|
|
||||||
assert.is_false(called_get_file)
|
|
||||||
assert.is_true(called_get_working)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('calls get_file_content + get_working_content for revision label', function()
|
|
||||||
local captured_rev
|
|
||||||
local called_get_working = false
|
|
||||||
mock_git({
|
|
||||||
get_file_content = function(rev)
|
|
||||||
captured_rev = rev
|
|
||||||
return { 'old' }
|
|
||||||
end,
|
|
||||||
get_working_content = function()
|
|
||||||
called_get_working = true
|
|
||||||
return { 'new' }
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
local bufnr = create_diffs_buffer('diffs://HEAD~3:dispatch_rev.lua', {
|
|
||||||
diffs_repo_root = '/tmp',
|
|
||||||
})
|
|
||||||
commands.read_buffer(bufnr)
|
|
||||||
|
|
||||||
assert.are.equal('HEAD~3', captured_rev)
|
|
||||||
assert.is_true(called_get_working)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('falls back from index to HEAD for unstaged when index returns nil', function()
|
|
||||||
local call_order = {}
|
|
||||||
mock_git({
|
|
||||||
get_index_content = function()
|
|
||||||
table.insert(call_order, 'index')
|
|
||||||
return nil
|
|
||||||
end,
|
|
||||||
get_file_content = function()
|
|
||||||
table.insert(call_order, 'head')
|
|
||||||
return { 'head content' }
|
|
||||||
end,
|
|
||||||
get_working_content = function()
|
|
||||||
return { 'working content' }
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
local bufnr = create_diffs_buffer('diffs://unstaged:dispatch_fallback.lua', {
|
|
||||||
diffs_repo_root = '/tmp',
|
|
||||||
})
|
|
||||||
commands.read_buffer(bufnr)
|
|
||||||
|
|
||||||
assert.are.same({ 'index', 'head' }, call_order)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('runs git diff for section diffs with path=all', function()
|
|
||||||
local captured_cmd
|
|
||||||
mock_systemlist(function(cmd)
|
|
||||||
captured_cmd = cmd
|
|
||||||
return {
|
|
||||||
'diff --git a/file.lua b/file.lua',
|
|
||||||
'--- a/file.lua',
|
|
||||||
'+++ b/file.lua',
|
|
||||||
'@@ -1 +1 @@',
|
|
||||||
'-old',
|
|
||||||
'+new',
|
|
||||||
}
|
|
||||||
end)
|
|
||||||
|
|
||||||
local bufnr = create_diffs_buffer('diffs://unstaged:all', {
|
|
||||||
diffs_repo_root = '/home/test/repo',
|
|
||||||
})
|
|
||||||
commands.read_buffer(bufnr)
|
|
||||||
|
|
||||||
assert.is_not_nil(captured_cmd)
|
|
||||||
assert.are.equal('git', captured_cmd[1])
|
|
||||||
assert.are.equal('/home/test/repo', captured_cmd[3])
|
|
||||||
assert.are.equal('diff', captured_cmd[4])
|
|
||||||
|
|
||||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
|
||||||
assert.are.equal('diff --git a/file.lua b/file.lua', lines[1])
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('passes --cached for staged section diffs', function()
|
|
||||||
local captured_cmd
|
|
||||||
mock_systemlist(function(cmd)
|
|
||||||
captured_cmd = cmd
|
|
||||||
return { 'diff --git a/f.lua b/f.lua', '@@ -1 +1 @@', '-a', '+b' }
|
|
||||||
end)
|
|
||||||
|
|
||||||
local bufnr = create_diffs_buffer('diffs://staged:all', {
|
|
||||||
diffs_repo_root = '/tmp',
|
|
||||||
})
|
|
||||||
commands.read_buffer(bufnr)
|
|
||||||
|
|
||||||
assert.is_truthy(vim.tbl_contains(captured_cmd, '--cached'))
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe('content', function()
|
|
||||||
it('generates valid unified diff header with correct paths', function()
|
|
||||||
mock_git({
|
|
||||||
get_file_content = function()
|
|
||||||
return { 'old' }
|
|
||||||
end,
|
|
||||||
get_working_content = function()
|
|
||||||
return { 'new' }
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
local bufnr = create_diffs_buffer('diffs://HEAD:lua/diffs/init.lua', {
|
|
||||||
diffs_repo_root = '/tmp',
|
|
||||||
})
|
|
||||||
commands.read_buffer(bufnr)
|
|
||||||
|
|
||||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
|
||||||
assert.are.equal('diff --git a/lua/diffs/init.lua b/lua/diffs/init.lua', lines[1])
|
|
||||||
assert.are.equal('--- a/lua/diffs/init.lua', lines[2])
|
|
||||||
assert.are.equal('+++ b/lua/diffs/init.lua', lines[3])
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('uses old_filepath for diff header in renames', function()
|
|
||||||
mock_git({
|
|
||||||
get_file_content = function(_, path)
|
|
||||||
assert.are.equal('/tmp/old_name.lua', path)
|
|
||||||
return { 'old content' }
|
|
||||||
end,
|
|
||||||
get_index_content = function()
|
|
||||||
return { 'new content' }
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
local bufnr = create_diffs_buffer('diffs://staged:new_name.lua', {
|
|
||||||
diffs_repo_root = '/tmp',
|
|
||||||
diffs_old_filepath = 'old_name.lua',
|
|
||||||
})
|
|
||||||
commands.read_buffer(bufnr)
|
|
||||||
|
|
||||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
|
||||||
assert.are.equal('diff --git a/old_name.lua b/new_name.lua', lines[1])
|
|
||||||
assert.are.equal('--- a/old_name.lua', lines[2])
|
|
||||||
assert.are.equal('+++ b/new_name.lua', lines[3])
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('produces empty buffer when old and new are identical', function()
|
|
||||||
mock_git({
|
|
||||||
get_file_content = function()
|
|
||||||
return { 'identical' }
|
|
||||||
end,
|
|
||||||
get_working_content = function()
|
|
||||||
return { 'identical' }
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
local bufnr = create_diffs_buffer('diffs://HEAD:nodiff.lua', {
|
|
||||||
diffs_repo_root = '/tmp',
|
|
||||||
})
|
|
||||||
commands.read_buffer(bufnr)
|
|
||||||
|
|
||||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
|
||||||
assert.are.same({ '' }, lines)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('replaces existing buffer content on reload', function()
|
|
||||||
mock_git({
|
|
||||||
get_file_content = function()
|
|
||||||
return { 'old' }
|
|
||||||
end,
|
|
||||||
get_working_content = function()
|
|
||||||
return { 'new' }
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
local bufnr = create_diffs_buffer('diffs://HEAD:replace_test.lua', {
|
|
||||||
diffs_repo_root = '/tmp',
|
|
||||||
})
|
|
||||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { 'stale', 'content', 'from', 'before' })
|
|
||||||
|
|
||||||
commands.read_buffer(bufnr)
|
|
||||||
|
|
||||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
|
||||||
assert.are.equal('diff --git a/replace_test.lua b/replace_test.lua', lines[1])
|
|
||||||
for _, line in ipairs(lines) do
|
|
||||||
assert.is_not_equal('stale', line)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe('attach integration', function()
|
|
||||||
it('calls attach on the buffer', function()
|
|
||||||
mock_git()
|
|
||||||
|
|
||||||
local attach_called_with
|
|
||||||
local original_attach = diffs.attach
|
|
||||||
diffs.attach = function(bufnr)
|
|
||||||
attach_called_with = bufnr
|
|
||||||
end
|
|
||||||
|
|
||||||
local bufnr = create_diffs_buffer('diffs://staged:attach_test.lua', {
|
|
||||||
diffs_repo_root = '/tmp',
|
|
||||||
})
|
|
||||||
commands.read_buffer(bufnr)
|
|
||||||
|
|
||||||
assert.are.equal(bufnr, attach_called_with)
|
|
||||||
|
|
||||||
diffs.attach = original_attach
|
|
||||||
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