diff --git a/.github/workflows/quality.yaml b/.github/workflows/quality.yaml
index c0b7770..77049fd 100644
--- a/.github/workflows/quality.yaml
+++ b/.github/workflows/quality.yaml
@@ -25,7 +25,6 @@ jobs:
- '*.lua'
- '.luarc.json'
- '*.toml'
- - 'vim.yaml'
markdown:
- '*.md'
@@ -36,8 +35,11 @@ jobs:
if: ${{ needs.changes.outputs.lua == 'true' }}
steps:
- uses: actions/checkout@v4
- - uses: cachix/install-nix-action@v31
- - run: nix develop --command stylua --check .
+ - uses: JohnnyMorganz/stylua-action@v4
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ version: 2.1.0
+ args: --check .
lua-lint:
name: Lua Lint Check
@@ -46,8 +48,11 @@ jobs:
if: ${{ needs.changes.outputs.lua == 'true' }}
steps:
- uses: actions/checkout@v4
- - uses: cachix/install-nix-action@v31
- - run: nix develop --command selene --display-style quiet .
+ - name: Lint with Selene
+ uses: NTBBloodbath/selene-action@v1.0.0
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ args: --display-style quiet .
lua-typecheck:
name: Lua Type Check
@@ -70,5 +75,15 @@ jobs:
if: ${{ needs.changes.outputs.markdown == 'true' }}
steps:
- uses: actions/checkout@v4
- - uses: cachix/install-nix-action@v31
- - run: nix develop --command prettier --check .
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 8
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ - name: Install prettier
+ run: pnpm add -g prettier@3.1.0
+ - name: Check markdown formatting with prettier
+ run: prettier --check .
diff --git a/.gitignore b/.gitignore
index b7a91ec..d14787c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,10 +5,4 @@ doc/tags
CLAUDE.md
.claude/
-bench/
node_modules/
-
-result
-result-*
-.direnv/
-.envrc
diff --git a/.luarc.json b/.luarc.json
index bfbf500..b438cce 100644
--- a/.luarc.json
+++ b/.luarc.json
@@ -1,15 +1,8 @@
{
- "runtime.version": "LuaJIT",
+ "runtime.version": "Lua 5.1",
"runtime.path": ["lua/?.lua", "lua/?/init.lua"],
"diagnostics.globals": ["vim", "jit"],
- "workspace.library": [
- "$VIMRUNTIME/lua",
- "${3rd}/luv/library",
- "${3rd}/busted/library",
- "${3rd}/luassert/library"
- ],
+ "workspace.library": ["$VIMRUNTIME/lua", "${3rd}/luv/library"],
"workspace.checkThirdParty": false,
- "diagnostics.libraryFiles": "Disable",
- "workspace.ignoreDir": [".direnv"],
"completion.callSnippet": "Replace"
}
diff --git a/.styluaignore b/.styluaignore
deleted file mode 100644
index 9b42106..0000000
--- a/.styluaignore
+++ /dev/null
@@ -1 +0,0 @@
-.direnv/
diff --git a/README.md b/README.md
index 0051f21..f0a04a8 100644
--- a/README.md
+++ b/README.md
@@ -2,25 +2,25 @@
**Syntax highlighting for diffs in Neovim**
-Enhance [vim-fugitive](https://github.com/tpope/vim-fugitive),
-[Neogit](https://github.com/NeogitOrg/neogit), and Neovim's built-in diff mode
-with language-aware syntax highlighting.
+Enhance `vim-fugitive` and Neovim's built-in diff mode with language-aware
+syntax highlighting.
-
+
## Features
-- Treesitter syntax highlighting in vim-fugitive, Neogit, and `diff` filetype
-- Character-level intra-line diff highlighting (with optional
- [vscode-diff](https://github.com/esmuellert/codediff.nvim) FFI backend for
- word-level accuracy)
-- `:Gdiff` unified diff against any revision
-- Inline merge conflict detection, highlighting, and resolution
-- gitsigns.nvim blame popup highlighting
-- Email quoting/patch syntax support (`> diff ...`)
-- Vim syntax fallback
-- Configurable highlighiting blend & priorities
-- Context-inclusive, high-accuracy highlights
+- Treesitter syntax highlighting in `:Git` diffs and commit views
+- Diff header highlighting (`diff --git`, `index`, `---`, `+++`)
+- `: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`)
+- Vim syntax fallback for languages without a treesitter parser
+- Hunk header context highlighting (`@@ ... @@ function foo()`)
+- Character-level (intra-line) diff highlighting for changed characters
+- Inline merge conflict detection, highlighting, and resolution keymaps
+- Configurable debouncing, max lines, diff prefix concealment, blend alpha, and
+ highlight overrides
## Requirements
@@ -41,59 +41,18 @@ luarocks install diffs.nvim
:help diffs.nvim
```
-## FAQ
-
-**Q: How do I install with lazy.nvim?**
-
-```lua
-{
- 'barrettruth/diffs.nvim',
- init = function()
- vim.g.diffs = {
- ...
- }
- end,
-}
-```
-
-Do not lazy load `diffs.nvim` with `event`, `lazy`, `ft`, `config`, or `keys` to
-control loading - `diffs.nvim` lazy-loads itself.
-
-**Q: Does diffs.nvim support vim-fugitive/Neogit/gitsigns?**
-
-Yes. Enable integrations in your config:
-
-```lua
-vim.g.diffs = {
- fugitive = true,
- neogit = true,
- gitsigns = true,
-}
-```
-
-See the documentation for more information.
-
## Known Limitations
- **Incomplete syntax context**: Treesitter parses each diff hunk in isolation.
- Context lines within the hunk provide syntactic context for the parser. In
- rare cases, hunks that start or end mid-expression may produce imperfect
- highlights due to treesitter error recovery.
+ To improve accuracy, `diffs.nvim` reads lines from disk before and after each
+ hunk for parsing context (`highlights.context`, enabled by default with 25
+ lines). This resolves most boundary issues. Set
+ `highlights.context.enabled = false` to disable.
-- **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
- painted. The decoration provider applies highlights on the next redraw cycle,
- causing a brief visual "flash".
-
-- **Cold Start**: Treesitter grammar loading (~10ms) and query compilation
- (~4ms) are one-time costs per language per Neovim session. Each language pays
- this cost on first encounter, which may cause a brief stutter when a diff
- containing a new language first enters the viewport.
-
-- **Vim syntax fallback is deferred**: The vim syntax fallback (for languages
- without a treesitter parser) cannot run inside the decoration provider's
- redraw cycle due to Neovim's restriction on buffer mutations. Vim syntax
- highlights for these hunks appear slightly delayed.
+ painted. The buffer is then re-painted after `debounce_ms` milliseconds,
+ causing an unavoidable visual "flash" even when `debounce_ms = 0`.
- **Conflicting diff plugins**: `diffs.nvim` may not interact well with other
plugins that modify diff highlighting. Known plugins that may conflict:
@@ -111,15 +70,11 @@ See the documentation for more information.
# Acknowledgements
- [`vim-fugitive`](https://github.com/tpope/vim-fugitive)
-- [@esmuellert](https://github.com/esmuellert) /
- [`codediff.nvim`](https://github.com/esmuellert/codediff.nvim) - vscode-diff
- algorithm FFI backend for word-level intra-line accuracy
+- [`codediff.nvim`](https://github.com/esmuellert/codediff.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,
- decoration provider highlighting architecture, gitsigns blame popup
- highlighting
+ filetype fix, shebang/modeline detection, treesitter injection support
diff --git a/doc/diffs.nvim.txt b/doc/diffs.nvim.txt
index f0205d5..7663150 100644
--- a/doc/diffs.nvim.txt
+++ b/doc/diffs.nvim.txt
@@ -7,15 +7,14 @@ License: MIT
INTRODUCTION *diffs.nvim*
diffs.nvim adds syntax highlighting to diff views. It overlays language-aware
-highlights on top of default diff highlighting in vim-fugitive, Neogit, and
-Neovim's built-in diff mode.
+highlights on top of default diff highlighting in vim-fugitive and Neovim's
+built-in diff mode.
Features: ~
- 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
- |:Gdiff| command for unified diff against any git revision
-- gitsigns.nvim blame popup highlighting (see |diffs-gitsigns|)
- Background-only diff colors for any `&diff` buffer (vimdiff, diffthis, etc.)
- Vim syntax fallback for languages without a treesitter parser
- Blended diff background colors that preserve syntax visibility
@@ -23,27 +22,6 @@ Features: ~
- Gutter (line number) highlighting
- Inline merge conflict marker detection, highlighting, and resolution
-==============================================================================
-CONTENTS *diffs-contents*
-
- 1. Introduction ............................................... |diffs.nvim|
- 2. Requirements ....................................... |diffs-requirements|
- 3. Setup ..................................................... |diffs-setup|
- 4. Configuration ............................................ |diffs-config|
- 5. Commands ............................................... |diffs-commands|
- 6. Mappings ............................................... |diffs-mappings|
- 7. Fugitive Status Keymaps ................................ |diffs-fugitive|
- 8. Conflict Resolution .................................... |diffs-conflict|
- 9. Merge Diff Resolution ..................................... |diffs-merge|
- 10. Neogit ................................................... |diffs-neogit|
- 11. Gitsigns ................................................ |diffs-gitsigns|
- 12. API ......................................................... |diffs-api|
- 13. Implementation ................................... |diffs-implementation|
- 14. Known Limitations ................................... |diffs-limitations|
- 15. Highlight Groups ..................................... |diffs-highlights|
- 16. Health Check ............................................. |diffs-health|
- 17. Acknowledgements ............................... |diffs-acknowledgements|
-
==============================================================================
REQUIREMENTS *diffs-requirements*
@@ -58,7 +36,7 @@ etc.) works without vim-fugitive.
==============================================================================
SETUP *diffs-setup*
-Install with lazy.nvim: >lua
+Using lazy.nvim: >lua
{
'barrettruth/diffs.nvim',
dependencies = { 'tpope/vim-fugitive' },
@@ -67,9 +45,6 @@ Install with lazy.nvim: >lua
The plugin works automatically with no configuration required. For
customization, see |diffs-config|.
-NOTE: Load your colorscheme before `diffs.nvim`. For example, with lazy.nvim,
-set `priority = 1000` and `lazy = false` on your colorscheme plugin.
-
==============================================================================
CONFIGURATION *diffs-config*
@@ -77,11 +52,8 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
>lua
vim.g.diffs = {
debug = false,
+ debounce_ms = 0,
hide_prefix = false,
- fugitive = false,
- neogit = false,
- gitsigns = false,
- extra_filetypes = {},
highlights = {
background = true,
gutter = true,
@@ -95,7 +67,7 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
max_lines = 500,
},
vim = {
- enabled = true,
+ enabled = false,
max_lines = 200,
},
intra = {
@@ -103,26 +75,23 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
algorithm = 'default',
max_lines = 500,
},
- priorities = {
- clear = 198,
- syntax = 199,
- line_bg = 200,
- char_bg = 201,
- },
overrides = {},
},
+ fugitive = {
+ horizontal = 'du',
+ vertical = 'dU',
+ },
conflict = {
enabled = true,
disable_diagnostics = true,
show_virtual_text = true,
- show_actions = false,
keymaps = {
ours = 'doo',
theirs = 'dot',
both = 'dob',
none = 'don',
- next = ']c',
- prev = '[c',
+ next = ']x',
+ prev = '[x',
},
},
}
@@ -133,6 +102,11 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
Enable debug logging to |:messages| with
`[diffs]` prefix.
+ {debounce_ms} (integer, default: 0)
+ Debounce delay in milliseconds for re-highlighting
+ after buffer changes. Lower values feel snappier
+ but use more CPU.
+
{hide_prefix} (boolean, default: false)
Hide diff prefixes (`+`/`-`/` `) using virtual
text overlay. Makes code appear without the
@@ -140,55 +114,14 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
is also enabled, the overlay inherits the line's
background color.
- {fugitive} (boolean|table, default: false)
- Enable vim-fugitive integration. Pass `true`
- for defaults, `false` to disable, or a table
- with sub-options (see |diffs.FugitiveConfig|).
- Passing a table implicitly enables the
- integration — no `enabled` field needed.
- When active, the `fugitive` filetype is
- registered and status buffer keymaps are set. >lua
- vim.g.diffs = { fugitive = true }
- vim.g.diffs = {
- fugitive = { horizontal = 'dd' },
- }
-<
-
- {neogit} (boolean|table, default: false)
- Enable Neogit integration. Pass `true` or
- `{}` to enable, `false` to disable. When
- active, `NeogitStatus`, `NeogitCommitView`,
- and `NeogitDiffView` filetypes are registered
- and Neogit highlight overrides are applied.
- See |diffs-neogit|. >lua
- vim.g.diffs = { neogit = true }
-<
-
- {gitsigns} (boolean|table, default: false)
- Enable gitsigns.nvim blame popup highlighting.
- Pass `true` or `{}` to enable, `false` to
- disable. When active, `:Gitsigns blame_line`
- popups receive treesitter syntax, line
- backgrounds, and intra-line character diffs.
- See |diffs-gitsigns|. >lua
- vim.g.diffs = { gitsigns = true }
-<
-
- {extra_filetypes} (table, default: {})
- Additional filetypes to attach to, beyond the
- built-in `git`, `gitcommit`, and any enabled
- integration filetypes. Use this to enable
- highlighting in plain `.diff` / `.patch`
- files: >lua
- vim.g.diffs = {
- extra_filetypes = { 'diff' },
- }
-<
-
{highlights} (table, default: see below)
Controls which highlight features are enabled.
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.
@@ -220,17 +153,13 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
See |diffs.TreesitterConfig| for fields.
{vim} (table, default: see below)
- Vim syntax fallback highlighting options.
+ Vim syntax highlighting options (experimental).
See |diffs.VimConfig| for fields.
{intra} (table, default: see below)
Character-level (intra-line) diff highlighting.
See |diffs.IntraConfig| for fields.
- {priorities} (table, default: see below)
- Extmark priority values.
- See |diffs.PrioritiesConfig| for fields.
-
{overrides} (table, default: {})
Map of highlight group names to highlight
definitions (see |nvim_set_hl()|). Applied
@@ -241,42 +170,16 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
*diffs.ContextConfig*
Context config fields: ~
{enabled} (boolean, default: true)
- Read surrounding code from the working tree
- file and feed it into the treesitter string
- parser. Uses the hunk's `@@ +start,count @@`
- line numbers to read lines before and after
- the hunk from disk. Improves syntax accuracy
- when the hunk is inside an incomplete construct
- (e.g., a table literal or function body whose
- opening is not visible in the hunk's own
- context lines).
+ 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)
- Max context lines to read in each direction.
- Files are read once per parse and cached across
- hunks in the same file.
-
- *diffs.PrioritiesConfig*
- Priorities config fields: ~
- {clear} (integer, default: 198)
- Priority for `DiffsClear` extmarks that reset
- underlying diff foreground colors. Must be
- below {syntax}.
-
- {syntax} (integer, default: 199)
- Priority for treesitter and vim syntax extmarks.
- Must be below {line_bg} so that colorscheme
- backgrounds on syntax groups do not obscure
- line-level diff backgrounds.
-
- {line_bg} (integer, default: 200)
- Priority for `DiffsAdd`/`DiffsDelete` line
- background extmarks. Must be below {char_bg}.
-
- {char_bg} (integer, default: 201)
- Priority for `DiffsAddText`/`DiffsDeleteText`
- character-level background extmarks. Highest
- priority so changed characters stand out.
+ Number of context lines to read in each
+ direction. Lines are read with early exit —
+ cost scales with this value, not file size.
*diffs.TreesitterConfig*
Treesitter config fields: ~
@@ -289,15 +192,13 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
*diffs.VimConfig*
Vim config fields: ~
- {enabled} (boolean, default: true)
+ {enabled} (boolean, default: false)
Use vim syntax highlighting as fallback when no
treesitter parser is available for a language.
Creates a scratch buffer, sets the filetype, and
queries |synID()| per character to extract
- highlight groups. Deferred via |vim.schedule()|
- so it never blocks the first paint. Slower than
- treesitter but covers languages without a TS
- parser installed (e.g., COBOL, Fortran).
+ highlight groups. Slower than treesitter but
+ covers languages without a TS parser installed.
{max_lines} (integer, default: 200)
Skip vim syntax highlighting for hunks larger than
@@ -410,36 +311,10 @@ Example configuration: >lua
vim.keymap.set('n', 'ct', '(diffs-conflict-theirs)')
vim.keymap.set('n', 'cb', '(diffs-conflict-both)')
vim.keymap.set('n', 'cn', '(diffs-conflict-none)')
- vim.keymap.set('n', ']c', '(diffs-conflict-next)')
- vim.keymap.set('n', '[c', '(diffs-conflict-prev)')
+ vim.keymap.set('n', ']x', '(diffs-conflict-next)')
+ vim.keymap.set('n', '[x', '(diffs-conflict-prev)')
<
- *(diffs-merge-ours)*
-(diffs-merge-ours)
- Accept ours in a merge diff view. Resolves the
- conflict in the working file with ours content.
-
- *(diffs-merge-theirs)*
-(diffs-merge-theirs)
- Accept theirs in a merge diff view.
-
- *(diffs-merge-both)*
-(diffs-merge-both)
- Accept both (ours then theirs) in a merge diff view.
-
- *(diffs-merge-none)*
-(diffs-merge-none)
- Reject both in a merge diff view.
-
- *(diffs-merge-next)*
-(diffs-merge-next)
- Jump to next unresolved conflict hunk in merge diff.
-
- *(diffs-merge-prev)*
-(diffs-merge-prev)
- Jump to previous unresolved conflict hunk in merge
- diff.
-
Diff buffer mappings: ~
*diffs-q*
q Close the diff window. Available in all `diffs://`
@@ -470,7 +345,6 @@ Behavior by file status: ~
A Staged (empty) index file as all-added
D Staged HEAD (empty) file as all-removed
R Staged HEAD:oldname index:newname content diff
- U Unstaged :2: (ours) :3: (theirs) merge diff
? Untracked (empty) working tree file as all-added
On section headers, the keymap runs `git diff` (or `git diff --cached` for
@@ -487,9 +361,6 @@ Configuration: ~
},
}
<
- Passing a table enables fugitive integration. Use `fugitive = false`
- to disable. There is no `enabled` field; table presence is sufficient.
-
Fields: ~
{horizontal} (string|false, default: 'du')
Keymap for unified diff in horizontal split.
@@ -518,15 +389,13 @@ Configuration: ~
enabled = true,
disable_diagnostics = true,
show_virtual_text = true,
- show_actions = false,
- priority = 200,
keymaps = {
ours = 'doo',
theirs = 'dot',
both = 'dob',
none = 'don',
- next = ']c',
- prev = '[c',
+ next = ']x',
+ prev = '[x',
},
},
}
@@ -546,37 +415,9 @@ Configuration: ~
diagnostics alone.
{show_virtual_text} (boolean, default: true)
- Show `(current)` and `(incoming)` labels at
- the end of `<<<<<<<` and `>>>>>>>` marker
- lines. Also controls hunk hints in merge
- diff views.
-
- {format_virtual_text} (function|nil, default: nil)
- Custom formatter for virtual text labels.
- Receives `(side, keymap)` where `side` is
- `"ours"` or `"theirs"` and `keymap` is the
- configured keymap string or `false`. Return
- a string (label text without parens) or
- `nil` to hide the label. Example: >lua
- format_virtual_text = function(side, keymap)
- if keymap then
- return side .. ' [' .. keymap .. ']'
- end
- return side
- end
-<
-
- {show_actions} (boolean, default: false)
- Show a codelens-style action line above each
- `<<<<<<<` marker listing available resolution
- keymaps. Renders as virtual lines using the
- `DiffsConflictActions` highlight group.
- Only keymaps that are not `false` appear.
-
- {priority} (integer, default: 200)
- Extmark priority for conflict region
- backgrounds and markers. Adjust if other
- plugins use the same priority range.
+ 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
@@ -597,10 +438,10 @@ Configuration: ~
{none} (string|false, default: 'don')
Reject both changes (delete entire block).
- {next} (string|false, default: ']c')
+ {next} (string|false, default: ']x')
Jump to next conflict marker. Wraps around.
- {prev} (string|false, default: '[c')
+ {prev} (string|false, default: '[x')
Jump to previous conflict marker. Wraps
around.
@@ -617,77 +458,6 @@ User events: ~
})
<
-==============================================================================
-MERGE DIFF RESOLUTION *diffs-merge*
-
-When pressing `du`/`dU` on an unmerged (`U`) file in the fugitive status
-buffer, diffs.nvim opens a unified diff of ours (`git show :2:path`) vs
-theirs (`git show :3:path`) with full treesitter and intra-line highlighting.
-
-The same conflict resolution keymaps (`doo`/`dot`/`dob`/`don`/`]c`/`[c`)
-are available on the diff buffer. They resolve conflicts in the working
-file by matching diff hunks to conflict markers:
-
-- `doo` replaces the conflict region with ours content
-- `dot` replaces the conflict region with theirs content
-- `dob` replaces with both (ours then theirs)
-- `don` removes the conflict region entirely
-- `]c`/`[c` navigate between unresolved conflict hunks
-
-Resolved hunks are marked with `(resolved)` virtual text. Hunks that
-correspond to auto-merged content (no conflict markers) show an
-informational notification and are left unchanged.
-
-The working file buffer is modified in place; save it when ready.
-Phase 1 inline conflict highlights (see |diffs-conflict|) are refreshed
-automatically after each resolution.
-
-==============================================================================
-NEOGIT *diffs-neogit*
-
-diffs.nvim works with Neogit (https://github.com/NeogitOrg/neogit) out of
-the box. Enable Neogit support in your config: >lua
- vim.g.diffs = { neogit = true }
-<
-
-When a diff is expanded in a Neogit buffer (e.g., via TAB on a file in the
-status view), diffs.nvim applies treesitter syntax highlighting and
-intra-line diffs to the hunk lines, just as it does for fugitive.
-
-Neogit highlight overrides: ~
-On first attach to a Neogit buffer, diffs.nvim overrides Neogit's diff
-highlight groups (`NeogitDiffAdd*`, `NeogitDiffDelete*`,
-`NeogitDiffContext*`, `NeogitHunkHeader*`, `NeogitDiffHeader*`, etc.) by
-setting them to empty (`{}`). This gives diffs.nvim sole control of diff
-line visuals. The overrides are reapplied on `ColorScheme` since Neogit
-re-defines its groups then. When `neogit = false`, no highlight overrides
-are applied.
-
-==============================================================================
-GITSIGNS *diffs-gitsigns*
-
-diffs.nvim can enhance gitsigns.nvim blame popups with syntax highlighting.
-Enable gitsigns support in your config: >lua
- vim.g.diffs = { gitsigns = true }
-<
-
-When `:Gitsigns blame_line full=true` opens a popup, diffs.nvim intercepts
-the popup and replaces gitsigns' flat `GitSignsAddPreview`/
-`GitSignsDeletePreview` highlights with:
-
-- Treesitter syntax highlighting on the code content
-- `DiffsAdd`/`DiffsDelete` line backgrounds
-- Character-level intra-line diffs (`DiffsAddText`/`DiffsDeleteText`)
-- `@diff.plus`/`@diff.minus` coloring on `+`/`-` prefix characters
-
-The integration patches `gitsigns.popup.create` and `gitsigns.popup.update`
-so highlights persist across gitsigns' two-phase render (initial popup, then
-update with GitHub/PR data). If gitsigns loads after diffs.nvim, a
-`User GitAttach` autocmd retries the setup automatically.
-
-Highlights are applied in a separate `diffs-gitsigns` namespace and do not
-interfere with the main decoration provider used for diff buffers.
-
==============================================================================
API *diffs-api*
@@ -709,8 +479,8 @@ refresh({bufnr}) *diffs.refresh()*
IMPLEMENTATION *diffs-implementation*
Summary / commit detail views: ~
-1. `FileType` autocmd for computed filetypes (see |diffs-config|) triggers
- |diffs.attach()|. For `git` buffers, only `fugitive://` URIs are attached.
+1. `FileType fugitive` or `FileType git` (for `fugitive://` buffers)
+ triggers |diffs.attach()|
2. The buffer is parsed to detect file headers (`M path/to/file`,
`diff --git a/... b/...`) and hunk headers (`@@ -10,3 +10,4 @@`)
3. For each hunk:
@@ -719,14 +489,13 @@ Summary / commit detail views: ~
- Code is parsed with |vim.treesitter.get_string_parser()|
- If no treesitter parser and `vim.enabled`: vim syntax fallback via
scratch buffer and |synID()|
- - `DiffsClear` extmarks at priority 198 clear underlying diff foreground
- - Syntax highlights are applied as extmarks at priority 199
- - Background extmarks (`DiffsAdd`/`DiffsDelete`) at priority 200
+ - `Normal` extmarks at priority 198 clear underlying diff foreground
+ - Background extmarks (`DiffsAdd`/`DiffsDelete`) at priority 199
+ - 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
- - All priorities are configurable via |diffs.PrioritiesConfig|
-4. A decoration provider re-highlights visible hunks on each redraw
+4. Re-highlighting occurs on `TextChanged` (debounced) and `Syntax` events
Diff mode views: ~
1. `OptionSet diff` detects when any window enters diff mode
@@ -740,14 +509,15 @@ KNOWN LIMITATIONS *diffs-limitations*
Incomplete Syntax Context ~
*diffs-syntax-context*
-Treesitter parses each diff hunk in isolation. When `highlights.context` is
-enabled (the default), surrounding code is read from the working tree file
-and fed into the parser to improve accuracy at hunk boundaries. This helps
-when a hunk is inside a table, function body, or loop whose opening is
-beyond the hunk's own context lines. Requires `repo_root` and
-`file_new_start` to be available on the hunk (true for standard unified
-diffs). In rare cases, hunks that start or end mid-expression may still
-produce imperfect highlights due to treesitter error recovery.
+Treesitter parses each diff hunk in isolation. To provide surrounding code
+context, diffs.nvim reads lines from disk before and after each hunk
+(see |diffs.ContextConfig|, enabled by default). This resolves most boundary
+issues where incomplete constructs (e.g., a function definition at the edge
+of a hunk with no body) would confuse the parser.
+
+Set `highlights.context.enabled = false` to disable context padding. In rare
+cases, context padding may not help if the relevant surrounding code is very
+far from the hunk boundaries.
Syntax Highlighting Flash ~
*diffs-flash*
@@ -756,8 +526,14 @@ the buffer briefly shows fugitive's default diff highlighting before
diffs.nvim applies treesitter highlights.
This occurs because diffs.nvim hooks into the `FileType fugitive` event,
-which fires after vim-fugitive has already painted the buffer. The
-decoration provider applies highlights on the next redraw cycle.
+which fires after vim-fugitive has already painted the buffer. Even with
+`debounce_ms = 0`, the re-painting goes through Neovim's event loop.
+
+To minimize the flash, use a low debounce value: >lua
+ vim.g.diffs = {
+ debounce_ms = 0,
+ }
+<
Conflicting Diff Plugins ~
*diffs-plugin-conflicts*
@@ -799,7 +575,6 @@ 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: ~
*DiffsAdd*
DiffsAdd Background for `+` lines. Derived by blending
@@ -860,10 +635,6 @@ Conflict highlights: ~
*DiffsConflictBaseNr*
DiffsConflictBaseNr Line number for base content lines (diff3).
- *DiffsConflictActions*
- DiffsConflictActions Dimmed foreground (no bold) for the codelens-style
- action line shown when `show_actions` is true.
-
Diff mode window highlights: ~
These are used for |winhighlight| remapping in `&diff` windows.
@@ -916,8 +687,7 @@ ACKNOWLEDGEMENTS *diffs-acknowledgements*
- 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, blame_hl.nvim (gitsigns blame popup
- highlighting inspiration)
+ treesitter injection support
==============================================================================
vim:tw=78:ts=8:ft=help:norl:
diff --git a/flake.lock b/flake.lock
deleted file mode 100644
index 0c2cb09..0000000
--- a/flake.lock
+++ /dev/null
@@ -1,43 +0,0 @@
-{
- "nodes": {
- "nixpkgs": {
- "locked": {
- "lastModified": 1770812194,
- "narHash": "sha256-OH+lkaIKAvPXR3nITO7iYZwew2nW9Y7Xxq0yfM/UcUU=",
- "owner": "NixOS",
- "repo": "nixpkgs",
- "rev": "8482c7ded03bae7550f3d69884f1e611e3bd19e8",
- "type": "github"
- },
- "original": {
- "owner": "NixOS",
- "ref": "nixpkgs-unstable",
- "repo": "nixpkgs",
- "type": "github"
- }
- },
- "root": {
- "inputs": {
- "nixpkgs": "nixpkgs",
- "systems": "systems"
- }
- },
- "systems": {
- "locked": {
- "lastModified": 1681028828,
- "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
- "owner": "nix-systems",
- "repo": "default",
- "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
- "type": "github"
- },
- "original": {
- "owner": "nix-systems",
- "repo": "default",
- "type": "github"
- }
- }
- },
- "root": "root",
- "version": 7
-}
diff --git a/flake.nix b/flake.nix
deleted file mode 100644
index 2221bdf..0000000
--- a/flake.nix
+++ /dev/null
@@ -1,53 +0,0 @@
-{
- description = "diffs.nvim — syntax highlighting for diffs in Neovim";
-
- inputs = {
- nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
- systems.url = "github:nix-systems/default";
- };
-
- outputs =
- {
- nixpkgs,
- systems,
- ...
- }:
- let
- forEachSystem =
- f: nixpkgs.lib.genAttrs (import systems) (system: f nixpkgs.legacyPackages.${system});
- in
- {
- formatter = forEachSystem (pkgs: pkgs.nixfmt-tree);
-
- devShells = forEachSystem (pkgs: {
- default =
- let
- ts-plugin = pkgs.vimPlugins.nvim-treesitter.withPlugins (p: [ p.diff ]);
- diff-grammar = pkgs.vimPlugins.nvim-treesitter-parsers.diff;
- luaEnv = pkgs.luajit.withPackages (
- ps: with ps; [
- busted
- nlua
- ]
- );
- busted-with-grammar = pkgs.writeShellScriptBin "busted" ''
- nvim_bin=$(which nvim)
- tmpdir=$(mktemp -d)
- trap 'rm -rf "$tmpdir"' EXIT
- printf '#!/bin/sh\nexec "%s" --cmd "set rtp+=${ts-plugin}/runtime" --cmd "set rtp+=${diff-grammar}" "$@"\n' "$nvim_bin" > "$tmpdir/nvim"
- chmod +x "$tmpdir/nvim"
- PATH="$tmpdir:$PATH" exec ${luaEnv}/bin/busted "$@"
- '';
- in
- pkgs.mkShell {
- packages = [
- busted-with-grammar
- pkgs.prettier
- pkgs.stylua
- pkgs.selene
- pkgs.lua-language-server
- ];
- };
- });
- };
-}
diff --git a/lua/diffs/commands.lua b/lua/diffs/commands.lua
index 01f1d3a..dc6cfe2 100644
--- a/lua/diffs/commands.lua
+++ b/lua/diffs/commands.lua
@@ -36,24 +36,6 @@ function M.find_hunk_line(diff_lines, hunk_position)
return nil
end
----@param lines string[]
----@return string[]
-function M.filter_combined_diffs(lines)
- local result = {}
- local skip = false
- for _, line in ipairs(lines) do
- if line:match('^diff %-%-cc ') then
- skip = true
- elseif line:match('^diff %-%-git ') then
- skip = false
- end
- if not skip then
- table.insert(result, line)
- end
- end
- return result
-end
-
---@param old_lines string[]
---@param new_lines string[]
---@param old_name string
@@ -87,33 +69,6 @@ local function generate_unified_diff(old_lines, new_lines, old_name, new_name)
return result
end
----@param raw_lines string[]
----@param repo_root string
----@return string[]
-local function replace_combined_diffs(raw_lines, repo_root)
- local unmerged_files = {}
- for _, line in ipairs(raw_lines) do
- local cc_file = line:match('^diff %-%-cc (.+)$')
- if cc_file then
- table.insert(unmerged_files, cc_file)
- end
- end
-
- local result = M.filter_combined_diffs(raw_lines)
-
- for _, filename in ipairs(unmerged_files) do
- local filepath = repo_root .. '/' .. filename
- local old_lines = git.get_file_content(':2', filepath) or {}
- local new_lines = git.get_file_content(':3', filepath) or {}
- local diff_lines = generate_unified_diff(old_lines, new_lines, filename, filename)
- for _, dl in ipairs(diff_lines) do
- table.insert(result, dl)
- end
- end
-
- return result
-end
-
---@param revision? string
---@param vertical? boolean
function M.gdiff(revision, vertical)
@@ -183,7 +138,6 @@ end
---@field vertical? boolean
---@field staged? boolean
---@field untracked? boolean
----@field unmerged? boolean
---@field old_filepath? string
---@field hunk_position? { hunk_header: string, offset: integer }
@@ -203,17 +157,7 @@ function M.gdiff_file(filepath, opts)
local old_lines, new_lines, err
local diff_label
- if opts.unmerged then
- old_lines = git.get_file_content(':2', filepath)
- if not old_lines then
- old_lines = {}
- end
- new_lines = git.get_file_content(':3', filepath)
- if not new_lines then
- new_lines = {}
- end
- diff_label = 'unmerged'
- elseif opts.untracked then
+ if opts.untracked then
old_lines = {}
new_lines, err = git.get_working_content(filepath)
if not new_lines then
@@ -292,14 +236,6 @@ function M.gdiff_file(filepath, opts)
end
M.setup_diff_buf(diff_buf)
-
- if diff_label == 'unmerged' then
- vim.api.nvim_buf_set_var(diff_buf, 'diffs_unmerged', true)
- vim.api.nvim_buf_set_var(diff_buf, 'diffs_working_path', filepath)
- local conflict_config = require('diffs').get_conflict_config()
- require('diffs.merge').setup_keymaps(diff_buf, conflict_config)
- end
-
dbg('opened diff buffer %d for %s (%s)', diff_buf, rel_path, diff_label)
vim.schedule(function()
@@ -327,8 +263,6 @@ function M.gdiff_section(repo_root, opts)
return
end
- result = replace_combined_diffs(result, repo_root)
-
if #result == 0 then
vim.notify('[diffs.nvim]: no changes in section', vim.log.levels.INFO)
return
@@ -391,8 +325,6 @@ function M.read_buffer(bufnr)
if vim.v.shell_error ~= 0 then
diff_lines = {}
end
-
- diff_lines = replace_combined_diffs(diff_lines, repo_root)
else
local abs_path = repo_root .. '/' .. path
@@ -402,10 +334,7 @@ function M.read_buffer(bufnr)
local old_lines, new_lines
- if label == 'unmerged' then
- old_lines = git.get_file_content(':2', abs_path) or {}
- new_lines = git.get_file_content(':3', abs_path) or {}
- elseif label == 'untracked' then
+ if label == 'untracked' then
old_lines = {}
new_lines = git.get_working_content(abs_path) or {}
elseif label == 'staged' then
diff --git a/lua/diffs/conflict.lua b/lua/diffs/conflict.lua
index 5ed8009..9e62a15 100644
--- a/lua/diffs/conflict.lua
+++ b/lua/diffs/conflict.lua
@@ -20,6 +20,8 @@ local attached_buffers = {}
---@type table
local diagnostics_suppressed = {}
+local PRIORITY_LINE_BG = 200
+
---@param lines string[]
---@return diffs.ConflictRegion[]
function M.parse(lines)
@@ -90,17 +92,6 @@ local function parse_buffer(bufnr)
return M.parse(lines)
end
----@param side string
----@param config diffs.ConflictConfig
----@return string?
-local function get_virtual_text_label(side, config)
- if config.format_virtual_text then
- local keymap = side == 'ours' and config.keymaps.ours or config.keymaps.theirs
- return config.format_virtual_text(side, keymap)
- end
- return side == 'ours' and 'current' or 'incoming'
-end
-
---@param bufnr integer
---@param regions diffs.ConflictRegion[]
---@param config diffs.ConflictConfig
@@ -112,41 +103,14 @@ local function apply_highlights(bufnr, regions, config)
end_row = region.marker_ours + 1,
hl_group = 'DiffsConflictMarker',
hl_eol = true,
- priority = config.priority,
+ priority = PRIORITY_LINE_BG,
})
if config.show_virtual_text then
- local ours_label = get_virtual_text_label('ours', config)
- if ours_label then
- pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, region.marker_ours, 0, {
- virt_text = { { ' (' .. ours_label .. ')', 'DiffsConflictMarker' } },
- virt_text_pos = 'eol',
- })
- end
- end
-
- if config.show_actions then
- local parts = {}
- local actions = {
- { 'Current', config.keymaps.ours },
- { 'Incoming', config.keymaps.theirs },
- { 'Both', config.keymaps.both },
- { 'None', config.keymaps.none },
- }
- for _, action in ipairs(actions) do
- if action[2] then
- if #parts > 0 then
- table.insert(parts, { ' \226\148\130 ', 'DiffsConflictActions' })
- end
- table.insert(parts, { ('%s (%s)'):format(action[1], action[2]), 'DiffsConflictActions' })
- end
- end
- if #parts > 0 then
- pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, region.marker_ours, 0, {
- virt_lines = { parts },
- virt_lines_above = true,
- })
- end
+ 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
@@ -154,11 +118,11 @@ local function apply_highlights(bufnr, regions, config)
end_row = line + 1,
hl_group = 'DiffsConflictOurs',
hl_eol = true,
- priority = config.priority,
+ priority = PRIORITY_LINE_BG,
})
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, line, 0, {
number_hl_group = 'DiffsConflictOursNr',
- priority = config.priority,
+ priority = PRIORITY_LINE_BG,
})
end
@@ -167,7 +131,7 @@ local function apply_highlights(bufnr, regions, config)
end_row = region.marker_base + 1,
hl_group = 'DiffsConflictMarker',
hl_eol = true,
- priority = config.priority,
+ priority = PRIORITY_LINE_BG,
})
for line = region.base_start, region.base_end - 1 do
@@ -175,11 +139,11 @@ local function apply_highlights(bufnr, regions, config)
end_row = line + 1,
hl_group = 'DiffsConflictBase',
hl_eol = true,
- priority = config.priority,
+ priority = PRIORITY_LINE_BG,
})
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, line, 0, {
number_hl_group = 'DiffsConflictBaseNr',
- priority = config.priority,
+ priority = PRIORITY_LINE_BG,
})
end
end
@@ -188,7 +152,7 @@ local function apply_highlights(bufnr, regions, config)
end_row = region.marker_sep + 1,
hl_group = 'DiffsConflictMarker',
hl_eol = true,
- priority = config.priority,
+ priority = PRIORITY_LINE_BG,
})
for line = region.theirs_start, region.theirs_end - 1 do
@@ -196,11 +160,11 @@ local function apply_highlights(bufnr, regions, config)
end_row = line + 1,
hl_group = 'DiffsConflictTheirs',
hl_eol = true,
- priority = config.priority,
+ priority = PRIORITY_LINE_BG,
})
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, line, 0, {
number_hl_group = 'DiffsConflictTheirsNr',
- priority = config.priority,
+ priority = PRIORITY_LINE_BG,
})
end
@@ -208,17 +172,14 @@ local function apply_highlights(bufnr, regions, config)
end_row = region.marker_theirs + 1,
hl_group = 'DiffsConflictMarker',
hl_eol = true,
- priority = config.priority,
+ priority = PRIORITY_LINE_BG,
})
if config.show_virtual_text then
- local theirs_label = get_virtual_text_label('theirs', config)
- if theirs_label then
- pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, region.marker_theirs, 0, {
- virt_text = { { ' (' .. theirs_label .. ')', 'DiffsConflictMarker' } },
- virt_text_pos = 'eol',
- })
- end
+ pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, region.marker_theirs, 0, {
+ virt_text = { { ' (incoming)', 'DiffsConflictMarker' } },
+ virt_text_pos = 'eol',
+ })
end
end
end
@@ -238,7 +199,7 @@ end
---@param bufnr integer
---@param region diffs.ConflictRegion
---@param replacement string[]
-function M.replace_region(bufnr, region, replacement)
+local function replace_region(bufnr, region, replacement)
vim.api.nvim_buf_set_lines(
bufnr,
region.marker_ours,
@@ -250,7 +211,7 @@ end
---@param bufnr integer
---@param config diffs.ConflictConfig
-function M.refresh(bufnr, config)
+local function refresh(bufnr, config)
local regions = parse_buffer(bufnr)
if #regions == 0 then
vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
@@ -283,8 +244,8 @@ function M.resolve_ours(bufnr, config)
return
end
local lines = vim.api.nvim_buf_get_lines(bufnr, region.ours_start, region.ours_end, false)
- M.replace_region(bufnr, region, lines)
- M.refresh(bufnr, config)
+ replace_region(bufnr, region, lines)
+ refresh(bufnr, config)
end
---@param bufnr integer
@@ -301,8 +262,8 @@ function M.resolve_theirs(bufnr, config)
return
end
local lines = vim.api.nvim_buf_get_lines(bufnr, region.theirs_start, region.theirs_end, false)
- M.replace_region(bufnr, region, lines)
- M.refresh(bufnr, config)
+ replace_region(bufnr, region, lines)
+ refresh(bufnr, config)
end
---@param bufnr integer
@@ -327,8 +288,8 @@ function M.resolve_both(bufnr, config)
for _, l in ipairs(theirs) do
table.insert(combined, l)
end
- M.replace_region(bufnr, region, combined)
- M.refresh(bufnr, config)
+ replace_region(bufnr, region, combined)
+ refresh(bufnr, config)
end
---@param bufnr integer
@@ -344,8 +305,8 @@ function M.resolve_none(bufnr, config)
if not region then
return
end
- M.replace_region(bufnr, region, {})
- M.refresh(bufnr, config)
+ replace_region(bufnr, region, {})
+ refresh(bufnr, config)
end
---@param bufnr integer
@@ -362,7 +323,6 @@ function M.goto_next(bufnr)
return
end
end
- vim.notify('[diffs.nvim]: wrapped to first conflict', vim.log.levels.INFO)
vim.api.nvim_win_set_cursor(0, { regions[1].marker_ours + 1, 0 })
end
@@ -380,7 +340,6 @@ function M.goto_prev(bufnr)
return
end
end
- vim.notify('[diffs.nvim]: wrapped to last conflict', vim.log.levels.INFO)
vim.api.nvim_win_set_cursor(0, { regions[#regions].marker_ours + 1, 0 })
end
@@ -458,7 +417,7 @@ function M.attach(bufnr, config)
if not attached_buffers[bufnr] then
return true
end
- M.refresh(bufnr, config)
+ refresh(bufnr, config)
end,
})
diff --git a/lua/diffs/fugitive.lua b/lua/diffs/fugitive.lua
index d4c3782..a588a22 100644
--- a/lua/diffs/fugitive.lua
+++ b/lua/diffs/fugitive.lua
@@ -26,65 +26,20 @@ function M.get_section_at_line(bufnr, lnum)
return nil
end
----@param s string
----@return string
-local function unquote(s)
- if s:sub(1, 1) ~= '"' then
- return s
- end
- local inner = s:sub(2, -2)
- local result = {}
- local i = 1
- while i <= #inner do
- if inner:sub(i, i) == '\\' and i < #inner then
- local next_char = inner:sub(i + 1, i + 1)
- if next_char == 'n' then
- table.insert(result, '\n')
- i = i + 2
- elseif next_char == 't' then
- table.insert(result, '\t')
- i = i + 2
- elseif next_char == '"' then
- table.insert(result, '"')
- i = i + 2
- elseif next_char == '\\' then
- table.insert(result, '\\')
- i = i + 2
- elseif next_char:match('%d') then
- local oct = inner:match('^(%d%d%d)', i + 1)
- if oct then
- table.insert(result, string.char(tonumber(oct, 8)))
- i = i + 4
- else
- table.insert(result, next_char)
- i = i + 2
- end
- else
- table.insert(result, next_char)
- i = i + 2
- end
- else
- table.insert(result, inner:sub(i, i))
- i = i + 1
- end
- end
- return table.concat(result)
-end
-
---@param line string
----@return string?, string?, 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 unquote(vim.trim(new)), unquote(vim.trim(old)), 'R'
+ return vim.trim(new), vim.trim(old)
end
- local status, filename = line:match('^([MADRCU?])[MADRCU%s]*%s+(.+)$')
- if status and filename then
- return unquote(vim.trim(filename)), nil, status
+ local filename = line:match('^[MADRCU?][MADRCU%s]*%s+(.+)$')
+ if filename then
+ return vim.trim(filename), nil
end
- return nil, nil, nil
+ return nil, nil
end
---@param line string
@@ -102,34 +57,34 @@ end
---@param bufnr integer
---@param lnum integer
----@return string?, diffs.FugitiveSection, boolean, string?, string?
+---@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, nil
+ return nil, nil, false, nil
end
local section_header = parse_section_header(current_line)
if section_header then
- return nil, section_header, true, nil, nil
+ return nil, section_header, true, nil
end
- local filename, old_filename, status = parse_file_line(current_line)
+ 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, status
+ 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, status = parse_file_line(prev_line)
+ 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, status
+ return filename, section, false, old_filename
end
if prev_line:match('^%w+ %(') or prev_line == '' then
break
@@ -137,7 +92,7 @@ function M.get_file_at_line(bufnr, lnum)
end
end
- return nil, nil, false, nil, nil
+ return nil, nil, false, nil
end
---@class diffs.HunkPosition
@@ -195,7 +150,7 @@ 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, status = M.get_file_at_line(bufnr, lnum)
+ 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
@@ -237,7 +192,6 @@ function M.diff_file_under_cursor(vertical)
vertical = vertical,
staged = section == 'staged',
untracked = section == 'untracked',
- unmerged = status == 'U',
old_filepath = old_filepath,
hunk_position = hunk_position,
})
diff --git a/lua/diffs/git.lua b/lua/diffs/git.lua
index 1aa6328..7695283 100644
--- a/lua/diffs/git.lua
+++ b/lua/diffs/git.lua
@@ -1,19 +1,13 @@
local M = {}
-local repo_root_cache = {}
-
---@param filepath string
---@return string?
function M.get_repo_root(filepath)
local dir = vim.fn.fnamemodify(filepath, ':h')
- if repo_root_cache[dir] ~= nil then
- return repo_root_cache[dir]
- end
local result = vim.fn.systemlist({ 'git', '-C', dir, 'rev-parse', '--show-toplevel' })
if vim.v.shell_error ~= 0 then
return nil
end
- repo_root_cache[dir] = result[1]
return result[1]
end
diff --git a/lua/diffs/gitsigns.lua b/lua/diffs/gitsigns.lua
deleted file mode 100644
index 0439fb2..0000000
--- a/lua/diffs/gitsigns.lua
+++ /dev/null
@@ -1,172 +0,0 @@
-local M = {}
-
-local api = vim.api
-local fn = vim.fn
-local dbg = require('diffs.log').dbg
-
-local ns = api.nvim_create_namespace('diffs-gitsigns')
-local gs_popup_ns = api.nvim_create_namespace('gitsigns_popup')
-
-local patched = false
-
----@param bufnr integer
----@param src_filename string
----@param src_ft string?
----@param src_lang string?
----@return diffs.Hunk[]
-function M.parse_blame_hunks(bufnr, src_filename, src_ft, src_lang)
- local lines = api.nvim_buf_get_lines(bufnr, 0, -1, false)
- local hunks = {}
- local hunk_lines = {}
- local hunk_start = nil
-
- for i, line in ipairs(lines) do
- if line:match('^Hunk %d+ of %d+') then
- if hunk_start and #hunk_lines > 0 then
- table.insert(hunks, {
- filename = src_filename,
- ft = src_ft,
- lang = src_lang,
- start_line = hunk_start,
- prefix_width = 1,
- quote_width = 0,
- lines = hunk_lines,
- })
- end
- hunk_lines = {}
- hunk_start = i
- elseif hunk_start then
- if line:match('^%(guessed:') then
- hunk_start = i
- else
- local prefix = line:sub(1, 1)
- if prefix == ' ' or prefix == '+' or prefix == '-' then
- if #hunk_lines == 0 then
- hunk_start = i - 1
- end
- table.insert(hunk_lines, line)
- end
- end
- end
- end
-
- if hunk_start and #hunk_lines > 0 then
- table.insert(hunks, {
- filename = src_filename,
- ft = src_ft,
- lang = src_lang,
- start_line = hunk_start,
- prefix_width = 1,
- quote_width = 0,
- lines = hunk_lines,
- })
- end
-
- return hunks
-end
-
----@param preview_winid integer
----@param preview_bufnr integer
-local function on_preview(preview_winid, preview_bufnr)
- local ok, err = pcall(function()
- if not api.nvim_buf_is_valid(preview_bufnr) then
- return
- end
- if not api.nvim_win_is_valid(preview_winid) then
- return
- end
-
- local win = api.nvim_get_current_win()
- if win == preview_winid then
- win = fn.win_getid(fn.winnr('#'))
- end
- if win == -1 or win == 0 or not api.nvim_win_is_valid(win) then
- return
- end
-
- local srcbuf = api.nvim_win_get_buf(win)
- if not api.nvim_buf_is_loaded(srcbuf) then
- return
- end
-
- local ft = vim.bo[srcbuf].filetype
- local name = api.nvim_buf_get_name(srcbuf)
- if not name or name == '' then
- name = ft and ('a.' .. ft) or 'unknown'
- end
- local lang = ft and require('diffs.parser').get_lang_from_ft(ft) or nil
-
- local hunks = M.parse_blame_hunks(preview_bufnr, name, ft, lang)
- if #hunks == 0 then
- return
- end
-
- local diff_start = hunks[1].start_line
- local last = hunks[#hunks]
- local diff_end = last.start_line + #last.lines
-
- api.nvim_buf_clear_namespace(preview_bufnr, gs_popup_ns, diff_start, diff_end)
- api.nvim_buf_clear_namespace(preview_bufnr, ns, diff_start, diff_end)
-
- local opts = require('diffs').get_highlight_opts()
- local highlight = require('diffs.highlight')
- for _, hunk in ipairs(hunks) do
- highlight.highlight_hunk(preview_bufnr, ns, hunk, opts)
- for j, line in ipairs(hunk.lines) do
- local ch = line:sub(1, 1)
- if ch == '+' or ch == '-' then
- pcall(api.nvim_buf_set_extmark, preview_bufnr, ns, hunk.start_line + j - 1, 0, {
- end_col = 1,
- hl_group = ch == '+' and '@diff.plus' or '@diff.minus',
- priority = opts.highlights.priorities.syntax,
- })
- end
- end
- end
-
- dbg('gitsigns blame: highlighted %d hunks in popup buf %d', #hunks, preview_bufnr)
- end)
- if not ok then
- dbg('gitsigns blame error: %s', err)
- end
-end
-
----@return boolean
-function M.setup()
- if patched then
- return true
- end
-
- local pop_ok, Popup = pcall(require, 'gitsigns.popup')
- if not pop_ok or not Popup then
- return false
- end
-
- Popup.create = (function(orig)
- return function(...)
- local winid, bufnr = orig(...)
- on_preview(winid, bufnr)
- return winid, bufnr
- end
- end)(Popup.create)
-
- Popup.update = (function(orig)
- return function(winid, bufnr, ...)
- orig(winid, bufnr, ...)
- on_preview(winid, bufnr)
- end
- end)(Popup.update)
-
- patched = true
- dbg('gitsigns popup patched')
- return true
-end
-
-M._test = {
- parse_blame_hunks = M.parse_blame_hunks,
- on_preview = on_preview,
- ns = ns,
- gs_popup_ns = gs_popup_ns,
-}
-
-return M
diff --git a/lua/diffs/highlight.lua b/lua/diffs/highlight.lua
index 493bcfc..306b0e5 100644
--- a/lua/diffs/highlight.lua
+++ b/lua/diffs/highlight.lua
@@ -3,6 +3,38 @@ local M = {}
local dbg = require('diffs.log').dbg
local diff = require('diffs.diff')
+---@param filepath string
+---@param from_line integer
+---@param count integer
+---@return string[]
+local function read_line_range(filepath, from_line, count)
+ if count <= 0 then
+ return {}
+ end
+ local f = io.open(filepath, 'r')
+ if not f then
+ return {}
+ end
+ local result = {}
+ local line_num = 0
+ for line in f:lines() do
+ line_num = line_num + 1
+ if line_num >= from_line then
+ table.insert(result, line)
+ if #result >= count then
+ break
+ end
+ end
+ end
+ f:close()
+ return result
+end
+
+local PRIORITY_CLEAR = 198
+local PRIORITY_SYNTAX = 199
+local PRIORITY_LINE_BG = 200
+local PRIORITY_CHAR_BG = 201
+
---@param bufnr integer
---@param ns integer
---@param hunk diffs.Hunk
@@ -10,9 +42,8 @@ local diff = require('diffs.diff')
---@param text string
---@param lang string
---@param context_lines? string[]
----@param priorities diffs.PrioritiesConfig
---@return integer
-local function highlight_text(bufnr, ns, hunk, col_offset, text, lang, context_lines, priorities)
+local function highlight_text(bufnr, ns, hunk, col_offset, text, lang, context_lines)
local parse_text = text
if context_lines and #context_lines > 0 then
parse_text = text .. '\n' .. table.concat(context_lines, '\n')
@@ -46,7 +77,7 @@ local function highlight_text(bufnr, ns, hunk, col_offset, text, lang, context_l
local buf_sc = col_offset + sc
local buf_ec = col_offset + ec
- local priority = lang == 'diff' and (tonumber(metadata.priority) or 100) or priorities.syntax
+ local priority = lang == 'diff' and (tonumber(metadata.priority) or 100) or PRIORITY_SYNTAX
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_sr, buf_sc, {
end_row = buf_er,
@@ -64,12 +95,6 @@ end
---@class diffs.HunkOpts
---@field hide_prefix boolean
---@field highlights diffs.Highlights
----@field defer_vim_syntax? boolean
----@field syntax_only? boolean
-
----@class diffs.TSContext
----@field before string[]?
----@field after string[]?
---@param bufnr integer
---@param ns integer
@@ -78,9 +103,6 @@ end
---@param line_map table
---@param col_offset integer
---@param covered_lines? table
----@param priorities diffs.PrioritiesConfig
----@param force_high_priority? boolean
----@param context? diffs.TSContext
---@return integer
local function highlight_treesitter(
bufnr,
@@ -89,36 +111,9 @@ local function highlight_treesitter(
lang,
line_map,
col_offset,
- covered_lines,
- priorities,
- force_high_priority,
- context
+ covered_lines
)
- local prefix_count = 0
- local parse_lines = code_lines
- if context then
- local before = context.before
- local after = context.after
- if (before and #before > 0) or (after and #after > 0) then
- parse_lines = {}
- if before then
- prefix_count = #before
- for _, l in ipairs(before) do
- parse_lines[#parse_lines + 1] = l
- end
- end
- for _, l in ipairs(code_lines) do
- parse_lines[#parse_lines + 1] = l
- end
- if after then
- for _, l in ipairs(after) do
- parse_lines[#parse_lines + 1] = l
- end
- end
- end
- end
-
- local code = table.concat(parse_lines, '\n')
+ local code = table.concat(code_lines, '\n')
if code == '' then
return 0
end
@@ -148,8 +143,6 @@ local function highlight_treesitter(
if capture ~= 'spell' and capture ~= 'nospell' then
local capture_name = '@' .. capture .. '.' .. tree_lang
local sr, sc, er, ec = node:range()
- sr = sr - prefix_count
- er = er - prefix_count
local buf_sr = line_map[sr]
if buf_sr then
@@ -158,10 +151,8 @@ local function highlight_treesitter(
local buf_sc = sc + col_offset
local buf_ec = ec + col_offset
- local meta_prio = tonumber(metadata.priority) or 100
- local priority = tree_lang == 'diff'
- and ((col_offset > 0 or force_high_priority) and (priorities.syntax + meta_prio - 100) or meta_prio)
- or priorities.syntax
+ local priority = tree_lang == 'diff' and (tonumber(metadata.priority) or 100)
+ or PRIORITY_SYNTAX
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_sr, buf_sc, {
end_row = buf_er,
@@ -228,17 +219,8 @@ end
---@param code_lines string[]
---@param covered_lines? table
---@param leading_offset? integer
----@param priorities diffs.PrioritiesConfig
---@return integer
-local function highlight_vim_syntax(
- bufnr,
- ns,
- hunk,
- code_lines,
- covered_lines,
- leading_offset,
- priorities
-)
+local function highlight_vim_syntax(bufnr, ns, hunk, code_lines, covered_lines, leading_offset)
local ft = hunk.ft
if not ft then
return 0
@@ -256,9 +238,9 @@ local function highlight_vim_syntax(
local spans = {}
- pcall(vim.api.nvim_buf_call, scratch, function()
+ vim.api.nvim_buf_call(scratch, function()
vim.cmd('setlocal syntax=' .. ft)
- vim.cmd.redraw()
+ vim.cmd('redraw')
---@param line integer
---@param col integer
@@ -274,19 +256,18 @@ local function highlight_vim_syntax(
spans = M.coalesce_syntax_spans(query_fn, code_lines)
end)
- pcall(vim.api.nvim_buf_delete, scratch, { force = true })
+ vim.api.nvim_buf_delete(scratch, { force = true })
local hunk_line_count = #hunk.lines
- local col_off = (hunk.prefix_width or 1) + (hunk.quote_width or 0) - 1
local extmark_count = 0
for _, span in ipairs(spans) do
local adj = span.line - leading_offset
if adj >= 1 and adj <= hunk_line_count then
local buf_line = hunk.start_line + adj - 1
- pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, span.col_start + col_off, {
- end_col = span.col_end + col_off,
+ pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, span.col_start, {
+ end_col = span.col_end,
hl_group = span.hl_name,
- priority = priorities.syntax,
+ priority = PRIORITY_SYNTAX,
})
extmark_count = extmark_count + 1
if covered_lines then
@@ -303,9 +284,6 @@ end
---@param hunk diffs.Hunk
---@param opts diffs.HunkOpts
function M.highlight_hunk(bufnr, ns, hunk, opts)
- local p = opts.highlights.priorities
- local pw = hunk.prefix_width or 1
- local qw = hunk.quote_width or 0
local use_ts = hunk.lang and opts.highlights.treesitter.enabled
local use_vim = not use_ts and hunk.ft and opts.highlights.vim.enabled
@@ -322,18 +300,28 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
use_vim = false
end
- if use_vim and opts.defer_vim_syntax then
- use_vim = false
- end
-
---@type table
local covered_lines = {}
- local extmark_count = 0
- ---@type string[]
- local new_code = {}
+ local ctx_cfg = opts.highlights.context
+ local context = (ctx_cfg and ctx_cfg.enabled) and ctx_cfg.lines or 0
+ local leading = {}
+ local trailing = {}
+ if (use_ts or use_vim) and context > 0 and hunk.file_new_start and hunk.repo_root then
+ local filepath = vim.fs.joinpath(hunk.repo_root, hunk.filename)
+ local lead_from = math.max(1, hunk.file_new_start - context)
+ local lead_count = hunk.file_new_start - lead_from
+ if lead_count > 0 then
+ leading = read_line_range(filepath, lead_from, lead_count)
+ end
+ local trail_from = hunk.file_new_start + (hunk.file_new_count or 0)
+ trailing = read_line_range(filepath, trail_from, context)
+ end
+ local extmark_count = 0
if use_ts then
+ ---@type string[]
+ local new_code = {}
---@type table
local new_map = {}
---@type string[]
@@ -341,17 +329,20 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
---@type table
local old_map = {}
- for i, line in ipairs(hunk.lines) do
- local prefix = line:sub(1, pw)
- local stripped = line:sub(pw + 1)
- local buf_line = hunk.start_line + i - 1
- local has_add = prefix:find('+', 1, true) ~= nil
- local has_del = prefix:find('-', 1, true) ~= nil
+ for _, pad_line in ipairs(leading) do
+ table.insert(new_code, pad_line)
+ table.insert(old_code, pad_line)
+ end
- if has_add and not has_del then
+ 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 has_del and not has_add then
+ elseif prefix == '-' then
old_map[#old_code] = buf_line
table.insert(old_code, stripped)
else
@@ -361,38 +352,22 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
end
end
- local ts_context = nil
- if opts.highlights.context.enabled and (hunk.context_before or hunk.context_after) then
- ts_context = { before = hunk.context_before, after = hunk.context_after }
+ 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,
- pw + qw,
- covered_lines,
- p,
- nil,
- ts_context
- )
+ 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,
- pw + qw,
- covered_lines,
- p,
- nil,
- ts_context
- )
+ + 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,
@@ -400,8 +375,7 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
hunk.header_context_col,
hunk.header_context,
hunk.lang,
- new_code,
- p
+ new_code
)
if header_extmarks > 0 then
dbg('header %s:%d applied %d extmarks', hunk.filename, hunk.start_line, header_extmarks)
@@ -411,10 +385,16 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
elseif use_vim then
---@type string[]
local code_lines = {}
- for _, line in ipairs(hunk.lines) do
- table.insert(code_lines, line:sub(pw + 1))
+ for _, pad_line in ipairs(leading) do
+ table.insert(code_lines, pad_line)
end
- extmark_count = highlight_vim_syntax(bufnr, ns, hunk, code_lines, covered_lines, 0, p)
+ 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
if
@@ -429,35 +409,13 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
header_map[i] = hunk.header_start_line - 1 + i
end
extmark_count = extmark_count
- + highlight_treesitter(
- bufnr,
- ns,
- hunk.header_lines,
- 'diff',
- header_map,
- qw,
- nil,
- p,
- qw > 0 or pw > 1
- )
- end
-
- local at_raw_line
- if (qw > 0 or pw > 1) and opts.highlights.treesitter.enabled then
- local at_buf_line = hunk.start_line - 1
- at_raw_line = vim.api.nvim_buf_get_lines(bufnr, at_buf_line, at_buf_line + 1, false)[1]
+ + 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
- not opts.syntax_only
- and intra_cfg
- and intra_cfg.enabled
- and pw == 1
- and #hunk.lines <= intra_cfg.max_lines
- then
+ 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
@@ -488,181 +446,69 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
end
end
- if
- (qw > 0 or pw > 1)
- and hunk.header_start_line
- and hunk.header_lines
- and #hunk.header_lines > 0
- and opts.highlights.treesitter.enabled
- then
- for i = 0, #hunk.header_lines - 1 do
- local buf_line = hunk.header_start_line - 1 + i
- pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, {
- end_col = #hunk.header_lines[i + 1] + qw,
- hl_group = 'DiffsClear',
- priority = p.clear,
- })
-
- if pw > 1 then
- local hline = hunk.header_lines[i + 1]
- if hline:match('^index ') then
- pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, qw, {
- end_col = 5 + qw,
- hl_group = '@keyword.diff',
- priority = p.syntax,
- })
- local dot_pos = hline:find('%.%.', 1, false)
- if dot_pos then
- local rest = hline:sub(dot_pos + 2)
- local hash = rest:match('^(%x+)')
- if hash then
- pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, dot_pos + 1 + qw, {
- end_col = dot_pos + 1 + #hash + qw,
- hl_group = '@constant.diff',
- priority = p.syntax,
- })
- end
- end
- end
- end
- end
- end
-
- if (qw > 0 or pw > 1) and at_raw_line then
- local at_buf_line = hunk.start_line - 1
- pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, at_buf_line, 0, {
- end_col = #at_raw_line,
- hl_group = 'DiffsClear',
- priority = p.clear,
- })
- if opts.highlights.treesitter.enabled then
- pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, at_buf_line, qw, {
- end_col = #at_raw_line,
- hl_group = '@attribute.diff',
- priority = p.syntax,
- })
- end
- end
-
- if use_ts and 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 = p.clear,
- })
- end
-
- local raw_body_lines
- if qw > 0 then
- raw_body_lines =
- vim.api.nvim_buf_get_lines(bufnr, hunk.start_line, hunk.start_line + #hunk.lines, false)
- end
-
for i, line in ipairs(hunk.lines) do
local buf_line = hunk.start_line + i - 1
local line_len = #line
- local raw_len = raw_body_lines and #raw_body_lines[i] or nil
- local prefix = line:sub(1, pw)
- local has_add = prefix:find('+', 1, true) ~= nil
- local has_del = prefix:find('-', 1, true) ~= nil
- local is_diff_line = has_add or has_del
- local line_hl = is_diff_line and (has_add and 'DiffsAdd' or 'DiffsDelete') or nil
- local number_hl = is_diff_line and (has_add and 'DiffsAddNr' or 'DiffsDeleteNr') or nil
+ local prefix = line:sub(1, 1)
- local is_marker = false
- if pw > 1 and line_hl and not prefix:find('[^+]') then
- local content = line:sub(pw + 1)
- is_marker = content:match('^<<<<<<<')
- or content:match('^=======')
- or content:match('^>>>>>>>')
- or content:match('^|||||||')
- end
+ local is_diff_line = prefix == '+' or prefix == '-'
+ local line_hl = is_diff_line and (prefix == '+' and 'DiffsAdd' or 'DiffsDelete') or nil
+ local number_hl = is_diff_line and (prefix == '+' and 'DiffsAddNr' or 'DiffsDeleteNr') or nil
- if not opts.syntax_only then
- if opts.hide_prefix then
- local virt_hl = (opts.highlights.background and line_hl) or nil
- pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, {
- virt_text = { { string.rep(' ', pw + qw), virt_hl } },
- virt_text_pos = 'overlay',
- })
- end
-
- if qw > 0 or pw > 1 then
- local prefix_end = pw + qw
- if raw_len and prefix_end > raw_len then
- prefix_end = raw_len
- end
- pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, {
- end_col = prefix_end,
- hl_group = 'DiffsClear',
- priority = p.clear,
- })
- for ci = 0, pw - 1 do
- local ch = line:sub(ci + 1, ci + 1)
- if ch == '+' or ch == '-' then
- local char_col = ci + qw
- if raw_len and char_col >= raw_len then
- break
- end
- pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, char_col, {
- end_col = char_col + 1,
- hl_group = ch == '+' and '@diff.plus' or '@diff.minus',
- priority = p.syntax,
- })
- end
- end
- end
-
- if opts.highlights.background and is_diff_line then
- pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, {
- line_hl_group = line_hl,
- number_hl_group = opts.highlights.gutter and number_hl or nil,
- priority = p.line_bg,
- })
- end
-
- if is_marker and line_len > pw then
- pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, pw + qw, {
- end_col = line_len + qw,
- hl_group = 'DiffsConflictMarker',
- priority = p.char_bg,
- })
- end
-
- if char_spans_by_line[i] then
- local char_hl = has_add 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 + qw, {
- end_col = span.col_end + qw,
- hl_group = char_hl,
- priority = p.char_bg,
- })
- if not ok then
- dbg('char extmark FAILED: %s', err)
- end
- extmark_count = extmark_count + 1
- end
- end
- end
-
- if line_len > pw and covered_lines[buf_line] then
- pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, pw + qw, {
- end_col = line_len + qw,
- hl_group = 'DiffsClear',
- priority = p.clear,
+ if opts.hide_prefix then
+ local virt_hl = (opts.highlights.background and line_hl) or nil
+ pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, {
+ virt_text = { { ' ', virt_hl } },
+ virt_text_pos = 'overlay',
})
end
+
+ if line_len > 1 and covered_lines[buf_line] then
+ pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 1, {
+ end_col = line_len,
+ hl_group = 'DiffsClear',
+ priority = PRIORITY_CLEAR,
+ })
+ 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
dbg('hunk %s:%d applied %d extmarks', hunk.filename, hunk.start_line, extmark_count)
diff --git a/lua/diffs/init.lua b/lua/diffs/init.lua
index 06b6ad9..cdc29d1 100644
--- a/lua/diffs/init.lua
+++ b/lua/diffs/init.lua
@@ -15,12 +15,6 @@
---@field enabled boolean
---@field lines integer
----@class diffs.PrioritiesConfig
----@field clear integer
----@field syntax integer
----@field line_bg integer
----@field char_bg integer
-
---@class diffs.Highlights
---@field background boolean
---@field gutter boolean
@@ -30,16 +24,11 @@
---@field treesitter diffs.TreesitterConfig
---@field vim diffs.VimConfig
---@field intra diffs.IntraConfig
----@field priorities diffs.PrioritiesConfig
---@class diffs.FugitiveConfig
---@field horizontal string|false
---@field vertical string|false
----@class diffs.NeogitConfig
-
----@class diffs.GitsignsConfig
-
---@class diffs.ConflictKeymaps
---@field ours string|false
---@field theirs string|false
@@ -52,19 +41,14 @@
---@field enabled boolean
---@field disable_diagnostics boolean
---@field show_virtual_text boolean
----@field format_virtual_text? fun(side: string, keymap: string|false): string?
----@field show_actions boolean
----@field priority integer
---@field keymaps diffs.ConflictKeymaps
---@class diffs.Config
----@field debug boolean|string
+---@field debug boolean
+---@field debounce_ms integer
---@field hide_prefix boolean
----@field extra_filetypes string[]
---@field highlights diffs.Highlights
----@field fugitive diffs.FugitiveConfig|false
----@field neogit diffs.NeogitConfig|false
----@field gitsigns diffs.GitsignsConfig|false
+---@field fugitive diffs.FugitiveConfig
---@field conflict diffs.ConflictConfig
---@class diffs
@@ -113,8 +97,8 @@ end
---@type diffs.Config
local default_config = {
debug = false,
+ debounce_ms = 0,
hide_prefix = false,
- extra_filetypes = {},
highlights = {
background = true,
gutter = true,
@@ -127,7 +111,7 @@ local default_config = {
max_lines = 500,
},
vim = {
- enabled = true,
+ enabled = false,
max_lines = 200,
},
intra = {
@@ -135,29 +119,22 @@ local default_config = {
algorithm = 'default',
max_lines = 500,
},
- priorities = {
- clear = 198,
- syntax = 199,
- line_bg = 200,
- char_bg = 201,
- },
},
- fugitive = false,
- neogit = false,
- gitsigns = false,
+ fugitive = {
+ horizontal = 'du',
+ vertical = 'dU',
+ },
conflict = {
enabled = true,
disable_diagnostics = true,
show_virtual_text = true,
- show_actions = false,
- priority = 200,
keymaps = {
ours = 'doo',
theirs = 'dot',
both = 'dob',
none = 'don',
- next = ']c',
- prev = '[c',
+ next = ']x',
+ prev = '[x',
},
},
}
@@ -166,306 +143,67 @@ local default_config = {
local config = vim.deepcopy(default_config)
local initialized = false
-local hl_retry_pending = false
-
----@diagnostic disable-next-line: missing-fields
-local fast_hl_opts = {} ---@type diffs.HunkOpts
---@type table
local attached_buffers = {}
----@type table
-local ft_retry_pending = {}
-
---@type table
local diff_windows = {}
----@class diffs.HunkCacheEntry
----@field hunks diffs.Hunk[]
----@field tick integer
----@field highlighted table
----@field pending_clear boolean
----@field line_count integer
----@field byte_count integer
-
----@type table
-local hunk_cache = {}
-
---@param bufnr integer
---@return boolean
function M.is_fugitive_buffer(bufnr)
return vim.api.nvim_buf_get_name(bufnr):match('^fugitive://') ~= nil
end
----@param opts table
----@return string[]
-function M.compute_filetypes(opts)
- local fts = { 'git', 'gitcommit' }
- local fug = opts.fugitive
- if fug == true or type(fug) == 'table' then
- table.insert(fts, 'fugitive')
- end
- local neo = opts.neogit
- if neo == true or type(neo) == 'table' then
- table.insert(fts, 'NeogitStatus')
- table.insert(fts, 'NeogitCommitView')
- table.insert(fts, 'NeogitDiffView')
- end
- if type(opts.extra_filetypes) == 'table' then
- for _, ft in ipairs(opts.extra_filetypes) do
- table.insert(fts, ft)
- end
- end
- return fts
-end
-
local dbg = log.dbg
---@param bufnr integer
-local function invalidate_cache(bufnr)
- local entry = hunk_cache[bufnr]
- if entry then
- entry.tick = -1
- entry.pending_clear = true
- end
-end
-
----@param a diffs.Hunk
----@param b diffs.Hunk
----@return boolean
-local function hunks_eq(a, b)
- local n = #a.lines
- if n ~= #b.lines or a.filename ~= b.filename then
- return false
- end
- if a.lines[1] ~= b.lines[1] then
- return false
- end
- if n > 1 and a.lines[n] ~= b.lines[n] then
- return false
- end
- if n > 2 then
- local mid = math.floor(n / 2) + 1
- if a.lines[mid] ~= b.lines[mid] then
- return false
- end
- end
- return true
-end
-
----@param old_entry diffs.HunkCacheEntry
----@param new_hunks diffs.Hunk[]
----@return table?
-local function carry_forward_highlighted(old_entry, new_hunks)
- local old_hunks = old_entry.hunks
- local old_hl = old_entry.highlighted
- local old_n = #old_hunks
- local new_n = #new_hunks
- local highlighted = {}
-
- local prefix_len = 0
- local limit = math.min(old_n, new_n)
- for i = 1, limit do
- if not hunks_eq(old_hunks[i], new_hunks[i]) then
- break
- end
- if old_hl[i] then
- highlighted[i] = true
- end
- prefix_len = i
+local function highlight_buffer(bufnr)
+ if not vim.api.nvim_buf_is_valid(bufnr) then
+ return
end
- local suffix_len = 0
- local max_suffix = limit - prefix_len
- for j = 0, max_suffix - 1 do
- local old_idx = old_n - j
- local new_idx = new_n - j
- if not hunks_eq(old_hunks[old_idx], new_hunks[new_idx]) then
- break
- end
- if old_hl[old_idx] then
- highlighted[new_idx] = true
- end
- suffix_len = j + 1
- end
-
- dbg(
- 'carry_forward: %d prefix + %d suffix of %d old -> %d new hunks',
- prefix_len,
- suffix_len,
- old_n,
- new_n
- )
- if next(highlighted) == nil then
- return nil
- end
- return highlighted
-end
-
----@param path string
----@return string[]?
-local function read_file_lines(path)
- local f = io.open(path, 'r')
- if not f then
- return nil
- end
- local lines = {}
- for line in f:lines() do
- lines[#lines + 1] = line
- end
- f:close()
- return lines
-end
-
----@param hunks diffs.Hunk[]
----@param max_lines integer
-local function compute_hunk_context(hunks, max_lines)
- ---@type table
- local file_cache = {}
+ vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
+ local hunks = parser.parse_buffer(bufnr)
+ dbg('found %d hunks in buffer %d', #hunks, bufnr)
for _, hunk in ipairs(hunks) do
- if not hunk.repo_root or not hunk.filename or not hunk.file_new_start then
- goto continue
- end
-
- local path = vim.fs.joinpath(hunk.repo_root, hunk.filename)
- local file_lines = file_cache[path]
- if file_lines == nil then
- file_lines = read_file_lines(path) or false
- file_cache[path] = file_lines
- end
- if not file_lines then
- goto continue
- end
-
- local new_start = hunk.file_new_start
- local new_count = hunk.file_new_count or 0
- local total = #file_lines
-
- local before_start = math.max(1, new_start - max_lines)
- if before_start < new_start then
- local before = {}
- for i = before_start, new_start - 1 do
- before[#before + 1] = file_lines[i]
- end
- hunk.context_before = before
- end
-
- local after_start = new_start + new_count
- local after_end = math.min(total, after_start + max_lines - 1)
- if after_start <= total then
- local after = {}
- for i = after_start, after_end do
- after[#after + 1] = file_lines[i]
- end
- hunk.context_after = after
- end
-
- ::continue::
+ highlight.highlight_hunk(bufnr, ns, hunk, {
+ hide_prefix = config.hide_prefix,
+ highlights = config.highlights,
+ })
end
end
---@param bufnr integer
-local function ensure_cache(bufnr)
- if not vim.api.nvim_buf_is_valid(bufnr) then
- return
- end
- local tick = vim.api.nvim_buf_get_changedtick(bufnr)
- local entry = hunk_cache[bufnr]
- if entry and entry.tick == tick then
- return
- end
- if entry and not entry.pending_clear then
- local lc = vim.api.nvim_buf_line_count(bufnr)
- local bc = vim.api.nvim_buf_get_offset(bufnr, lc)
- if lc == entry.line_count and bc == entry.byte_count then
- entry.tick = tick
- entry.pending_clear = true
- dbg('content unchanged in buffer %d (tick %d), skipping reparse', bufnr, tick)
+---@return fun()
+local function create_debounced_highlight(bufnr)
+ local timer = nil ---@type table?
+ return function()
+ if timer then
+ timer:stop() ---@diagnostic disable-line: undefined-field
+ timer:close() ---@diagnostic disable-line: undefined-field
+ timer = nil
+ end
+ local t = vim.uv.new_timer()
+ if not t then
+ highlight_buffer(bufnr)
return
end
+ timer = t
+ t:start(
+ config.debounce_ms,
+ 0,
+ vim.schedule_wrap(function()
+ if timer == t then
+ timer = nil
+ t:close()
+ end
+ highlight_buffer(bufnr)
+ end)
+ )
end
- local hunks = parser.parse_buffer(bufnr)
- local lc = vim.api.nvim_buf_line_count(bufnr)
- local bc = vim.api.nvim_buf_get_offset(bufnr, lc)
- dbg('parsed %d hunks in buffer %d (tick %d)', #hunks, bufnr, tick)
- if config.highlights.context.enabled then
- compute_hunk_context(hunks, config.highlights.context.lines)
- end
- local carried = entry and not entry.pending_clear and carry_forward_highlighted(entry, hunks)
- hunk_cache[bufnr] = {
- hunks = hunks,
- tick = tick,
- highlighted = carried or {},
- pending_clear = not carried,
- line_count = lc,
- byte_count = bc,
- }
-
- local has_nil_ft = false
- for _, hunk in ipairs(hunks) do
- if not has_nil_ft and not hunk.ft and hunk.filename then
- has_nil_ft = true
- end
- end
- if has_nil_ft and vim.fn.did_filetype() ~= 0 and not ft_retry_pending[bufnr] then
- ft_retry_pending[bufnr] = true
- vim.schedule(function()
- if vim.api.nvim_buf_is_valid(bufnr) and hunk_cache[bufnr] then
- dbg('retrying filetype detection for buffer %d (was blocked by did_filetype)', bufnr)
- invalidate_cache(bufnr)
- vim.cmd('redraw!')
- end
- ft_retry_pending[bufnr] = nil
- end)
- end
-end
-
----@param hunks diffs.Hunk[]
----@param toprow integer
----@param botrow integer
----@return integer first
----@return integer last
-local function find_visible_hunks(hunks, toprow, botrow)
- local n = #hunks
- if n == 0 then
- return 0, 0
- end
-
- local lo, hi = 1, n + 1
- while lo < hi do
- local mid = math.floor((lo + hi) / 2)
- local h = hunks[mid]
- local bottom = h.start_line - 1 + #h.lines - 1
- if bottom < toprow then
- lo = mid + 1
- else
- hi = mid
- end
- end
-
- if lo > n then
- return 0, 0
- end
-
- local first = lo
- local h = hunks[first]
- local top = (h.header_start_line and (h.header_start_line - 1)) or (h.start_line - 1)
- if top >= botrow then
- return 0, 0
- end
-
- local last = first
- for i = first + 1, n do
- h = hunks[i]
- top = (h.header_start_line and (h.header_start_line - 1)) or (h.start_line - 1)
- if top >= botrow then
- break
- end
- last = i
- end
-
- return first, last
end
local function compute_highlight_groups()
@@ -475,23 +213,11 @@ local function compute_highlight_groups()
local diff_added = resolve_hl('diffAdded')
local diff_removed = resolve_hl('diffRemoved')
- local dark = vim.o.background ~= 'light'
- local bg = normal.bg or (dark and 0x1a1a1a or 0xf0f0f0)
- local add_bg = diff_add.bg or (dark and 0x1a3a1a or 0xd0ffd0)
- local del_bg = diff_delete.bg or (dark and 0x3a1a1a or 0xffd0d0)
- local add_fg = diff_added.fg or diff_add.fg or (dark and 0x80d080 or 0x206020)
- local del_fg = diff_removed.fg or diff_delete.fg or (dark and 0xd08080 or 0x802020)
-
- if not normal.bg and not hl_retry_pending then
- hl_retry_pending = true
- vim.schedule(function()
- hl_retry_pending = false
- compute_highlight_groups()
- for bufnr, _ in pairs(attached_buffers) do
- invalidate_cache(bufnr)
- end
- end)
- end
+ local bg = normal.bg or 0x1e1e2e
+ local add_bg = diff_add.bg or 0x2e4a3a
+ local del_bg = diff_delete.bg or 0x4a2e3a
+ local add_fg = diff_added.fg or diff_add.fg or 0x80c080
+ local del_fg = diff_removed.fg or diff_delete.fg or 0xc08080
local blended_add = blend_color(add_bg, bg, 0.4)
local blended_del = blend_color(del_bg, bg, 0.4)
@@ -500,11 +226,7 @@ local function compute_highlight_groups()
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 (dark and 0xcccccc or 0x333333), bg = bg }
- )
+ vim.api.nvim_set_hl(0, 'DiffsClear', { default = true, fg = normal.fg or 0xc0c0c0 })
vim.api.nvim_set_hl(0, 'DiffsAdd', { default = true, bg = blended_add })
vim.api.nvim_set_hl(0, 'DiffsDelete', { default = true, bg = blended_del })
vim.api.nvim_set_hl(0, 'DiffsAddNr', { default = true, fg = blended_add_text, bg = blended_add })
@@ -552,7 +274,6 @@ local function compute_highlight_groups()
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, 'DiffsConflictActions', { default = true, fg = 0x808080 })
vim.api.nvim_set_hl(
0,
'DiffsConflictOursNr',
@@ -584,144 +305,111 @@ local function init()
local opts = vim.g.diffs or {}
- local fugitive_defaults = { horizontal = 'du', vertical = 'dU' }
- if opts.fugitive == true then
- opts.fugitive = vim.deepcopy(fugitive_defaults)
- elseif type(opts.fugitive) == 'table' then
- opts.fugitive = vim.tbl_extend('keep', opts.fugitive, fugitive_defaults)
- end
-
- if opts.neogit == true then
- opts.neogit = {}
- end
-
- if opts.gitsigns == true then
- opts.gitsigns = {}
- end
-
- vim.validate('debug', opts.debug, function(v)
- return v == nil or type(v) == 'boolean' or type(v) == 'string'
- end, 'boolean or string (file path)')
- vim.validate('hide_prefix', opts.hide_prefix, 'boolean', true)
- vim.validate('fugitive', opts.fugitive, function(v)
- return v == nil or v == false or type(v) == 'table'
- end, 'table or false')
- vim.validate('neogit', opts.neogit, function(v)
- return v == nil or v == false or type(v) == 'table'
- end, 'table or false')
- vim.validate('gitsigns', opts.gitsigns, function(v)
- return v == nil or v == false or type(v) == 'table'
- end, 'table or false')
- vim.validate('extra_filetypes', opts.extra_filetypes, 'table', true)
- vim.validate('highlights', opts.highlights, 'table', true)
+ vim.validate({
+ debug = { opts.debug, 'boolean', true },
+ debounce_ms = { opts.debounce_ms, 'number', true },
+ hide_prefix = { opts.hide_prefix, 'boolean', true },
+ highlights = { opts.highlights, 'table', true },
+ })
if opts.highlights then
- vim.validate('highlights.background', opts.highlights.background, 'boolean', true)
- vim.validate('highlights.gutter', opts.highlights.gutter, 'boolean', true)
- vim.validate('highlights.blend_alpha', opts.highlights.blend_alpha, 'number', true)
- vim.validate('highlights.overrides', opts.highlights.overrides, 'table', true)
- vim.validate('highlights.context', opts.highlights.context, 'table', true)
- vim.validate('highlights.treesitter', opts.highlights.treesitter, 'table', true)
- vim.validate('highlights.vim', opts.highlights.vim, 'table', true)
- vim.validate('highlights.intra', opts.highlights.intra, 'table', true)
- vim.validate('highlights.priorities', opts.highlights.priorities, 'table', true)
+ vim.validate({
+ ['highlights.background'] = { opts.highlights.background, 'boolean', true },
+ ['highlights.gutter'] = { opts.highlights.gutter, 'boolean', true },
+ ['highlights.blend_alpha'] = { opts.highlights.blend_alpha, 'number', true },
+ ['highlights.overrides'] = { opts.highlights.overrides, 'table', true },
+ ['highlights.context'] = { opts.highlights.context, 'table', true },
+ ['highlights.treesitter'] = { opts.highlights.treesitter, 'table', true },
+ ['highlights.vim'] = { opts.highlights.vim, 'table', true },
+ ['highlights.intra'] = { opts.highlights.intra, 'table', true },
+ })
if opts.highlights.context then
- vim.validate('highlights.context.enabled', opts.highlights.context.enabled, 'boolean', true)
- vim.validate('highlights.context.lines', opts.highlights.context.lines, 'number', true)
+ vim.validate({
+ ['highlights.context.enabled'] = { opts.highlights.context.enabled, 'boolean', true },
+ ['highlights.context.lines'] = { opts.highlights.context.lines, 'number', true },
+ })
end
if opts.highlights.treesitter then
- vim.validate(
- 'highlights.treesitter.enabled',
- opts.highlights.treesitter.enabled,
- 'boolean',
- true
- )
- vim.validate(
- 'highlights.treesitter.max_lines',
- opts.highlights.treesitter.max_lines,
- 'number',
- true
- )
+ vim.validate({
+ ['highlights.treesitter.enabled'] = { opts.highlights.treesitter.enabled, 'boolean', true },
+ ['highlights.treesitter.max_lines'] = {
+ opts.highlights.treesitter.max_lines,
+ 'number',
+ true,
+ },
+ })
end
if opts.highlights.vim then
- vim.validate('highlights.vim.enabled', opts.highlights.vim.enabled, 'boolean', true)
- vim.validate('highlights.vim.max_lines', opts.highlights.vim.max_lines, 'number', true)
+ vim.validate({
+ ['highlights.vim.enabled'] = { opts.highlights.vim.enabled, 'boolean', true },
+ ['highlights.vim.max_lines'] = { opts.highlights.vim.max_lines, 'number', true },
+ })
end
if opts.highlights.intra then
- vim.validate('highlights.intra.enabled', opts.highlights.intra.enabled, 'boolean', true)
- vim.validate('highlights.intra.algorithm', opts.highlights.intra.algorithm, function(v)
- return v == nil or v == 'default' or v == 'vscode'
- end, "'default' or 'vscode'")
- vim.validate('highlights.intra.max_lines', opts.highlights.intra.max_lines, 'number', true)
- end
-
- if opts.highlights.priorities then
- vim.validate('highlights.priorities.clear', opts.highlights.priorities.clear, 'number', true)
- vim.validate(
- 'highlights.priorities.syntax',
- opts.highlights.priorities.syntax,
- 'number',
- true
- )
- vim.validate(
- 'highlights.priorities.line_bg',
- opts.highlights.priorities.line_bg,
- 'number',
- true
- )
- vim.validate(
- 'highlights.priorities.char_bg',
- opts.highlights.priorities.char_bg,
- 'number',
- true
- )
+ 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 type(opts.fugitive) == 'table' then
- ---@type diffs.FugitiveConfig
- local fug = opts.fugitive
- vim.validate('fugitive.horizontal', fug.horizontal, function(v)
- return v == nil or v == false or type(v) == 'string'
- end, 'string or false')
- vim.validate('fugitive.vertical', fug.vertical, function(v)
- return v == nil or v == false or type(v) == 'string'
- end, 'string or false')
+ 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)
- vim.validate('conflict.disable_diagnostics', opts.conflict.disable_diagnostics, 'boolean', true)
- vim.validate('conflict.show_virtual_text', opts.conflict.show_virtual_text, 'boolean', true)
- vim.validate(
- 'conflict.format_virtual_text',
- opts.conflict.format_virtual_text,
- 'function',
- true
- )
- vim.validate('conflict.show_actions', opts.conflict.show_actions, 'boolean', true)
- vim.validate('conflict.priority', opts.conflict.priority, 'number', true)
- vim.validate('conflict.keymaps', opts.conflict.keymaps, 'table', true)
+ 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'
- )
+ vim.validate({
+ ['conflict.keymaps.' .. key] = {
+ opts.conflict.keymaps[key],
+ keymap_validator,
+ 'string or false',
+ },
+ })
end
end
end
+ if opts.debounce_ms and opts.debounce_ms < 0 then
+ error('diffs: debounce_ms must be >= 0')
+ end
if
opts.highlights
and opts.highlights.context
@@ -761,132 +449,17 @@ local function init()
then
error('diffs: highlights.blend_alpha must be >= 0 and <= 1')
end
- if opts.highlights and opts.highlights.priorities then
- for _, key in ipairs({ 'clear', 'syntax', 'line_bg', 'char_bg' }) do
- local v = opts.highlights.priorities[key]
- if v and v < 0 then
- error('diffs: highlights.priorities.' .. key .. ' must be >= 0')
- end
- end
- end
- if opts.conflict and opts.conflict.priority and opts.conflict.priority < 0 then
- error('diffs: conflict.priority must be >= 0')
- end
config = vim.tbl_deep_extend('force', default_config, opts)
log.set_enabled(config.debug)
- fast_hl_opts = {
- hide_prefix = config.hide_prefix,
- highlights = vim.tbl_deep_extend('force', config.highlights, {
- treesitter = { enabled = false },
- }),
- defer_vim_syntax = true,
- }
-
compute_highlight_groups()
vim.api.nvim_create_autocmd('ColorScheme', {
callback = function()
compute_highlight_groups()
for bufnr, _ in pairs(attached_buffers) do
- invalidate_cache(bufnr)
- end
- end,
- })
-
- vim.api.nvim_set_decoration_provider(ns, {
- on_buf = function(_, bufnr)
- if not attached_buffers[bufnr] then
- return false
- end
- local t0 = config.debug and vim.uv.hrtime() or nil
- ensure_cache(bufnr)
- local entry = hunk_cache[bufnr]
- if entry and entry.pending_clear then
- vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
- entry.highlighted = {}
- entry.pending_clear = false
- end
- if t0 then
- dbg('on_buf %d: %.2fms', bufnr, (vim.uv.hrtime() - t0) / 1e6)
- end
- end,
- on_win = function(_, _, bufnr, toprow, botrow)
- if not attached_buffers[bufnr] then
- return false
- end
- local entry = hunk_cache[bufnr]
- if not entry then
- return
- end
- local first, last = find_visible_hunks(entry.hunks, toprow, botrow)
- if first == 0 then
- return
- end
- local t0 = config.debug and vim.uv.hrtime() or nil
- local deferred_syntax = {}
- local count = 0
- for i = first, last do
- if not entry.highlighted[i] then
- local hunk = entry.hunks[i]
- local clear_start = hunk.start_line - 1
- local clear_end = hunk.start_line + #hunk.lines
- if hunk.header_start_line then
- clear_start = hunk.header_start_line - 1
- end
- vim.api.nvim_buf_clear_namespace(bufnr, ns, clear_start, clear_end)
- highlight.highlight_hunk(bufnr, ns, hunk, fast_hl_opts)
- entry.highlighted[i] = true
- count = count + 1
- local has_syntax = hunk.lang and config.highlights.treesitter.enabled
- local needs_vim = not hunk.lang and hunk.ft and config.highlights.vim.enabled
- if has_syntax or needs_vim then
- table.insert(deferred_syntax, hunk)
- end
- end
- end
- if #deferred_syntax > 0 then
- local tick = entry.tick
- dbg('deferred syntax scheduled: %d hunks tick=%d', #deferred_syntax, tick)
- vim.schedule(function()
- if not vim.api.nvim_buf_is_valid(bufnr) then
- return
- end
- local cur = hunk_cache[bufnr]
- if not cur or cur.tick ~= tick then
- dbg(
- 'deferred syntax stale: cur.tick=%s captured=%d',
- cur and tostring(cur.tick) or 'nil',
- tick
- )
- return
- end
- local t1 = config.debug and vim.uv.hrtime() or nil
- local syntax_opts = {
- hide_prefix = config.hide_prefix,
- highlights = config.highlights,
- syntax_only = true,
- }
- for _, hunk in ipairs(deferred_syntax) do
- highlight.highlight_hunk(bufnr, ns, hunk, syntax_opts)
- end
- if t1 then
- dbg('deferred pass: %d hunks in %.2fms', #deferred_syntax, (vim.uv.hrtime() - t1) / 1e6)
- end
- end)
- end
- if t0 and count > 0 then
- dbg(
- 'on_win %d: %d hunks [%d..%d] in %.2fms (viewport %d-%d)',
- bufnr,
- count,
- first,
- last,
- (vim.uv.hrtime() - t0) / 1e6,
- toprow,
- botrow
- )
+ highlight_buffer(bufnr)
end
end,
})
@@ -911,34 +484,37 @@ function M.attach(bufnr)
end
attached_buffers[bufnr] = true
- local neogit_augroup = nil
- if config.neogit and vim.bo[bufnr].filetype:match('^Neogit') then
- vim.b[bufnr].neogit_disable_hunk_highlight = true
- neogit_augroup = vim.api.nvim_create_augroup('diffs_neogit_' .. bufnr, { clear = true })
- vim.api.nvim_create_autocmd('User', {
- pattern = 'NeogitDiffLoaded',
- group = neogit_augroup,
- callback = function()
- if vim.api.nvim_buf_is_valid(bufnr) and attached_buffers[bufnr] then
- M.refresh(bufnr)
- end
- end,
- })
- end
-
dbg('attaching to buffer %d', bufnr)
- ensure_cache(bufnr)
+ local debounced = create_debounced_highlight(bufnr)
+
+ highlight_buffer(bufnr)
+
+ vim.api.nvim_create_autocmd({ 'TextChanged', 'TextChangedI' }, {
+ buffer = bufnr,
+ callback = debounced,
+ })
+
+ vim.api.nvim_create_autocmd('Syntax', {
+ buffer = bufnr,
+ callback = function()
+ dbg('syntax event, re-highlighting buffer %d', bufnr)
+ highlight_buffer(bufnr)
+ end,
+ })
+
+ vim.api.nvim_create_autocmd('BufReadPost', {
+ buffer = bufnr,
+ callback = function()
+ dbg('BufReadPost event, re-highlighting buffer %d', bufnr)
+ highlight_buffer(bufnr)
+ end,
+ })
vim.api.nvim_create_autocmd('BufWipeout', {
buffer = bufnr,
callback = function()
attached_buffers[bufnr] = nil
- hunk_cache[bufnr] = nil
- ft_retry_pending[bufnr] = nil
- if neogit_augroup then
- pcall(vim.api.nvim_del_augroup_by_id, neogit_augroup)
- end
end,
})
end
@@ -946,7 +522,7 @@ end
---@param bufnr? integer
function M.refresh(bufnr)
bufnr = bufnr or vim.api.nvim_get_current_buf()
- invalidate_cache(bufnr)
+ highlight_buffer(bufnr)
end
local DIFF_WINHIGHLIGHT = table.concat({
@@ -989,7 +565,7 @@ function M.detach_diff()
end
end
----@return diffs.FugitiveConfig|false
+---@return diffs.FugitiveConfig
function M.get_fugitive_config()
init()
return config.fugitive
@@ -1001,30 +577,4 @@ function M.get_conflict_config()
return config.conflict
end
----@return diffs.HunkOpts
-function M.get_highlight_opts()
- init()
- return { hide_prefix = config.hide_prefix, highlights = config.highlights }
-end
-
-local function process_pending_clear(bufnr)
- local entry = hunk_cache[bufnr]
- if entry and entry.pending_clear then
- vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
- entry.highlighted = {}
- entry.pending_clear = false
- end
-end
-
-M._test = {
- find_visible_hunks = find_visible_hunks,
- hunk_cache = hunk_cache,
- ensure_cache = ensure_cache,
- invalidate_cache = invalidate_cache,
- hunks_eq = hunks_eq,
- process_pending_clear = process_pending_clear,
- ft_retry_pending = ft_retry_pending,
- compute_hunk_context = compute_hunk_context,
-}
-
return M
diff --git a/lua/diffs/lib.lua b/lua/diffs/lib.lua
index 8376925..5b3254b 100644
--- a/lua/diffs/lib.lua
+++ b/lua/diffs/lib.lua
@@ -8,9 +8,6 @@ local cached_handle = nil
---@type boolean
local download_in_progress = false
----@type fun(handle: table?)[]
-local pending_callbacks = {}
-
---@return string
local function get_os()
local os_name = jit.os:lower()
@@ -167,10 +164,9 @@ function M.ensure(callback)
return
end
- table.insert(pending_callbacks, callback)
-
if download_in_progress then
- dbg('download already in progress, queued callback')
+ dbg('download already in progress')
+ callback(nil)
return
end
@@ -196,25 +192,21 @@ function M.ensure(callback)
vim.system(cmd, {}, function(result)
download_in_progress = false
vim.schedule(function()
- local handle = nil
if result.code ~= 0 then
vim.notify('[diffs] failed to download libvscode_diff', vim.log.levels.WARN)
dbg('curl failed: %s', result.stderr or '')
- else
- 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)
- handle = M.load()
+ callback(nil)
+ return
end
- local cbs = pending_callbacks
- pending_callbacks = {}
- for _, cb in ipairs(cbs) do
- cb(handle)
+ 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
diff --git a/lua/diffs/log.lua b/lua/diffs/log.lua
index 525f578..08abcc6 100644
--- a/lua/diffs/log.lua
+++ b/lua/diffs/log.lua
@@ -1,17 +1,10 @@
local M = {}
local enabled = false
-local log_file = nil
----@param val boolean|string
+---@param val boolean
function M.set_enabled(val)
- if type(val) == 'string' then
- enabled = true
- log_file = val
- else
- enabled = val
- log_file = nil
- end
+ enabled = val
end
---@param msg string
@@ -20,16 +13,7 @@ function M.dbg(msg, ...)
if not enabled then
return
end
- local formatted = '[diffs.nvim]: ' .. string.format(msg, ...)
- if log_file then
- local f = io.open(log_file, 'a')
- if f then
- f:write(string.format('%.6fs', vim.uv.hrtime() / 1e9) .. ' ' .. formatted .. '\n')
- f:close()
- end
- else
- vim.notify(formatted, vim.log.levels.DEBUG)
- end
+ vim.notify('[diffs.nvim]: ' .. string.format(msg, ...), vim.log.levels.DEBUG)
end
return M
diff --git a/lua/diffs/merge.lua b/lua/diffs/merge.lua
deleted file mode 100644
index daf72ac..0000000
--- a/lua/diffs/merge.lua
+++ /dev/null
@@ -1,418 +0,0 @@
-local M = {}
-
-local conflict = require('diffs.conflict')
-
-local ns = vim.api.nvim_create_namespace('diffs-merge')
-
----@type table>
-local resolved_hunks = {}
-
----@class diffs.MergeHunkInfo
----@field index integer
----@field start_line integer
----@field end_line integer
----@field del_lines string[]
----@field add_lines string[]
-
----@param bufnr integer
----@return diffs.MergeHunkInfo[]
-function M.parse_hunks(bufnr)
- local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
- local hunks = {}
- local current = nil
-
- for i, line in ipairs(lines) do
- local idx = i - 1
- if line:match('^@@') then
- if current then
- current.end_line = idx - 1
- table.insert(hunks, current)
- end
- current = {
- index = #hunks + 1,
- start_line = idx,
- end_line = idx,
- del_lines = {},
- add_lines = {},
- }
- elseif current then
- local prefix = line:sub(1, 1)
- if prefix == '-' then
- table.insert(current.del_lines, line:sub(2))
- elseif prefix == '+' then
- table.insert(current.add_lines, line:sub(2))
- elseif prefix ~= ' ' and prefix ~= '\\' then
- current.end_line = idx - 1
- table.insert(hunks, current)
- current = nil
- end
- if current then
- current.end_line = idx
- end
- end
- end
-
- if current then
- table.insert(hunks, current)
- end
-
- return hunks
-end
-
----@param bufnr integer
----@return diffs.MergeHunkInfo?
-function M.find_hunk_at_cursor(bufnr)
- local hunks = M.parse_hunks(bufnr)
- local cursor_line = vim.api.nvim_win_get_cursor(0)[1] - 1
-
- for _, hunk in ipairs(hunks) do
- if cursor_line >= hunk.start_line and cursor_line <= hunk.end_line then
- return hunk
- end
- end
-
- return nil
-end
-
----@param hunk diffs.MergeHunkInfo
----@param working_bufnr integer
----@return diffs.ConflictRegion?
-function M.match_hunk_to_conflict(hunk, working_bufnr)
- local working_lines = vim.api.nvim_buf_get_lines(working_bufnr, 0, -1, false)
- local regions = conflict.parse(working_lines)
-
- for _, region in ipairs(regions) do
- local ours_lines = {}
- for line = region.ours_start + 1, region.ours_end do
- table.insert(ours_lines, working_lines[line])
- end
-
- if #ours_lines == #hunk.del_lines then
- local match = true
- for j = 1, #ours_lines do
- if ours_lines[j] ~= hunk.del_lines[j] then
- match = false
- break
- end
- end
- if match then
- return region
- end
- end
- end
-
- return nil
-end
-
----@param diff_bufnr integer
----@return integer?
-function M.get_or_load_working_buf(diff_bufnr)
- local ok, working_path = pcall(vim.api.nvim_buf_get_var, diff_bufnr, 'diffs_working_path')
- if not ok or not working_path then
- return nil
- end
-
- local existing = vim.fn.bufnr(working_path)
- if existing ~= -1 then
- return existing
- end
-
- local bufnr = vim.fn.bufadd(working_path)
- vim.fn.bufload(bufnr)
- return bufnr
-end
-
----@param diff_bufnr integer
----@param hunk_index integer
-local function mark_resolved(diff_bufnr, hunk_index)
- if not resolved_hunks[diff_bufnr] then
- resolved_hunks[diff_bufnr] = {}
- end
- resolved_hunks[diff_bufnr][hunk_index] = true
-end
-
----@param diff_bufnr integer
----@param hunk_index integer
----@return boolean
-function M.is_resolved(diff_bufnr, hunk_index)
- return resolved_hunks[diff_bufnr] and resolved_hunks[diff_bufnr][hunk_index] or false
-end
-
----@param diff_bufnr integer
----@param hunk diffs.MergeHunkInfo
-local function add_resolved_virtual_text(diff_bufnr, hunk)
- pcall(vim.api.nvim_buf_set_extmark, diff_bufnr, ns, hunk.start_line, 0, {
- virt_text = { { ' (resolved)', 'Comment' } },
- virt_text_pos = 'eol',
- })
-end
-
----@param bufnr integer
----@param config diffs.ConflictConfig
-function M.resolve_ours(bufnr, config)
- local hunk = M.find_hunk_at_cursor(bufnr)
- if not hunk then
- return
- end
- if M.is_resolved(bufnr, hunk.index) then
- vim.notify('[diffs.nvim]: hunk already resolved', vim.log.levels.INFO)
- return
- end
- local working_bufnr = M.get_or_load_working_buf(bufnr)
- if not working_bufnr then
- return
- end
- local region = M.match_hunk_to_conflict(hunk, working_bufnr)
- if not region then
- vim.notify('[diffs.nvim]: hunk does not correspond to a conflict region', vim.log.levels.INFO)
- return
- end
- local lines = vim.api.nvim_buf_get_lines(working_bufnr, region.ours_start, region.ours_end, false)
- conflict.replace_region(working_bufnr, region, lines)
- conflict.refresh(working_bufnr, config)
- mark_resolved(bufnr, hunk.index)
- add_resolved_virtual_text(bufnr, hunk)
-end
-
----@param bufnr integer
----@param config diffs.ConflictConfig
-function M.resolve_theirs(bufnr, config)
- local hunk = M.find_hunk_at_cursor(bufnr)
- if not hunk then
- return
- end
- if M.is_resolved(bufnr, hunk.index) then
- vim.notify('[diffs.nvim]: hunk already resolved', vim.log.levels.INFO)
- return
- end
- local working_bufnr = M.get_or_load_working_buf(bufnr)
- if not working_bufnr then
- return
- end
- local region = M.match_hunk_to_conflict(hunk, working_bufnr)
- if not region then
- vim.notify('[diffs.nvim]: hunk does not correspond to a conflict region', vim.log.levels.INFO)
- return
- end
- local lines =
- vim.api.nvim_buf_get_lines(working_bufnr, region.theirs_start, region.theirs_end, false)
- conflict.replace_region(working_bufnr, region, lines)
- conflict.refresh(working_bufnr, config)
- mark_resolved(bufnr, hunk.index)
- add_resolved_virtual_text(bufnr, hunk)
-end
-
----@param bufnr integer
----@param config diffs.ConflictConfig
-function M.resolve_both(bufnr, config)
- local hunk = M.find_hunk_at_cursor(bufnr)
- if not hunk then
- return
- end
- if M.is_resolved(bufnr, hunk.index) then
- vim.notify('[diffs.nvim]: hunk already resolved', vim.log.levels.INFO)
- return
- end
- local working_bufnr = M.get_or_load_working_buf(bufnr)
- if not working_bufnr then
- return
- end
- local region = M.match_hunk_to_conflict(hunk, working_bufnr)
- if not region then
- vim.notify('[diffs.nvim]: hunk does not correspond to a conflict region', vim.log.levels.INFO)
- return
- end
- local ours = vim.api.nvim_buf_get_lines(working_bufnr, region.ours_start, region.ours_end, false)
- local theirs =
- vim.api.nvim_buf_get_lines(working_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
- conflict.replace_region(working_bufnr, region, combined)
- conflict.refresh(working_bufnr, config)
- mark_resolved(bufnr, hunk.index)
- add_resolved_virtual_text(bufnr, hunk)
-end
-
----@param bufnr integer
----@param config diffs.ConflictConfig
-function M.resolve_none(bufnr, config)
- local hunk = M.find_hunk_at_cursor(bufnr)
- if not hunk then
- return
- end
- if M.is_resolved(bufnr, hunk.index) then
- vim.notify('[diffs.nvim]: hunk already resolved', vim.log.levels.INFO)
- return
- end
- local working_bufnr = M.get_or_load_working_buf(bufnr)
- if not working_bufnr then
- return
- end
- local region = M.match_hunk_to_conflict(hunk, working_bufnr)
- if not region then
- vim.notify('[diffs.nvim]: hunk does not correspond to a conflict region', vim.log.levels.INFO)
- return
- end
- conflict.replace_region(working_bufnr, region, {})
- conflict.refresh(working_bufnr, config)
- mark_resolved(bufnr, hunk.index)
- add_resolved_virtual_text(bufnr, hunk)
-end
-
----@param bufnr integer
-function M.goto_next(bufnr)
- local hunks = M.parse_hunks(bufnr)
- if #hunks == 0 then
- return
- end
-
- local working_bufnr = M.get_or_load_working_buf(bufnr)
- if not working_bufnr then
- return
- end
-
- local cursor_line = vim.api.nvim_win_get_cursor(0)[1] - 1
-
- local candidates = {}
- for _, hunk in ipairs(hunks) do
- if not M.is_resolved(bufnr, hunk.index) then
- if M.match_hunk_to_conflict(hunk, working_bufnr) then
- table.insert(candidates, hunk)
- end
- end
- end
-
- if #candidates == 0 then
- return
- end
-
- for _, hunk in ipairs(candidates) do
- if hunk.start_line > cursor_line then
- vim.api.nvim_win_set_cursor(0, { hunk.start_line + 1, 0 })
- return
- end
- end
-
- vim.notify('[diffs.nvim]: wrapped to first hunk', vim.log.levels.INFO)
- vim.api.nvim_win_set_cursor(0, { candidates[1].start_line + 1, 0 })
-end
-
----@param bufnr integer
-function M.goto_prev(bufnr)
- local hunks = M.parse_hunks(bufnr)
- if #hunks == 0 then
- return
- end
-
- local working_bufnr = M.get_or_load_working_buf(bufnr)
- if not working_bufnr then
- return
- end
-
- local cursor_line = vim.api.nvim_win_get_cursor(0)[1] - 1
-
- local candidates = {}
- for _, hunk in ipairs(hunks) do
- if not M.is_resolved(bufnr, hunk.index) then
- if M.match_hunk_to_conflict(hunk, working_bufnr) then
- table.insert(candidates, hunk)
- end
- end
- end
-
- if #candidates == 0 then
- return
- end
-
- for i = #candidates, 1, -1 do
- if candidates[i].start_line < cursor_line then
- vim.api.nvim_win_set_cursor(0, { candidates[i].start_line + 1, 0 })
- return
- end
- end
-
- vim.notify('[diffs.nvim]: wrapped to last hunk', vim.log.levels.INFO)
- vim.api.nvim_win_set_cursor(0, { candidates[#candidates].start_line + 1, 0 })
-end
-
----@param bufnr integer
----@param config diffs.ConflictConfig
-local function apply_hunk_hints(bufnr, config)
- if not config.show_virtual_text then
- return
- end
-
- local hunks = M.parse_hunks(bufnr)
- for _, hunk in ipairs(hunks) do
- if M.is_resolved(bufnr, hunk.index) then
- add_resolved_virtual_text(bufnr, hunk)
- else
- local parts = {}
- local actions = {
- { 'current', config.keymaps.ours },
- { 'incoming', config.keymaps.theirs },
- { 'both', config.keymaps.both },
- { 'none', config.keymaps.none },
- }
- for _, action in ipairs(actions) do
- if action[2] then
- if #parts > 0 then
- table.insert(parts, { ' | ', 'Comment' })
- end
- table.insert(parts, { ('%s: %s'):format(action[2], action[1]), 'Comment' })
- end
- end
- if #parts > 0 then
- pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, hunk.start_line, 0, {
- virt_text = parts,
- virt_text_pos = 'eol',
- })
- end
- end
- end
-end
-
----@param bufnr integer
----@param config diffs.ConflictConfig
-function M.setup_keymaps(bufnr, config)
- resolved_hunks[bufnr] = nil
- vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
-
- local km = config.keymaps
-
- local maps = {
- { km.ours, '(diffs-merge-ours)' },
- { km.theirs, '(diffs-merge-theirs)' },
- { km.both, '(diffs-merge-both)' },
- { km.none, '(diffs-merge-none)' },
- { km.next, '(diffs-merge-next)' },
- { km.prev, '(diffs-merge-prev)' },
- }
-
- for _, map in ipairs(maps) do
- if map[1] then
- vim.keymap.set('n', map[1], map[2], { buffer = bufnr })
- end
- end
-
- apply_hunk_hints(bufnr, config)
-
- vim.api.nvim_create_autocmd('BufWipeout', {
- buffer = bufnr,
- callback = function()
- resolved_hunks[bufnr] = nil
- end,
- })
-end
-
----@return integer
-function M.get_namespace()
- return ns
-end
-
-return M
diff --git a/lua/diffs/parser.lua b/lua/diffs/parser.lua
index aa63daf..43eb1f6 100644
--- a/lua/diffs/parser.lua
+++ b/lua/diffs/parser.lua
@@ -12,19 +12,12 @@
---@field file_old_count integer?
---@field file_new_start integer?
---@field file_new_count integer?
----@field prefix_width integer
----@field quote_width integer
---@field repo_root string?
----@field context_before string[]?
----@field context_after string[]?
local M = {}
local dbg = require('diffs.log').dbg
----@type table
-local ft_lang_cache = {}
-
---@param filepath string
---@param n integer
---@return string[]?
@@ -63,15 +56,6 @@ local function get_ft_from_filename(filename, repo_root)
end
local ft = vim.filetype.match({ filename = filename })
- if not ft and vim.fn.did_filetype() ~= 0 then
- dbg('retrying filetype match for %s (clearing did_filetype)', filename)
- local saved = rawget(vim.fn, 'did_filetype')
- rawset(vim.fn, 'did_filetype', function()
- return 0
- end)
- ft = vim.filetype.match({ filename = filename })
- rawset(vim.fn, 'did_filetype', saved)
- end
if ft then
dbg('filetype from filename: %s', ft)
return ft
@@ -122,14 +106,7 @@ local function get_repo_root(bufnr)
return vim.fn.fnamemodify(git_dir, ':h')
end
- local ok3, neogit_git_dir = pcall(vim.api.nvim_buf_get_var, bufnr, 'neogit_git_dir')
- if ok3 and neogit_git_dir then
- return vim.fn.fnamemodify(neogit_git_dir, ':h')
- end
-
- local cwd = vim.fn.getcwd()
- local git = require('diffs.git')
- return git.get_repo_root(cwd .. '/.')
+ return nil
end
---@param bufnr integer
@@ -137,18 +114,6 @@ end
function M.parse_buffer(bufnr)
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local repo_root = get_repo_root(bufnr)
-
- local quote_prefix = nil
- local quote_width = 0
- for _, l in ipairs(lines) do
- local qp = l:match('^(>+ )diff %-%-') or l:match('^(>+ )@@ %-')
- if qp then
- quote_prefix = qp
- quote_width = #qp
- break
- end
- end
-
---@type diffs.Hunk[]
local hunks = {}
@@ -168,8 +133,6 @@ function M.parse_buffer(bufnr)
local hunk_lines = {}
---@type integer?
local hunk_count = nil
- ---@type integer
- local hunk_prefix_width = 1
---@type integer?
local header_start = nil
---@type string[]
@@ -182,11 +145,6 @@ function M.parse_buffer(bufnr)
local file_new_start = nil
---@type integer?
local file_new_count = nil
- ---@type integer?
- local old_remaining = nil
- ---@type integer?
- local new_remaining = nil
- local current_quote_width = 0
local function flush_hunk()
if hunk_start and #hunk_lines > 0 then
@@ -198,8 +156,6 @@ function M.parse_buffer(bufnr)
header_context = hunk_header_context,
header_context_col = hunk_header_context_col,
lines = hunk_lines,
- prefix_width = hunk_prefix_width,
- quote_width = current_quote_width,
file_old_start = file_old_start,
file_old_count = file_old_count,
file_new_start = file_new_start,
@@ -220,121 +176,51 @@ function M.parse_buffer(bufnr)
file_old_count = nil
file_new_start = nil
file_new_count = nil
- old_remaining = nil
- new_remaining = nil
end
for i, line in ipairs(lines) do
- local logical = line
- if quote_prefix then
- if line:sub(1, quote_width) == quote_prefix then
- logical = line:sub(quote_width + 1)
- elseif line:match('^>+$') then
- logical = ''
- end
- end
-
- local diff_git_file = logical:match('^diff %-%-git a/.+ b/(.+)$')
- or logical:match('^diff %-%-combined (.+)$')
- or logical:match('^diff %-%-cc (.+)$')
- local neogit_file = logical:match('^modified%s+(.+)$')
- or (not logical:match('^new file mode') and logical:match('^new file%s+(.+)$'))
- or (not logical:match('^deleted file mode') and logical:match('^deleted%s+(.+)$'))
- or logical:match('^renamed%s+(.+)$')
- or logical:match('^copied%s+(.+)$')
- local bare_file = not hunk_start and logical:match('^([^%s]+%.[^%s]+)$')
- local filename = logical:match('^[MADRCU%?!]%s+(.+)$')
- or diff_git_file
- or neogit_file
- or bare_file
+ local filename = line:match('^[MADRC%?!]%s+(.+)$') or line:match('^diff %-%-git a/.+ b/(.+)$')
if filename then
flush_hunk()
current_filename = filename
- current_quote_width = (logical ~= line) and quote_width or 0
- local cache_key = (repo_root or '') .. '\0' .. filename
- local cached = ft_lang_cache[cache_key]
- if cached then
- current_ft = cached.ft
- current_lang = cached.lang
- else
- current_ft = get_ft_from_filename(filename, repo_root)
- current_lang = current_ft and get_lang_from_ft(current_ft) or nil
- if current_ft or vim.fn.did_filetype() == 0 then
- ft_lang_cache[cache_key] = { ft = current_ft, lang = current_lang }
- end
- end
+ current_ft = get_ft_from_filename(filename, repo_root)
+ current_lang = current_ft and get_lang_from_ft(current_ft) or nil
if current_lang then
dbg('file: %s -> lang: %s', filename, current_lang)
elseif current_ft then
dbg('file: %s -> ft: %s (no ts parser)', filename, current_ft)
end
hunk_count = 0
- hunk_prefix_width = 1
header_start = i
header_lines = {}
- elseif logical:match('^@@+') then
+ elseif line:match('^@@.-@@') then
flush_hunk()
hunk_start = i
- local at_prefix = logical:match('^(@@+)')
- hunk_prefix_width = #at_prefix - 1
- if #at_prefix == 2 then
- local hs, hc, hs2, hc2 = logical: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
- old_remaining = file_old_count
- new_remaining = file_new_count
- end
- else
- local hs, hc = logical:match('%-(%d+),?(%d*)')
- if hs then
- file_old_start = tonumber(hs)
- file_old_count = tonumber(hc) or 1
- old_remaining = file_old_count
- end
- local hs2, hc2 = logical:match('%+(%d+),?(%d*) @@')
- if hs2 then
- file_new_start = tonumber(hs2)
- file_new_count = tonumber(hc2) or 1
- new_remaining = file_new_count
- end
+ 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 at_end, context = logical:match('^(@@+.-@@+%s*)(.*)')
+ local prefix, context = line:match('^(@@.-@@%s*)(.*)')
if context and context ~= '' then
hunk_header_context = context
- hunk_header_context_col = #at_end + current_quote_width
+ hunk_header_context_col = #prefix
end
if hunk_count then
hunk_count = hunk_count + 1
end
elseif hunk_start then
- local prefix = logical:sub(1, 1)
+ local prefix = line:sub(1, 1)
if prefix == ' ' or prefix == '+' or prefix == '-' then
- table.insert(hunk_lines, logical)
- if old_remaining and (prefix == ' ' or prefix == '-') then
- old_remaining = old_remaining - 1
- end
- if new_remaining and (prefix == ' ' or prefix == '+') then
- new_remaining = new_remaining - 1
- end
+ table.insert(hunk_lines, line)
elseif
- logical == ''
- and old_remaining
- and old_remaining > 0
- and new_remaining
- and new_remaining > 0
- then
- table.insert(hunk_lines, string.rep(' ', hunk_prefix_width))
- old_remaining = old_remaining - 1
- new_remaining = new_remaining - 1
- elseif
- logical == ''
- or logical:match('^[MADRC%?!]%s+')
- or logical:match('^diff ')
- or logical:match('^index ')
- or logical:match('^Binary ')
+ line == ''
+ or line:match('^[MADRC%?!]%s+')
+ or line:match('^diff ')
+ or line:match('^index ')
+ or line:match('^Binary ')
then
flush_hunk()
current_filename = nil
@@ -344,7 +230,7 @@ function M.parse_buffer(bufnr)
end
end
if header_start and not hunk_start then
- table.insert(header_lines, logical)
+ table.insert(header_lines, line)
end
end
@@ -353,10 +239,4 @@ function M.parse_buffer(bufnr)
return hunks
end
-M.get_lang_from_ft = get_lang_from_ft
-
-M._test = {
- ft_lang_cache = ft_lang_cache,
-}
-
return M
diff --git a/plugin/diffs.lua b/plugin/diffs.lua
index 43c5564..5d3c8b2 100644
--- a/plugin/diffs.lua
+++ b/plugin/diffs.lua
@@ -5,27 +5,12 @@ vim.g.loaded_diffs = 1
require('diffs.commands').setup()
-local gs_cfg = (vim.g.diffs or {}).gitsigns
-if gs_cfg == true or type(gs_cfg) == 'table' then
- if not require('diffs.gitsigns').setup() then
- vim.api.nvim_create_autocmd('User', {
- pattern = 'GitAttach',
- once = true,
- callback = function()
- require('diffs.gitsigns').setup()
- end,
- })
- end
-end
-
vim.api.nvim_create_autocmd('FileType', {
- pattern = require('diffs').compute_filetypes(vim.g.diffs or {}),
+ pattern = { 'fugitive', 'git' },
callback = function(args)
local diffs = require('diffs')
- if args.match == 'git' then
- if not diffs.get_fugitive_config() or not diffs.is_fugitive_buffer(args.buf) then
- return
- end
+ if args.match == 'git' and not diffs.is_fugitive_buffer(args.buf) then
+ return
end
diffs.attach(args.buf)
@@ -97,28 +82,3 @@ end, { desc = 'Jump to next conflict' })
vim.keymap.set('n', '(diffs-conflict-prev)', function()
require('diffs.conflict').goto_prev(vim.api.nvim_get_current_buf())
end, { desc = 'Jump to previous conflict' })
-
-local function merge_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', '(diffs-merge-ours)', function()
- merge_action(require('diffs.merge').resolve_ours)
-end, { desc = 'Accept ours in merge diff' })
-vim.keymap.set('n', '(diffs-merge-theirs)', function()
- merge_action(require('diffs.merge').resolve_theirs)
-end, { desc = 'Accept theirs in merge diff' })
-vim.keymap.set('n', '(diffs-merge-both)', function()
- merge_action(require('diffs.merge').resolve_both)
-end, { desc = 'Accept both in merge diff' })
-vim.keymap.set('n', '(diffs-merge-none)', function()
- merge_action(require('diffs.merge').resolve_none)
-end, { desc = 'Reject both in merge diff' })
-vim.keymap.set('n', '(diffs-merge-next)', function()
- require('diffs.merge').goto_next(vim.api.nvim_get_current_buf())
-end, { desc = 'Jump to next conflict hunk' })
-vim.keymap.set('n', '(diffs-merge-prev)', function()
- require('diffs.merge').goto_prev(vim.api.nvim_get_current_buf())
-end, { desc = 'Jump to previous conflict hunk' })
diff --git a/scripts/ci.sh b/scripts/ci.sh
deleted file mode 100755
index e06bf09..0000000
--- a/scripts/ci.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/sh
-set -eu
-
-nix develop --command stylua --check .
-git ls-files '*.lua' | xargs nix develop --command selene --display-style quiet
-nix develop --command prettier --check .
-nix fmt
-git diff --exit-code -- '*.nix'
-nix develop --command lua-language-server --check . --checklevel=Warning
-nix develop --command busted
diff --git a/selene.toml b/selene.toml
index f2ada4b..96cf5ab 100644
--- a/selene.toml
+++ b/selene.toml
@@ -1,4 +1 @@
std = 'vim'
-
-[lints]
-bad_string_escape = 'allow'
diff --git a/spec/commands_spec.lua b/spec/commands_spec.lua
index 1bf9e61..daba5d5 100644
--- a/spec/commands_spec.lua
+++ b/spec/commands_spec.lua
@@ -40,78 +40,6 @@ describe('commands', function()
end)
end)
- describe('filter_combined_diffs', function()
- it('strips diff --cc entries entirely', function()
- local lines = {
- 'diff --cc main.lua',
- 'index d13ab94,b113aee..0000000',
- '--- a/main.lua',
- '+++ b/main.lua',
- '@@@ -1,7 -1,7 +1,11 @@@',
- ' local M = {}',
- '++<<<<<<< HEAD',
- ' + return 1',
- '++=======',
- '+ return 2',
- '++>>>>>>> theirs',
- ' end',
- }
- local result = commands.filter_combined_diffs(lines)
- assert.are.equal(0, #result)
- end)
-
- it('preserves diff --git entries', function()
- local lines = {
- 'diff --git a/file.lua b/file.lua',
- '--- a/file.lua',
- '+++ b/file.lua',
- '@@ -1,3 +1,3 @@',
- ' local M = {}',
- '-local x = 1',
- '+local x = 2',
- ' return M',
- }
- local result = commands.filter_combined_diffs(lines)
- assert.are.equal(8, #result)
- assert.are.same(lines, result)
- end)
-
- it('strips combined but keeps unified in mixed output', function()
- local lines = {
- 'diff --cc conflict.lua',
- 'index aaa,bbb..000',
- '@@@ -1,1 -1,1 +1,5 @@@',
- '++<<<<<<< HEAD',
- 'diff --git a/clean.lua b/clean.lua',
- '--- a/clean.lua',
- '+++ b/clean.lua',
- '@@ -1,1 +1,1 @@',
- '-old',
- '+new',
- }
- local result = commands.filter_combined_diffs(lines)
- assert.are.equal(6, #result)
- assert.are.equal('diff --git a/clean.lua b/clean.lua', result[1])
- assert.are.equal('+new', result[6])
- end)
-
- it('returns empty for empty input', function()
- local result = commands.filter_combined_diffs({})
- assert.are.equal(0, #result)
- end)
-
- it('returns empty when all entries are combined', function()
- local lines = {
- 'diff --cc a.lua',
- 'some content',
- 'diff --cc b.lua',
- 'more content',
- }
- local result = commands.filter_combined_diffs(lines)
- assert.are.equal(0, #result)
- end)
- end)
-
describe('find_hunk_line', function()
it('finds matching @@ header and returns target line', function()
local diff_lines = {
diff --git a/spec/conflict_spec.lua b/spec/conflict_spec.lua
index 9f3df5d..75eac23 100644
--- a/spec/conflict_spec.lua
+++ b/spec/conflict_spec.lua
@@ -6,14 +6,13 @@ local function default_config(overrides)
enabled = true,
disable_diagnostics = false,
show_virtual_text = true,
- show_actions = false,
keymaps = {
ours = 'doo',
theirs = 'dot',
both = 'dob',
none = 'don',
- next = ']c',
- prev = '[c',
+ next = ']x',
+ prev = '[x',
},
}
if overrides then
@@ -235,6 +234,29 @@ describe('conflict', function()
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',
@@ -509,33 +531,6 @@ describe('conflict', function()
helpers.delete_buffer(bufnr)
end)
- it('goto_next notifies on wrap-around', 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 })
-
- local notified = false
- local orig_notify = vim.notify
- vim.notify = function(msg)
- if msg:match('wrapped to first conflict') then
- notified = true
- end
- end
-
- conflict.goto_next(bufnr)
- vim.notify = orig_notify
-
- assert.is_true(notified)
-
- helpers.delete_buffer(bufnr)
- end)
-
it('goto_prev jumps to previous conflict', function()
local bufnr = create_file_buffer({
'<<<<<<< HEAD',
@@ -580,33 +575,6 @@ describe('conflict', function()
helpers.delete_buffer(bufnr)
end)
- it('goto_prev notifies on wrap-around', 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 })
-
- local notified = false
- local orig_notify = vim.notify
- vim.notify = function(msg)
- if msg:match('wrapped to last conflict') then
- notified = true
- end
- end
-
- conflict.goto_prev(bufnr)
- vim.notify = orig_notify
-
- assert.is_true(notified)
-
- 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)
@@ -717,158 +685,4 @@ describe('conflict', function()
helpers.delete_buffer(bufnr)
end)
end)
-
- describe('virtual text formatting', function()
- after_each(function()
- conflict.detach(vim.api.nvim_get_current_buf())
- end)
-
- it('default labels show current and incoming without keymaps', 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 labels = {}
- for _, mark in ipairs(extmarks) do
- if mark[4] and mark[4].virt_text then
- table.insert(labels, mark[4].virt_text[1][1])
- end
- end
- assert.are.equal(2, #labels)
- assert.are.equal(' (current)', labels[1])
- assert.are.equal(' (incoming)', labels[2])
-
- helpers.delete_buffer(bufnr)
- end)
-
- it('uses custom format_virtual_text function', function()
- local bufnr = create_file_buffer({
- '<<<<<<< HEAD',
- 'local x = 1',
- '=======',
- 'local x = 2',
- '>>>>>>> feature',
- })
-
- conflict.attach(
- bufnr,
- default_config({
- format_virtual_text = function(side)
- return side == 'ours' and 'OURS' or 'THEIRS'
- end,
- })
- )
-
- local extmarks = get_extmarks(bufnr)
- local labels = {}
- for _, mark in ipairs(extmarks) do
- if mark[4] and mark[4].virt_text then
- table.insert(labels, mark[4].virt_text[1][1])
- end
- end
- assert.are.equal(2, #labels)
- assert.are.equal(' (OURS)', labels[1])
- assert.are.equal(' (THEIRS)', labels[2])
-
- helpers.delete_buffer(bufnr)
- end)
-
- it('hides label when format_virtual_text returns nil', function()
- local bufnr = create_file_buffer({
- '<<<<<<< HEAD',
- 'local x = 1',
- '=======',
- 'local x = 2',
- '>>>>>>> feature',
- })
-
- conflict.attach(
- bufnr,
- default_config({
- format_virtual_text = function()
- return nil
- end,
- })
- )
-
- 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)
- end)
-
- describe('action lines', function()
- after_each(function()
- conflict.detach(vim.api.nvim_get_current_buf())
- end)
-
- it('adds virt_lines when show_actions is true', function()
- local bufnr = create_file_buffer({
- '<<<<<<< HEAD',
- 'local x = 1',
- '=======',
- 'local x = 2',
- '>>>>>>> feature',
- })
-
- conflict.attach(bufnr, default_config({ show_actions = true }))
-
- local extmarks = get_extmarks(bufnr)
- local virt_lines_count = 0
- for _, mark in ipairs(extmarks) do
- if mark[4] and mark[4].virt_lines then
- virt_lines_count = virt_lines_count + 1
- end
- end
- assert.are.equal(1, virt_lines_count)
-
- helpers.delete_buffer(bufnr)
- end)
-
- it('omits disabled keymaps from action line', function()
- local bufnr = create_file_buffer({
- '<<<<<<< HEAD',
- 'local x = 1',
- '=======',
- 'local x = 2',
- '>>>>>>> feature',
- })
-
- conflict.attach(
- bufnr,
- default_config({ show_actions = true, keymaps = { both = false, none = false } })
- )
-
- local extmarks = get_extmarks(bufnr)
- for _, mark in ipairs(extmarks) do
- if mark[4] and mark[4].virt_lines then
- local line = mark[4].virt_lines[1]
- local text = ''
- for _, chunk in ipairs(line) do
- text = text .. chunk[1]
- end
- assert.is_truthy(text:find('Current'))
- assert.is_truthy(text:find('Incoming'))
- assert.is_falsy(text:find('Both'))
- assert.is_falsy(text:find('None'))
- end
- end
-
- helpers.delete_buffer(bufnr)
- end)
- end)
end)
diff --git a/spec/context_spec.lua b/spec/context_spec.lua
deleted file mode 100644
index 237b7f3..0000000
--- a/spec/context_spec.lua
+++ /dev/null
@@ -1,382 +0,0 @@
-require('spec.helpers')
-local diffs = require('diffs')
-local highlight = require('diffs.highlight')
-local compute_hunk_context = diffs._test.compute_hunk_context
-
-describe('context', function()
- describe('compute_hunk_context', function()
- local tmpdir
-
- before_each(function()
- tmpdir = vim.fn.tempname()
- vim.fn.mkdir(tmpdir, 'p')
- end)
-
- after_each(function()
- vim.fn.delete(tmpdir, 'rf')
- end)
-
- local function write_file(filename, lines)
- local path = vim.fs.joinpath(tmpdir, filename)
- local dir = vim.fn.fnamemodify(path, ':h')
- if vim.fn.isdirectory(dir) == 0 then
- vim.fn.mkdir(dir, 'p')
- end
- local f = io.open(path, 'w')
- f:write(table.concat(lines, '\n') .. '\n')
- f:close()
- end
-
- local function make_hunk(filename, opts)
- return {
- filename = filename,
- ft = 'lua',
- lang = 'lua',
- start_line = opts.start_line or 1,
- lines = opts.lines,
- prefix_width = opts.prefix_width or 1,
- quote_width = 0,
- repo_root = tmpdir,
- file_new_start = opts.file_new_start,
- file_new_count = opts.file_new_count,
- }
- end
-
- it('reads context_before from file lines preceding the hunk', function()
- write_file('a.lua', {
- 'local M = {}',
- 'function M.foo()',
- ' local x = 1',
- ' local y = 2',
- 'end',
- 'return M',
- })
-
- local hunks = {
- make_hunk('a.lua', {
- file_new_start = 3,
- file_new_count = 3,
- lines = { ' local x = 1', '+local new = true', ' local y = 2' },
- }),
- }
- compute_hunk_context(hunks, 25)
-
- assert.same({ 'local M = {}', 'function M.foo()' }, hunks[1].context_before)
- end)
-
- it('reads context_after from file lines following the hunk', function()
- write_file('a.lua', {
- 'local M = {}',
- 'function M.foo()',
- ' local x = 1',
- 'end',
- 'return M',
- })
-
- local hunks = {
- make_hunk('a.lua', {
- file_new_start = 2,
- file_new_count = 2,
- lines = { ' function M.foo()', '+ local x = 1' },
- }),
- }
- compute_hunk_context(hunks, 25)
-
- assert.same({ 'end', 'return M' }, hunks[1].context_after)
- end)
-
- it('caps context_before to max_lines', function()
- write_file('a.lua', {
- 'line1',
- 'line2',
- 'line3',
- 'line4',
- 'line5',
- 'target',
- })
-
- local hunks = {
- make_hunk('a.lua', {
- file_new_start = 6,
- file_new_count = 1,
- lines = { '+target' },
- }),
- }
- compute_hunk_context(hunks, 2)
-
- assert.same({ 'line4', 'line5' }, hunks[1].context_before)
- end)
-
- it('caps context_after to max_lines', function()
- write_file('a.lua', {
- 'target',
- 'after1',
- 'after2',
- 'after3',
- 'after4',
- })
-
- local hunks = {
- make_hunk('a.lua', {
- file_new_start = 1,
- file_new_count = 1,
- lines = { '+target' },
- }),
- }
- compute_hunk_context(hunks, 2)
-
- assert.same({ 'after1', 'after2' }, hunks[1].context_after)
- end)
-
- it('skips hunks without file_new_start', function()
- write_file('a.lua', { 'line1', 'line2' })
-
- local hunks = {
- make_hunk('a.lua', {
- file_new_start = nil,
- file_new_count = nil,
- lines = { '+something' },
- }),
- }
- compute_hunk_context(hunks, 25)
-
- assert.is_nil(hunks[1].context_before)
- assert.is_nil(hunks[1].context_after)
- end)
-
- it('skips hunks without repo_root', function()
- local hunks = {
- {
- filename = 'a.lua',
- ft = 'lua',
- lang = 'lua',
- start_line = 1,
- lines = { '+x' },
- prefix_width = 1,
- quote_width = 0,
- repo_root = nil,
- file_new_start = 1,
- file_new_count = 1,
- },
- }
- compute_hunk_context(hunks, 25)
-
- assert.is_nil(hunks[1].context_before)
- assert.is_nil(hunks[1].context_after)
- end)
-
- it('skips when file does not exist on disk', function()
- local hunks = {
- make_hunk('nonexistent.lua', {
- file_new_start = 1,
- file_new_count = 1,
- lines = { '+x' },
- }),
- }
- compute_hunk_context(hunks, 25)
-
- assert.is_nil(hunks[1].context_before)
- assert.is_nil(hunks[1].context_after)
- end)
-
- it('returns nil context_before for hunk at line 1', function()
- write_file('a.lua', { 'first', 'second' })
-
- local hunks = {
- make_hunk('a.lua', {
- file_new_start = 1,
- file_new_count = 1,
- lines = { '+first' },
- }),
- }
- compute_hunk_context(hunks, 25)
-
- assert.is_nil(hunks[1].context_before)
- end)
-
- it('returns nil context_after for hunk at end of file', function()
- write_file('a.lua', { 'first', 'last' })
-
- local hunks = {
- make_hunk('a.lua', {
- file_new_start = 1,
- file_new_count = 2,
- lines = { ' first', '+last' },
- }),
- }
- compute_hunk_context(hunks, 25)
-
- assert.is_nil(hunks[1].context_after)
- end)
-
- it('reads file once for multiple hunks in same file', function()
- write_file('a.lua', {
- 'local M = {}',
- 'function M.foo()',
- ' return 1',
- 'end',
- 'function M.bar()',
- ' return 2',
- 'end',
- 'return M',
- })
-
- local hunks = {
- make_hunk('a.lua', {
- file_new_start = 2,
- file_new_count = 3,
- lines = { ' function M.foo()', '+ return 1', ' end' },
- }),
- make_hunk('a.lua', {
- file_new_start = 5,
- file_new_count = 3,
- lines = { ' function M.bar()', '+ return 2', ' end' },
- }),
- }
- compute_hunk_context(hunks, 25)
-
- assert.same({ 'local M = {}' }, hunks[1].context_before)
- assert.same({ 'function M.bar()', ' return 2', 'end', 'return M' }, hunks[1].context_after)
- assert.same({
- 'local M = {}',
- 'function M.foo()',
- ' return 1',
- 'end',
- }, hunks[2].context_before)
- assert.same({ 'return M' }, hunks[2].context_after)
- end)
- end)
-
- describe('highlight_treesitter with context', function()
- local ns
-
- before_each(function()
- ns = vim.api.nvim_create_namespace('diffs_context_test')
- local normal = vim.api.nvim_get_hl(0, { name = 'Normal' })
- vim.api.nvim_set_hl(0, 'DiffsClear', { fg = normal.fg or 0xc0c0c0 })
- end)
-
- local function create_buffer(lines)
- local bufnr = vim.api.nvim_create_buf(false, true)
- vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
- return bufnr
- end
-
- local function delete_buffer(bufnr)
- if vim.api.nvim_buf_is_valid(bufnr) then
- vim.api.nvim_buf_delete(bufnr, { force = true })
- end
- end
-
- local function get_extmarks(bufnr)
- return vim.api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, { details = true })
- end
-
- local function default_opts(overrides)
- local opts = {
- hide_prefix = false,
- highlights = {
- background = false,
- gutter = false,
- context = { enabled = true, lines = 25 },
- treesitter = { enabled = true, max_lines = 500 },
- vim = { enabled = false, max_lines = 200 },
- intra = { enabled = false, algorithm = 'default', max_lines = 500 },
- priorities = { clear = 198, syntax = 199, line_bg = 200, char_bg = 201 },
- },
- }
- if overrides then
- if overrides.highlights then
- opts.highlights = vim.tbl_deep_extend('force', opts.highlights, overrides.highlights)
- end
- end
- return opts
- end
-
- it('applies extmarks only to hunk lines, not context lines', function()
- local bufnr = create_buffer({
- '@@ -1,2 +1,3 @@',
- ' local x = 1',
- ' local y = 2',
- '+local z = 3',
- })
-
- local hunk = {
- filename = 'test.lua',
- lang = 'lua',
- start_line = 2,
- lines = { ' local x = 1', ' local y = 2', '+local z = 3' },
- prefix_width = 1,
- quote_width = 0,
- context_before = { 'local function foo()' },
- context_after = { 'end' },
- }
-
- highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
-
- local extmarks = get_extmarks(bufnr)
- for _, mark in ipairs(extmarks) do
- local row = mark[2]
- assert.is_true(row >= 1 and row <= 3, 'extmark row ' .. row .. ' outside hunk range')
- end
- assert.is_true(#extmarks > 0)
- delete_buffer(bufnr)
- end)
-
- it('does not pass context when context.enabled = false', function()
- local bufnr = create_buffer({
- '@@ -1,1 +1,2 @@',
- ' local x = 1',
- '+local y = 2',
- })
-
- local hunk = {
- filename = 'test.lua',
- lang = 'lua',
- start_line = 2,
- lines = { ' local x = 1', '+local y = 2' },
- prefix_width = 1,
- quote_width = 0,
- context_before = { 'local function foo()' },
- context_after = { 'end' },
- }
-
- local opts_enabled = default_opts({ highlights = { context = { enabled = true } } })
- highlight.highlight_hunk(bufnr, ns, hunk, opts_enabled)
- local extmarks_with = get_extmarks(bufnr)
-
- vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
-
- local opts_disabled = default_opts({ highlights = { context = { enabled = false } } })
- highlight.highlight_hunk(bufnr, ns, hunk, opts_disabled)
- local extmarks_without = get_extmarks(bufnr)
-
- assert.is_true(#extmarks_with > 0)
- assert.is_true(#extmarks_without > 0)
- delete_buffer(bufnr)
- end)
-
- it('skips context fields that are nil', function()
- local bufnr = create_buffer({
- '@@ -1,1 +1,2 @@',
- ' local x = 1',
- '+local y = 2',
- })
-
- local hunk = {
- filename = 'test.lua',
- lang = 'lua',
- start_line = 2,
- lines = { ' local x = 1', '+local y = 2' },
- prefix_width = 1,
- quote_width = 0,
- }
-
- highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
-
- local extmarks = get_extmarks(bufnr)
- assert.is_true(#extmarks > 0)
- delete_buffer(bufnr)
- end)
- end)
-end)
diff --git a/spec/decoration_provider_spec.lua b/spec/decoration_provider_spec.lua
deleted file mode 100644
index 172ca11..0000000
--- a/spec/decoration_provider_spec.lua
+++ /dev/null
@@ -1,329 +0,0 @@
-require('spec.helpers')
-local diffs = require('diffs')
-
-describe('decoration_provider', function()
- local function create_buffer(lines)
- local bufnr = vim.api.nvim_create_buf(false, true)
- vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines or {})
- return bufnr
- end
-
- local function delete_buffer(bufnr)
- if vim.api.nvim_buf_is_valid(bufnr) then
- vim.api.nvim_buf_delete(bufnr, { force = true })
- end
- end
-
- describe('ensure_cache', function()
- it('populates hunk cache for a buffer with diff content', function()
- local bufnr = create_buffer({
- 'M test.lua',
- '@@ -1,2 +1,3 @@',
- ' local x = 1',
- '-local y = 2',
- '+local y = 3',
- ' local z = 4',
- })
- diffs.attach(bufnr)
- local entry = diffs._test.hunk_cache[bufnr]
- assert.is_not_nil(entry)
- assert.is_table(entry.hunks)
- assert.is_true(#entry.hunks > 0)
- delete_buffer(bufnr)
- end)
-
- it('cache tick matches buffer changedtick after attach', function()
- local bufnr = create_buffer({
- 'M test.lua',
- '@@ -1,1 +1,2 @@',
- ' local x = 1',
- '+local y = 2',
- })
- diffs.attach(bufnr)
- local entry = diffs._test.hunk_cache[bufnr]
- local tick = vim.api.nvim_buf_get_changedtick(bufnr)
- assert.are.equal(tick, entry.tick)
- delete_buffer(bufnr)
- end)
-
- it('re-parses and advances tick when buffer content changes', function()
- local bufnr = create_buffer({
- 'M test.lua',
- '@@ -1,1 +1,2 @@',
- ' local x = 1',
- '+local y = 2',
- })
- diffs.attach(bufnr)
- local tick_before = diffs._test.hunk_cache[bufnr].tick
- vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, { '+local z = 3' })
- diffs._test.ensure_cache(bufnr)
- local tick_after = diffs._test.hunk_cache[bufnr].tick
- assert.is_true(tick_after > tick_before)
- delete_buffer(bufnr)
- end)
-
- it('skips reparse when fingerprint unchanged but sets pending_clear', function()
- local bufnr = create_buffer({
- 'M test.lua',
- '@@ -1,1 +1,2 @@',
- ' local x = 1',
- '+local y = 2',
- })
- diffs.attach(bufnr)
- local entry = diffs._test.hunk_cache[bufnr]
- local original_hunks = entry.hunks
- entry.pending_clear = false
-
- local lc = vim.api.nvim_buf_line_count(bufnr)
- local bc = vim.api.nvim_buf_get_offset(bufnr, lc)
- entry.line_count = lc
- entry.byte_count = bc
- entry.tick = -1
-
- diffs._test.ensure_cache(bufnr)
-
- local updated = diffs._test.hunk_cache[bufnr]
- local current_tick = vim.api.nvim_buf_get_changedtick(bufnr)
- assert.are.equal(original_hunks, updated.hunks)
- assert.are.equal(current_tick, updated.tick)
- assert.is_true(updated.pending_clear)
- delete_buffer(bufnr)
- end)
-
- it('does nothing for invalid buffer', function()
- local bufnr = create_buffer({})
- diffs.attach(bufnr)
- vim.api.nvim_buf_delete(bufnr, { force = true })
- assert.has_no.errors(function()
- diffs._test.ensure_cache(bufnr)
- end)
- end)
- end)
-
- describe('pending_clear', function()
- it('is true after invalidate_cache', function()
- local bufnr = create_buffer({})
- diffs.attach(bufnr)
- diffs._test.invalidate_cache(bufnr)
- local entry = diffs._test.hunk_cache[bufnr]
- assert.is_true(entry.pending_clear)
- delete_buffer(bufnr)
- end)
-
- it('is true immediately after fresh ensure_cache', function()
- local bufnr = create_buffer({
- 'M test.lua',
- '@@ -1,1 +1,2 @@',
- ' local x = 1',
- '+local y = 2',
- })
- diffs.attach(bufnr)
- local entry = diffs._test.hunk_cache[bufnr]
- assert.is_true(entry.pending_clear)
- delete_buffer(bufnr)
- end)
-
- it('clears namespace extmarks when on_buf processes pending_clear', function()
- local bufnr = create_buffer({
- 'M test.lua',
- '@@ -1,1 +1,2 @@',
- ' local x = 1',
- '+local y = 2',
- })
- diffs.attach(bufnr)
- local ns_id = vim.api.nvim_create_namespace('diffs')
- vim.api.nvim_buf_set_extmark(bufnr, ns_id, 0, 0, { line_hl_group = 'DiffAdd' })
- assert.are.equal(1, #vim.api.nvim_buf_get_extmarks(bufnr, ns_id, 0, -1, {}))
-
- diffs._test.invalidate_cache(bufnr)
- diffs._test.ensure_cache(bufnr)
- local entry = diffs._test.hunk_cache[bufnr]
- assert.is_true(entry.pending_clear)
-
- diffs._test.process_pending_clear(bufnr)
-
- entry = diffs._test.hunk_cache[bufnr]
- assert.is_false(entry.pending_clear)
- assert.are.same({}, vim.api.nvim_buf_get_extmarks(bufnr, ns_id, 0, -1, {}))
- delete_buffer(bufnr)
- end)
- end)
-
- describe('BufWipeout cleanup', function()
- it('removes hunk_cache entry after buffer wipeout', function()
- local bufnr = create_buffer({
- 'M test.lua',
- '@@ -1,1 +1,2 @@',
- ' local x = 1',
- '+local y = 2',
- })
- diffs.attach(bufnr)
- assert.is_not_nil(diffs._test.hunk_cache[bufnr])
- vim.api.nvim_buf_delete(bufnr, { force = true })
- assert.is_nil(diffs._test.hunk_cache[bufnr])
- end)
- end)
-
- describe('hunk stability', function()
- it('carries forward highlighted for stable hunks on section expansion', function()
- local bufnr = create_buffer({
- 'M test.lua',
- '@@ -1,2 +1,2 @@',
- ' local x = 1',
- '-local y = 2',
- '+local y = 3',
- '@@ -10,2 +10,3 @@',
- ' function M.foo()',
- '+ return true',
- ' end',
- })
- diffs.attach(bufnr)
- local entry = diffs._test.hunk_cache[bufnr]
- assert.are.equal(2, #entry.hunks)
-
- entry.pending_clear = false
- entry.highlighted = { [1] = true, [2] = true }
-
- vim.api.nvim_buf_set_lines(bufnr, 5, 5, false, {
- '@@ -5,1 +5,2 @@',
- ' local z = 4',
- '+local w = 5',
- })
- diffs._test.ensure_cache(bufnr)
-
- local updated = diffs._test.hunk_cache[bufnr]
- assert.are.equal(3, #updated.hunks)
- assert.is_true(updated.highlighted[1] == true)
- assert.is_nil(updated.highlighted[2])
- assert.is_true(updated.highlighted[3] == true)
- assert.is_false(updated.pending_clear)
- delete_buffer(bufnr)
- end)
-
- it('carries forward highlighted for stable hunks on section collapse', function()
- local bufnr = create_buffer({
- 'M test.lua',
- '@@ -1,2 +1,2 @@',
- ' local x = 1',
- '-local y = 2',
- '+local y = 3',
- '@@ -5,1 +5,2 @@',
- ' local z = 4',
- '+local w = 5',
- '@@ -10,2 +10,3 @@',
- ' function M.foo()',
- '+ return true',
- ' end',
- })
- diffs.attach(bufnr)
- local entry = diffs._test.hunk_cache[bufnr]
- assert.are.equal(3, #entry.hunks)
-
- entry.pending_clear = false
- entry.highlighted = { [1] = true, [2] = true, [3] = true }
-
- vim.api.nvim_buf_set_lines(bufnr, 5, 8, false, {})
- diffs._test.ensure_cache(bufnr)
-
- local updated = diffs._test.hunk_cache[bufnr]
- assert.are.equal(2, #updated.hunks)
- assert.is_true(updated.highlighted[1] == true)
- assert.is_true(updated.highlighted[2] == true)
- assert.is_false(updated.pending_clear)
- delete_buffer(bufnr)
- end)
-
- it('bypasses carry-forward when pending_clear was true', function()
- local bufnr = create_buffer({
- 'M test.lua',
- '@@ -1,2 +1,2 @@',
- ' local x = 1',
- '-local y = 2',
- '+local y = 3',
- '@@ -10,2 +10,3 @@',
- ' function M.foo()',
- '+ return true',
- ' end',
- })
- diffs.attach(bufnr)
- local entry = diffs._test.hunk_cache[bufnr]
- entry.highlighted = { [1] = true, [2] = true }
- entry.pending_clear = true
-
- vim.api.nvim_buf_set_lines(bufnr, 5, 5, false, {
- '@@ -5,1 +5,2 @@',
- ' local z = 4',
- '+local w = 5',
- })
- diffs._test.ensure_cache(bufnr)
-
- local updated = diffs._test.hunk_cache[bufnr]
- assert.are.same({}, updated.highlighted)
- assert.is_true(updated.pending_clear)
- delete_buffer(bufnr)
- end)
-
- it('does not carry forward when all hunks changed', function()
- local bufnr = create_buffer({
- 'M test.lua',
- '@@ -1,2 +1,2 @@',
- ' local x = 1',
- '-local y = 2',
- '+local y = 3',
- })
- diffs.attach(bufnr)
- local entry = diffs._test.hunk_cache[bufnr]
-
- entry.pending_clear = false
- entry.highlighted = { [1] = true }
-
- vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
- 'M other.lua',
- '@@ -1,1 +1,2 @@',
- ' local a = 1',
- '+local b = 2',
- })
- diffs._test.ensure_cache(bufnr)
-
- local updated = diffs._test.hunk_cache[bufnr]
- assert.is_nil(updated.highlighted[1])
- assert.is_true(updated.pending_clear)
- delete_buffer(bufnr)
- end)
- end)
-
- describe('multiple hunks in cache', function()
- it('stores all parsed hunks for a multi-hunk buffer', function()
- local bufnr = create_buffer({
- 'M test.lua',
- '@@ -1,2 +1,2 @@',
- ' local x = 1',
- '-local y = 2',
- '+local y = 3',
- '@@ -10,2 +10,3 @@',
- ' function M.foo()',
- '+ return true',
- ' end',
- })
- diffs.attach(bufnr)
- local entry = diffs._test.hunk_cache[bufnr]
- assert.is_not_nil(entry)
- assert.are.equal(2, #entry.hunks)
- delete_buffer(bufnr)
- end)
-
- it('stores empty hunks table for buffer with no diff content', function()
- local bufnr = create_buffer({
- 'Head: main',
- 'Help: g?',
- '',
- 'Nothing to see here',
- })
- diffs.attach(bufnr)
- local entry = diffs._test.hunk_cache[bufnr]
- assert.is_not_nil(entry)
- assert.are.same({}, entry.hunks)
- delete_buffer(bufnr)
- end)
- end)
-end)
diff --git a/spec/email_quote_spec.lua b/spec/email_quote_spec.lua
deleted file mode 100644
index e1744a1..0000000
--- a/spec/email_quote_spec.lua
+++ /dev/null
@@ -1,477 +0,0 @@
-require('spec.helpers')
-local highlight = require('diffs.highlight')
-local parser = require('diffs.parser')
-
-describe('email-quoted diffs', function()
- local function create_buffer(lines)
- local bufnr = vim.api.nvim_create_buf(false, true)
- vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
- return bufnr
- end
-
- local function delete_buffer(bufnr)
- if vim.api.nvim_buf_is_valid(bufnr) then
- vim.api.nvim_buf_delete(bufnr, { force = true })
- end
- end
-
- describe('parser', function()
- it('parses a fully email-quoted unified diff', function()
- local bufnr = create_buffer({
- '> diff --git a/foo.py b/foo.py',
- '> index abc1234..def5678 100644',
- '> --- a/foo.py',
- '> +++ b/foo.py',
- '> @@ -0,0 +1,3 @@',
- '> +from typing import Annotated, final',
- '> +',
- '> +class Foo:',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal('foo.py', hunks[1].filename)
- assert.are.equal(3, #hunks[1].lines)
- assert.are.equal('+from typing import Annotated, final', hunks[1].lines[1])
- assert.are.equal(2, hunks[1].quote_width)
- delete_buffer(bufnr)
- end)
-
- it('parses a quoted diff embedded in an email reply', function()
- local bufnr = create_buffer({
- 'Looks good, one nit:',
- '',
- '> diff --git a/foo.py b/foo.py',
- '> @@ -0,0 +1,3 @@',
- '> +from typing import Annotated, final',
- '> +',
- '> +class Foo:',
- '',
- 'Maybe rename Foo to Bar?',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal('foo.py', hunks[1].filename)
- assert.are.equal(3, #hunks[1].lines)
- assert.are.equal(2, hunks[1].quote_width)
- delete_buffer(bufnr)
- end)
-
- it('sets quote_width = 0 on normal (unquoted) diffs', function()
- local bufnr = create_buffer({
- 'diff --git a/bar.lua b/bar.lua',
- '@@ -1,2 +1,2 @@',
- '-old_line',
- '+new_line',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal(0, hunks[1].quote_width)
- delete_buffer(bufnr)
- end)
-
- it('treats bare > lines as empty quoted lines', function()
- local bufnr = create_buffer({
- '> diff --git a/foo.py b/foo.py',
- '> @@ -1,3 +1,3 @@',
- '> -old',
- '>',
- '> +new',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal(3, #hunks[1].lines)
- assert.are.equal('-old', hunks[1].lines[1])
- assert.are.equal(' ', hunks[1].lines[2])
- assert.are.equal('+new', hunks[1].lines[3])
- delete_buffer(bufnr)
- end)
-
- it('handles deeply nested quotes', function()
- local bufnr = create_buffer({
- '>> diff --git a/foo.py b/foo.py',
- '>> @@ -0,0 +1,2 @@',
- '>> +line1',
- '>> +line2',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal(3, hunks[1].quote_width)
- assert.are.equal('+line1', hunks[1].lines[1])
- delete_buffer(bufnr)
- end)
-
- it('adjusts header_context_col for quote width', function()
- local bufnr = create_buffer({
- '> diff --git a/foo.py b/foo.py',
- '> @@ -1,2 +1,2 @@ def hello():',
- '> -old',
- '> +new',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal('def hello():', hunks[1].header_context)
- assert.are.equal(#'@@ -1,2 +1,2 @@ ' + 2, hunks[1].header_context_col)
- delete_buffer(bufnr)
- end)
-
- it('does not false-positive on prose containing > diff', function()
- local bufnr = create_buffer({
- '> diff between approaches is small',
- '> I think we should go with option A',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(0, #hunks)
- delete_buffer(bufnr)
- end)
-
- it('stores header lines stripped of quote prefix', function()
- local bufnr = create_buffer({
- '> diff --git a/foo.lua b/foo.lua',
- '> index abc1234..def5678 100644',
- '> --- a/foo.lua',
- '> +++ b/foo.lua',
- '> @@ -1,1 +1,1 @@',
- '> -old',
- '> +new',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.is_not_nil(hunks[1].header_lines)
- for _, hline in ipairs(hunks[1].header_lines) do
- assert.is_nil(hline:match('^> '))
- end
- delete_buffer(bufnr)
- end)
- end)
-
- describe('highlight', function()
- local ns
-
- before_each(function()
- ns = vim.api.nvim_create_namespace('diffs_email_test')
- vim.api.nvim_set_hl(0, 'DiffsClear', { fg = 0xc0c0c0, bg = 0x1e1e1e })
- vim.api.nvim_set_hl(0, 'DiffsAdd', { bg = 0x1a3a1a })
- vim.api.nvim_set_hl(0, 'DiffsDelete', { bg = 0x3a1a1a })
- vim.api.nvim_set_hl(0, 'DiffsConflictMarker', { fg = 0x808080, bold = true })
- end)
-
- local function get_extmarks(bufnr)
- return vim.api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, { details = true })
- end
-
- local function default_opts(overrides)
- local opts = {
- hide_prefix = false,
- highlights = {
- background = true,
- gutter = false,
- context = { enabled = false, lines = 0 },
- treesitter = {
- enabled = true,
- max_lines = 500,
- },
- vim = {
- enabled = false,
- max_lines = 200,
- },
- intra = {
- enabled = false,
- algorithm = 'default',
- max_lines = 500,
- },
- priorities = {
- clear = 198,
- syntax = 199,
- line_bg = 200,
- char_bg = 201,
- },
- },
- }
- if overrides then
- if overrides.highlights then
- opts.highlights = vim.tbl_deep_extend('force', opts.highlights, overrides.highlights)
- end
- for k, v in pairs(overrides) do
- if k ~= 'highlights' then
- opts[k] = v
- end
- end
- end
- return opts
- end
-
- it('applies DiffsClear on email-quoted header lines covering full buffer width', function()
- local buf_lines = {
- '> diff --git a/foo.lua b/foo.lua',
- '> index abc1234..def5678 100644',
- '> --- a/foo.lua',
- '> +++ b/foo.lua',
- '> @@ -1,1 +1,1 @@',
- '> -old',
- '> +new',
- }
- local bufnr = create_buffer(buf_lines)
-
- local hunk = {
- filename = 'foo.lua',
- lang = 'lua',
- ft = 'lua',
- start_line = 5,
- lines = { '-old', '+new' },
- prefix_width = 1,
- quote_width = 2,
- header_start_line = 1,
- header_lines = {
- 'diff --git a/foo.lua b/foo.lua',
- 'index abc1234..def5678 100644',
- '--- a/foo.lua',
- '+++ b/foo.lua',
- },
- }
-
- highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
-
- local extmarks = get_extmarks(bufnr)
- local header_clears = {}
- for _, mark in ipairs(extmarks) do
- local d = mark[4]
- if d and d.hl_group == 'DiffsClear' and mark[2] < 4 then
- table.insert(header_clears, { row = mark[2], col = mark[3], end_col = d.end_col })
- end
- end
- assert.is_true(#header_clears > 0)
- for _, c in ipairs(header_clears) do
- assert.are.equal(0, c.col)
- local buf_line_len = #buf_lines[c.row + 1]
- assert.are.equal(buf_line_len, c.end_col)
- end
-
- delete_buffer(bufnr)
- end)
-
- it('applies body prefix DiffsClear covering [0, pw+qw)', function()
- local bufnr = create_buffer({
- '> @@ -1,1 +1,1 @@',
- '> -old',
- '> +new',
- })
-
- local hunk = {
- filename = 'foo.lua',
- lang = 'lua',
- ft = 'lua',
- start_line = 1,
- lines = { '-old', '+new' },
- prefix_width = 1,
- quote_width = 2,
- }
-
- highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
-
- local extmarks = get_extmarks(bufnr)
- local prefix_clears = {}
- for _, mark in ipairs(extmarks) do
- local d = mark[4]
- if d and d.hl_group == 'DiffsClear' and d.end_col == 3 and mark[3] == 0 then
- table.insert(prefix_clears, { row = mark[2] })
- end
- end
- assert.are.equal(2, #prefix_clears)
-
- delete_buffer(bufnr)
- end)
-
- it('clamps body prefix DiffsClear on bare > lines (1-byte buffer line)', function()
- local bufnr = create_buffer({
- '> @@ -1,3 +1,3 @@',
- '> -old',
- '>',
- '> +new',
- })
-
- local hunk = {
- filename = 'foo.lua',
- ft = 'lua',
- lang = 'lua',
- start_line = 1,
- lines = { '-old', ' ', '+new' },
- prefix_width = 1,
- quote_width = 2,
- }
-
- highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
-
- local extmarks = get_extmarks(bufnr)
- local bare_line_row = 2
- local bare_clears = {}
- for _, mark in ipairs(extmarks) do
- local d = mark[4]
- if d and d.hl_group == 'DiffsClear' and mark[2] == bare_line_row and mark[3] == 0 then
- table.insert(bare_clears, { end_col = d.end_col })
- end
- end
- assert.are.equal(1, #bare_clears)
- assert.are.equal(1, bare_clears[1].end_col)
-
- delete_buffer(bufnr)
- end)
-
- it('applies per-char @diff.plus/@diff.minus at ci + qw', function()
- local bufnr = create_buffer({
- '> @@ -1,1 +1,1 @@',
- '> -old',
- '> +new',
- })
-
- local hunk = {
- filename = 'foo.lua',
- lang = 'lua',
- ft = 'lua',
- start_line = 1,
- lines = { '-old', '+new' },
- prefix_width = 1,
- quote_width = 2,
- }
-
- highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
-
- local extmarks = get_extmarks(bufnr)
- local diff_marks = {}
- for _, mark in ipairs(extmarks) do
- local d = mark[4]
- if d and (d.hl_group == '@diff.plus' or d.hl_group == '@diff.minus') then
- table.insert(
- diff_marks,
- { row = mark[2], col = mark[3], end_col = d.end_col, hl = d.hl_group }
- )
- end
- end
- assert.is_true(#diff_marks >= 2)
- for _, dm in ipairs(diff_marks) do
- assert.are.equal(2, dm.col)
- assert.are.equal(3, dm.end_col)
- end
-
- delete_buffer(bufnr)
- end)
-
- it('offsets treesitter extmarks by pw + qw', function()
- local bufnr = create_buffer({
- '> @@ -1,1 +1,2 @@',
- '> local x = 1',
- '> +local y = 2',
- })
-
- local hunk = {
- filename = 'test.lua',
- lang = 'lua',
- ft = 'lua',
- start_line = 1,
- lines = { ' local x = 1', '+local y = 2' },
- prefix_width = 1,
- quote_width = 2,
- }
-
- highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
-
- local extmarks = get_extmarks(bufnr)
- local ts_marks = {}
- for _, mark in ipairs(extmarks) do
- local d = mark[4]
- if d and d.hl_group and d.hl_group:match('^@.*%.lua$') then
- table.insert(ts_marks, { row = mark[2], col = mark[3] })
- end
- end
- assert.is_true(#ts_marks > 0)
- for _, tm in ipairs(ts_marks) do
- assert.is_true(tm.col >= 3)
- end
-
- delete_buffer(bufnr)
- end)
-
- it('offsets intra-line char span extmarks by qw', function()
- local bufnr = create_buffer({
- '> @@ -1,1 +1,1 @@',
- '> -hello world',
- '> +hello earth',
- })
-
- local hunk = {
- filename = 'test.txt',
- ft = nil,
- lang = nil,
- start_line = 1,
- lines = { '-hello world', '+hello earth' },
- prefix_width = 1,
- quote_width = 2,
- }
-
- highlight.highlight_hunk(
- bufnr,
- ns,
- hunk,
- default_opts({
- highlights = { intra = { enabled = true, algorithm = 'default', max_lines = 500 } },
- })
- )
-
- local extmarks = get_extmarks(bufnr)
- local char_marks = {}
- for _, mark in ipairs(extmarks) do
- local d = mark[4]
- if d and (d.hl_group == 'DiffsAddText' or d.hl_group == 'DiffsDeleteText') then
- table.insert(char_marks, { row = mark[2], col = mark[3], end_col = d.end_col })
- end
- end
- if #char_marks > 0 then
- for _, cm in ipairs(char_marks) do
- assert.is_true(cm.col >= 2)
- end
- end
-
- delete_buffer(bufnr)
- end)
-
- it('does not produce duplicate extmarks with syntax_only + qw', function()
- local bufnr = create_buffer({
- '> @@ -1,1 +1,2 @@',
- '> local x = 1',
- '> +local y = 2',
- })
-
- local hunk = {
- filename = 'test.lua',
- lang = 'lua',
- ft = 'lua',
- start_line = 1,
- lines = { ' local x = 1', '+local y = 2' },
- prefix_width = 1,
- quote_width = 2,
- }
-
- highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
- highlight.highlight_hunk(bufnr, ns, hunk, default_opts({ syntax_only = true }))
-
- local extmarks = get_extmarks(bufnr)
- local line_bg_count = 0
- for _, mark in ipairs(extmarks) do
- local d = mark[4]
- if d and d.line_hl_group == 'DiffsAdd' then
- line_bg_count = line_bg_count + 1
- end
- end
- assert.are.equal(1, line_bg_count)
-
- delete_buffer(bufnr)
- end)
- end)
-end)
diff --git a/spec/fugitive_spec.lua b/spec/fugitive_spec.lua
index 95b40a3..244e1b7 100644
--- a/spec/fugitive_spec.lua
+++ b/spec/fugitive_spec.lua
@@ -87,6 +87,28 @@ describe('fugitive', function()
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)',
@@ -135,6 +157,28 @@ describe('fugitive', function()
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)',
@@ -146,54 +190,77 @@ describe('fugitive', function()
vim.api.nvim_buf_delete(buf, { force = true })
end)
- it('unquotes git-quoted filenames with spaces', function()
- local buf = create_status_buffer({
- 'Unstaged (1)',
- 'M "path with spaces/file.lua"',
- })
- local filename = fugitive.get_file_at_line(buf, 2)
- assert.equals('path with spaces/file.lua', filename)
- vim.api.nvim_buf_delete(buf, { force = true })
- end)
-
- it('unquotes escaped quotes in filenames', function()
- local buf = create_status_buffer({
- 'Unstaged (1)',
- 'M "file\\"name.lua"',
- })
- local filename = fugitive.get_file_at_line(buf, 2)
- assert.equals('file"name.lua', filename)
- vim.api.nvim_buf_delete(buf, { force = true })
- end)
-
- it('unquotes octal escapes in filenames', function()
- local buf = create_status_buffer({
- 'Unstaged (1)',
- 'M "\\303\\251le.lua"',
- })
- local filename = fugitive.get_file_at_line(buf, 2)
- assert.equals('\195\169le.lua', filename)
- vim.api.nvim_buf_delete(buf, { force = true })
- end)
-
- it('passes through unquoted filenames unchanged', function()
- local buf = create_status_buffer({
- 'Unstaged (1)',
- 'M normal.lua',
- })
- local filename = fugitive.get_file_at_line(buf, 2)
- assert.equals('normal.lua', filename)
- vim.api.nvim_buf_delete(buf, { force = true })
- end)
-
- it('unquotes renamed files with quotes', function()
+ it('handles double extensions', function()
local buf = create_status_buffer({
'Staged (1)',
- 'R100 "old name.lua" -> "new name.lua"',
+ 'M test.spec.lua',
})
local filename, _, _, old_filename = fugitive.get_file_at_line(buf, 2)
- assert.equals('new name.lua', filename)
- assert.equals('old name.lua', old_filename)
+ 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)
@@ -254,6 +321,30 @@ describe('fugitive', function()
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)',
@@ -315,6 +406,22 @@ describe('fugitive', function()
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)',
diff --git a/spec/gitsigns_spec.lua b/spec/gitsigns_spec.lua
deleted file mode 100644
index b2b6e2a..0000000
--- a/spec/gitsigns_spec.lua
+++ /dev/null
@@ -1,236 +0,0 @@
-require('spec.helpers')
-local gs = require('diffs.gitsigns')
-
-local function setup_highlight_groups()
- local normal = vim.api.nvim_get_hl(0, { name = 'Normal' })
- local diff_add = vim.api.nvim_get_hl(0, { name = 'DiffAdd' })
- local diff_delete = vim.api.nvim_get_hl(0, { name = 'DiffDelete' })
- vim.api.nvim_set_hl(0, 'DiffsClear', { fg = normal.fg or 0xc0c0c0 })
- vim.api.nvim_set_hl(0, 'DiffsAdd', { bg = diff_add.bg or 0x2e4a3a })
- vim.api.nvim_set_hl(0, 'DiffsDelete', { bg = diff_delete.bg or 0x4a2e3a })
- vim.api.nvim_set_hl(0, 'DiffsAddText', { bg = 0x00FF00 })
- vim.api.nvim_set_hl(0, 'DiffsDeleteText', { bg = 0xFF0000 })
-end
-
-local function create_buffer(lines)
- local bufnr = vim.api.nvim_create_buf(false, true)
- vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines or {})
- return bufnr
-end
-
-local function delete_buffer(bufnr)
- if bufnr and vim.api.nvim_buf_is_valid(bufnr) then
- vim.api.nvim_buf_delete(bufnr, { force = true })
- end
-end
-
-describe('gitsigns', function()
- describe('parse_blame_hunks', function()
- it('parses a single hunk', function()
- local bufnr = create_buffer({
- 'commit abc1234',
- 'Author: Test User',
- '',
- 'Hunk 1 of 1',
- ' local x = 1',
- '-local y = 2',
- '+local y = 3',
- ' local z = 4',
- })
- local hunks = gs.parse_blame_hunks(bufnr, 'test.lua', 'lua', 'lua')
- assert.are.equal(1, #hunks)
- assert.are.equal('test.lua', hunks[1].filename)
- assert.are.equal('lua', hunks[1].ft)
- assert.are.equal('lua', hunks[1].lang)
- assert.are.equal(1, hunks[1].prefix_width)
- assert.are.equal(0, hunks[1].quote_width)
- assert.are.equal(4, #hunks[1].lines)
- assert.are.equal(4, hunks[1].start_line)
- assert.are.equal(' local x = 1', hunks[1].lines[1])
- assert.are.equal('-local y = 2', hunks[1].lines[2])
- assert.are.equal('+local y = 3', hunks[1].lines[3])
- delete_buffer(bufnr)
- end)
-
- it('parses multiple hunks', function()
- local bufnr = create_buffer({
- 'commit abc1234',
- '',
- 'Hunk 1 of 2',
- '-local a = 1',
- '+local a = 2',
- 'Hunk 2 of 2',
- ' local b = 3',
- '+local c = 4',
- })
- local hunks = gs.parse_blame_hunks(bufnr, 'test.lua', 'lua', 'lua')
- assert.are.equal(2, #hunks)
- assert.are.equal(2, #hunks[1].lines)
- assert.are.equal(2, #hunks[2].lines)
- delete_buffer(bufnr)
- end)
-
- it('skips guessed-offset lines', function()
- local bufnr = create_buffer({
- 'commit abc1234',
- '',
- 'Hunk 1 of 1',
- '(guessed: hunk offset may be wrong)',
- ' local x = 1',
- '+local y = 2',
- })
- local hunks = gs.parse_blame_hunks(bufnr, 'test.lua', 'lua', 'lua')
- assert.are.equal(1, #hunks)
- assert.are.equal(2, #hunks[1].lines)
- assert.are.equal(' local x = 1', hunks[1].lines[1])
- delete_buffer(bufnr)
- end)
-
- it('returns empty table when no hunks present', function()
- local bufnr = create_buffer({
- 'commit abc1234',
- 'Author: Test User',
- 'Date: 2024-01-01',
- })
- local hunks = gs.parse_blame_hunks(bufnr, 'test.lua', 'lua', 'lua')
- assert.are.equal(0, #hunks)
- delete_buffer(bufnr)
- end)
-
- it('handles hunk with no diff lines after header', function()
- local bufnr = create_buffer({
- 'Hunk 1 of 1',
- 'some non-diff text',
- })
- local hunks = gs.parse_blame_hunks(bufnr, 'test.lua', 'lua', 'lua')
- assert.are.equal(0, #hunks)
- delete_buffer(bufnr)
- end)
- end)
-
- describe('on_preview', function()
- before_each(function()
- setup_highlight_groups()
- end)
-
- it('applies extmarks to popup buffer with diff content', function()
- local bufnr = create_buffer({
- 'commit abc1234',
- '',
- 'Hunk 1 of 1',
- ' local x = 1',
- '-local y = 2',
- '+local y = 3',
- })
-
- local winid = vim.api.nvim_open_win(bufnr, false, {
- relative = 'editor',
- width = 40,
- height = 10,
- row = 0,
- col = 0,
- })
-
- gs._test.on_preview(winid, bufnr)
-
- local extmarks = vim.api.nvim_buf_get_extmarks(bufnr, gs._test.ns, 0, -1, { details = true })
- assert.is_true(#extmarks > 0)
-
- vim.api.nvim_win_close(winid, true)
- delete_buffer(bufnr)
- end)
-
- it('clears gitsigns_popup namespace on diff region', function()
- local bufnr = create_buffer({
- 'commit abc1234',
- '',
- 'Hunk 1 of 1',
- ' local x = 1',
- '+local y = 2',
- })
-
- vim.api.nvim_buf_set_extmark(bufnr, gs._test.gs_popup_ns, 3, 0, {
- end_col = 12,
- hl_group = 'GitSignsAddPreview',
- })
- vim.api.nvim_buf_set_extmark(bufnr, gs._test.gs_popup_ns, 4, 0, {
- end_col = 12,
- hl_group = 'GitSignsAddPreview',
- })
-
- local winid = vim.api.nvim_open_win(bufnr, false, {
- relative = 'editor',
- width = 40,
- height = 10,
- row = 0,
- col = 0,
- })
-
- gs._test.on_preview(winid, bufnr)
-
- local gs_extmarks =
- vim.api.nvim_buf_get_extmarks(bufnr, gs._test.gs_popup_ns, 0, -1, { details = true })
- assert.are.equal(0, #gs_extmarks)
-
- vim.api.nvim_win_close(winid, true)
- delete_buffer(bufnr)
- end)
-
- it('does not error on invalid buffer', function()
- assert.has_no.errors(function()
- gs._test.on_preview(0, 99999)
- end)
- end)
- end)
-
- describe('setup', function()
- it('returns false when gitsigns.popup is not available', function()
- local saved = package.loaded['gitsigns.popup']
- package.loaded['gitsigns.popup'] = nil
- package.preload['gitsigns.popup'] = nil
-
- local fresh = loadfile('lua/diffs/gitsigns.lua')()
- local result = fresh.setup()
- assert.is_false(result)
-
- package.loaded['gitsigns.popup'] = saved
- end)
-
- it('patches gitsigns.popup when available', function()
- local create_called = false
- local update_called = false
- local mock_popup = {
- create = function()
- create_called = true
- local bufnr = create_buffer({ 'test' })
- local winid = vim.api.nvim_open_win(bufnr, false, {
- relative = 'editor',
- width = 10,
- height = 1,
- row = 0,
- col = 0,
- })
- return winid, bufnr
- end,
- update = function()
- update_called = true
- end,
- }
-
- local saved = package.loaded['gitsigns.popup']
- package.loaded['gitsigns.popup'] = mock_popup
-
- local fresh = loadfile('lua/diffs/gitsigns.lua')()
- local result = fresh.setup()
- assert.is_true(result)
-
- mock_popup.create()
- assert.is_true(create_called)
-
- mock_popup.update(0, 0)
- assert.is_true(update_called)
-
- package.loaded['gitsigns.popup'] = saved
- end)
- end)
-end)
diff --git a/spec/highlight_spec.lua b/spec/highlight_spec.lua
index 39cc23c..0b9051e 100644
--- a/spec/highlight_spec.lua
+++ b/spec/highlight_spec.lua
@@ -13,7 +13,6 @@ describe('highlight', function()
vim.api.nvim_set_hl(0, 'DiffsClear', { fg = normal.fg or 0xc0c0c0 })
vim.api.nvim_set_hl(0, 'DiffsAdd', { bg = diff_add.bg })
vim.api.nvim_set_hl(0, 'DiffsDelete', { bg = diff_delete.bg })
- vim.api.nvim_set_hl(0, 'DiffsConflictMarker', { fg = 0x808080, bold = true })
end)
local function create_buffer(lines)
@@ -52,12 +51,6 @@ describe('highlight', function()
algorithm = 'default',
max_lines = 500,
},
- priorities = {
- clear = 198,
- syntax = 199,
- line_bg = 200,
- char_bg = 201,
- },
},
}
if overrides then
@@ -71,6 +64,27 @@ describe('highlight', function()
return opts
end
+ it('applies extmarks for lua code', function()
+ local bufnr = create_buffer({
+ '@@ -1,1 +1,2 @@',
+ ' local x = 1',
+ '+local y = 2',
+ })
+
+ local hunk = {
+ filename = 'test.lua',
+ lang = 'lua',
+ start_line = 1,
+ lines = { ' local x = 1', '+local y = 2' },
+ }
+
+ highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
+
+ local extmarks = get_extmarks(bufnr)
+ assert.is_true(#extmarks > 0)
+ delete_buffer(bufnr)
+ end)
+
it('applies DiffsClear extmarks to clear diff colors', function()
local bufnr = create_buffer({
'@@ -1,1 +1,2 @@',
@@ -176,6 +190,36 @@ describe('highlight', function()
delete_buffer(bufnr)
end)
+ it('highlights header context when enabled', function()
+ local bufnr = create_buffer({
+ '@@ -10,3 +10,4 @@ function hello()',
+ ' local x = 1',
+ '+local y = 2',
+ })
+
+ local hunk = {
+ filename = 'test.lua',
+ lang = 'lua',
+ start_line = 1,
+ header_context = 'function hello()',
+ header_context_col = 18,
+ lines = { ' local x = 1', '+local y = 2' },
+ }
+
+ highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
+
+ local extmarks = get_extmarks(bufnr)
+ local has_header_extmark = false
+ for _, mark in ipairs(extmarks) do
+ if mark[2] == 0 then
+ has_header_extmark = true
+ break
+ end
+ end
+ assert.is_true(has_header_extmark)
+ delete_buffer(bufnr)
+ end)
+
it('highlights function keyword in header context', function()
local bufnr = create_buffer({
'@@ -5,3 +5,4 @@ function M.setup()',
@@ -236,6 +280,44 @@ describe('highlight', function()
delete_buffer(bufnr)
end)
+ it('handles empty hunk lines', function()
+ local bufnr = create_buffer({
+ '@@ -1,0 +1,0 @@',
+ })
+
+ local hunk = {
+ filename = 'test.lua',
+ lang = 'lua',
+ start_line = 1,
+ lines = {},
+ }
+
+ assert.has_no.errors(function()
+ highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
+ end)
+ delete_buffer(bufnr)
+ end)
+
+ it('handles code that is just whitespace', function()
+ local bufnr = create_buffer({
+ '@@ -1,1 +1,2 @@',
+ ' ',
+ '+ ',
+ })
+
+ local hunk = {
+ filename = 'test.lua',
+ lang = 'lua',
+ start_line = 1,
+ lines = { ' ', '+ ' },
+ }
+
+ assert.has_no.errors(function()
+ highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
+ end)
+ delete_buffer(bufnr)
+ end)
+
it('applies overlay extmarks when hide_prefix enabled', function()
local bufnr = create_buffer({
'@@ -1,1 +1,2 @@',
@@ -263,6 +345,33 @@ describe('highlight', function()
delete_buffer(bufnr)
end)
+ it('does not apply overlay extmarks when hide_prefix disabled', function()
+ local bufnr = create_buffer({
+ '@@ -1,1 +1,2 @@',
+ ' local x = 1',
+ '+local y = 2',
+ })
+
+ local hunk = {
+ filename = 'test.lua',
+ lang = 'lua',
+ start_line = 1,
+ lines = { ' local x = 1', '+local y = 2' },
+ }
+
+ highlight.highlight_hunk(bufnr, ns, hunk, default_opts({ hide_prefix = false }))
+
+ local extmarks = get_extmarks(bufnr)
+ local overlay_count = 0
+ for _, mark in ipairs(extmarks) do
+ if mark[4] and mark[4].virt_text_pos == 'overlay' then
+ overlay_count = overlay_count + 1
+ end
+ end
+ assert.are.equal(0, overlay_count)
+ delete_buffer(bufnr)
+ end)
+
it('applies DiffAdd background to + lines when background enabled', function()
local bufnr = create_buffer({
'@@ -1,1 +1,2 @@',
@@ -287,7 +396,7 @@ describe('highlight', function()
local extmarks = get_extmarks(bufnr)
local has_diff_add = false
for _, mark in ipairs(extmarks) do
- if mark[4] and mark[4].line_hl_group == 'DiffsAdd' then
+ if mark[4] and mark[4].hl_group == 'DiffsAdd' then
has_diff_add = true
break
end
@@ -320,7 +429,7 @@ describe('highlight', function()
local extmarks = get_extmarks(bufnr)
local has_diff_delete = false
for _, mark in ipairs(extmarks) do
- if mark[4] and mark[4].line_hl_group == 'DiffsDelete' then
+ if mark[4] and mark[4].hl_group == 'DiffsDelete' then
has_diff_delete = true
break
end
@@ -329,6 +438,39 @@ describe('highlight', function()
delete_buffer(bufnr)
end)
+ it('does not apply background when background disabled', function()
+ local bufnr = create_buffer({
+ '@@ -1,1 +1,2 @@',
+ ' local x = 1',
+ '+local y = 2',
+ })
+
+ local hunk = {
+ filename = 'test.lua',
+ lang = 'lua',
+ start_line = 1,
+ lines = { ' local x = 1', '+local y = 2' },
+ }
+
+ highlight.highlight_hunk(
+ bufnr,
+ ns,
+ hunk,
+ default_opts({ highlights = { background = false } })
+ )
+
+ local extmarks = get_extmarks(bufnr)
+ local has_line_hl = false
+ for _, mark in ipairs(extmarks) do
+ if mark[4] and (mark[4].hl_group == 'DiffsAdd' or mark[4].hl_group == 'DiffsDelete') then
+ has_line_hl = true
+ break
+ end
+ end
+ assert.is_false(has_line_hl)
+ delete_buffer(bufnr)
+ end)
+
it('applies number_hl_group when gutter enabled', function()
local bufnr = create_buffer({
'@@ -1,1 +1,2 @@',
@@ -362,7 +504,7 @@ describe('highlight', function()
delete_buffer(bufnr)
end)
- it('line bg uses line_hl_group not hl_group with end_row', function()
+ it('does not apply number_hl_group when gutter disabled', function()
local bufnr = create_buffer({
'@@ -1,1 +1,2 @@',
' local x = 1',
@@ -380,101 +522,51 @@ describe('highlight', function()
bufnr,
ns,
hunk,
- default_opts({ highlights = { background = true } })
+ default_opts({ highlights = { background = true, gutter = false } })
)
local extmarks = get_extmarks(bufnr)
+ local has_number_hl = false
for _, mark in ipairs(extmarks) do
- local d = mark[4]
- assert.is_not_equal('DiffsAdd', d and d.hl_group)
- assert.is_not_equal('DiffsDelete', d and d.hl_group)
- end
- delete_buffer(bufnr)
- end)
-
- it('line bg extmark survives adjacent clear_namespace starting at next row', function()
- local bufnr = create_buffer({
- 'diff --git a/foo.py b/foo.py',
- '@@ -1,2 +1,2 @@',
- '-old',
- '+new',
- })
-
- local hunk = {
- filename = 'foo.py',
- header_start_line = 1,
- start_line = 2,
- lines = { '-old', '+new' },
- prefix_width = 1,
- }
-
- highlight.highlight_hunk(
- bufnr,
- ns,
- hunk,
- default_opts({ highlights = { background = true, treesitter = { enabled = false } } })
- )
-
- local last_body_row = hunk.start_line + #hunk.lines - 1
- vim.api.nvim_buf_clear_namespace(bufnr, ns, last_body_row + 1, last_body_row + 10)
-
- local marks = vim.api.nvim_buf_get_extmarks(
- bufnr,
- ns,
- { last_body_row, 0 },
- { last_body_row, -1 },
- { details = true }
- )
- local has_line_bg = false
- for _, mark in ipairs(marks) do
- if mark[4] and mark[4].line_hl_group == 'DiffsAdd' then
- has_line_bg = true
+ if mark[4] and mark[4].number_hl_group then
+ has_number_hl = true
+ break
end
end
- assert.is_true(has_line_bg)
+ assert.is_false(has_number_hl)
delete_buffer(bufnr)
end)
- it('clear range covers last body line of hunk with header', function()
+ it('skips treesitter highlights when treesitter disabled', function()
local bufnr = create_buffer({
- 'diff --git a/foo.py b/foo.py',
- 'index abc..def 100644',
- '--- a/foo.py',
- '+++ b/foo.py',
- '@@ -1,3 +1,3 @@',
- ' ctx',
- '-old',
- '+new',
+ '@@ -1,1 +1,2 @@',
+ ' local x = 1',
+ '+local y = 2',
})
local hunk = {
- filename = 'foo.py',
- header_start_line = 1,
- start_line = 5,
- lines = { ' ctx', '-old', '+new' },
- prefix_width = 1,
+ filename = 'test.lua',
+ lang = 'lua',
+ start_line = 1,
+ lines = { ' local x = 1', '+local y = 2' },
}
highlight.highlight_hunk(
bufnr,
ns,
hunk,
- default_opts({ highlights = { background = true, treesitter = { enabled = false } } })
+ default_opts({ highlights = { treesitter = { enabled = false }, background = true } })
)
- local last_body_row = hunk.start_line + #hunk.lines - 1
- local clear_start = hunk.header_start_line - 1
- local clear_end = hunk.start_line + #hunk.lines
- vim.api.nvim_buf_clear_namespace(bufnr, ns, clear_start, clear_end)
-
- local marks = vim.api.nvim_buf_get_extmarks(
- bufnr,
- ns,
- { last_body_row, 0 },
- { last_body_row, -1 },
- { details = false }
- )
- assert.are.equal(0, #marks)
+ local extmarks = get_extmarks(bufnr)
+ local has_ts_highlight = false
+ for _, mark in ipairs(extmarks) do
+ if mark[4] and mark[4].hl_group and mark[4].hl_group:match('^@') then
+ has_ts_highlight = true
+ break
+ end
+ end
+ assert.is_false(has_ts_highlight)
delete_buffer(bufnr)
end)
@@ -502,7 +594,7 @@ describe('highlight', function()
local extmarks = get_extmarks(bufnr)
local has_diff_add = false
for _, mark in ipairs(extmarks) do
- if mark[4] and mark[4].line_hl_group == 'DiffsAdd' then
+ if mark[4] and mark[4].hl_group == 'DiffsAdd' then
has_diff_add = true
break
end
@@ -562,6 +654,40 @@ describe('highlight', function()
delete_buffer(bufnr)
end)
+ it('skips vim fallback when vim.enabled is false', function()
+ local bufnr = create_buffer({
+ '@@ -1,1 +1,2 @@',
+ ' local x = 1',
+ '+local y = 2',
+ })
+
+ local hunk = {
+ filename = 'test.lua',
+ ft = 'abap',
+ lang = nil,
+ start_line = 1,
+ lines = { ' local x = 1', '+local y = 2' },
+ }
+
+ highlight.highlight_hunk(
+ bufnr,
+ ns,
+ hunk,
+ default_opts({ highlights = { vim = { enabled = false } } })
+ )
+
+ local extmarks = get_extmarks(bufnr)
+ local has_syntax_hl = false
+ for _, mark in ipairs(extmarks) do
+ if mark[4] and mark[4].hl_group and mark[4].hl_group ~= 'DiffsClear' then
+ has_syntax_hl = true
+ break
+ end
+ end
+ assert.is_false(has_syntax_hl)
+ delete_buffer(bufnr)
+ end)
+
it('respects vim.max_lines', function()
local lines = { '@@ -1,100 +1,101 @@' }
local hunk_lines = {}
@@ -616,7 +742,7 @@ describe('highlight', function()
local extmarks = get_extmarks(bufnr)
local has_diff_add = false
for _, mark in ipairs(extmarks) do
- if mark[4] and mark[4].line_hl_group == 'DiffsAdd' then
+ if mark[4] and mark[4].hl_group == 'DiffsAdd' then
has_diff_add = true
break
end
@@ -676,7 +802,7 @@ describe('highlight', function()
delete_buffer(bufnr)
end)
- it('uses hl_group with hl_eol for line backgrounds', function()
+ it('uses hl_group not line_hl_group for line backgrounds', function()
local bufnr = create_buffer({
'@@ -1,2 +1,1 @@',
'-local x = 1',
@@ -698,14 +824,44 @@ describe('highlight', function()
)
local extmarks = get_extmarks(bufnr)
- local found = false
for _, mark in ipairs(extmarks) do
local d = mark[4]
- if d and (d.line_hl_group == 'DiffsAdd' or d.line_hl_group == 'DiffsDelete') then
- found = true
+ if d and (d.hl_group == 'DiffsAdd' or d.hl_group == 'DiffsDelete') then
+ assert.is_true(d.hl_eol == true)
+ assert.is_nil(d.line_hl_group)
+ end
+ end
+ delete_buffer(bufnr)
+ end)
+
+ it('hl_eol background extmarks are multiline so hl_eol takes effect', function()
+ local bufnr = create_buffer({
+ '@@ -1,2 +1,1 @@',
+ '-local x = 1',
+ '+local y = 2',
+ })
+
+ local hunk = {
+ filename = 'test.lua',
+ lang = 'lua',
+ start_line = 1,
+ lines = { '-local x = 1', '+local y = 2' },
+ }
+
+ highlight.highlight_hunk(
+ bufnr,
+ ns,
+ hunk,
+ default_opts({ highlights = { background = true } })
+ )
+
+ local extmarks = get_extmarks(bufnr)
+ for _, mark in ipairs(extmarks) do
+ local d = mark[4]
+ if d and (d.hl_group == 'DiffsAdd' or d.hl_group == 'DiffsDelete') then
+ assert.is_true(d.end_row > mark[2])
end
end
- assert.is_true(found)
delete_buffer(bufnr)
end)
@@ -744,6 +900,92 @@ describe('highlight', function()
delete_buffer(bufnr)
end)
+ it('line bg priority > DiffsClear priority', function()
+ local bufnr = create_buffer({
+ '@@ -1,2 +1,1 @@',
+ '-local x = 1',
+ '+local y = 2',
+ })
+
+ local hunk = {
+ filename = 'test.lua',
+ lang = 'lua',
+ start_line = 1,
+ lines = { '-local x = 1', '+local y = 2' },
+ }
+
+ highlight.highlight_hunk(
+ bufnr,
+ ns,
+ hunk,
+ default_opts({ highlights = { background = true } })
+ )
+
+ local extmarks = get_extmarks(bufnr)
+ local clear_priority = nil
+ local line_bg_priority = nil
+ for _, mark in ipairs(extmarks) do
+ local d = mark[4]
+ if d and d.hl_group == 'DiffsClear' then
+ clear_priority = d.priority
+ end
+ if d and (d.hl_group == 'DiffsAdd' or d.hl_group == 'DiffsDelete') then
+ line_bg_priority = d.priority
+ end
+ end
+ assert.is_not_nil(clear_priority)
+ assert.is_not_nil(line_bg_priority)
+ assert.is_true(line_bg_priority > clear_priority)
+ delete_buffer(bufnr)
+ end)
+
+ it('char-level extmarks have higher priority than line bg', function()
+ vim.api.nvim_set_hl(0, 'DiffsAddText', { bg = 0x00FF00 })
+ vim.api.nvim_set_hl(0, 'DiffsDeleteText', { bg = 0xFF0000 })
+
+ local bufnr = create_buffer({
+ '@@ -1,2 +1,2 @@',
+ '-local x = 1',
+ '+local x = 2',
+ })
+
+ local hunk = {
+ filename = 'test.lua',
+ lang = 'lua',
+ start_line = 1,
+ lines = { '-local x = 1', '+local x = 2' },
+ }
+
+ highlight.highlight_hunk(
+ bufnr,
+ ns,
+ hunk,
+ default_opts({
+ highlights = {
+ background = true,
+ intra = { enabled = true, algorithm = 'default', max_lines = 500 },
+ },
+ })
+ )
+
+ local extmarks = get_extmarks(bufnr)
+ local line_bg_priority = nil
+ local char_bg_priority = nil
+ for _, mark in ipairs(extmarks) do
+ local d = mark[4]
+ if d and (d.hl_group == 'DiffsAdd' or d.hl_group == 'DiffsDelete') then
+ line_bg_priority = d.priority
+ end
+ if d and (d.hl_group == 'DiffsAddText' or d.hl_group == 'DiffsDeleteText') then
+ char_bg_priority = d.priority
+ end
+ end
+ assert.is_not_nil(line_bg_priority)
+ assert.is_not_nil(char_bg_priority)
+ assert.is_true(char_bg_priority > line_bg_priority)
+ delete_buffer(bufnr)
+ end)
+
it('creates char-level extmarks for changed characters', function()
vim.api.nvim_set_hl(0, 'DiffsAddText', { bg = 0x00FF00 })
vim.api.nvim_set_hl(0, 'DiffsDeleteText', { bg = 0xFF0000 })
@@ -787,6 +1029,38 @@ describe('highlight', function()
delete_buffer(bufnr)
end)
+ it('does not create char-level extmarks when intra disabled', function()
+ local bufnr = create_buffer({
+ '@@ -1,2 +1,2 @@',
+ '-local x = 1',
+ '+local x = 2',
+ })
+
+ local hunk = {
+ filename = 'test.lua',
+ lang = 'lua',
+ start_line = 1,
+ lines = { '-local x = 1', '+local x = 2' },
+ }
+
+ highlight.highlight_hunk(
+ bufnr,
+ ns,
+ hunk,
+ default_opts({
+ highlights = { intra = { enabled = false, algorithm = 'default', max_lines = 500 } },
+ })
+ )
+
+ local extmarks = get_extmarks(bufnr)
+ for _, mark in ipairs(extmarks) do
+ local d = mark[4]
+ assert.is_not_equal('DiffsAddText', d and d.hl_group)
+ assert.is_not_equal('DiffsDeleteText', d and d.hl_group)
+ end
+ delete_buffer(bufnr)
+ end)
+
it('does not create char-level extmarks for pure additions', function()
vim.api.nvim_set_hl(0, 'DiffsAddText', { bg = 0x00FF00 })
@@ -857,7 +1131,7 @@ describe('highlight', function()
if d then
if d.hl_group == 'DiffsClear' then
table.insert(priorities.clear, d.priority)
- elseif d.line_hl_group == 'DiffsAdd' or d.line_hl_group == 'DiffsDelete' then
+ elseif d.hl_group == 'DiffsAdd' or d.hl_group == 'DiffsDelete' then
table.insert(priorities.line_bg, d.priority)
elseif d.hl_group == 'DiffsAddText' or d.hl_group == 'DiffsDeleteText' then
table.insert(priorities.char_bg, d.priority)
@@ -883,6 +1157,142 @@ describe('highlight', function()
delete_buffer(bufnr)
end)
+ it('context padding produces no extmarks on padding lines', function()
+ local repo_root = '/tmp/diffs-test-context'
+ vim.fn.mkdir(repo_root, 'p')
+
+ local f = io.open(repo_root .. '/test.lua', 'w')
+ f:write('local M = {}\n')
+ f:write('function M.hello()\n')
+ f:write(' return "hi"\n')
+ f:write('end\n')
+ f:write('return M\n')
+ f:close()
+
+ local bufnr = create_buffer({
+ '@@ -3,1 +3,2 @@',
+ ' return "hi"',
+ '+"bye"',
+ })
+
+ local hunk = {
+ filename = 'test.lua',
+ lang = 'lua',
+ start_line = 1,
+ lines = { ' return "hi"', '+"bye"' },
+ file_old_start = 3,
+ file_old_count = 1,
+ file_new_start = 3,
+ file_new_count = 2,
+ repo_root = repo_root,
+ }
+
+ highlight.highlight_hunk(
+ bufnr,
+ ns,
+ hunk,
+ default_opts({ highlights = { context = { enabled = true, lines = 25 } } })
+ )
+
+ local extmarks = get_extmarks(bufnr)
+ for _, mark in ipairs(extmarks) do
+ local row = mark[2]
+ assert.is_true(row >= 1 and row <= 2)
+ end
+
+ delete_buffer(bufnr)
+ os.remove(repo_root .. '/test.lua')
+ vim.fn.delete(repo_root, 'rf')
+ end)
+
+ it('context disabled matches behavior without padding', function()
+ local bufnr = create_buffer({
+ '@@ -1,1 +1,2 @@',
+ ' local x = 1',
+ '+local y = 2',
+ })
+
+ local hunk = {
+ filename = 'test.lua',
+ lang = 'lua',
+ start_line = 1,
+ lines = { ' local x = 1', '+local y = 2' },
+ file_new_start = 1,
+ file_new_count = 2,
+ repo_root = '/nonexistent',
+ }
+
+ highlight.highlight_hunk(
+ bufnr,
+ ns,
+ hunk,
+ default_opts({ highlights = { context = { enabled = false, lines = 0 } } })
+ )
+
+ local extmarks = get_extmarks(bufnr)
+ assert.is_true(#extmarks > 0)
+ delete_buffer(bufnr)
+ end)
+
+ it('gracefully handles missing file for context padding', function()
+ local bufnr = create_buffer({
+ '@@ -1,1 +1,2 @@',
+ ' local x = 1',
+ '+local y = 2',
+ })
+
+ local hunk = {
+ filename = 'test.lua',
+ lang = 'lua',
+ start_line = 1,
+ lines = { ' local x = 1', '+local y = 2' },
+ file_new_start = 1,
+ file_new_count = 2,
+ repo_root = '/nonexistent/path',
+ }
+
+ assert.has_no.errors(function()
+ highlight.highlight_hunk(
+ bufnr,
+ ns,
+ hunk,
+ default_opts({ highlights = { context = { enabled = true, lines = 25 } } })
+ )
+ end)
+
+ local extmarks = get_extmarks(bufnr)
+ assert.is_true(#extmarks > 0)
+ delete_buffer(bufnr)
+ end)
+
+ it('highlights treesitter injections', function()
+ local bufnr = create_buffer({
+ '@@ -1,1 +1,2 @@',
+ ' local x = 1',
+ '+vim.cmd([[ echo 1 ]])',
+ })
+
+ local hunk = {
+ filename = 'test.lua',
+ lang = 'lua',
+ start_line = 1,
+ lines = { ' local x = 1', '+vim.cmd([[ echo 1 ]])' },
+ }
+
+ highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
+
+ local extmarks = get_extmarks(bufnr)
+ local has_vim_capture = false
+ for _, mark in ipairs(extmarks) do
+ if mark[4] and mark[4].hl_group and mark[4].hl_group:match('^@.*%.vim$') then
+ has_vim_capture = true
+ break
+ end
+ end
+ assert.is_true(has_vim_capture)
+ delete_buffer(bufnr)
+ end)
+
it('includes captures from both base and injected languages', function()
local bufnr = create_buffer({
'@@ -1,1 +1,2 @@',
@@ -917,313 +1327,6 @@ describe('highlight', function()
delete_buffer(bufnr)
end)
- it('classifies all combined diff prefix types for background', function()
- local bufnr = create_buffer({
- '@@@ -1,5 -1,5 +1,9 @@@',
- ' local M = {}',
- '++<<<<<<< HEAD',
- ' + return 1',
- '+ local greeting = "hi"',
- '++=======',
- '+ return 2',
- '++>>>>>>> feature',
- ' end',
- })
-
- local hunk = {
- filename = 'test.lua',
- lang = 'lua',
- start_line = 1,
- prefix_width = 2,
- lines = {
- ' local M = {}',
- '++<<<<<<< HEAD',
- ' + return 1',
- '+ local greeting = "hi"',
- '++=======',
- '+ return 2',
- '++>>>>>>> feature',
- ' end',
- },
- }
-
- highlight.highlight_hunk(
- bufnr,
- ns,
- hunk,
- default_opts({ highlights = { background = true } })
- )
-
- local extmarks = get_extmarks(bufnr)
- local line_bgs = {}
- for _, mark in ipairs(extmarks) do
- local d = mark[4]
- if d and (d.line_hl_group == 'DiffsAdd' or d.line_hl_group == 'DiffsDelete') then
- line_bgs[mark[2]] = d.line_hl_group
- end
- end
- assert.is_nil(line_bgs[1])
- assert.are.equal('DiffsAdd', line_bgs[2])
- assert.are.equal('DiffsAdd', line_bgs[3])
- assert.are.equal('DiffsAdd', line_bgs[4])
- assert.are.equal('DiffsAdd', line_bgs[5])
- assert.are.equal('DiffsAdd', line_bgs[6])
- assert.are.equal('DiffsAdd', line_bgs[7])
- assert.is_nil(line_bgs[8])
- delete_buffer(bufnr)
- end)
-
- it('conceals full 2-char prefix for all combined diff line types', function()
- local bufnr = create_buffer({
- '@@@ -1,3 -1,3 +1,5 @@@',
- ' local M = {}',
- '++<<<<<<< HEAD',
- ' + return 1',
- '+ local x = 2',
- ' end',
- })
-
- local hunk = {
- filename = 'test.lua',
- lang = 'lua',
- start_line = 1,
- prefix_width = 2,
- lines = {
- ' local M = {}',
- '++<<<<<<< HEAD',
- ' + return 1',
- '+ local x = 2',
- ' end',
- },
- }
-
- highlight.highlight_hunk(bufnr, ns, hunk, default_opts({ hide_prefix = true }))
-
- local extmarks = get_extmarks(bufnr)
- local overlays = {}
- for _, mark in ipairs(extmarks) do
- if mark[4] and mark[4].virt_text_pos == 'overlay' then
- overlays[mark[2]] = mark[4].virt_text[1][1]
- end
- end
- assert.are.equal(5, vim.tbl_count(overlays))
- for _, text in pairs(overlays) do
- assert.are.equal(' ', text)
- end
- delete_buffer(bufnr)
- end)
-
- it('places treesitter captures at col_offset 2 for combined diffs', function()
- local bufnr = create_buffer({
- '@@@ -1,2 -1,2 +1,2 @@@',
- ' local x = 1',
- ' +local y = 2',
- })
-
- local hunk = {
- filename = 'test.lua',
- lang = 'lua',
- start_line = 1,
- prefix_width = 2,
- lines = { ' local x = 1', ' +local y = 2' },
- }
-
- highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
-
- local extmarks = get_extmarks(bufnr)
- local ts_marks = {}
- for _, mark in ipairs(extmarks) do
- if mark[4] and mark[4].hl_group and mark[4].hl_group:match('^@.*%.lua$') then
- table.insert(ts_marks, mark)
- end
- end
- assert.is_true(#ts_marks > 0)
- for _, mark in ipairs(ts_marks) do
- assert.is_true(mark[3] >= 2)
- end
- delete_buffer(bufnr)
- end)
-
- it('applies DiffsClear starting at col 2 for combined diffs', function()
- local bufnr = create_buffer({
- '@@@ -1,1 -1,1 +1,2 @@@',
- ' local x = 1',
- ' +local y = 2',
- })
-
- local hunk = {
- filename = 'test.lua',
- lang = 'lua',
- start_line = 1,
- prefix_width = 2,
- lines = { ' local x = 1', ' +local y = 2' },
- }
-
- highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
-
- local extmarks = get_extmarks(bufnr)
- local content_clear_count = 0
- for _, mark in ipairs(extmarks) do
- if mark[4] and mark[4].hl_group == 'DiffsClear' then
- assert.is_true(mark[3] == 0 or mark[3] == 2, 'DiffsClear at unexpected col ' .. mark[3])
- if mark[3] == 2 then
- content_clear_count = content_clear_count + 1
- end
- end
- end
- assert.are.equal(2, content_clear_count)
- delete_buffer(bufnr)
- end)
-
- it('skips intra-line diffing for combined diffs', function()
- vim.api.nvim_set_hl(0, 'DiffsAddText', { bg = 0x00FF00 })
- vim.api.nvim_set_hl(0, 'DiffsDeleteText', { bg = 0xFF0000 })
-
- local bufnr = create_buffer({
- '@@@ -1,2 -1,2 +1,3 @@@',
- ' local x = 1',
- ' +local y = 2',
- '+ local y = 3',
- })
-
- local hunk = {
- filename = 'test.lua',
- lang = 'lua',
- start_line = 1,
- prefix_width = 2,
- lines = { ' local x = 1', ' +local y = 2', '+ local y = 3' },
- }
-
- highlight.highlight_hunk(
- bufnr,
- ns,
- hunk,
- default_opts({
- highlights = { intra = { enabled = true, algorithm = 'default', max_lines = 500 } },
- })
- )
-
- local extmarks = get_extmarks(bufnr)
- for _, mark in ipairs(extmarks) do
- local d = mark[4]
- assert.is_not_equal('DiffsAddText', d and d.hl_group)
- assert.is_not_equal('DiffsDeleteText', d and d.hl_group)
- end
- delete_buffer(bufnr)
- end)
-
- it('applies DiffsConflictMarker text on markers with DiffsAdd bg', function()
- local bufnr = create_buffer({
- '@@@ -1,5 -1,5 +1,9 @@@',
- ' local M = {}',
- '++<<<<<<< HEAD',
- '+ local x = 1',
- '++||||||| base',
- '++=======',
- ' +local y = 2',
- '++>>>>>>> feature',
- ' return M',
- })
-
- local hunk = {
- filename = 'test.lua',
- lang = 'lua',
- start_line = 1,
- prefix_width = 2,
- lines = {
- ' local M = {}',
- '++<<<<<<< HEAD',
- '+ local x = 1',
- '++||||||| base',
- '++=======',
- ' +local y = 2',
- '++>>>>>>> feature',
- ' return M',
- },
- }
-
- highlight.highlight_hunk(
- bufnr,
- ns,
- hunk,
- default_opts({ highlights = { background = true, gutter = true } })
- )
-
- local extmarks = get_extmarks(bufnr)
- local line_bgs = {}
- local gutter_hls = {}
- local marker_text = {}
- for _, mark in ipairs(extmarks) do
- local d = mark[4]
- if d and (d.line_hl_group == 'DiffsAdd' or d.line_hl_group == 'DiffsDelete') then
- line_bgs[mark[2]] = d.line_hl_group
- end
- if d and d.number_hl_group then
- gutter_hls[mark[2]] = d.number_hl_group
- end
- if d and d.hl_group == 'DiffsConflictMarker' then
- marker_text[mark[2]] = true
- end
- end
-
- assert.is_nil(line_bgs[1])
- assert.are.equal('DiffsAdd', line_bgs[2])
- assert.are.equal('DiffsAdd', line_bgs[3])
- assert.are.equal('DiffsAdd', line_bgs[4])
- assert.are.equal('DiffsAdd', line_bgs[5])
- assert.are.equal('DiffsAdd', line_bgs[6])
- assert.are.equal('DiffsAdd', line_bgs[7])
- assert.is_nil(line_bgs[8])
-
- assert.is_nil(gutter_hls[1])
- assert.are.equal('DiffsAddNr', gutter_hls[2])
- assert.are.equal('DiffsAddNr', gutter_hls[3])
- assert.are.equal('DiffsAddNr', gutter_hls[4])
- assert.are.equal('DiffsAddNr', gutter_hls[5])
- assert.are.equal('DiffsAddNr', gutter_hls[6])
- assert.are.equal('DiffsAddNr', gutter_hls[7])
- assert.is_nil(gutter_hls[8])
-
- assert.is_true(marker_text[2] ~= nil)
- assert.is_nil(marker_text[3])
- assert.is_true(marker_text[4] ~= nil)
- assert.is_true(marker_text[5] ~= nil)
- assert.is_nil(marker_text[6])
- assert.is_true(marker_text[7] ~= nil)
- delete_buffer(bufnr)
- end)
-
- it('does not apply DiffsConflictMarker in unified diffs', function()
- local bufnr = create_buffer({
- '@@ -1,1 +1,4 @@',
- ' local M = {}',
- '+<<<<<<< HEAD',
- '+local x = 1',
- '+=======',
- })
-
- local hunk = {
- filename = 'test.lua',
- lang = 'lua',
- start_line = 1,
- lines = { ' local M = {}', '+<<<<<<< HEAD', '+local x = 1', '+=======' },
- }
-
- highlight.highlight_hunk(
- bufnr,
- ns,
- hunk,
- default_opts({ highlights = { background = true } })
- )
-
- local extmarks = get_extmarks(bufnr)
- for _, mark in ipairs(extmarks) do
- local d = mark[4]
- assert.is_not_equal('DiffsConflictMarker', d and d.hl_group)
- end
- delete_buffer(bufnr)
- end)
-
it('filters @spell and @nospell captures from injections', function()
local bufnr = create_buffer({
'@@ -1,1 +1,2 @@',
@@ -1249,123 +1352,6 @@ describe('highlight', function()
end
delete_buffer(bufnr)
end)
-
- it('two-pass rendering produces no duplicate extmarks', function()
- vim.api.nvim_set_hl(0, 'DiffsAddText', { bg = 0x00FF00 })
- vim.api.nvim_set_hl(0, 'DiffsDeleteText', { bg = 0xFF0000 })
- vim.api.nvim_set_hl(0, 'DiffsAddNr', { fg = 0x80c080, bg = 0x2e4a3a })
- vim.api.nvim_set_hl(0, 'DiffsDeleteNr', { fg = 0xc08080, bg = 0x4a2e3a })
-
- local bufnr = create_buffer({
- '@@ -1,2 +1,2 @@',
- '-local x = 1',
- '+local x = 2',
- })
-
- local hunk = {
- filename = 'test.lua',
- lang = 'lua',
- start_line = 1,
- lines = { '-local x = 1', '+local x = 2' },
- }
-
- local fast = default_opts({
- highlights = {
- treesitter = { enabled = false },
- background = true,
- gutter = true,
- intra = { enabled = true, algorithm = 'default', max_lines = 500 },
- },
- })
-
- local syntax = default_opts({
- highlights = {
- treesitter = { enabled = true },
- background = true,
- gutter = true,
- intra = { enabled = true, algorithm = 'default', max_lines = 500 },
- },
- })
- syntax.syntax_only = true
-
- highlight.highlight_hunk(bufnr, ns, hunk, fast)
- highlight.highlight_hunk(bufnr, ns, hunk, syntax)
-
- local extmarks = get_extmarks(bufnr)
- for row = 1, 2 do
- local line_hl_count = 0
- local number_hl_count = 0
- local intra_count = 0
- for _, mark in ipairs(extmarks) do
- if mark[2] == row then
- local d = mark[4]
- if d.line_hl_group then
- line_hl_count = line_hl_count + 1
- end
- if d.number_hl_group then
- number_hl_count = number_hl_count + 1
- end
- if d.hl_group == 'DiffsAddText' or d.hl_group == 'DiffsDeleteText' then
- intra_count = intra_count + 1
- end
- end
- end
- assert.are.equal(1, line_hl_count, 'row ' .. row .. ' has duplicate line_hl_group')
- assert.are.equal(1, number_hl_count, 'row ' .. row .. ' has duplicate number_hl_group')
- assert.is_true(intra_count <= 1, 'row ' .. row .. ' has duplicate intra extmarks')
- end
- delete_buffer(bufnr)
- end)
-
- it('syntax_only pass adds treesitter without duplicating backgrounds', function()
- local bufnr = create_buffer({
- '@@ -1,2 +1,3 @@',
- ' local x = 1',
- '+local y = 2',
- ' return x',
- })
-
- local hunk = {
- filename = 'test.lua',
- lang = 'lua',
- start_line = 1,
- lines = { ' local x = 1', '+local y = 2', ' return x' },
- }
-
- local fast = default_opts({
- highlights = {
- treesitter = { enabled = false },
- background = true,
- },
- })
-
- local syntax = default_opts({
- highlights = {
- treesitter = { enabled = true },
- background = true,
- },
- })
- syntax.syntax_only = true
-
- highlight.highlight_hunk(bufnr, ns, hunk, fast)
- highlight.highlight_hunk(bufnr, ns, hunk, syntax)
-
- local extmarks = get_extmarks(bufnr)
- local has_ts = false
- local line_hl_count = 0
- for _, mark in ipairs(extmarks) do
- local d = mark[4]
- if d and d.hl_group and d.hl_group:match('^@.*%.lua$') then
- has_ts = true
- end
- if d and d.line_hl_group then
- line_hl_count = line_hl_count + 1
- end
- end
- assert.is_true(has_ts)
- assert.are.equal(1, line_hl_count)
- delete_buffer(bufnr)
- end)
end)
describe('diff header highlighting', function()
@@ -1400,7 +1386,6 @@ describe('highlight', function()
context = { enabled = false, lines = 0 },
treesitter = { enabled = true, max_lines = 500 },
vim = { enabled = false, max_lines = 200 },
- priorities = { clear = 198, syntax = 199, line_bg = 200, char_bg = 201 },
},
}
end
@@ -1484,7 +1469,7 @@ describe('highlight', function()
delete_buffer(bufnr)
end)
- it('does not apply DiffsClear to header lines for non-quoted diffs', function()
+ it('does not apply header highlights when treesitter disabled', function()
local bufnr = create_buffer({
'diff --git a/parser.lua b/parser.lua',
'index 3e8afa0..018159c 100644',
@@ -1509,439 +1494,19 @@ describe('highlight', function()
},
}
- highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
+ local opts = default_opts()
+ opts.highlights.treesitter.enabled = false
+
+ highlight.highlight_hunk(bufnr, ns, hunk, opts)
local extmarks = get_extmarks(bufnr)
+ local header_extmarks = 0
for _, mark in ipairs(extmarks) do
- local d = mark[4]
- if d and d.hl_group == 'DiffsClear' and mark[3] == 0 and mark[2] < 4 then
- error('unexpected DiffsClear on header row ' .. mark[2] .. ' for non-quoted diff')
+ if mark[2] < 4 and mark[4] and mark[4].hl_group and mark[4].hl_group:match('^@') then
+ header_extmarks = header_extmarks + 1
end
end
- delete_buffer(bufnr)
- end)
-
- it('preserves diff grammar treesitter on headers for non-quoted diffs', function()
- local bufnr = create_buffer({
- 'diff --git a/parser.lua b/parser.lua',
- '--- a/parser.lua',
- '+++ b/parser.lua',
- '@@ -1,2 +1,3 @@',
- ' local M = {}',
- '+local x = 1',
- })
-
- local hunk = {
- filename = 'parser.lua',
- lang = 'lua',
- start_line = 4,
- lines = { ' local M = {}', '+local x = 1' },
- header_start_line = 1,
- header_lines = {
- 'diff --git a/parser.lua b/parser.lua',
- '--- a/parser.lua',
- '+++ b/parser.lua',
- },
- }
-
- highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
-
- local extmarks = get_extmarks(bufnr)
- local header_ts_count = 0
- for _, mark in ipairs(extmarks) do
- local d = mark[4]
- if mark[2] < 3 and d and d.hl_group and d.hl_group:match('^@.*%.diff$') then
- header_ts_count = header_ts_count + 1
- end
- end
- assert.is_true(header_ts_count > 0, 'expected diff grammar treesitter on header lines')
- delete_buffer(bufnr)
- end)
-
- it('applies syntax extmarks to combined diff body lines', function()
- local bufnr = create_buffer({
- '@@@ -1,2 -1,2 +1,3 @@@',
- ' local M = {}',
- '+ local x = 1',
- ' -local y = 2',
- })
-
- local hunk = {
- filename = 'test.lua',
- lang = 'lua',
- prefix_width = 2,
- start_line = 1,
- lines = { ' local M = {}', '+ local x = 1', ' -local y = 2' },
- }
-
- highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
-
- local extmarks = get_extmarks(bufnr)
- local syntax_on_body = 0
- for _, mark in ipairs(extmarks) do
- local d = mark[4]
- if mark[2] >= 1 and d and d.hl_group and d.hl_group:match('^@.*%.lua$') then
- syntax_on_body = syntax_on_body + 1
- end
- end
- assert.is_true(syntax_on_body > 0, 'expected lua treesitter syntax on combined diff body')
- delete_buffer(bufnr)
- end)
-
- it('applies DiffsClear and per-char diff fg to combined diff body prefixes', function()
- local bufnr = create_buffer({
- '@@@',
- ' unchanged',
- '+ added',
- ' -removed',
- '++both',
- })
-
- local hunk = {
- filename = 'test.lua',
- lang = 'lua',
- prefix_width = 2,
- start_line = 1,
- lines = { ' unchanged', '+ added', ' -removed', '++both' },
- }
-
- highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
-
- local extmarks = get_extmarks(bufnr)
- local prefix_clears = {}
- local plus_marks = {}
- local minus_marks = {}
- for _, mark in ipairs(extmarks) do
- local d = mark[4]
- if mark[2] >= 1 and d then
- if d.hl_group == 'DiffsClear' and mark[3] == 0 and d.end_col == 2 then
- prefix_clears[mark[2]] = true
- end
- if d.hl_group == '@diff.plus' and d.priority == 199 then
- if not plus_marks[mark[2]] then
- plus_marks[mark[2]] = {}
- end
- table.insert(plus_marks[mark[2]], mark[3])
- end
- if d.hl_group == '@diff.minus' and d.priority == 199 then
- if not minus_marks[mark[2]] then
- minus_marks[mark[2]] = {}
- end
- table.insert(minus_marks[mark[2]], mark[3])
- end
- end
- end
-
- assert.is_true(prefix_clears[1] ~= nil, 'DiffsClear on context prefix')
- assert.is_true(prefix_clears[2] ~= nil, 'DiffsClear on add prefix')
- assert.is_true(prefix_clears[3] ~= nil, 'DiffsClear on del prefix')
- assert.is_true(prefix_clears[4] ~= nil, 'DiffsClear on both-add prefix')
-
- assert.is_true(plus_marks[2] ~= nil, '@diff.plus on + in "+ added"')
- assert.are.equal(0, plus_marks[2][1])
-
- assert.is_true(minus_marks[3] ~= nil, '@diff.minus on - in " -removed"')
- assert.are.equal(1, minus_marks[3][1])
-
- assert.is_true(plus_marks[4] ~= nil, '@diff.plus on ++ in "++both"')
- assert.are.equal(2, #plus_marks[4])
-
- assert.is_nil(plus_marks[1], 'no @diff.plus on context " unchanged"')
- assert.is_nil(minus_marks[1], 'no @diff.minus on context " unchanged"')
- delete_buffer(bufnr)
- end)
-
- it('applies DiffsClear to headers for combined diffs', function()
- local bufnr = create_buffer({
- 'diff --combined lua/merge/target.lua',
- 'index abc1234,def5678..ghi9012',
- '--- a/lua/merge/target.lua',
- '+++ b/lua/merge/target.lua',
- '@@@ -1,2 -1,2 +1,3 @@@',
- ' local M = {}',
- '+ local x = 1',
- })
-
- local hunk = {
- filename = 'lua/merge/target.lua',
- lang = 'lua',
- prefix_width = 2,
- start_line = 5,
- lines = { ' local M = {}', '+ local x = 1' },
- header_start_line = 1,
- header_lines = {
- 'diff --combined lua/merge/target.lua',
- 'index abc1234,def5678..ghi9012',
- '--- a/lua/merge/target.lua',
- '+++ b/lua/merge/target.lua',
- },
- }
-
- highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
-
- local extmarks = get_extmarks(bufnr)
- local clear_lines = {}
- for _, mark in ipairs(extmarks) do
- local d = mark[4]
- if d and d.hl_group == 'DiffsClear' and mark[3] == 0 and mark[2] < 4 then
- clear_lines[mark[2]] = true
- end
- end
- assert.is_true(clear_lines[0] ~= nil, 'DiffsClear on diff --combined line')
- assert.is_true(clear_lines[1] ~= nil, 'DiffsClear on index line')
- assert.is_true(clear_lines[2] ~= nil, 'DiffsClear on --- line')
- assert.is_true(clear_lines[3] ~= nil, 'DiffsClear on +++ line')
- delete_buffer(bufnr)
- end)
-
- it('applies @attribute.diff at syntax priority to @@@ line for combined diffs', function()
- local bufnr = create_buffer({
- '@@@ -1,2 -1,2 +1,3 @@@',
- ' local M = {}',
- '+ local x = 1',
- })
-
- local hunk = {
- filename = 'test.lua',
- lang = 'lua',
- prefix_width = 2,
- start_line = 1,
- lines = { ' local M = {}', '+ local x = 1' },
- }
-
- highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
-
- local extmarks = get_extmarks(bufnr)
- local has_attr = false
- for _, mark in ipairs(extmarks) do
- local d = mark[4]
- if mark[2] == 0 and d and d.hl_group == '@attribute.diff' and (d.priority or 0) >= 199 then
- has_attr = true
- end
- end
- assert.is_true(has_attr, '@attribute.diff at p>=199 on @@@ line')
- delete_buffer(bufnr)
- end)
-
- it('applies DiffsClear to @@@ line for combined diffs', function()
- local bufnr = create_buffer({
- '@@@ -1,2 -1,2 +1,3 @@@',
- ' local M = {}',
- '+ local x = 1',
- })
-
- local hunk = {
- filename = 'test.lua',
- lang = 'lua',
- prefix_width = 2,
- start_line = 1,
- lines = { ' local M = {}', '+ local x = 1' },
- }
-
- highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
-
- local extmarks = get_extmarks(bufnr)
- local has_at_clear = false
- for _, mark in ipairs(extmarks) do
- local d = mark[4]
- if mark[2] == 0 and d and d.hl_group == 'DiffsClear' and mark[3] == 0 then
- has_at_clear = true
- end
- end
- assert.is_true(has_at_clear, 'DiffsClear on @@@ line')
- delete_buffer(bufnr)
- end)
-
- it('applies header diff grammar at syntax priority for combined diffs', function()
- local bufnr = create_buffer({
- 'diff --combined lua/merge/target.lua',
- 'index abc1234,def5678..ghi9012',
- '--- a/lua/merge/target.lua',
- '+++ b/lua/merge/target.lua',
- '@@@ -1,2 -1,2 +1,3 @@@',
- ' local M = {}',
- '+ local x = 1',
- })
-
- local hunk = {
- filename = 'lua/merge/target.lua',
- lang = 'lua',
- prefix_width = 2,
- start_line = 5,
- lines = { ' local M = {}', '+ local x = 1' },
- header_start_line = 1,
- header_lines = {
- 'diff --combined lua/merge/target.lua',
- 'index abc1234,def5678..ghi9012',
- '--- a/lua/merge/target.lua',
- '+++ b/lua/merge/target.lua',
- },
- }
-
- highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
-
- local extmarks = get_extmarks(bufnr)
- local high_prio_diff = {}
- for _, mark in ipairs(extmarks) do
- local d = mark[4]
- if
- mark[2] < 4
- and d
- and d.hl_group
- and d.hl_group:match('^@.*%.diff$')
- and (d.priority or 0) >= 199
- then
- high_prio_diff[mark[2]] = true
- end
- end
- assert.is_true(high_prio_diff[2] ~= nil, 'diff grammar at p>=199 on --- line')
- assert.is_true(high_prio_diff[3] ~= nil, 'diff grammar at p>=199 on +++ line')
- delete_buffer(bufnr)
- end)
-
- it('@diff.minus wins over @punctuation.special on combined diff headers', function()
- local bufnr = create_buffer({
- 'diff --combined lua/merge/target.lua',
- 'index abc1234,def5678..ghi9012',
- '--- a/lua/merge/target.lua',
- '+++ b/lua/merge/target.lua',
- '@@@ -1,2 -1,2 +1,3 @@@',
- ' local M = {}',
- '+ local x = 1',
- })
-
- local hunk = {
- filename = 'lua/merge/target.lua',
- lang = 'lua',
- prefix_width = 2,
- start_line = 5,
- lines = { ' local M = {}', '+ local x = 1' },
- header_start_line = 1,
- header_lines = {
- 'diff --combined lua/merge/target.lua',
- 'index abc1234,def5678..ghi9012',
- '--- a/lua/merge/target.lua',
- '+++ b/lua/merge/target.lua',
- },
- }
-
- highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
-
- local extmarks = get_extmarks(bufnr)
- local minus_prio, punct_prio_minus = 0, 0
- local plus_prio, punct_prio_plus = 0, 0
- for _, mark in ipairs(extmarks) do
- local d = mark[4]
- if d and d.hl_group then
- if mark[2] == 2 then
- if d.hl_group == '@diff.minus.diff' then
- minus_prio = math.max(minus_prio, d.priority or 0)
- elseif d.hl_group == '@punctuation.special.diff' then
- punct_prio_minus = math.max(punct_prio_minus, d.priority or 0)
- end
- elseif mark[2] == 3 then
- if d.hl_group == '@diff.plus.diff' then
- plus_prio = math.max(plus_prio, d.priority or 0)
- elseif d.hl_group == '@punctuation.special.diff' then
- punct_prio_plus = math.max(punct_prio_plus, d.priority or 0)
- end
- end
- end
- end
- assert.is_true(
- minus_prio > punct_prio_minus,
- '@diff.minus.diff should beat @punctuation.special.diff on --- line'
- )
- assert.is_true(
- plus_prio > punct_prio_plus,
- '@diff.plus.diff should beat @punctuation.special.diff on +++ line'
- )
- delete_buffer(bufnr)
- end)
-
- it('applies @keyword.diff on index word for combined diffs', function()
- local bufnr = create_buffer({
- 'diff --combined lua/merge/target.lua',
- 'index abc1234,def5678..ghi9012',
- '--- a/lua/merge/target.lua',
- '+++ b/lua/merge/target.lua',
- '@@@ -1,2 -1,2 +1,3 @@@',
- ' local M = {}',
- '+ local x = 1',
- })
-
- local hunk = {
- filename = 'lua/merge/target.lua',
- lang = 'lua',
- prefix_width = 2,
- start_line = 5,
- lines = { ' local M = {}', '+ local x = 1' },
- header_start_line = 1,
- header_lines = {
- 'diff --combined lua/merge/target.lua',
- 'index abc1234,def5678..ghi9012',
- '--- a/lua/merge/target.lua',
- '+++ b/lua/merge/target.lua',
- },
- }
-
- highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
-
- local extmarks = get_extmarks(bufnr)
- local has_keyword = false
- for _, mark in ipairs(extmarks) do
- local d = mark[4]
- if
- mark[2] == 1
- and d
- and d.hl_group == '@keyword.diff'
- and mark[3] == 0
- and (d.end_col or 0) == 5
- then
- has_keyword = true
- end
- end
- assert.is_true(has_keyword, '@keyword.diff at row 1, cols 0-5')
- delete_buffer(bufnr)
- end)
-
- it('applies @constant.diff on result hash for combined diffs', function()
- local bufnr = create_buffer({
- 'diff --combined lua/merge/target.lua',
- 'index abc1234,def5678..ghi9012',
- '--- a/lua/merge/target.lua',
- '+++ b/lua/merge/target.lua',
- '@@@ -1,2 -1,2 +1,3 @@@',
- ' local M = {}',
- '+ local x = 1',
- })
-
- local hunk = {
- filename = 'lua/merge/target.lua',
- lang = 'lua',
- prefix_width = 2,
- start_line = 5,
- lines = { ' local M = {}', '+ local x = 1' },
- header_start_line = 1,
- header_lines = {
- 'diff --combined lua/merge/target.lua',
- 'index abc1234,def5678..ghi9012',
- '--- a/lua/merge/target.lua',
- '+++ b/lua/merge/target.lua',
- },
- }
-
- highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
-
- local extmarks = get_extmarks(bufnr)
- local has_constant = false
- for _, mark in ipairs(extmarks) do
- local d = mark[4]
- if mark[2] == 1 and d and d.hl_group == '@constant.diff' and (d.priority or 0) >= 199 then
- has_constant = true
- end
- end
- assert.is_true(has_constant, '@constant.diff on result hash')
+ assert.are.equal(0, header_extmarks)
delete_buffer(bufnr)
end)
end)
@@ -1978,11 +1543,40 @@ describe('highlight', function()
context = { enabled = false, lines = 0 },
treesitter = { enabled = true, max_lines = 500 },
vim = { enabled = false, max_lines = 200 },
- priorities = { clear = 198, syntax = 199, line_bg = 200, char_bg = 201 },
},
}
end
+ it('uses priority 199 for code languages', function()
+ local bufnr = create_buffer({
+ '@@ -1,1 +1,2 @@',
+ ' local x = 1',
+ '+local y = 2',
+ })
+
+ local hunk = {
+ filename = 'test.lua',
+ lang = 'lua',
+ start_line = 1,
+ lines = { ' local x = 1', '+local y = 2' },
+ }
+
+ highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
+
+ local extmarks = get_extmarks(bufnr)
+ local has_priority_199 = false
+ for _, mark in ipairs(extmarks) do
+ if mark[4] and mark[4].hl_group and mark[4].hl_group:match('^@.*%.lua$') then
+ if mark[4].priority == 199 then
+ has_priority_199 = true
+ break
+ end
+ end
+ end
+ assert.is_true(has_priority_199)
+ delete_buffer(bufnr)
+ end)
+
it('uses treesitter priority for diff language', function()
local bufnr = create_buffer({
'diff --git a/test.lua b/test.lua',
diff --git a/spec/init_spec.lua b/spec/init_spec.lua
index 2e1952e..1bf71d1 100644
--- a/spec/init_spec.lua
+++ b/spec/init_spec.lua
@@ -24,6 +24,7 @@ describe('diffs', function()
it('accepts full config', function()
vim.g.diffs = {
debug = true,
+ debounce_ms = 100,
hide_prefix = false,
highlights = {
background = true,
@@ -45,7 +46,7 @@ describe('diffs', function()
it('accepts partial config', function()
vim.g.diffs = {
- hide_prefix = true,
+ debounce_ms = 25,
}
assert.has_no.errors(function()
diffs.attach()
@@ -151,247 +152,6 @@ describe('diffs', function()
end)
end)
- describe('find_visible_hunks', function()
- local find_visible_hunks = diffs._test.find_visible_hunks
-
- local function make_hunk(start_row, end_row, opts)
- local lines = {}
- for i = 1, end_row - start_row + 1 do
- lines[i] = 'line' .. i
- end
- local h = { start_line = start_row + 1, lines = lines }
- if opts and opts.header_start_line then
- h.header_start_line = opts.header_start_line
- end
- return h
- end
-
- it('returns (0, 0) for empty hunk list', function()
- local first, last = find_visible_hunks({}, 0, 50)
- assert.are.equal(0, first)
- assert.are.equal(0, last)
- end)
-
- it('finds single hunk fully inside viewport', function()
- local h = make_hunk(5, 10)
- local first, last = find_visible_hunks({ h }, 0, 50)
- assert.are.equal(1, first)
- assert.are.equal(1, last)
- end)
-
- it('returns (0, 0) for single hunk fully above viewport', function()
- local h = make_hunk(5, 10)
- local first, last = find_visible_hunks({ h }, 20, 50)
- assert.are.equal(0, first)
- assert.are.equal(0, last)
- end)
-
- it('returns (0, 0) for single hunk fully below viewport', function()
- local h = make_hunk(50, 60)
- local first, last = find_visible_hunks({ h }, 0, 20)
- assert.are.equal(0, first)
- assert.are.equal(0, last)
- end)
-
- it('finds single hunk partially visible at top edge', function()
- local h = make_hunk(5, 15)
- local first, last = find_visible_hunks({ h }, 10, 30)
- assert.are.equal(1, first)
- assert.are.equal(1, last)
- end)
-
- it('finds single hunk partially visible at bottom edge', function()
- local h = make_hunk(25, 35)
- local first, last = find_visible_hunks({ h }, 10, 30)
- assert.are.equal(1, first)
- assert.are.equal(1, last)
- end)
-
- it('finds subset of visible hunks', function()
- local h1 = make_hunk(5, 10)
- local h2 = make_hunk(25, 30)
- local h3 = make_hunk(55, 60)
- local first, last = find_visible_hunks({ h1, h2, h3 }, 20, 40)
- assert.are.equal(2, first)
- assert.are.equal(2, last)
- end)
-
- it('finds all hunks when all are visible', function()
- local h1 = make_hunk(5, 10)
- local h2 = make_hunk(15, 20)
- local h3 = make_hunk(25, 30)
- local first, last = find_visible_hunks({ h1, h2, h3 }, 0, 50)
- assert.are.equal(1, first)
- assert.are.equal(3, last)
- end)
-
- it('returns (0, 0) when no hunks are visible', function()
- local h1 = make_hunk(5, 10)
- local h2 = make_hunk(15, 20)
- local first, last = find_visible_hunks({ h1, h2 }, 30, 50)
- assert.are.equal(0, first)
- assert.are.equal(0, last)
- end)
-
- it('uses header_start_line for top boundary', function()
- local h = make_hunk(5, 10, { header_start_line = 4 })
- local first, last = find_visible_hunks({ h }, 0, 50)
- assert.are.equal(1, first)
- assert.are.equal(1, last)
- end)
-
- it('finds both adjacent hunks at viewport edge', function()
- local h1 = make_hunk(10, 20)
- local h2 = make_hunk(20, 30)
- local first, last = find_visible_hunks({ h1, h2 }, 15, 25)
- assert.are.equal(1, first)
- assert.are.equal(2, last)
- end)
- end)
-
- describe('hunk_cache', function()
- local function create_buffer(lines)
- local bufnr = vim.api.nvim_create_buf(false, true)
- vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines or {})
- return bufnr
- end
-
- local function delete_buffer(bufnr)
- if vim.api.nvim_buf_is_valid(bufnr) then
- vim.api.nvim_buf_delete(bufnr, { force = true })
- end
- end
-
- it('creates entry on attach', function()
- local bufnr = create_buffer({
- '@@ -1,1 +1,2 @@',
- ' local x = 1',
- '+local y = 2',
- })
- diffs.attach(bufnr)
- local entry = diffs._test.hunk_cache[bufnr]
- assert.is_not_nil(entry)
- assert.is_table(entry.hunks)
- assert.is_number(entry.tick)
- assert.is_true(entry.tick >= 0)
- delete_buffer(bufnr)
- end)
-
- it('is idempotent on repeated attach', function()
- local bufnr = create_buffer({
- '@@ -1,1 +1,2 @@',
- ' local x = 1',
- '+local y = 2',
- })
- diffs.attach(bufnr)
- local entry1 = diffs._test.hunk_cache[bufnr]
- local tick1 = entry1.tick
- local hunks1 = entry1.hunks
- diffs._test.ensure_cache(bufnr)
- local entry2 = diffs._test.hunk_cache[bufnr]
- assert.are.equal(tick1, entry2.tick)
- assert.are.equal(hunks1, entry2.hunks)
- delete_buffer(bufnr)
- end)
-
- it('marks stale on invalidate', function()
- local bufnr = create_buffer({})
- diffs.attach(bufnr)
- diffs._test.invalidate_cache(bufnr)
- local entry = diffs._test.hunk_cache[bufnr]
- assert.are.equal(-1, entry.tick)
- assert.is_true(entry.pending_clear)
- delete_buffer(bufnr)
- end)
-
- it('evicts on buffer wipeout', function()
- local bufnr = create_buffer({})
- diffs.attach(bufnr)
- assert.is_not_nil(diffs._test.hunk_cache[bufnr])
- vim.api.nvim_buf_delete(bufnr, { force = true })
- assert.is_nil(diffs._test.hunk_cache[bufnr])
- end)
-
- it('detects content change via tick', function()
- local bufnr = create_buffer({
- '@@ -1,1 +1,2 @@',
- ' local x = 1',
- '+local y = 2',
- })
- diffs.attach(bufnr)
- local tick_before = diffs._test.hunk_cache[bufnr].tick
- vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, { '+local z = 3' })
- diffs._test.ensure_cache(bufnr)
- local tick_after = diffs._test.hunk_cache[bufnr].tick
- assert.is_true(tick_after > tick_before)
- delete_buffer(bufnr)
- end)
- end)
-
- describe('compute_filetypes', function()
- local compute = diffs.compute_filetypes
-
- it('returns core filetypes with empty config', function()
- local fts = compute({})
- assert.are.same({ 'git', 'gitcommit' }, fts)
- end)
-
- it('includes fugitive when fugitive = true', function()
- local fts = compute({ fugitive = true })
- assert.is_true(vim.tbl_contains(fts, 'fugitive'))
- end)
-
- it('includes fugitive when fugitive is a table', function()
- local fts = compute({ fugitive = { horizontal = 'dd' } })
- assert.is_true(vim.tbl_contains(fts, 'fugitive'))
- end)
-
- it('excludes fugitive when fugitive = false', function()
- local fts = compute({ fugitive = false })
- assert.is_false(vim.tbl_contains(fts, 'fugitive'))
- end)
-
- it('excludes fugitive when fugitive is nil', function()
- local fts = compute({})
- assert.is_false(vim.tbl_contains(fts, 'fugitive'))
- end)
-
- it('includes neogit filetypes when neogit = true', function()
- local fts = compute({ neogit = true })
- assert.is_true(vim.tbl_contains(fts, 'NeogitStatus'))
- assert.is_true(vim.tbl_contains(fts, 'NeogitCommitView'))
- assert.is_true(vim.tbl_contains(fts, 'NeogitDiffView'))
- end)
-
- it('includes neogit filetypes when neogit is a table', function()
- local fts = compute({ neogit = {} })
- assert.is_true(vim.tbl_contains(fts, 'NeogitStatus'))
- end)
-
- it('excludes neogit when neogit = false', function()
- local fts = compute({ neogit = false })
- assert.is_false(vim.tbl_contains(fts, 'NeogitStatus'))
- end)
-
- it('excludes neogit when neogit is nil', function()
- local fts = compute({})
- assert.is_false(vim.tbl_contains(fts, 'NeogitStatus'))
- end)
-
- it('includes extra_filetypes', function()
- local fts = compute({ extra_filetypes = { 'diff' } })
- assert.is_true(vim.tbl_contains(fts, 'diff'))
- end)
-
- it('combines fugitive, neogit, and extra_filetypes', function()
- local fts = compute({ fugitive = true, neogit = true, extra_filetypes = { 'diff' } })
- assert.is_true(vim.tbl_contains(fts, 'git'))
- assert.is_true(vim.tbl_contains(fts, 'fugitive'))
- assert.is_true(vim.tbl_contains(fts, 'NeogitStatus'))
- assert.is_true(vim.tbl_contains(fts, 'diff'))
- end)
- end)
-
describe('diff mode', function()
local function create_diff_window()
vim.cmd('new')
diff --git a/spec/integration_spec.lua b/spec/integration_spec.lua
deleted file mode 100644
index 23e870b..0000000
--- a/spec/integration_spec.lua
+++ /dev/null
@@ -1,434 +0,0 @@
-require('spec.helpers')
-local diffs = require('diffs')
-local highlight = require('diffs.highlight')
-
-local function setup_highlight_groups()
- local normal = vim.api.nvim_get_hl(0, { name = 'Normal' })
- local diff_add = vim.api.nvim_get_hl(0, { name = 'DiffAdd' })
- local diff_delete = vim.api.nvim_get_hl(0, { name = 'DiffDelete' })
- vim.api.nvim_set_hl(0, 'DiffsClear', { fg = normal.fg or 0xc0c0c0 })
- vim.api.nvim_set_hl(0, 'DiffsAdd', { bg = diff_add.bg or 0x2e4a3a })
- vim.api.nvim_set_hl(0, 'DiffsDelete', { bg = diff_delete.bg or 0x4a2e3a })
- vim.api.nvim_set_hl(0, 'DiffsAddText', { bg = 0x00FF00 })
- vim.api.nvim_set_hl(0, 'DiffsDeleteText', { bg = 0xFF0000 })
-end
-
-local function create_buffer(lines)
- local bufnr = vim.api.nvim_create_buf(false, true)
- vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines or {})
- return bufnr
-end
-
-local function delete_buffer(bufnr)
- if vim.api.nvim_buf_is_valid(bufnr) then
- vim.api.nvim_buf_delete(bufnr, { force = true })
- end
-end
-
-local function get_diffs_ns()
- return vim.api.nvim_get_namespaces()['diffs']
-end
-
-local function get_extmarks(bufnr, ns)
- return vim.api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, { details = true })
-end
-
-local function highlight_opts_with_background()
- return {
- hide_prefix = false,
- highlights = {
- background = true,
- gutter = false,
- context = { enabled = false, lines = 0 },
- treesitter = { enabled = true, max_lines = 500 },
- vim = { enabled = false, max_lines = 200 },
- intra = { enabled = false, algorithm = 'default', max_lines = 500 },
- priorities = { clear = 198, syntax = 199, line_bg = 200, char_bg = 201 },
- },
- }
-end
-
-describe('integration', function()
- before_each(function()
- setup_highlight_groups()
- end)
-
- describe('attach and parse', function()
- it('attach populates hunk cache for unified diff buffer', function()
- local bufnr = create_buffer({
- 'diff --git a/foo.lua b/foo.lua',
- 'index abc..def 100644',
- '--- a/foo.lua',
- '+++ b/foo.lua',
- '@@ -1,3 +1,3 @@',
- ' local x = 1',
- '-local y = 2',
- '+local y = 3',
- ' local z = 4',
- })
- diffs.attach(bufnr)
- local entry = diffs._test.hunk_cache[bufnr]
- assert.is_not_nil(entry)
- assert.are.equal(1, #entry.hunks)
- assert.are.equal('foo.lua', entry.hunks[1].filename)
- delete_buffer(bufnr)
- end)
-
- it('attach parses multiple hunks across multiple files', function()
- local bufnr = create_buffer({
- 'M foo.lua',
- '@@ -1,1 +1,2 @@',
- ' local x = 1',
- '+local y = 2',
- 'M bar.lua',
- '@@ -1,1 +1,2 @@',
- ' local a = 1',
- '+local b = 2',
- })
- diffs.attach(bufnr)
- local entry = diffs._test.hunk_cache[bufnr]
- assert.is_not_nil(entry)
- assert.are.equal(2, #entry.hunks)
- delete_buffer(bufnr)
- end)
-
- it('re-attach on same buffer is idempotent', function()
- local bufnr = create_buffer({
- 'M test.lua',
- '@@ -1,1 +1,2 @@',
- ' local x = 1',
- '+local y = 2',
- })
- diffs.attach(bufnr)
- local entry_before = diffs._test.hunk_cache[bufnr]
- local tick_before = entry_before.tick
- diffs.attach(bufnr)
- local entry_after = diffs._test.hunk_cache[bufnr]
- assert.are.equal(tick_before, entry_after.tick)
- delete_buffer(bufnr)
- end)
-
- it('refresh after content change invalidates cache', function()
- local bufnr = create_buffer({
- 'M test.lua',
- '@@ -1,1 +1,2 @@',
- ' local x = 1',
- '+local y = 2',
- })
- diffs.attach(bufnr)
- local tick_before = diffs._test.hunk_cache[bufnr].tick
- vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, { '+local z = 3' })
- diffs.refresh(bufnr)
- local entry = diffs._test.hunk_cache[bufnr]
- assert.are.equal(-1, entry.tick)
- assert.is_true(entry.pending_clear)
- assert.is_true(tick_before >= 0)
- delete_buffer(bufnr)
- end)
- end)
-
- describe('ft_retry_pending', function()
- before_each(function()
- rawset(vim.fn, 'did_filetype', function()
- return 1
- end)
- require('diffs.parser')._test.ft_lang_cache = {}
- end)
-
- after_each(function()
- rawset(vim.fn, 'did_filetype', nil)
- end)
-
- it('sets ft_retry_pending when nil-ft hunks detected under did_filetype', function()
- local bufnr = create_buffer({
- 'diff --git a/app.conf b/app.conf',
- '@@ -1,2 +1,2 @@',
- ' server {',
- '- listen 80;',
- '+ listen 8080;',
- })
- diffs.attach(bufnr)
- local entry = diffs._test.hunk_cache[bufnr]
- assert.is_not_nil(entry)
- assert.is_nil(entry.hunks[1].ft)
- assert.is_true(diffs._test.ft_retry_pending[bufnr] == true)
- delete_buffer(bufnr)
- end)
-
- it('clears ft_retry_pending after scheduled callback fires', function()
- local bufnr = create_buffer({
- 'diff --git a/app.conf b/app.conf',
- '@@ -1,2 +1,2 @@',
- ' server {',
- '- listen 80;',
- '+ listen 8080;',
- })
- diffs.attach(bufnr)
- assert.is_true(diffs._test.ft_retry_pending[bufnr] == true)
-
- local done = false
- vim.schedule(function()
- done = true
- end)
- vim.wait(1000, function()
- return done
- end)
-
- assert.is_nil(diffs._test.ft_retry_pending[bufnr])
- delete_buffer(bufnr)
- end)
-
- it('invalidates cache after scheduled callback fires', function()
- local bufnr = create_buffer({
- 'diff --git a/app.conf b/app.conf',
- '@@ -1,2 +1,2 @@',
- ' server {',
- '- listen 80;',
- '+ listen 8080;',
- })
- diffs.attach(bufnr)
- local tick_after_attach = diffs._test.hunk_cache[bufnr].tick
- assert.is_true(tick_after_attach >= 0)
-
- local done = false
- vim.schedule(function()
- done = true
- end)
- vim.wait(1000, function()
- return done
- end)
-
- local entry = diffs._test.hunk_cache[bufnr]
- assert.are.equal(-1, entry.tick)
- assert.is_true(entry.pending_clear)
- delete_buffer(bufnr)
- end)
-
- it('does not set ft_retry_pending when did_filetype() is zero', function()
- rawset(vim.fn, 'did_filetype', nil)
- local bufnr = create_buffer({
- 'diff --git a/test.sh b/test.sh',
- '@@ -1,2 +1,3 @@',
- ' #!/usr/bin/env bash',
- '-old line',
- '+new line',
- })
- diffs.attach(bufnr)
- assert.is_falsy(diffs._test.ft_retry_pending[bufnr])
- delete_buffer(bufnr)
- end)
-
- it('does not set ft_retry_pending for files with resolvable ft', function()
- local bufnr = create_buffer({
- 'M test.lua',
- '@@ -1,1 +1,2 @@',
- ' local x = 1',
- '+local y = 2',
- })
- diffs.attach(bufnr)
- assert.is_falsy(diffs._test.ft_retry_pending[bufnr])
- delete_buffer(bufnr)
- end)
- end)
-
- describe('extmarks from highlight pipeline', function()
- it('DiffsAdd background applied to + lines', function()
- local bufnr = create_buffer({
- '@@ -1,1 +1,2 @@',
- ' local x = 1',
- '+local y = 2',
- })
- local ns = vim.api.nvim_create_namespace('diffs_integration_test_add')
- local hunk = {
- filename = 'test.lua',
- lang = 'lua',
- start_line = 1,
- lines = { ' local x = 1', '+local y = 2' },
- }
- highlight.highlight_hunk(bufnr, ns, hunk, highlight_opts_with_background())
- local extmarks = get_extmarks(bufnr, ns)
- local has_diff_add = false
- for _, mark in ipairs(extmarks) do
- if mark[4] and mark[4].line_hl_group == 'DiffsAdd' then
- has_diff_add = true
- break
- end
- end
- assert.is_true(has_diff_add)
- delete_buffer(bufnr)
- end)
-
- it('DiffsDelete background applied to - lines', function()
- local bufnr = create_buffer({
- '@@ -1,2 +1,1 @@',
- ' local x = 1',
- '-local y = 2',
- })
- local ns = vim.api.nvim_create_namespace('diffs_integration_test_del')
- local hunk = {
- filename = 'test.lua',
- lang = 'lua',
- start_line = 1,
- lines = { ' local x = 1', '-local y = 2' },
- }
- highlight.highlight_hunk(bufnr, ns, hunk, highlight_opts_with_background())
- local extmarks = get_extmarks(bufnr, ns)
- local has_diff_delete = false
- for _, mark in ipairs(extmarks) do
- if mark[4] and mark[4].line_hl_group == 'DiffsDelete' then
- has_diff_delete = true
- break
- end
- end
- assert.is_true(has_diff_delete)
- delete_buffer(bufnr)
- end)
-
- it('mixed hunk produces both DiffsAdd and DiffsDelete backgrounds', function()
- local bufnr = create_buffer({
- '@@ -1,2 +1,2 @@',
- '-local x = 1',
- '+local x = 2',
- })
- local ns = vim.api.nvim_create_namespace('diffs_integration_test_mixed')
- local hunk = {
- filename = 'test.lua',
- lang = 'lua',
- start_line = 1,
- lines = { '-local x = 1', '+local x = 2' },
- }
- highlight.highlight_hunk(bufnr, ns, hunk, highlight_opts_with_background())
- local extmarks = get_extmarks(bufnr, ns)
- local has_add = false
- local has_delete = false
- for _, mark in ipairs(extmarks) do
- if mark[4] and mark[4].line_hl_group == 'DiffsAdd' then
- has_add = true
- end
- if mark[4] and mark[4].line_hl_group == 'DiffsDelete' then
- has_delete = true
- end
- end
- assert.is_true(has_add)
- assert.is_true(has_delete)
- delete_buffer(bufnr)
- end)
-
- it('no background extmarks for context lines', function()
- local bufnr = create_buffer({
- '@@ -1,3 +1,3 @@',
- ' local x = 1',
- '-local y = 2',
- '+local y = 3',
- ' local z = 4',
- })
- local ns = vim.api.nvim_create_namespace('diffs_integration_test_ctx')
- local hunk = {
- filename = 'test.lua',
- lang = 'lua',
- start_line = 1,
- lines = { ' local x = 1', '-local y = 2', '+local y = 3', ' local z = 4' },
- }
- highlight.highlight_hunk(bufnr, ns, hunk, highlight_opts_with_background())
- local extmarks = get_extmarks(bufnr, ns)
- local line_bgs = {}
- for _, mark in ipairs(extmarks) do
- local d = mark[4]
- if d and (d.line_hl_group == 'DiffsAdd' or d.line_hl_group == 'DiffsDelete') then
- line_bgs[mark[2]] = d.line_hl_group
- end
- end
- assert.is_nil(line_bgs[1])
- assert.is_nil(line_bgs[4])
- assert.are.equal('DiffsDelete', line_bgs[2])
- assert.are.equal('DiffsAdd', line_bgs[3])
- delete_buffer(bufnr)
- end)
-
- it('treesitter extmarks applied for lua hunks', function()
- local bufnr = create_buffer({
- '@@ -1,2 +1,3 @@',
- ' local x = 1',
- '+local y = 2',
- ' return x',
- })
- local ns = vim.api.nvim_create_namespace('diffs_integration_test_ts')
- local hunk = {
- filename = 'test.lua',
- lang = 'lua',
- start_line = 1,
- lines = { ' local x = 1', '+local y = 2', ' return x' },
- }
- highlight.highlight_hunk(bufnr, ns, hunk, highlight_opts_with_background())
- local extmarks = get_extmarks(bufnr, ns)
- local has_ts = false
- for _, mark in ipairs(extmarks) do
- if mark[4] and mark[4].hl_group and mark[4].hl_group:match('^@.*%.lua$') then
- has_ts = true
- break
- end
- end
- assert.is_true(has_ts)
- delete_buffer(bufnr)
- end)
-
- it('diffs namespace exists after attach', function()
- local bufnr = create_buffer({
- 'M test.lua',
- '@@ -1,1 +1,2 @@',
- ' local x = 1',
- '+local y = 2',
- })
- diffs.attach(bufnr)
- local ns = get_diffs_ns()
- assert.is_not_nil(ns)
- assert.is_number(ns)
- delete_buffer(bufnr)
- end)
- end)
-
- describe('multiple hunks highlighting', function()
- it('both hunks in multi-hunk buffer get background extmarks', function()
- local bufnr = create_buffer({
- '@@ -1,2 +1,2 @@',
- '-local x = 1',
- '+local x = 10',
- '@@ -10,2 +10,2 @@',
- '-local y = 2',
- '+local y = 20',
- })
- local ns = vim.api.nvim_create_namespace('diffs_integration_test_multi')
- local hunk1 = {
- filename = 'test.lua',
- lang = 'lua',
- start_line = 1,
- lines = { '-local x = 1', '+local x = 10' },
- }
- local hunk2 = {
- filename = 'test.lua',
- lang = 'lua',
- start_line = 4,
- lines = { '-local y = 2', '+local y = 20' },
- }
- highlight.highlight_hunk(bufnr, ns, hunk1, highlight_opts_with_background())
- highlight.highlight_hunk(bufnr, ns, hunk2, highlight_opts_with_background())
- local extmarks = get_extmarks(bufnr, ns)
- local add_lines = {}
- local del_lines = {}
- for _, mark in ipairs(extmarks) do
- local d = mark[4]
- if d and d.line_hl_group == 'DiffsAdd' then
- add_lines[mark[2]] = true
- end
- if d and d.line_hl_group == 'DiffsDelete' then
- del_lines[mark[2]] = true
- end
- end
- assert.is_true(del_lines[1] ~= nil)
- assert.is_true(add_lines[2] ~= nil)
- assert.is_true(del_lines[4] ~= nil)
- assert.is_true(add_lines[5] ~= nil)
- delete_buffer(bufnr)
- end)
- end)
-end)
diff --git a/spec/merge_spec.lua b/spec/merge_spec.lua
deleted file mode 100644
index 5e4b854..0000000
--- a/spec/merge_spec.lua
+++ /dev/null
@@ -1,815 +0,0 @@
-local helpers = require('spec.helpers')
-local merge = require('diffs.merge')
-
-local function default_config(overrides)
- local cfg = {
- enabled = true,
- disable_diagnostics = false,
- show_virtual_text = true,
- show_actions = false,
- keymaps = {
- ours = 'doo',
- theirs = 'dot',
- both = 'dob',
- none = 'don',
- next = ']c',
- prev = '[c',
- },
- }
- if overrides then
- cfg = vim.tbl_deep_extend('force', cfg, overrides)
- end
- return cfg
-end
-
-local function create_diff_buffer(lines, working_path)
- local bufnr = helpers.create_buffer(lines)
- if working_path then
- vim.api.nvim_buf_set_var(bufnr, 'diffs_working_path', working_path)
- end
- return bufnr
-end
-
-local function create_working_buffer(lines, name)
- local bufnr = vim.api.nvim_create_buf(true, false)
- vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
- if name then
- vim.api.nvim_buf_set_name(bufnr, name)
- end
- return bufnr
-end
-
-describe('merge', function()
- describe('parse_hunks', function()
- it('parses a single hunk', function()
- local bufnr = helpers.create_buffer({
- 'diff --git a/file.lua b/file.lua',
- '--- a/file.lua',
- '+++ b/file.lua',
- '@@ -1,3 +1,3 @@',
- ' local M = {}',
- '-local x = 1',
- '+local x = 2',
- ' return M',
- })
-
- local hunks = merge.parse_hunks(bufnr)
- assert.are.equal(1, #hunks)
- assert.are.equal(3, hunks[1].start_line)
- assert.are.equal(7, hunks[1].end_line)
- assert.are.same({ 'local x = 1' }, hunks[1].del_lines)
- assert.are.same({ 'local x = 2' }, hunks[1].add_lines)
-
- helpers.delete_buffer(bufnr)
- end)
-
- it('parses multiple hunks', function()
- local bufnr = helpers.create_buffer({
- 'diff --git a/file.lua b/file.lua',
- '--- a/file.lua',
- '+++ b/file.lua',
- '@@ -1,3 +1,3 @@',
- ' local M = {}',
- '-local x = 1',
- '+local x = 2',
- ' return M',
- '@@ -10,3 +10,3 @@',
- ' function M.foo()',
- '- return 1',
- '+ return 2',
- ' end',
- })
-
- local hunks = merge.parse_hunks(bufnr)
- assert.are.equal(2, #hunks)
- assert.are.equal(3, hunks[1].start_line)
- assert.are.equal(8, hunks[2].start_line)
-
- helpers.delete_buffer(bufnr)
- end)
-
- it('parses add-only hunk', function()
- local bufnr = helpers.create_buffer({
- 'diff --git a/file.lua b/file.lua',
- '--- a/file.lua',
- '+++ b/file.lua',
- '@@ -1,2 +1,3 @@',
- ' local M = {}',
- '+local new = true',
- ' return M',
- })
-
- local hunks = merge.parse_hunks(bufnr)
- assert.are.equal(1, #hunks)
- assert.are.same({}, hunks[1].del_lines)
- assert.are.same({ 'local new = true' }, hunks[1].add_lines)
-
- helpers.delete_buffer(bufnr)
- end)
-
- it('parses delete-only hunk', function()
- local bufnr = helpers.create_buffer({
- 'diff --git a/file.lua b/file.lua',
- '--- a/file.lua',
- '+++ b/file.lua',
- '@@ -1,3 +1,2 @@',
- ' local M = {}',
- '-local old = false',
- ' return M',
- })
-
- local hunks = merge.parse_hunks(bufnr)
- assert.are.equal(1, #hunks)
- assert.are.same({ 'local old = false' }, hunks[1].del_lines)
- assert.are.same({}, hunks[1].add_lines)
-
- helpers.delete_buffer(bufnr)
- end)
-
- it('returns empty for buffer with no hunks', function()
- local bufnr = helpers.create_buffer({
- 'diff --git a/file.lua b/file.lua',
- '--- a/file.lua',
- '+++ b/file.lua',
- })
-
- local hunks = merge.parse_hunks(bufnr)
- assert.are.equal(0, #hunks)
-
- helpers.delete_buffer(bufnr)
- end)
- end)
-
- describe('match_hunk_to_conflict', function()
- it('matches hunk to conflict region', function()
- local working_bufnr = create_working_buffer({
- '<<<<<<< HEAD',
- 'local x = 1',
- '=======',
- 'local x = 2',
- '>>>>>>> feature',
- }, '/tmp/diffs_test_match.lua')
-
- local hunk = {
- index = 1,
- start_line = 3,
- end_line = 7,
- del_lines = { 'local x = 1' },
- add_lines = { 'local x = 2' },
- }
-
- local region = merge.match_hunk_to_conflict(hunk, working_bufnr)
- assert.is_not_nil(region)
- assert.are.equal(0, region.marker_ours)
-
- helpers.delete_buffer(working_bufnr)
- end)
-
- it('returns nil for auto-merged content', function()
- local working_bufnr = create_working_buffer({
- '<<<<<<< HEAD',
- 'local x = 1',
- '=======',
- 'local x = 2',
- '>>>>>>> feature',
- }, '/tmp/diffs_test_auto.lua')
-
- local hunk = {
- index = 1,
- start_line = 3,
- end_line = 7,
- del_lines = { 'local y = 3' },
- add_lines = { 'local y = 4' },
- }
-
- local region = merge.match_hunk_to_conflict(hunk, working_bufnr)
- assert.is_nil(region)
-
- helpers.delete_buffer(working_bufnr)
- end)
-
- it('matches with empty ours section', function()
- local working_bufnr = create_working_buffer({
- '<<<<<<< HEAD',
- '=======',
- 'local x = 2',
- '>>>>>>> feature',
- }, '/tmp/diffs_test_empty_ours.lua')
-
- local hunk = {
- index = 1,
- start_line = 3,
- end_line = 5,
- del_lines = {},
- add_lines = { 'local x = 2' },
- }
-
- local region = merge.match_hunk_to_conflict(hunk, working_bufnr)
- assert.is_not_nil(region)
-
- helpers.delete_buffer(working_bufnr)
- end)
-
- it('matches correct region among multiple conflicts', function()
- local working_bufnr = create_working_buffer({
- '<<<<<<< HEAD',
- 'local a = 1',
- '=======',
- 'local a = 2',
- '>>>>>>> feature',
- 'middle',
- '<<<<<<< HEAD',
- 'local b = 3',
- '=======',
- 'local b = 4',
- '>>>>>>> feature',
- }, '/tmp/diffs_test_multi.lua')
-
- local hunk = {
- index = 2,
- start_line = 8,
- end_line = 12,
- del_lines = { 'local b = 3' },
- add_lines = { 'local b = 4' },
- }
-
- local region = merge.match_hunk_to_conflict(hunk, working_bufnr)
- assert.is_not_nil(region)
- assert.are.equal(6, region.marker_ours)
-
- helpers.delete_buffer(working_bufnr)
- end)
-
- it('matches with diff3 format', function()
- local working_bufnr = create_working_buffer({
- '<<<<<<< HEAD',
- 'local x = 1',
- '||||||| base',
- 'local x = 0',
- '=======',
- 'local x = 2',
- '>>>>>>> feature',
- }, '/tmp/diffs_test_diff3.lua')
-
- local hunk = {
- index = 1,
- start_line = 3,
- end_line = 7,
- del_lines = { 'local x = 1' },
- add_lines = { 'local x = 2' },
- }
-
- local region = merge.match_hunk_to_conflict(hunk, working_bufnr)
- assert.is_not_nil(region)
- assert.are.equal(2, region.marker_base)
-
- helpers.delete_buffer(working_bufnr)
- end)
- end)
-
- describe('resolution', function()
- local diff_bufnr, working_bufnr
-
- local function setup_buffers()
- local working_path = '/tmp/diffs_test_resolve.lua'
- working_bufnr = create_working_buffer({
- '<<<<<<< HEAD',
- 'local x = 1',
- '=======',
- 'local x = 2',
- '>>>>>>> feature',
- }, working_path)
-
- diff_bufnr = create_diff_buffer({
- 'diff --git a/file.lua b/file.lua',
- '--- a/file.lua',
- '+++ b/file.lua',
- '@@ -1,1 +1,1 @@',
- '-local x = 1',
- '+local x = 2',
- }, working_path)
- vim.api.nvim_set_current_buf(diff_bufnr)
- end
-
- local function cleanup()
- helpers.delete_buffer(diff_bufnr)
- helpers.delete_buffer(working_bufnr)
- end
-
- it('resolve_ours keeps ours content in working file', function()
- setup_buffers()
- vim.api.nvim_win_set_cursor(0, { 5, 0 })
-
- merge.resolve_ours(diff_bufnr, default_config())
-
- local lines = vim.api.nvim_buf_get_lines(working_bufnr, 0, -1, false)
- assert.are.equal(1, #lines)
- assert.are.equal('local x = 1', lines[1])
-
- cleanup()
- end)
-
- it('resolve_theirs keeps theirs content in working file', function()
- setup_buffers()
- vim.api.nvim_win_set_cursor(0, { 5, 0 })
-
- merge.resolve_theirs(diff_bufnr, default_config())
-
- local lines = vim.api.nvim_buf_get_lines(working_bufnr, 0, -1, false)
- assert.are.equal(1, #lines)
- assert.are.equal('local x = 2', lines[1])
-
- cleanup()
- end)
-
- it('resolve_both keeps ours then theirs in working file', function()
- setup_buffers()
- vim.api.nvim_win_set_cursor(0, { 5, 0 })
-
- merge.resolve_both(diff_bufnr, default_config())
-
- local lines = vim.api.nvim_buf_get_lines(working_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])
-
- cleanup()
- end)
-
- it('resolve_none removes entire block from working file', function()
- setup_buffers()
- vim.api.nvim_win_set_cursor(0, { 5, 0 })
-
- merge.resolve_none(diff_bufnr, default_config())
-
- local lines = vim.api.nvim_buf_get_lines(working_bufnr, 0, -1, false)
- assert.are.equal(1, #lines)
- assert.are.equal('', lines[1])
-
- cleanup()
- end)
-
- it('tracks resolved hunks', function()
- setup_buffers()
- vim.api.nvim_win_set_cursor(0, { 5, 0 })
-
- assert.is_false(merge.is_resolved(diff_bufnr, 1))
- merge.resolve_ours(diff_bufnr, default_config())
- assert.is_true(merge.is_resolved(diff_bufnr, 1))
-
- cleanup()
- end)
-
- it('adds virtual text for resolved hunks', function()
- setup_buffers()
- vim.api.nvim_win_set_cursor(0, { 5, 0 })
-
- merge.resolve_ours(diff_bufnr, default_config())
-
- local extmarks =
- vim.api.nvim_buf_get_extmarks(diff_bufnr, merge.get_namespace(), 0, -1, { details = true })
- local has_resolved_text = false
- for _, mark in ipairs(extmarks) do
- if mark[4] and mark[4].virt_text then
- for _, chunk in ipairs(mark[4].virt_text) do
- if chunk[1]:match('resolved') then
- has_resolved_text = true
- end
- end
- end
- end
- assert.is_true(has_resolved_text)
-
- cleanup()
- end)
-
- it('notifies when hunk is already resolved', function()
- setup_buffers()
- vim.api.nvim_win_set_cursor(0, { 5, 0 })
-
- merge.resolve_ours(diff_bufnr, default_config())
-
- local notified = false
- local orig_notify = vim.notify
- vim.notify = function(msg)
- if msg:match('already resolved') then
- notified = true
- end
- end
-
- merge.resolve_ours(diff_bufnr, default_config())
- vim.notify = orig_notify
-
- assert.is_true(notified)
-
- cleanup()
- end)
-
- it('notifies when hunk does not match a conflict', function()
- local working_path = '/tmp/diffs_test_no_conflict.lua'
- local w_bufnr = create_working_buffer({
- 'local y = 1',
- }, working_path)
-
- local d_bufnr = create_diff_buffer({
- 'diff --git a/file.lua b/file.lua',
- '--- a/file.lua',
- '+++ b/file.lua',
- '@@ -1,1 +1,1 @@',
- '-local x = 1',
- '+local x = 2',
- }, working_path)
- vim.api.nvim_set_current_buf(d_bufnr)
- vim.api.nvim_win_set_cursor(0, { 5, 0 })
-
- local notified = false
- local orig_notify = vim.notify
- vim.notify = function(msg)
- if msg:match('does not correspond') then
- notified = true
- end
- end
-
- merge.resolve_ours(d_bufnr, default_config())
- vim.notify = orig_notify
-
- assert.is_true(notified)
-
- helpers.delete_buffer(d_bufnr)
- helpers.delete_buffer(w_bufnr)
- end)
- end)
-
- describe('navigation', function()
- it('goto_next jumps to next conflict hunk', function()
- local working_path = '/tmp/diffs_test_nav.lua'
- local w_bufnr = create_working_buffer({
- '<<<<<<< HEAD',
- 'local a = 1',
- '=======',
- 'local a = 2',
- '>>>>>>> feature',
- 'middle',
- '<<<<<<< HEAD',
- 'local b = 3',
- '=======',
- 'local b = 4',
- '>>>>>>> feature',
- }, working_path)
-
- local d_bufnr = create_diff_buffer({
- 'diff --git a/file.lua b/file.lua',
- '--- a/file.lua',
- '+++ b/file.lua',
- '@@ -1,1 +1,1 @@',
- '-local a = 1',
- '+local a = 2',
- '@@ -5,1 +5,1 @@',
- '-local b = 3',
- '+local b = 4',
- }, working_path)
- vim.api.nvim_set_current_buf(d_bufnr)
- vim.api.nvim_win_set_cursor(0, { 1, 0 })
-
- merge.goto_next(d_bufnr)
- assert.are.equal(4, vim.api.nvim_win_get_cursor(0)[1])
-
- merge.goto_next(d_bufnr)
- assert.are.equal(7, vim.api.nvim_win_get_cursor(0)[1])
-
- helpers.delete_buffer(d_bufnr)
- helpers.delete_buffer(w_bufnr)
- end)
-
- it('goto_next wraps around', function()
- local working_path = '/tmp/diffs_test_wrap.lua'
- local w_bufnr = create_working_buffer({
- '<<<<<<< HEAD',
- 'local x = 1',
- '=======',
- 'local x = 2',
- '>>>>>>> feature',
- }, working_path)
-
- local d_bufnr = create_diff_buffer({
- 'diff --git a/file.lua b/file.lua',
- '--- a/file.lua',
- '+++ b/file.lua',
- '@@ -1,1 +1,1 @@',
- '-local x = 1',
- '+local x = 2',
- }, working_path)
- vim.api.nvim_set_current_buf(d_bufnr)
- vim.api.nvim_win_set_cursor(0, { 6, 0 })
-
- merge.goto_next(d_bufnr)
- assert.are.equal(4, vim.api.nvim_win_get_cursor(0)[1])
-
- helpers.delete_buffer(d_bufnr)
- helpers.delete_buffer(w_bufnr)
- end)
-
- it('goto_next notifies on wrap-around', function()
- local working_path = '/tmp/diffs_test_wrap_notify.lua'
- local w_bufnr = create_working_buffer({
- '<<<<<<< HEAD',
- 'local x = 1',
- '=======',
- 'local x = 2',
- '>>>>>>> feature',
- }, working_path)
-
- local d_bufnr = create_diff_buffer({
- 'diff --git a/file.lua b/file.lua',
- '--- a/file.lua',
- '+++ b/file.lua',
- '@@ -1,1 +1,1 @@',
- '-local x = 1',
- '+local x = 2',
- }, working_path)
- vim.api.nvim_set_current_buf(d_bufnr)
- vim.api.nvim_win_set_cursor(0, { 6, 0 })
-
- local notified = false
- local orig_notify = vim.notify
- vim.notify = function(msg)
- if msg:match('wrapped to first hunk') then
- notified = true
- end
- end
-
- merge.goto_next(d_bufnr)
- vim.notify = orig_notify
-
- assert.is_true(notified)
-
- helpers.delete_buffer(d_bufnr)
- helpers.delete_buffer(w_bufnr)
- end)
-
- it('goto_prev jumps to previous conflict hunk', function()
- local working_path = '/tmp/diffs_test_prev.lua'
- local w_bufnr = create_working_buffer({
- '<<<<<<< HEAD',
- 'local a = 1',
- '=======',
- 'local a = 2',
- '>>>>>>> feature',
- 'middle',
- '<<<<<<< HEAD',
- 'local b = 3',
- '=======',
- 'local b = 4',
- '>>>>>>> feature',
- }, working_path)
-
- local d_bufnr = create_diff_buffer({
- 'diff --git a/file.lua b/file.lua',
- '--- a/file.lua',
- '+++ b/file.lua',
- '@@ -1,1 +1,1 @@',
- '-local a = 1',
- '+local a = 2',
- '@@ -5,1 +5,1 @@',
- '-local b = 3',
- '+local b = 4',
- }, working_path)
- vim.api.nvim_set_current_buf(d_bufnr)
- vim.api.nvim_win_set_cursor(0, { 9, 0 })
-
- merge.goto_prev(d_bufnr)
- assert.are.equal(7, vim.api.nvim_win_get_cursor(0)[1])
-
- merge.goto_prev(d_bufnr)
- assert.are.equal(4, vim.api.nvim_win_get_cursor(0)[1])
-
- helpers.delete_buffer(d_bufnr)
- helpers.delete_buffer(w_bufnr)
- end)
-
- it('goto_prev wraps around', function()
- local working_path = '/tmp/diffs_test_prev_wrap.lua'
- local w_bufnr = create_working_buffer({
- '<<<<<<< HEAD',
- 'local x = 1',
- '=======',
- 'local x = 2',
- '>>>>>>> feature',
- }, working_path)
-
- local d_bufnr = create_diff_buffer({
- 'diff --git a/file.lua b/file.lua',
- '--- a/file.lua',
- '+++ b/file.lua',
- '@@ -1,1 +1,1 @@',
- '-local x = 1',
- '+local x = 2',
- }, working_path)
- vim.api.nvim_set_current_buf(d_bufnr)
- vim.api.nvim_win_set_cursor(0, { 1, 0 })
-
- merge.goto_prev(d_bufnr)
- assert.are.equal(4, vim.api.nvim_win_get_cursor(0)[1])
-
- helpers.delete_buffer(d_bufnr)
- helpers.delete_buffer(w_bufnr)
- end)
-
- it('goto_prev notifies on wrap-around', function()
- local working_path = '/tmp/diffs_test_prev_wrap_notify.lua'
- local w_bufnr = create_working_buffer({
- '<<<<<<< HEAD',
- 'local x = 1',
- '=======',
- 'local x = 2',
- '>>>>>>> feature',
- }, working_path)
-
- local d_bufnr = create_diff_buffer({
- 'diff --git a/file.lua b/file.lua',
- '--- a/file.lua',
- '+++ b/file.lua',
- '@@ -1,1 +1,1 @@',
- '-local x = 1',
- '+local x = 2',
- }, working_path)
- vim.api.nvim_set_current_buf(d_bufnr)
- vim.api.nvim_win_set_cursor(0, { 1, 0 })
-
- local notified = false
- local orig_notify = vim.notify
- vim.notify = function(msg)
- if msg:match('wrapped to last hunk') then
- notified = true
- end
- end
-
- merge.goto_prev(d_bufnr)
- vim.notify = orig_notify
-
- assert.is_true(notified)
-
- helpers.delete_buffer(d_bufnr)
- helpers.delete_buffer(w_bufnr)
- end)
-
- it('skips resolved hunks', function()
- local working_path = '/tmp/diffs_test_skip_resolved.lua'
- local w_bufnr = create_working_buffer({
- '<<<<<<< HEAD',
- 'local a = 1',
- '=======',
- 'local a = 2',
- '>>>>>>> feature',
- 'middle',
- '<<<<<<< HEAD',
- 'local b = 3',
- '=======',
- 'local b = 4',
- '>>>>>>> feature',
- }, working_path)
-
- local d_bufnr = create_diff_buffer({
- 'diff --git a/file.lua b/file.lua',
- '--- a/file.lua',
- '+++ b/file.lua',
- '@@ -1,1 +1,1 @@',
- '-local a = 1',
- '+local a = 2',
- '@@ -5,1 +5,1 @@',
- '-local b = 3',
- '+local b = 4',
- }, working_path)
- vim.api.nvim_set_current_buf(d_bufnr)
- vim.api.nvim_win_set_cursor(0, { 5, 0 })
-
- merge.resolve_ours(d_bufnr, default_config())
-
- vim.api.nvim_win_set_cursor(0, { 1, 0 })
- merge.goto_next(d_bufnr)
- assert.are.equal(7, vim.api.nvim_win_get_cursor(0)[1])
-
- helpers.delete_buffer(d_bufnr)
- helpers.delete_buffer(w_bufnr)
- end)
- end)
-
- describe('hunk hints', function()
- it('adds keymap hints on hunk header lines', function()
- local d_bufnr = create_diff_buffer({
- 'diff --git a/file.lua b/file.lua',
- '--- a/file.lua',
- '+++ b/file.lua',
- '@@ -1,1 +1,1 @@',
- '-local x = 1',
- '+local x = 2',
- })
-
- merge.setup_keymaps(d_bufnr, default_config())
-
- local extmarks =
- vim.api.nvim_buf_get_extmarks(d_bufnr, merge.get_namespace(), 0, -1, { details = true })
- local hint_marks = {}
- for _, mark in ipairs(extmarks) do
- if mark[4] and mark[4].virt_text then
- local text = ''
- for _, chunk in ipairs(mark[4].virt_text) do
- text = text .. chunk[1]
- end
- table.insert(hint_marks, { line = mark[2], text = text })
- end
- end
- assert.are.equal(1, #hint_marks)
- assert.are.equal(3, hint_marks[1].line)
- assert.is_truthy(hint_marks[1].text:find('doo'))
- assert.is_truthy(hint_marks[1].text:find('dot'))
-
- helpers.delete_buffer(d_bufnr)
- end)
- end)
-
- describe('setup_keymaps', function()
- it('clears resolved state on re-init', function()
- local working_path = '/tmp/diffs_test_reinit.lua'
- local w_bufnr = create_working_buffer({
- '<<<<<<< HEAD',
- 'local x = 1',
- '=======',
- 'local x = 2',
- '>>>>>>> feature',
- }, working_path)
-
- local d_bufnr = create_diff_buffer({
- 'diff --git a/file.lua b/file.lua',
- '--- a/file.lua',
- '+++ b/file.lua',
- '@@ -1,1 +1,1 @@',
- '-local x = 1',
- '+local x = 2',
- }, working_path)
- vim.api.nvim_set_current_buf(d_bufnr)
- vim.api.nvim_win_set_cursor(0, { 5, 0 })
-
- local cfg = default_config()
- merge.resolve_ours(d_bufnr, cfg)
- assert.is_true(merge.is_resolved(d_bufnr, 1))
-
- local extmarks =
- vim.api.nvim_buf_get_extmarks(d_bufnr, merge.get_namespace(), 0, -1, { details = true })
- assert.is_true(#extmarks > 0)
-
- merge.setup_keymaps(d_bufnr, cfg)
-
- assert.is_false(merge.is_resolved(d_bufnr, 1))
- extmarks =
- vim.api.nvim_buf_get_extmarks(d_bufnr, merge.get_namespace(), 0, -1, { details = true })
- local resolved_count = 0
- for _, mark in ipairs(extmarks) do
- if mark[4] and mark[4].virt_text then
- for _, chunk in ipairs(mark[4].virt_text) do
- if chunk[1]:match('resolved') then
- resolved_count = resolved_count + 1
- end
- end
- end
- end
- assert.are.equal(0, resolved_count)
-
- helpers.delete_buffer(d_bufnr)
- helpers.delete_buffer(w_bufnr)
- end)
- end)
-
- describe('fugitive integration', function()
- it('parse_file_line returns status for unmerged files', function()
- local fugitive = require('diffs.fugitive')
- local buf = vim.api.nvim_create_buf(false, true)
- vim.api.nvim_buf_set_lines(buf, 0, -1, false, {
- 'Unstaged (1)',
- 'U conflict.lua',
- })
- local filename, section, is_header, old_filename, status = fugitive.get_file_at_line(buf, 2)
- assert.are.equal('conflict.lua', filename)
- assert.are.equal('unstaged', section)
- assert.is_false(is_header)
- assert.is_nil(old_filename)
- assert.are.equal('U', status)
- vim.api.nvim_buf_delete(buf, { force = true })
- end)
-
- it('walkback from hunk line propagates status', function()
- local fugitive = require('diffs.fugitive')
- local buf = vim.api.nvim_create_buf(false, true)
- vim.api.nvim_buf_set_lines(buf, 0, -1, false, {
- 'Unstaged (1)',
- 'U conflict.lua',
- '@@ -1,3 +1,4 @@',
- ' local M = {}',
- '+local new = true',
- })
- local _, _, _, _, status = fugitive.get_file_at_line(buf, 5)
- assert.are.equal('U', status)
- vim.api.nvim_buf_delete(buf, { force = true })
- end)
- end)
-end)
diff --git a/spec/neogit_integration_spec.lua b/spec/neogit_integration_spec.lua
deleted file mode 100644
index ef33049..0000000
--- a/spec/neogit_integration_spec.lua
+++ /dev/null
@@ -1,126 +0,0 @@
-require('spec.helpers')
-
-vim.g.diffs = { neogit = true }
-
-local diffs = require('diffs')
-local parser = require('diffs.parser')
-
-local function create_buffer(lines)
- local bufnr = vim.api.nvim_create_buf(false, true)
- vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines or {})
- return bufnr
-end
-
-local function delete_buffer(bufnr)
- if vim.api.nvim_buf_is_valid(bufnr) then
- vim.api.nvim_buf_delete(bufnr, { force = true })
- end
-end
-
-describe('neogit_integration', function()
- describe('neogit_disable_hunk_highlight', function()
- it('sets neogit_disable_hunk_highlight on NeogitStatus buffer after attach', function()
- local bufnr = create_buffer({
- 'modified test.lua',
- '@@ -1,1 +1,2 @@',
- ' local x = 1',
- '+local y = 2',
- })
- vim.api.nvim_set_option_value('filetype', 'NeogitStatus', { buf = bufnr })
- diffs.attach(bufnr)
-
- assert.is_true(vim.b[bufnr].neogit_disable_hunk_highlight)
-
- delete_buffer(bufnr)
- end)
-
- it('does not set neogit_disable_hunk_highlight on non-Neogit buffer', function()
- local bufnr = create_buffer({})
- vim.api.nvim_set_option_value('filetype', 'git', { buf = bufnr })
- diffs.attach(bufnr)
-
- assert.is_not_true(vim.b[bufnr].neogit_disable_hunk_highlight)
-
- delete_buffer(bufnr)
- end)
- end)
-
- describe('NeogitStatus buffer attach', function()
- it('populates hunk_cache for NeogitStatus buffer with diff content', function()
- local bufnr = create_buffer({
- 'modified hello.lua',
- '@@ -1,2 +1,3 @@',
- ' local M = {}',
- '+local x = 1',
- ' return M',
- })
- vim.api.nvim_set_option_value('filetype', 'NeogitStatus', { buf = bufnr })
- diffs.attach(bufnr)
- local entry = diffs._test.hunk_cache[bufnr]
- assert.is_not_nil(entry)
- assert.is_table(entry.hunks)
- assert.are.equal(1, #entry.hunks)
- assert.are.equal('hello.lua', entry.hunks[1].filename)
- delete_buffer(bufnr)
- end)
-
- it('populates hunk_cache for NeogitDiffView buffer', function()
- local bufnr = create_buffer({
- 'new file newmod.lua',
- '@@ -0,0 +1,2 @@',
- '+local M = {}',
- '+return M',
- })
- vim.api.nvim_set_option_value('filetype', 'NeogitDiffView', { buf = bufnr })
- diffs.attach(bufnr)
- local entry = diffs._test.hunk_cache[bufnr]
- assert.is_not_nil(entry)
- assert.is_table(entry.hunks)
- assert.are.equal(1, #entry.hunks)
- delete_buffer(bufnr)
- end)
- end)
-
- describe('parser neogit patterns', function()
- it('detects renamed prefix via parser', function()
- local bufnr = create_buffer({
- 'renamed old.lua',
- '@@ -1,2 +1,3 @@',
- ' local M = {}',
- '+local x = 1',
- ' return M',
- })
- local hunks = parser.parse_buffer(bufnr)
- assert.are.equal(1, #hunks)
- assert.are.equal('old.lua', hunks[1].filename)
- delete_buffer(bufnr)
- end)
-
- it('detects copied prefix via parser', function()
- local bufnr = create_buffer({
- 'copied orig.lua',
- '@@ -1,2 +1,3 @@',
- ' local M = {}',
- '+local x = 1',
- ' return M',
- })
- local hunks = parser.parse_buffer(bufnr)
- assert.are.equal(1, #hunks)
- assert.are.equal('orig.lua', hunks[1].filename)
- delete_buffer(bufnr)
- end)
-
- it('detects deleted prefix via parser', function()
- local bufnr = create_buffer({
- 'deleted gone.lua',
- '@@ -1,2 +0,0 @@',
- '-local M = {}',
- '-return M',
- })
- local hunks = parser.parse_buffer(bufnr)
- assert.are.equal(1, #hunks)
- assert.are.equal('gone.lua', hunks[1].filename)
- delete_buffer(bufnr)
- end)
- end)
-end)
diff --git a/spec/parser_spec.lua b/spec/parser_spec.lua
index adbbd37..11ac3be 100644
--- a/spec/parser_spec.lua
+++ b/spec/parser_spec.lua
@@ -163,10 +163,10 @@ describe('parser', function()
end
end)
- it('stops hunk at blank line when remaining counts exhausted', function()
+ it('stops hunk at blank line', function()
local bufnr = create_buffer({
'M test.lua',
- '@@ -1,1 +1,2 @@',
+ '@@ -1,2 +1,3 @@',
' local x = 1',
'+local y = 2',
'',
@@ -391,27 +391,35 @@ describe('parser', function()
vim.fn.delete(repo_root, 'rf')
end)
- it('detects filetype for .sh files when did_filetype() is non-zero', function()
- rawset(vim.fn, 'did_filetype', function()
- return 1
- end)
+ it('detects python from shebang without open buffer', function()
+ local repo_root = '/tmp/diffs-test-shebang-py'
+ vim.fn.mkdir(repo_root, 'p')
- parser._test.ft_lang_cache = {}
- local bufnr = create_buffer({
- 'diff --git a/test.sh b/test.sh',
- '@@ -1,3 +1,4 @@',
- ' #!/usr/bin/env bash',
- ' set -euo pipefail',
- '-echo "running tests..."',
- '+echo "running tests with coverage..."',
+ 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")',
})
- local hunks = parser.parse_buffer(bufnr)
+ 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('test.sh', hunks[1].filename)
- assert.are.equal('sh', hunks[1].ft)
- delete_buffer(bufnr)
- rawset(vim.fn, 'did_filetype', nil)
+ 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()
@@ -432,6 +440,22 @@ describe('parser', function()
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',
@@ -448,154 +472,6 @@ describe('parser', function()
delete_buffer(bufnr)
end)
- it('recognizes U prefix for unmerged files', function()
- local bufnr = create_buffer({
- 'U merge_me.lua',
- '@@@ -1,3 -1,5 +1,9 @@@',
- ' local M = {}',
- '++<<<<<<< HEAD',
- ' + return 1',
- '++=======',
- '+ return 2',
- '++>>>>>>> feature',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal('merge_me.lua', hunks[1].filename)
- assert.are.equal('lua', hunks[1].ft)
- delete_buffer(bufnr)
- end)
-
- it('sets prefix_width 2 from @@@ combined diff header', function()
- local bufnr = create_buffer({
- 'U test.lua',
- '@@@ -1,3 -1,5 +1,9 @@@',
- ' local M = {}',
- '++<<<<<<< HEAD',
- ' + return 1',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal(2, hunks[1].prefix_width)
- delete_buffer(bufnr)
- end)
-
- it('sets prefix_width 1 for standard @@ unified diff', function()
- local bufnr = create_buffer({
- 'M test.lua',
- '@@ -1,2 +1,3 @@',
- ' local x = 1',
- '+local y = 2',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal(1, hunks[1].prefix_width)
- delete_buffer(bufnr)
- end)
-
- it('collects all combined diff line types as hunk content', function()
- local bufnr = create_buffer({
- 'U test.lua',
- '@@@ -1,3 -1,3 +1,5 @@@',
- ' local M = {}',
- '++<<<<<<< HEAD',
- ' + return 1',
- '+ local x = 2',
- ' end',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal(5, #hunks[1].lines)
- assert.are.equal(' local M = {}', hunks[1].lines[1])
- assert.are.equal('++<<<<<<< HEAD', hunks[1].lines[2])
- assert.are.equal(' + return 1', hunks[1].lines[3])
- assert.are.equal('+ local x = 2', hunks[1].lines[4])
- assert.are.equal(' end', hunks[1].lines[5])
- delete_buffer(bufnr)
- end)
-
- it('extracts new range from combined diff header', function()
- local bufnr = create_buffer({
- 'U test.lua',
- '@@@ -1,3 -1,5 +1,9 @@@',
- ' local M = {}',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal(1, hunks[1].file_new_start)
- assert.are.equal(9, hunks[1].file_new_count)
- assert.are.equal(1, hunks[1].file_old_start)
- assert.are.equal(3, hunks[1].file_old_count)
- delete_buffer(bufnr)
- end)
-
- it('extracts header context from combined diff header', function()
- local bufnr = create_buffer({
- 'U test.lua',
- '@@@ -1,3 -1,5 +1,9 @@@ function M.greet()',
- ' local M = {}',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal('function M.greet()', hunks[1].header_context)
- delete_buffer(bufnr)
- end)
-
- it('resets prefix_width when switching from combined to unified diff', function()
- local bufnr = create_buffer({
- 'U merge.lua',
- '@@@ -1,1 -1,1 +1,3 @@@',
- ' local M = {}',
- '++<<<<<<< HEAD',
- 'M other.lua',
- '@@ -1,1 +1,2 @@',
- ' local x = 1',
- '+local y = 2',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(2, #hunks)
- assert.are.equal(2, hunks[1].prefix_width)
- assert.are.equal(1, hunks[2].prefix_width)
- delete_buffer(bufnr)
- end)
-
- it('parses diff from gitcommit verbose buffer', function()
- local bufnr = create_buffer({
- '',
- '# Please enter the commit message for your changes.',
- '#',
- '# On branch main',
- '# Changes to be committed:',
- '#\tmodified: test.lua',
- '#',
- '# ------------------------ >8 ------------------------',
- '# Do not modify or remove the line above.',
- 'diff --git a/test.lua b/test.lua',
- 'index abc1234..def5678 100644',
- '--- a/test.lua',
- '+++ b/test.lua',
- '@@ -1,3 +1,3 @@',
- ' local function hello()',
- '- print("hello world")',
- '+ print("hello universe")',
- ' return true',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal('test.lua', hunks[1].filename)
- assert.are.equal('lua', hunks[1].ft)
- assert.are.equal(4, #hunks[1].lines)
- delete_buffer(bufnr)
- end)
-
it('stores repo_root on hunk when available', function()
local bufnr = create_buffer({
'M lua/test.lua',
@@ -612,388 +488,16 @@ describe('parser', function()
delete_buffer(bufnr)
end)
- it('detects neogit modified prefix', function()
+ it('repo_root is nil when not available', function()
local bufnr = create_buffer({
- 'modified hello.lua',
- '@@ -1,2 +1,3 @@',
- ' local M = {}',
- '+local x = 1',
- ' return M',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal('hello.lua', hunks[1].filename)
- assert.are.equal('lua', hunks[1].ft)
- assert.are.equal(3, #hunks[1].lines)
- delete_buffer(bufnr)
- end)
-
- it('detects neogit new file prefix', function()
- local bufnr = create_buffer({
- 'new file hello.lua',
- '@@ -0,0 +1,2 @@',
- '+local M = {}',
- '+return M',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal('hello.lua', hunks[1].filename)
- assert.are.equal('lua', hunks[1].ft)
- assert.are.equal(2, #hunks[1].lines)
- delete_buffer(bufnr)
- end)
-
- it('detects neogit deleted prefix', function()
- local bufnr = create_buffer({
- 'deleted hello.lua',
- '@@ -1,2 +0,0 @@',
- '-local M = {}',
- '-return M',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal('hello.lua', hunks[1].filename)
- assert.are.equal('lua', hunks[1].ft)
- assert.are.equal(2, #hunks[1].lines)
- delete_buffer(bufnr)
- end)
-
- it('detects neogit renamed prefix', function()
- local bufnr = create_buffer({
- 'renamed old.lua',
- '@@ -1,2 +1,3 @@',
- ' local M = {}',
- '+local x = 1',
- ' return M',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal('old.lua', hunks[1].filename)
- assert.are.equal('lua', hunks[1].ft)
- delete_buffer(bufnr)
- end)
-
- it('detects neogit copied prefix', function()
- local bufnr = create_buffer({
- 'copied orig.lua',
- '@@ -1,2 +1,3 @@',
- ' local M = {}',
- '+local x = 1',
- ' return M',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal('orig.lua', hunks[1].filename)
- assert.are.equal('lua', hunks[1].ft)
- delete_buffer(bufnr)
- end)
-
- it('does not treat "new file mode" as a filename', function()
- local bufnr = create_buffer({
- 'diff --git a/src/new.lua b/src/new.lua',
- 'new file mode 100644',
- 'index 0000000..abc1234',
- '--- /dev/null',
- '+++ b/src/new.lua',
- '@@ -0,0 +1,2 @@',
- '+local M = {}',
- '+return M',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal('src/new.lua', hunks[1].filename)
- assert.are.equal('lua', hunks[1].ft)
- delete_buffer(bufnr)
- end)
-
- it('does not treat "new file mode 100755" as a filename', function()
- local bufnr = create_buffer({
- 'diff --git a/bin/run b/bin/run',
- 'new file mode 100755',
- 'index 0000000..abc1234',
- '--- /dev/null',
- '+++ b/bin/run',
- '@@ -0,0 +1,2 @@',
- '+#!/bin/bash',
- '+echo hello',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal('bin/run', hunks[1].filename)
- delete_buffer(bufnr)
- end)
-
- it('does not treat "deleted file mode" as a filename', function()
- local bufnr = create_buffer({
- 'diff --git a/src/old.lua b/src/old.lua',
- 'deleted file mode 100644',
- 'index abc1234..0000000',
- '--- a/src/old.lua',
- '+++ /dev/null',
- '@@ -1,2 +0,0 @@',
- '-local M = {}',
- '-return M',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal('src/old.lua', hunks[1].filename)
- assert.are.equal('lua', hunks[1].ft)
- delete_buffer(bufnr)
- end)
-
- it('does not treat "deleted file mode 100755" as a filename', function()
- local bufnr = create_buffer({
- 'diff --git a/bin/old b/bin/old',
- 'deleted file mode 100755',
- 'index abc1234..0000000',
- '--- a/bin/old',
- '+++ /dev/null',
- '@@ -1,1 +0,0 @@',
- '-#!/bin/bash',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal('bin/old', hunks[1].filename)
- delete_buffer(bufnr)
- end)
-
- it('does not treat "old mode" or "new mode" as filenames', function()
- local bufnr = create_buffer({
- 'diff --git a/script.sh b/script.sh',
- 'old mode 100644',
- 'new mode 100755',
- '@@ -1,1 +1,2 @@',
- ' echo hello',
- '+echo world',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal('script.sh', hunks[1].filename)
- delete_buffer(bufnr)
- end)
-
- it('does not treat "rename from/to" as filenames', function()
- local bufnr = create_buffer({
- 'diff --git a/old.lua b/new.lua',
- 'similarity index 95%',
- 'rename from old.lua',
- 'rename to new.lua',
- '@@ -1,2 +1,2 @@',
- ' local M = {}',
- '-local x = 1',
- '+local x = 2',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal('new.lua', hunks[1].filename)
- delete_buffer(bufnr)
- end)
-
- it('does not treat "copy from/to" as filenames', function()
- local bufnr = create_buffer({
- 'diff --git a/orig.lua b/copy.lua',
- 'similarity index 100%',
- 'copy from orig.lua',
- 'copy to copy.lua',
- '@@ -1,1 +1,1 @@',
+ 'M lua/test.lua',
+ '@@ -1,3 +1,4 @@',
' local M = {}',
})
local hunks = parser.parse_buffer(bufnr)
assert.are.equal(1, #hunks)
- assert.are.equal('copy.lua', hunks[1].filename)
- delete_buffer(bufnr)
- end)
-
- it('does not treat "similarity index" or "dissimilarity index" as filenames', function()
- local bufnr = create_buffer({
- 'diff --git a/foo.lua b/bar.lua',
- 'similarity index 85%',
- 'rename from foo.lua',
- 'rename to bar.lua',
- '@@ -1,2 +1,2 @@',
- ' local M = {}',
- '-return 1',
- '+return 2',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal('bar.lua', hunks[1].filename)
- delete_buffer(bufnr)
- end)
-
- it('does not treat "index" line as a filename', function()
- local bufnr = create_buffer({
- 'diff --git a/test.lua b/test.lua',
- 'index abc1234..def5678 100644',
- '--- a/test.lua',
- '+++ b/test.lua',
- '@@ -1,1 +1,2 @@',
- ' local x = 1',
- '+local y = 2',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal('test.lua', hunks[1].filename)
- delete_buffer(bufnr)
- end)
-
- it('neogit new file with diff containing new file mode metadata', function()
- local bufnr = create_buffer({
- 'new file src/foo.lua',
- 'diff --git a/src/foo.lua b/src/foo.lua',
- 'new file mode 100644',
- 'index 0000000..abc1234',
- '--- /dev/null',
- '+++ b/src/foo.lua',
- '@@ -0,0 +1,3 @@',
- '+local M = {}',
- '+M.x = 1',
- '+return M',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal('src/foo.lua', hunks[1].filename)
- assert.are.equal('lua', hunks[1].ft)
- assert.are.equal(3, #hunks[1].lines)
- delete_buffer(bufnr)
- end)
-
- it('neogit deleted with diff containing deleted file mode metadata', function()
- local bufnr = create_buffer({
- 'deleted src/old.lua',
- 'diff --git a/src/old.lua b/src/old.lua',
- 'deleted file mode 100644',
- 'index abc1234..0000000',
- '--- a/src/old.lua',
- '+++ /dev/null',
- '@@ -1,2 +0,0 @@',
- '-local M = {}',
- '-return M',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal('src/old.lua', hunks[1].filename)
- assert.are.equal('lua', hunks[1].ft)
- assert.are.equal(2, #hunks[1].lines)
- delete_buffer(bufnr)
- end)
-
- it('multiple new files with mode metadata do not corrupt filenames', function()
- local bufnr = create_buffer({
- 'diff --git a/a.lua b/a.lua',
- 'new file mode 100644',
- 'index 0000000..abc1234',
- '--- /dev/null',
- '+++ b/a.lua',
- '@@ -0,0 +1,1 @@',
- '+local a = 1',
- 'diff --git a/b.lua b/b.lua',
- 'new file mode 100644',
- 'index 0000000..def5678',
- '--- /dev/null',
- '+++ b/b.lua',
- '@@ -0,0 +1,1 @@',
- '+local b = 2',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(2, #hunks)
- assert.are.equal('a.lua', hunks[1].filename)
- assert.are.equal('b.lua', hunks[2].filename)
- delete_buffer(bufnr)
- end)
-
- it('fugitive status with new and deleted files containing mode metadata', function()
- local bufnr = create_buffer({
- 'Head: main',
- '',
- 'Staged (2)',
- 'A src/new.lua',
- 'diff --git a/src/new.lua b/src/new.lua',
- 'new file mode 100644',
- 'index 0000000..abc1234',
- '--- /dev/null',
- '+++ b/src/new.lua',
- '@@ -0,0 +1,2 @@',
- '+local M = {}',
- '+return M',
- 'D src/old.lua',
- 'diff --git a/src/old.lua b/src/old.lua',
- 'deleted file mode 100644',
- 'index abc1234..0000000',
- '--- a/src/old.lua',
- '+++ /dev/null',
- '@@ -1,1 +0,0 @@',
- '-local x = 1',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(2, #hunks)
- assert.are.equal('src/new.lua', hunks[1].filename)
- assert.are.equal('lua', hunks[1].ft)
- assert.are.equal('src/old.lua', hunks[2].filename)
- assert.are.equal('lua', hunks[2].ft)
- delete_buffer(bufnr)
- end)
-
- it('neogit new file with deep nested path', function()
- local bufnr = create_buffer({
- 'new file src/deep/nested/path/module.lua',
- '@@ -0,0 +1,1 @@',
- '+return {}',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal('src/deep/nested/path/module.lua', hunks[1].filename)
- delete_buffer(bufnr)
- end)
-
- it('detects bare filename for untracked files', function()
- local bufnr = create_buffer({
- 'newfile.rs',
- '@@ -0,0 +1,3 @@',
- '+fn main() {',
- '+ println!("hello");',
- '+}',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal('newfile.rs', hunks[1].filename)
- assert.are.equal(3, #hunks[1].lines)
- delete_buffer(bufnr)
- end)
-
- it('does not match section headers as bare filenames', function()
- local bufnr = create_buffer({
- 'Untracked files (1)',
- 'newfile.rs',
- '@@ -0,0 +1,3 @@',
- '+fn main() {',
- '+ println!("hello");',
- '+}',
- })
- local hunks = parser.parse_buffer(bufnr)
-
- assert.are.equal(1, #hunks)
- assert.are.equal('newfile.rs', hunks[1].filename)
+ assert.is_nil(hunks[1].repo_root)
delete_buffer(bufnr)
end)
end)
diff --git a/vim.toml b/vim.toml
new file mode 100644
index 0000000..3a84ac2
--- /dev/null
+++ b/vim.toml
@@ -0,0 +1,33 @@
+[selene]
+base = "lua51"
+name = "vim"
+
+[vim]
+any = true
+
+[jit]
+any = true
+
+[bit]
+any = true
+
+[assert]
+any = true
+
+[describe]
+any = true
+
+[it]
+any = true
+
+[before_each]
+any = true
+
+[after_each]
+any = true
+
+[spy]
+any = true
+
+[stub]
+any = true
diff --git a/vim.yaml b/vim.yaml
deleted file mode 100644
index 3821d25..0000000
--- a/vim.yaml
+++ /dev/null
@@ -1,26 +0,0 @@
----
-base: lua51
-name: vim
-lua_versions:
- - luajit
-globals:
- vim:
- any: true
- jit:
- any: true
- assert:
- any: true
- describe:
- any: true
- it:
- any: true
- before_each:
- any: true
- after_each:
- any: true
- spy:
- any: true
- stub:
- any: true
- bit:
- any: true