*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, treesitter = { enabled = true, max_lines = 500, }, vim = { enabled = false, max_lines = 200, }, intra = { enabled = true, algorithm = 'auto', max_lines = 200, }, }, 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. {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: 'auto') Diff algorithm for character-level analysis. `'auto'`: use libvscodediff if available, else native `vim.diff()`. `'native'`: always use `vim.diff()`. `'vscode'`: require libvscodediff (falls back to native if not available). {max_lines} (integer, default: 200) 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()| - Background extmarks (`DiffsAdd`/`DiffsDelete`) at priority 198 - `Normal` extmarks at priority 199 clear underlying diff foreground - 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 without surrounding code context. When a hunk shows lines added to an existing block (e.g., adding a plugin inside `return { ... }`), the parser doesn't see the `return` statement and may produce incorrect or unusual highlighting. This is inherent to parsing code fragments. No diff tooling solves this problem without significant complexity—the parser simply doesn't have enough information to understand the full syntactic structure. 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 `DiffAdd` background with `Normal` at 70% alpha (brighter than line-level `DiffsAdd`). Only sets `bg`, so treesitter foreground colors show through. *DiffsDeleteText* DiffsDeleteText Character-level background for changed characters within `-` lines. Derived by blending `DiffDelete` background with `Normal` 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: