*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 = { enabled = true, lines = 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} (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. *diffs.ContextConfig* Context config fields: ~ {enabled} (boolean, default: true) Read lines from disk before and after each hunk to provide surrounding syntax context. Improves accuracy at hunk boundaries where incomplete constructs (e.g., a function definition with no body) would otherwise confuse the parser. {lines} (integer, default: 25) Number of context lines to read in each direction. Lines are read with early exit — cost scales with this value, not file size. *diffs.TreesitterConfig* 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 (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* 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: