## Problem
`highlight_hunk` applied DiffsClear extmarks across 5 scattered sites
with
ad-hoc column arithmetic. This fragmentation produced the 1-column
DiffsClear
gap on email-quoted body context lines (#142 issue 1). A redundant
`highlight_hunk_vim_syntax` function duplicated the inline vim syntax
path,
and the deferred pass in init.lua double-called it, creating duplicate
scratch
buffers and extmarks.
## Solution
Reorganize `highlight_hunk` into two clean phases:
- **Phase 1** — multi-line syntax computation (treesitter, vim syntax,
diff
grammar, header context text). Sets syntax extmarks only, no DiffsClear.
- **Phase 2** — per-line chrome (DiffsClear, backgrounds, gutter,
overlays,
intra-line). All non-syntax extmarks consolidated in one pass.
Hoist `new_code` to function scope (needed by `highlight_text` outside
the
`use_ts` block). Hoist `at_raw_line` so Phase 1d and Phase 2b share one
`nvim_buf_get_lines` call.
Delete `highlight_hunk_vim_syntax` (redundant with inline path). Remove
the
double-call from the deferred pass in init.lua.
Extend body prefix DiffsClear `end_col` from `qw` to `pw + qw`, fixing
the
1-column gap where native treesitter background bled through on context
lines
in email-quoted diffs (#142 issue 1).
### Email-quoted diff support
The parser now strips `> ` (and `>> `, etc.) email quote prefixes before
pattern matching, enabling syntax highlighting for diffs embedded in
email
replies and `git-send-email` / sourcehut-style patch review threads.
Each hunk stores `quote_width` so the highlight pipeline can apply
`DiffsClear` at the correct column offsets to suppress native treesitter
on quoted regions.
Closes#141
### #142 status after this PR
| Sub-issue | Status |
|-----------|--------|
| 1. Col gap on context lines | Fixed |
| 2. Bare `>` context lines | Improved, edge case remains |
| 3. Diff prefix marker fg | Not addressed (follow-up) |
## Problem
Repeatedly toggling `=` in fugitive left green gutter
(`number_hl_group`)
extmarks on lines between sections. When fugitive collapses a diff
section,
Neovim compresses extmarks from deleted lines onto the next surviving
line
(the `M ...` file entry). Two issues prevented cleanup:
1. `carry_forward_highlighted` returned `{}` (truthy in Lua) when zero
hunks
matched, so `pending_clear` stayed `false` and the compressed extmarks
were never cleared.
2. The `nvim_buf_clear_namespace` call in `on_buf`'s `pending_clear`
path was
removed in 2feb8a8, so even when `pending_clear` was `true` the extmarks
survived.
## Solution
Return `nil` from `carry_forward_highlighted` when no hunks were carried
forward (`next(highlighted) == nil`), so `pending_clear` is correctly
set to
`true`. Restore `nvim_buf_clear_namespace` in `on_buf`'s `pending_clear`
block. Add `process_pending_clear` test helper and spec coverage.
## Problem
Toggling large diffs via fugitive's `=` caused the top of the buffer to
re-render and glitch. `ensure_cache` always created a new cache entry
with
`pending_clear=true` and `highlighted={}`, forcing `on_win` to clear and
re-highlight every visible hunk — including stable ones above the toggle
point that never changed.
## Solution
On reparse, compare old and new hunk lists using a prefix + suffix
matching
strategy. Hunks that match (same filename, line count, and sampled
content)
carry forward their `highlighted` state so `on_win` skips them.
Comparison
is O(1) per hunk. Only runs when the old entry had
`pending_clear=false`;
`invalidate_cache`/`ColorScheme` paths still force full re-highlight.
Closes#131
## Problem
Regressions #119 and #120 showed the test suite had no coverage of the
decoration provider cache pipeline, no end-to-end pipeline tests from
buffer content to extmarks, and no Neogit-specific integration tests.
## Solution
Adds three new spec files (28 new tests, 316 total):
- `spec/decoration_provider_spec.lua` — indirect cache pipeline tests
via `_test` table: `ensure_cache` population, content fingerprint guard,
`pending_clear` semantics, BufWipeout cleanup
- `spec/integration_spec.lua` — full pipeline: diff buffer → `attach` →
extmarks; verifies `DiffsAdd`/`DiffsDelete` on correct lines, treesitter
captures, multi-hunk coverage
- `spec/neogit_integration_spec.lua` — `neogit_disable_hunk_highlight`
behavior, NeogitStatus/NeogitDiffView attach and cache population,
parser neogit filename patterns
Depends on #133.
Closes#122
## Problem
The default conflict navigation keymaps `]x`/`[x` are non-standard. Vim
natively uses `]c`/`[c` for diff navigation, so the same keys are far
more
intuitive for conflict jumping.
## Solution
Change the defaults for `conflict.keymaps.next` and
`conflict.keymaps.prev`
to `]c` and `[c`. This is a breaking change for users relying on the
previous
defaults without explicit configuration.
## Problem
Git diff metadata lines like "new file mode 100644" and "deleted file
mode 100644" matched the neogit "new file" and "deleted" filename
patterns in the parser, corrupting the current filename and breaking
syntax highlighting for subsequent hunks.
Closes#120
## Solution
Add negative guards so "new file mode" and "deleted file mode" lines
are skipped before the neogit filename capture runs. The guard must
evaluate before the capture due to Lua's and/or short-circuit semantics
— otherwise the and-operator returns true instead of the captured
string.
Added 16 parser tests covering all neogit filename patterns, all git
diff extended header lines that could collide, and integration scenarios
with mixed neogit status + diff metadata buffers.
## Problem
Users had to pass `enabled = true` or `enabled = false` inside
fugitive/neogit config tables, which was redundant — table presence
already implied the integration should be active.
## Solution
Remove the `enabled` field from the public API. Table presence now
implies enabled, `false` disables, `true` expands to sub-defaults.
The `enabled` field is still accepted for backward compatibility.
Added 20 `compute_filetypes` tests covering all config shapes (true,
false, table, nil, backward-compat enabled field). Updated docs
and type annotations.
## Problem
The neogit commit (3d640c2) switched line background extmarks from
`hl_group`+`hl_eol` to `line_hl_group`. Due to [neovim#31151][1],
`line_hl_group` bg overrides `hl_group` bg regardless of extmark
priority. This made `DiffsAddText`/`DiffsDeleteText` intra-line
highlights invisible beneath line backgrounds — the extmarks were
placed correctly but Neovim rendered the line bg on top.
[1]: https://github.com/neovim/neovim/issues/31151
## Solution
Revert line backgrounds to `hl_group`+`hl_eol` where priority stacking
works correctly. Keep `number_hl_group` in a separate point extmark to
prevent gutter color bleeding to adjacent lines. The Neogit highlight
override (clearing their groups to `{}`) is independent and unaffected.
## TODO
1. docs (vimdoc + readme) - this is a non-trivial feature
2. push luarocks version
## Problem
diffs.nvim only activates on `fugitive`, `git`, and `gitcommit`
filetypes.
Neogit uses its own custom filetypes (`NeogitStatus`,
`NeogitCommitView`,
`NeogitDiffView`) and doesn't set `b:git_dir`, so the plugin never
attaches
and repo root resolution fails for filetype detection within diff hunks.
## Solution
Two changes:
1. **`lua/diffs/init.lua`** — Add the three Neogit filetypes to the
default
`filetypes` list. The `FileType` autocmd in `plugin/diffs.lua` already
handles them correctly since the `is_fugitive_buffer` guard only applies
to the `git` filetype.
2. **`lua/diffs/parser.lua`** — Add a CWD-based fallback in
`get_repo_root()`.
After the existing `b:diffs_repo_root` and `b:git_dir` checks, fall back
to
`vim.fn.getcwd()` via `git.get_repo_root()` (already cached). Without
this,
the parser can't resolve filetypes for files in Neogit buffers.
Neogit's expanded diffs use standard unified diff format, so the parser
handles
them without modification.
Closes#110.
## Problem
Fugitive shows combined diffs (`@@@` headers, 2-character prefixes like
`++`, ` +`, `+ `) for unmerged (`UU`) files. The parser and highlight
pipeline assumed unified diff format (`@@`, 1-char prefix), causing:
- Prefix concealment only hiding 1 of 2 prefix chars
- Missing background colors on ` +` and `+ ` lines (first char is space
→ misclassified as context)
- No treesitter highlights (extra prefix char poisoned code arrays)
- `U` file header not recognized by parser (missing from filename
pattern)
## Solution
Detect prefix width from leading `@` count in hunk headers (`@@` → 1,
`@@@` → 2). Propagate `prefix_width` through the pipeline:
- **Parser**: new `prefix_width` field on `diffs.Hunk`, `U` added to
filename pattern, combined diff range extraction
- **Highlight**: prefix stripping, `col_offset`, concealment width, and
line classification all use `prefix_width`
- **Intra-line**: skipped for combined diffs (`prefix_width > 1`) since
2-char prefix semantics don't produce meaningful change groups
## Problem
Three minor issues remain before the v0.2.0 release:
1. Git quotes filenames containing spaces, unicode, or special
characters
in the fugitive status buffer. `parse_file_line` passed the quotes
through verbatim, causing file-not-found errors on diff operations.
2. Navigation wrap-around in both conflict and merge modules was silent,
giving no indication when jumping past the last/first item back to the
beginning/end.
3. `resolved_hunks` and `(resolved)` virtual text in the merge module
persisted across buffer re-reads, showing stale markers for hunks that
were no longer resolved.
## Solution
1. Add an `unquote()` helper to fugitive.lua that strips surrounding
quotes and unescapes `\\`, `\"`, `\n`, `\t`, and octal `\NNN`
sequences. Applied to both return paths in `parse_file_line`.
2. Add `vim.notify` before the wrap-around jump in all four navigation
functions (`goto_next`/`goto_prev` in conflict.lua and merge.lua).
3. Clear `resolved_hunks[bufnr]` and the merge namespace at the top of
`setup_keymaps` so each buffer init starts fresh.
Closes#66
## Problem
Conflict resolution virtual text only showed plain "current" /
"incoming"
labels with no keymap hints. Users had no way to discover available
resolution keymaps without reading docs.
## Solution
Default virtual text labels now include keymap hints: `(current — doo)`
and
`(incoming — dot)`. A new `format_virtual_text` config option lets users
customize or hide labels entirely. A new `show_actions` option (off by
default) renders a codelens-style action line above each `<<<<<<<`
marker
listing all enabled resolution keymaps. Merge diff views also gain hunk
hints on `@@` header lines showing available keymaps.
New config fields: `conflict.format_virtual_text` (function|nil),
`conflict.show_actions` (boolean). New highlight group:
`DiffsConflictActions`.
## Problem
Pressing `du` on a `UU` (unmerged) file in the fugitive status buffer
had no
effect. There was no way to see a proper ours-vs-theirs diff with syntax
highlighting and intra-line changes, or to resolve conflicts from within
a
unified diff view.
Additionally, pressing `du` on a section header containing only unmerged
files
showed "no changes in section" because `git diff` produces combined
(`diff --cc`)
output for unmerged files, which was stripped entirely.
## Solution
Fetch `:2:` (ours) and `:3:` (theirs) from the git index and generate a
standard
unified diff. The existing highlight pipeline (treesitter + intra-line)
applies
automatically. Resolution keymaps (`doo`/`dot`/`dob`/`don`) on hunks in
the diff
view write changes back to the working file's conflict markers.
Navigation
(`]x`/`[x`) jumps between unresolved conflict hunks.
For section diffs, combined diff entries are now replaced with generated
ours-vs-theirs unified diffs instead of being stripped.
Works for merge, cherry-pick, and rebase conflicts — git populates
`:2:`/`:3:`
the same way for all three.
Closes#61
Problem: resolving the last conflict called M.detach(), which cleared
attached_buffers[bufnr]. The TextChanged callback then returned true,
permanently deleting the autocmd. Undo restored conflict markers but
nothing re-highlighted or re-suppressed diagnostics.
Solution: inline the cleanup in refresh() instead of calling detach().
Keep attached_buffers set so the autocmd survives. Re-suppress
diagnostics when conflicts reappear after undo.
Problem: when git hits a merge conflict, users stare at raw <<<<<<<
markers with broken treesitter and noisy LSP diagnostics. Existing
solutions (git-conflict.nvim) use their own highlighting rather than
integrating with diffs.nvim's color blending pipeline.
Solution: add conflict.lua module that detects <<<<<<</=======/>>>>>>>
markers (with diff3 ||||||| support), highlights ours/theirs/base
regions with blended DiffsConflict* highlight groups, provides
resolution keymaps (doo/dot/dob/don) and navigation (]x/[x),
suppresses diagnostics while markers are present, and auto-detaches
when all conflicts are resolved. Fires DiffsConflictResolved user
event on last resolution.
Problem: diffs:// buffers could trigger spurious LSP diagnostics,
opening multiple diffs from fugitive created redundant splits, and
there was no quick way to close diff windows.
Solution: disable diagnostics on diff buffers, reuse existing
diffs:// windows in the tabpage instead of creating new splits,
and add a buffer-local q keymap to close diff windows.
Problem: the header context string (e.g. "function M.setup()") was
parsed in isolation by treesitter, which couldn't recognize "function"
as @keyword.function because the snippet is an incomplete definition
with no body or "end".
Solution: append the already-built new_code lines as trailing context
when parsing the header string, giving treesitter a complete function
definition. Filter captures to row 0 only so body-line captures don't
produce extmarks on the header line.
Problem: hl_eol requires a multiline extmark to extend the background
past end-of-line. The extmark used end_col = line_len on the same
row, so it was single-line and hl_eol was effectively a no-op.
Solution: replace end_col with end_row targeting the next line so
the extmark is multiline and hl_eol takes effect. Split
number_hl_group into a separate extmark to prevent it bleeding to
adjacent lines.
Problem: highlight_treesitter only iterated captures from the base
language tree (trees[1]:root()). When code contained injected
languages (e.g. VimL inside vim.cmd()), those captures were never
read, so injected code got no syntax highlighting in diffs.
Solution: pass true to parse() to trigger injection discovery, then
use parser_obj:for_each_tree() to iterate all trees including
injected ones. Each tree gets its own highlights query looked up by
ltree:lang(), and @spell/@nospell captures are filtered out.
Problem: highlights.context was a plain integer, inconsistent with the
table structure used by treesitter, vim, and intra sub-configs.
Solution: change to { enabled = true, lines = 25 } with full
vim.validate() coverage matching the existing pattern.
Problem: treesitter parses each diff hunk in isolation, so incomplete
syntax constructs at hunk boundaries (e.g., a function definition with
no body) produce ERROR nodes and drop captures.
Solution: read N lines from the on-disk file before/after each hunk and
prepend/append them as unmapped padding lines. The line_map guard in
highlight_treesitter skips extmarks for unmapped lines, so padding
provides syntax context without visual output. Controlled by
highlights.context (default 25, 0 to disable). Also applies to the vim
syntax fallback path via a leading_offset filter.
Problem: highlight_treesitter concatenated all hunk lines (context, -,
+) into a single string. Mixed old/new code produced invalid syntax
(e.g. two return statements), causing treesitter error recovery to drop
captures on lines after the syntax error.
Solution: split hunk lines into two versions — new (context + added)
and old (context + deleted) — each parsed independently. Use a line_map
to resolve treesitter row indices to buffer lines, with the old version
only mapping deleted lines to avoid duplicate extmarks on context.
Also fixes three related issues exposed by the improved TS coverage:
- Replace Normal extmark with DiffsClear (explicit fg from Normal.fg).
Normal in extmarks doesn't reliably override vim :syntax foreground.
- Reorder priority stack to DiffsClear(198) < syntax(199) < line
bg(200) < char bg(201). TS capture groups can carry colorscheme
backgrounds that would override diff line backgrounds at higher
priority.
- Gate DiffsClear on per-line coverage tracking. Only clear fugitive
syntax fg on lines where TS/vim actually produced captures, preventing
force-clearing on lines where error recovery drops captures.
Problem: running :e on a :Gdiff buffer cleared all content because
diffs:// buffers had no BufReadCmd handler. Neovim tried to read the
buffer name as a file path, found nothing on disk, and emptied the
buffer. This affected all three buffer creation paths (gdiff,
gdiff_file, gdiff_section).
Solution: register a BufReadCmd autocmd for diffs://* that parses the
URL and regenerates diff content from git. Change buffer options from
nofile/wipe to nowrite/delete (matching fugitive's approach) so
buffer-local autocmds and variables survive across unload/reload
cycles. Store old filepath as buffer variable for rename support.
'default' inherits algorithm and linematch from diffopt, 'vscode' uses
the FFI library. Removes the need for diffs.nvim to duplicate settings
that users already control globally.
line_hl_group bg occupies a separate rendering channel from hl_group in
Neovim's extmark system, causing character-level bg-only highlights to be
invisible regardless of priority. Switching to hl_group + hl_eol ensures
all backgrounds compete in the same channel.
Also reorders priorities (Normal 198 < line bg 199 < syntax 200 < char
bg 201), bumps char-level blend alpha from 0.4 to 0.7 for visibility,
and adds debug logging throughout the intra pipeline.
Line-level backgrounds (DiffsAdd/DiffsDelete) now get a second tier:
changed characters within modified lines receive an intense background
overlay (DiffsAddText/DiffsDeleteText at 70% alpha vs 40% for lines).
Treesitter foreground colors show through since the extmarks only set bg.
diff.lua extracts contiguous -/+ change groups from hunk lines and diffs
each group byte-by-byte using vim.diff(). An optional libvscodediff FFI
backend (lib.lua) auto-downloads the .so from codediff.nvim releases and
falls back to native if unavailable.
New config: highlights.intra.{enabled, algorithm, max_lines}. Gated by
max_lines (default 200) to avoid stalling on huge hunks. Priority 201
sits above treesitter (200) so the character bg always wins.
Closes#60
When a file has no extension but contains a shebang (e.g., `#!/bin/bash`),
filetype detection now reads the first 10 lines from disk and uses
`vim.filetype.match({ filename, contents })` for content-based detection.
This enables syntax highlighting for files like `build` scripts that rely
on shebang detection, even when the file isn't open in a buffer.
Detection order:
1. Existing buffer's filetype (already implemented in #69)
2. File content (shebang/modeline) - NEW
3. Filename extension only
Also adds `filetype on` to test helpers to ensure `vim.g.ft_ignore_pat`
is set, which is required for shell detection.
When pressing `du`/`dU` from a hunk line in the fugitive status buffer
(after expanding with `=`), the unified diff now opens at the
corresponding line instead of line 1.
Implementation:
- `fugitive.get_hunk_position()` returns @@ header and offset when on a hunk line
- `commands.find_hunk_line()` finds matching @@ header in diff buffer
- `commands.gdiff_file()` accepts optional `hunk_position` and jumps after opening
Also updates @phanen's README credit for the previous two fixes.
Closes#65
Files detected via shebang or modeline (e.g., `build` with `#!/bin/bash`)
weren't getting syntax highlighting because `vim.filetype.match()` only
does filename-based detection.
Now `get_ft_from_filename()` first checks if a buffer already exists for
the file and uses its detected filetype. This requires knowing the repo
root to construct the full path, so:
- `parse_buffer()` reads `b:diffs_repo_root` or `b:git_dir` (fugitive)
- `commands.lua` sets `b:diffs_repo_root` on diff buffers it creates
Co-authored-by: phanen <phanen@qq.com>
Previously, hunks were discarded entirely if vim.filetype.match()
returned nil. This meant files with unrecognized extensions got no
highlighting at all - not even the basic green/red backgrounds for
added/deleted lines.
Remove the (current_lang or current_ft) condition from flush_hunk()
so all hunks are collected. highlight_hunk() already handles the
case where ft/lang are nil by skipping syntax highlighting but still
applying background colors.
Co-authored-by: phanen <phanen@qq.com>
Parse both old and new filenames from rename lines (R old -> new).
When diffing staged renames, use old filename as base to correctly
show content changes rather than treating the file as entirely new.
Also adds comprehensive tests for filename edge cases:
- Double extensions, hyphens, underscores, dotfiles
- Deep nested paths, complex renames
- Documents known limitation with filenames containing ' -> '
Section headers (Staged/Unstaged) now show all diffs in that section,
matching fugitive's behavior. Untracked files show as all-added diffs.
Deleted files show as all-removed diffs.
Also handles edge cases:
- Empty new/old content for deleted/new files
- Section header detection returns is_header flag
Compares current buffer against any git revision (default HEAD), opens result
with full diffs.nvim syntax highlighting. Follows fugitive convention:
:Gdiff/:Gvdiff open vertical split, :Ghdiff opens horizontal split.
Apply treesitter highlighting to diff metadata lines (diff --git, index,
---, +++) using the diff language parser. Header info is attached only
to the first hunk of each file to avoid duplicate highlighting.
Based on PR #52 by @phanen with fixes:
- header_lines now only contains diff metadata, not hunk content
- header info attached only to first hunk per file
- removed arbitrary hunk count restriction