*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 ============================================================================== 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, debounce_ms = 0, hide_prefix = false, highlights = { background = true, gutter = true, context = 25, treesitter = { enabled = true, max_lines = 500, }, vim = { enabled = false, max_lines = 500, }, intra = { enabled = true, algorithm = 'default', max_lines = 500, }, }, fugitive = { horizontal = 'du', vertical = 'dU', }, } < *diffs.Config* Fields: ~ {debug} (boolean, default: false) 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 leading diff character. When `highlights.background` is also enabled, the overlay inherits the line's background color. {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. *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. {context} (integer, default: 25) Number of lines to read from the source file before and after each hunk for syntax parsing context. Improves accuracy at hunk boundaries where incomplete constructs (e.g., a function definition with no body) would otherwise confuse the parser. Set to 0 to disable. Lines are read from disk with early exit — cost scales with the context value, not file size. {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. *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. 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. ============================================================================== FUGITIVE STATUS KEYMAPS *diffs-fugitive* When inside a vim-fugitive |:Git| status buffer, diffs.nvim provides keymaps to open unified diffs for files or entire sections. Keymaps: ~ *diffs-du* *diffs-dU* du Open unified diff in a horizontal split. dU Open unified diff in a vertical split. These keymaps work on: - File lines (e.g., `M src/foo.lua`) - opens diff for that file - Section headers (e.g., `Staged (3)`) - opens diff for all files in section - Hunk/context lines below a file - opens diff for the parent file Behavior by file status: ~ Status Section Base Current Result ~ M Unstaged index working tree unstaged changes M Staged HEAD index staged changes A Staged (empty) index file as all-added D Staged HEAD (empty) file as all-removed R Staged HEAD:oldname index:newname content diff ? Untracked (empty) working tree file as all-added On section headers, the keymap runs `git diff` (or `git diff --cached` for staged) and displays all changes in that section as a single unified diff. Untracked section headers show a warning since there is no meaningful diff. Configuration: ~ *diffs.FugitiveConfig* >lua vim.g.diffs = { fugitive = { horizontal = 'du', -- keymap for horizontal split, false to disable vertical = 'dU', -- keymap for vertical split, false to disable }, } < Fields: ~ {horizontal} (string|false, default: 'du') Keymap for unified diff in horizontal split. Set to `false` to disable. {vertical} (string|false, default: 'dU') Keymap for unified diff in vertical split. Set to `false` to disable. ============================================================================== 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 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: - 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()| - `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 4. Re-highlighting occurs on `TextChanged` (debounced) and `Syntax` events 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. To provide surrounding code context, diffs.nvim reads lines from disk before and after each hunk (controlled by `highlights.context`, default 25). 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 = 0` to disable context padding and restore the previous behavior. 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* 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. 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* 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 that may overlap with diffs.nvim's highlighting in conflict scenarios. If you experience visual conflicts, try disabling the conflicting plugin's diff-related features. ============================================================================== HIGHLIGHT GROUPS *diffs-highlights* diffs.nvim defines custom highlight groups. Fugitive unified diff groups use `default = true`, so colorschemes can override them. Diff mode groups are always derived from the corresponding `Diff*` groups. Fugitive unified diff highlights: ~ *DiffsAdd* 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 `diffAdded`, background from `DiffsAdd`. *DiffsDeleteNr* DiffsDeleteNr Line number for `-` lines. Foreground from `diffRemoved`, background from `DiffsDelete`. *DiffsAddText* DiffsAddText Character-level background for changed characters within `+` lines. Derived by blending `diffAdded` foreground with `Normal` background at 70% alpha. Only sets `bg`, so treesitter foreground colors show through. *DiffsDeleteText* DiffsDeleteText Character-level background for changed characters within `-` lines. Derived by blending `diffRemoved` foreground with `Normal` background at 70% alpha. 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' }) < ============================================================================== 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 ============================================================================== vim:tw=78:ts=8:ft=help:norl: