*diffs.nvim.txt* Syntax highlighting for diffs in Neovim Author: Barrett Ruth 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 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 - 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 - Optional diff prefix (`+`/`-`/` `) concealment - Gutter (line number) highlighting - Inline merge conflict marker detection, highlighting, and resolution ============================================================================== REQUIREMENTS *diffs-requirements* - Neovim 0.9.0+ - vim-fugitive (https://github.com/tpope/vim-fugitive) (optional, for unified diff syntax highlighting in |:Git| and commit views) - Treesitter parsers for languages you want highlighted Note: The diff mode feature (background-only colors for |:diffthis|, vimdiff, etc.) works without vim-fugitive. ============================================================================== SETUP *diffs-setup* Using lazy.nvim: >lua { 'barrettruth/diffs.nvim', dependencies = { 'tpope/vim-fugitive' }, } < The plugin works automatically with no configuration required. For customization, see |diffs-config|. ============================================================================== CONFIGURATION *diffs-config* Configuration is done via `vim.g.diffs`. Set this before the plugin loads: >lua vim.g.diffs = { debug = false, hide_prefix = false, filetypes = { 'fugitive', 'git', 'gitcommit' }, highlights = { background = true, gutter = true, blend_alpha = 0.6, context = { enabled = true, lines = 25, }, treesitter = { enabled = true, max_lines = 500, }, vim = { enabled = false, max_lines = 200, }, intra = { enabled = true, 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 = ']x', prev = '[x', }, }, } < *diffs.Config* Fields: ~ {debug} (boolean, default: false) Enable debug logging to |:messages| with `[diffs]` prefix. {hide_prefix} (boolean, default: false) Hide diff prefixes (`+`/`-`/` `) using virtual text overlay. Makes code appear without the leading diff character. When `highlights.background` is also enabled, the overlay inherits the line's background color. {filetypes} (table, default: {'fugitive','git','gitcommit'}) List of filetypes that trigger attachment. Add `'diff'` to enable highlighting in plain `.diff` and `.patch` files: >lua vim.g.diffs = { filetypes = { 'fugitive', 'git', 'gitcommit', '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. *diffs.Highlights* Highlights table fields: ~ {background} (boolean, default: true) Apply background highlighting to `+`/`-` lines using `DiffsAdd`/`DiffsDelete` groups (derived from `DiffAdd`/`DiffDelete` backgrounds). {gutter} (boolean, default: true) Highlight line numbers with matching colors. Only visible if line numbers are enabled. {blend_alpha} (number, default: 0.6) Alpha value for character-level blend intensity. Controls how strongly changed characters stand out from the line-level background. Must be between 0 and 1 (inclusive). Higher values produce more vivid character-level highlights. {context} (table, default: see below) Syntax parsing context options. See |diffs.ContextConfig| for fields. {treesitter} (table, default: see below) Treesitter highlighting options. See |diffs.TreesitterConfig| for fields. {vim} (table, default: see below) 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 after all computed groups without `default`, so overrides always win over both computed defaults and colorscheme definitions. *diffs.ContextConfig* Context config fields: ~ {enabled} (boolean, default: true) Read lines from disk before and after each hunk to provide surrounding syntax context. Improves accuracy at hunk boundaries where incomplete constructs (e.g., a function definition with no body) would otherwise confuse the parser. {lines} (integer, default: 25) Number of context lines to read in each direction. Lines are read with early exit — cost scales with this value, not file size. *diffs.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. *diffs.TreesitterConfig* Treesitter config fields: ~ {enabled} (boolean, default: true) Apply treesitter syntax highlighting to code. {max_lines} (integer, default: 500) Skip treesitter highlighting for hunks larger than this many lines. Prevents lag on massive diffs. *diffs.VimConfig* Vim config fields: ~ {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. Slower than treesitter but covers languages without a TS parser installed. {max_lines} (integer, default: 200) Skip vim syntax highlighting for hunks larger than this many lines. Lower than the treesitter default due to the per-character cost of |synID()|. *diffs.IntraConfig* Intra config fields: ~ {enabled} (boolean, default: true) Enable character-level diff highlighting within changed lines. When a line changes from `local x = 1` to `local x = 2`, only the `1`/`2` characters get an intense background overlay while the rest of the line keeps the softer line-level background. {algorithm} (string, default: 'default') Diff algorithm for character-level analysis. `'default'`: use |vim.diff()| with settings inherited from |'diffopt'| (`algorithm` and `linematch`). `'vscode'`: use libvscodediff FFI (falls back to default if not available). {max_lines} (integer, default: 500) Skip character-level highlighting for hunks larger than this many lines. Note: Header context (e.g., `@@ -10,3 +10,4 @@ func()`) is always highlighted with treesitter when a parser is available. Language detection uses Neovim's built-in |vim.filetype.match()| and |vim.treesitter.language.get_lang()|. To customize filetype detection or register treesitter parsers for custom filetypes, use |vim.filetype.add()| and |vim.treesitter.language.register()|. ============================================================================== COMMANDS *diffs-commands* :Gdiff [revision] *:Gdiff* Open a unified diff of the current file against a git revision. Displays in a horizontal split below the current window. The diff buffer shows `+`/`-` lines with full syntax highlighting for the code language, plus diff header highlighting for `diff --git`, `---`, `+++`, and `@@` lines. If a `diffs://` window already exists in the current tabpage, the new diff replaces its buffer instead of creating another split. Parameters: ~ {revision} (string, optional) Git revision to diff against. Defaults to HEAD. Examples: >vim :Gdiff " diff against HEAD :Gdiff main " diff against main branch :Gdiff HEAD~3 " diff against 3 commits ago :Gdiff abc123 " diff against specific commit < :Gvdiff [revision] *:Gvdiff* Like |:Gdiff| but opens in a vertical split. :Ghdiff [revision] *:Ghdiff* Like |:Gdiff| but explicitly opens in a horizontal split. ============================================================================== MAPPINGS *diffs-mappings* *(diffs-gdiff)* (diffs-gdiff) Show unified diff against HEAD in a horizontal split. Equivalent to |:Gdiff| with no arguments. *(diffs-gvdiff)* (diffs-gvdiff) Show unified diff against HEAD in a vertical split. Equivalent to |:Gvdiff| with no arguments. Example configuration: >lua vim.keymap.set('n', 'gd', '(diffs-gdiff)') vim.keymap.set('n', 'gD', '(diffs-gvdiff)') < *(diffs-conflict-ours)* (diffs-conflict-ours) Accept current (ours) change. Replaces the conflict block with ours content. *(diffs-conflict-theirs)* (diffs-conflict-theirs) Accept incoming (theirs) change. Replaces the conflict block with theirs content. *(diffs-conflict-both)* (diffs-conflict-both) Accept both changes (ours then theirs). *(diffs-conflict-none)* (diffs-conflict-none) Reject both changes (delete entire block). *(diffs-conflict-next)* (diffs-conflict-next) Jump to next conflict marker. Wraps around. *(diffs-conflict-prev)* (diffs-conflict-prev) Jump to previous conflict marker. Wraps around. Example configuration: >lua vim.keymap.set('n', 'co', '(diffs-conflict-ours)') 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', ']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://` buffers created by |:Gdiff|, |:Gvdiff|, |:Ghdiff|, or the fugitive status keymaps. ============================================================================== FUGITIVE STATUS KEYMAPS *diffs-fugitive* When inside a vim-fugitive |:Git| status buffer, diffs.nvim provides keymaps to open unified diffs for files or entire sections. Keymaps: ~ *diffs-du* *diffs-dU* du Open unified diff in a horizontal split. dU Open unified diff in a vertical split. These keymaps work on: - File lines (e.g., `M src/foo.lua`) - opens diff for that file - Section headers (e.g., `Staged (3)`) - opens diff for all files in section - Hunk/context lines below a file - opens diff for the parent file Behavior by file status: ~ Status Section Base Current Result ~ M Unstaged index working tree unstaged changes M Staged HEAD index staged changes A Staged (empty) index file as all-added D Staged HEAD (empty) file as all-removed R Staged HEAD:oldname index:newname content diff 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 staged) and displays all changes in that section as a single unified diff. Untracked section headers show a warning since there is no meaningful diff. Configuration: ~ *diffs.FugitiveConfig* >lua vim.g.diffs = { fugitive = { horizontal = 'du', -- keymap for horizontal split, false to disable vertical = 'dU', -- keymap for vertical split, false to disable }, } < Fields: ~ {horizontal} (string|false, default: 'du') Keymap for unified diff in horizontal split. Set to `false` to disable. {vertical} (string|false, default: 'dU') Keymap for unified diff in vertical split. Set to `false` to disable. ============================================================================== CONFLICT RESOLUTION *diffs-conflict* diffs.nvim detects inline merge conflict markers (`<<<<<<<`/`=======`/ `>>>>>>>`) in working files and provides highlighting and resolution keymaps. Both standard and diff3 (`|||||||`) formats are supported. Conflict regions are detected automatically on `BufReadPost` and re-scanned on `TextChanged`. When all conflicts in a buffer are resolved, highlighting is removed and diagnostics are re-enabled. Configuration: ~ *diffs.ConflictConfig* >lua vim.g.diffs = { conflict = { enabled = true, disable_diagnostics = true, show_virtual_text = true, show_actions = false, priority = 200, keymaps = { ours = 'doo', theirs = 'dot', both = 'dob', none = 'don', next = ']x', prev = '[x', }, }, } < Fields: ~ {enabled} (boolean, default: true) Enable conflict marker detection and resolution. Set to `false` to disable entirely. {disable_diagnostics} (boolean, default: true) Suppress LSP diagnostics on buffers with conflict markers. Markers produce syntax errors that clutter the diagnostic list. Diagnostics are re-enabled when all conflicts are resolved. Set `false` to leave diagnostics alone. {show_virtual_text} (boolean, default: true) Show `(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. {keymaps} (table, default: see above) Buffer-local keymaps for conflict resolution and navigation. Each value accepts a string (custom key) or `false` (disabled). *diffs.ConflictKeymaps* Keymap fields: ~ {ours} (string|false, default: 'doo') Accept current (ours) change. {theirs} (string|false, default: 'dot') Accept incoming (theirs) change. {both} (string|false, default: 'dob') Accept both changes (ours then theirs). {none} (string|false, default: 'don') Reject both changes (delete entire block). {next} (string|false, default: ']x') Jump to next conflict marker. Wraps around. {prev} (string|false, default: '[x') Jump to previous conflict marker. Wraps around. User events: ~ *DiffsConflictResolved* DiffsConflictResolved Fired when the last conflict in a buffer is resolved. Useful for triggering custom actions (e.g., auto-staging the file). >lua vim.api.nvim_create_autocmd('User', { pattern = 'DiffsConflictResolved', callback = function() print('all conflicts resolved!') end, }) < ============================================================================== 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`/`]x`/`[x`) 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 - `]x`/`[x` 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. ============================================================================== API *diffs-api* attach({bufnr}) *diffs.attach()* Manually attach highlighting to a buffer. Called automatically for fugitive buffers via the `FileType fugitive` autocmd. Parameters: ~ {bufnr} (integer, optional) Buffer number. Defaults to current buffer. refresh({bufnr}) *diffs.refresh()* Manually refresh highlighting for a buffer. Useful after external changes or for debugging. Parameters: ~ {bufnr} (integer, optional) Buffer number. Defaults to current buffer. ============================================================================== IMPLEMENTATION *diffs-implementation* Summary / commit detail views: ~ 1. `FileType` autocmd for configured filetypes (see {filetypes}) triggers |diffs.attach()|. For `git` buffers, only `fugitive://` URIs are attached. 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: - Language is detected from the filename using |vim.filetype.match()| - Diff prefixes (`+`/`-`/` `) are stripped from code lines - 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 - 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 Diff mode views: ~ 1. `OptionSet diff` detects when any window enters diff mode 2. All `&diff` windows in the tabpage receive a window-local 'winhighlight' override that remaps `DiffAdd`/`DiffDelete`/`DiffChange`/`DiffText` to background-only variants, allowing existing treesitter highlighting to show through the diff colors ============================================================================== KNOWN LIMITATIONS *diffs-limitations* Incomplete Syntax Context ~ *diffs-syntax-context* Treesitter parses each diff hunk in isolation. Context lines within the hunk (lines with a ` ` prefix) 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. Syntax Highlighting Flash ~ *diffs-flash* When opening a fugitive buffer, there is an unavoidable visual "flash" where 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. Conflicting Diff Plugins ~ *diffs-plugin-conflicts* diffs.nvim may not interact well with other plugins that modify diff highlighting or the sign column in diff views. Known plugins that may conflict: - diffview.nvim (sindrets/diffview.nvim) Provides its own diff highlighting and conflict resolution UI. When using diffview.nvim for viewing diffs, you may want to disable diffs.nvim's diff mode attachment or use one plugin exclusively. - mini.diff (echasnovski/mini.diff) Visualizes buffer differences with its own highlighting system. May override or conflict with diffs.nvim's background highlighting. - gitsigns.nvim (lewis6991/gitsigns.nvim) Generally compatible for sign column decorations, but both plugins modifying line highlights may produce unexpected results. - git-conflict.nvim (akinsho/git-conflict.nvim) Provides conflict marker highlighting and resolution keymaps. diffs.nvim now has built-in conflict resolution (see |diffs-conflict|). Disable one or the other to avoid overlap. If you experience visual conflicts, try disabling the conflicting plugin's diff-related features. ============================================================================== HIGHLIGHT GROUPS *diffs-highlights* diffs.nvim defines custom highlight groups. All groups use `default = true`, so colorschemes can override them by defining the group before the plugin loads. All derived groups are computed by alpha-blending a source color into the `Normal` background. Line-level groups blend at 40% alpha for a subtle tint; character-level groups blend at 60% for more contrast. Line-number groups combine both: background from the line-level blend, foreground from the character-level blend. Fugitive unified diff highlights: ~ *DiffsAdd* DiffsAdd Background for `+` lines. Derived by blending `DiffAdd` background with `Normal` at 40% alpha. *DiffsDelete* DiffsDelete Background for `-` lines. Derived by blending `DiffDelete` background with `Normal` at 40% alpha. *DiffsAddNr* DiffsAddNr Line number for `+` lines. Foreground from `DiffsAddText`, background from `DiffsAdd`. *DiffsDeleteNr* DiffsDeleteNr Line number for `-` lines. Foreground from `DiffsDeleteText`, background from `DiffsDelete`. *DiffsAddText* DiffsAddText Character-level background for changed characters within `+` lines. Derived by blending `diffAdded` foreground with `Normal` background at 60% alpha. Only sets `bg`, so treesitter foreground colors show through. *DiffsDeleteText* DiffsDeleteText Character-level background for changed characters within `-` lines. Derived by blending `diffRemoved` foreground with `Normal` background at 60% alpha. Conflict highlights: ~ *DiffsConflictOurs* DiffsConflictOurs Background for "ours" (current) content lines. Derived by blending `DiffAdd` background with `Normal` at 40% alpha (green tint). *DiffsConflictTheirs* DiffsConflictTheirs Background for "theirs" (incoming) content lines. Derived by blending `DiffChange` background with `Normal` at 40% alpha. *DiffsConflictBase* DiffsConflictBase Background for base (ancestor) content lines in diff3 conflicts. Derived by blending `DiffText` background with `Normal` at 30% alpha (muted). *DiffsConflictMarker* DiffsConflictMarker Dimmed foreground with bold for `<<<<<<<`, `=======`, `>>>>>>>`, and `|||||||` marker lines. *DiffsConflictOursNr* DiffsConflictOursNr Line number for "ours" content lines. Foreground from higher-alpha blend, background from line-level blend. *DiffsConflictTheirsNr* DiffsConflictTheirsNr Line number for "theirs" content lines. *DiffsConflictBaseNr* DiffsConflictBaseNr Line number for base content lines (diff3). *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. *DiffsDiffAdd* DiffsDiffAdd Background-only. Derived from `DiffAdd` bg. Treesitter provides foreground syntax highlighting. *DiffsDiffDelete* DiffsDiffDelete Foreground and background from `DiffDelete`. Used for filler lines (`/////`) which have no real code content to highlight. *DiffsDiffChange* DiffsDiffChange Background-only. Derived from `DiffChange` bg. Treesitter provides foreground syntax highlighting. *DiffsDiffText* DiffsDiffText Background-only. Derived from `DiffText` bg. Treesitter provides foreground syntax highlighting. To customize these in your colorscheme: >lua vim.api.nvim_set_hl(0, 'DiffsAdd', { bg = '#2e4a3a' }) vim.api.nvim_set_hl(0, 'DiffsDiffDelete', { link = 'DiffDelete' }) < Or via `highlights.overrides` in config: >lua vim.g.diffs = { highlights = { overrides = { DiffsAdd = { bg = '#2e4a3a' }, DiffsDiffDelete = { link = 'DiffDelete' }, }, }, } < ============================================================================== HEALTH CHECK *diffs-health* Run |:checkhealth| diffs to verify your setup. Checks performed: - Neovim version >= 0.9.0 - vim-fugitive is installed (optional) - libvscode_diff shared library is available (optional) ============================================================================== ACKNOWLEDGEMENTS *diffs-acknowledgements* - vim-fugitive (https://github.com/tpope/vim-fugitive) - codediff.nvim (https://github.com/esmuellert/codediff.nvim) - diffview.nvim (https://github.com/sindrets/diffview.nvim) - @phanen (https://github.com/phanen) - diff header highlighting, treesitter injection support ============================================================================== vim:tw=78:ts=8:ft=help:norl: