From de04381298483ab914759c5bf2fa277ae36ce457 Mon Sep 17 00:00:00 2001 From: Barrett Ruth <62671086+barrettruth@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:36:36 -0400 Subject: [PATCH] feat(highlight): warn when hunks exceed max_lines (#184) --- doc/diffs.nvim.txt | 52 +++++++++++++++++++++++++++++++++++------ lua/diffs/highlight.lua | 41 +++++++++++++++++++++----------- lua/diffs/init.lua | 22 +++++++++++++++++ lua/diffs/parser.lua | 2 ++ spec/highlight_spec.lua | 4 ++-- 5 files changed, 99 insertions(+), 22 deletions(-) diff --git a/doc/diffs.nvim.txt b/doc/diffs.nvim.txt index b65e3a6..a3c5c9c 100644 --- a/doc/diffs.nvim.txt +++ b/doc/diffs.nvim.txt @@ -93,6 +93,7 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads: background = true, gutter = true, blend_alpha = 0.6, + warn_max_lines = true, context = { enabled = true, lines = 25, @@ -249,6 +250,11 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads: (inclusive). Higher values produce more vivid backgrounds. + {warn_max_lines} (boolean, default: true) + Show a |vim.notify()| warning when syntax + highlighting is skipped because a hunk exceeds + {max_lines}. See |diffs-max-lines|. + {context} (table, default: see below) Syntax parsing context options. See |diffs.ContextConfig| for fields. @@ -322,8 +328,10 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads: 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. + Skip treesitter highlighting for hunks with more + highlighted lines (`+`/`-`) than this threshold. + Context lines are not counted. Prevents lag on + massive diffs. *diffs.VimConfig* Vim config fields: ~ @@ -338,9 +346,11 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads: parser installed (e.g., COBOL, Fortran). {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()|. + Skip vim syntax highlighting for hunks with more + highlighted lines (`+`/`-`) than this threshold. + Context lines are not counted. Lower than the + treesitter default due to the per-character cost + of |synID()|. *diffs.IntraConfig* Intra config fields: ~ @@ -359,8 +369,9 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads: (falls back to default if not available). {max_lines} (integer, default: 500) - Skip character-level highlighting for hunks larger - than this many lines. + Skip character-level highlighting for hunks with + more highlighted lines (`+`/`-`) than this + threshold. Context lines are not counted. Note: Header context (e.g., `@@ -10,3 +10,4 @@ func()`) is always highlighted with treesitter when a parser is available. @@ -370,6 +381,33 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads: or register treesitter parsers for custom filetypes, use |vim.filetype.add()| and |vim.treesitter.language.register()|. +============================================================================== +MAX LINES *diffs-max-lines* + +When a hunk contains more highlighted lines (`+`/`-`) than the configured +threshold, diffs.nvim skips syntax highlighting for that hunk to avoid lag. +Context lines (lines with a space prefix) are not counted toward the limit. + +A warning is shown when this happens: > + [diffs.nvim]: Syntax highlighting skipped for 1 hunk(s) — too large. +< +To increase the threshold: >lua + vim.g.diffs = { + highlights = { + treesitter = { max_lines = 1000 }, -- default: 500 + vim = { max_lines = 500 }, -- default: 200 + }, + } +< +To suppress the warning without changing the threshold: >lua + vim.g.diffs = { + highlights = { warn_max_lines = false }, + } +< +The `intra.max_lines` threshold (default: 500) is separate and controls +character-level diff highlighting within changed lines. It does not affect +the syntax highlighting warning. + ============================================================================== COMMANDS *diffs-commands* diff --git a/lua/diffs/highlight.lua b/lua/diffs/highlight.lua index 434702d..ed9329b 100644 --- a/lua/diffs/highlight.lua +++ b/lua/diffs/highlight.lua @@ -310,16 +310,27 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) local use_vim = not use_ts and hunk.ft and opts.highlights.vim.enabled local max_lines = use_ts and opts.highlights.treesitter.max_lines or opts.highlights.vim.max_lines - if (use_ts or use_vim) and #hunk.lines > max_lines then - dbg( - 'skipping hunk %s:%d (%d lines > %d max)', - hunk.filename, - hunk.start_line, - #hunk.lines, - max_lines - ) - use_ts = false - use_vim = false + if use_ts or use_vim then + local hl_count = 0 + for _, line in ipairs(hunk.lines) do + local c = line:sub(1, 1) + if c == '+' or c == '-' then + hl_count = hl_count + 1 + end + end + hunk._hl_line_count = hl_count + if hl_count > max_lines then + dbg( + 'skipping hunk %s:%d (%d highlighted lines > %d max)', + hunk.filename, + hunk.start_line, + hl_count, + max_lines + ) + hunk._skipped_max_lines = true + use_ts = false + use_vim = false + end end if use_vim and opts.defer_vim_syntax then @@ -456,7 +467,7 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) and intra_cfg and intra_cfg.enabled and pw == 1 - and #hunk.lines <= intra_cfg.max_lines + and (hunk._hl_line_count or #hunk.lines) <= intra_cfg.max_lines then dbg('computing intra for hunk %s:%d (%d lines)', hunk.filename, hunk.start_line, #hunk.lines) intra = diff.compute_intra_hunks(hunk.lines, intra_cfg.algorithm) @@ -467,8 +478,12 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) end elseif intra_cfg and not intra_cfg.enabled then dbg('intra disabled by config') - elseif intra_cfg and #hunk.lines > intra_cfg.max_lines then - dbg('intra skipped: %d lines > %d max', #hunk.lines, intra_cfg.max_lines) + elseif intra_cfg and (hunk._hl_line_count or #hunk.lines) > intra_cfg.max_lines then + dbg( + 'intra skipped: %d highlighted lines > %d max', + hunk._hl_line_count or #hunk.lines, + intra_cfg.max_lines + ) end ---@type table diff --git a/lua/diffs/init.lua b/lua/diffs/init.lua index 474e066..1244a3a 100644 --- a/lua/diffs/init.lua +++ b/lua/diffs/init.lua @@ -26,6 +26,7 @@ ---@field gutter boolean ---@field blend_alpha? number ---@field overrides? table +---@field warn_max_lines boolean ---@field context diffs.ContextConfig ---@field treesitter diffs.TreesitterConfig ---@field vim diffs.VimConfig @@ -132,6 +133,7 @@ local default_config = { highlights = { background = true, gutter = true, + warn_max_lines = true, context = { enabled = true, lines = 25, @@ -203,6 +205,7 @@ local diff_windows = {} ---@field tick integer ---@field highlighted table ---@field pending_clear boolean +---@field warned_max_lines boolean ---@field line_count integer ---@field byte_count integer @@ -423,6 +426,7 @@ local function ensure_cache(bufnr) tick = tick, highlighted = carried or {}, pending_clear = not carried, + warned_max_lines = false, line_count = lc, byte_count = bc, } @@ -703,6 +707,7 @@ local function init() vim.validate('highlights.gutter', opts.highlights.gutter, 'boolean', true) vim.validate('highlights.blend_alpha', opts.highlights.blend_alpha, 'number', true) vim.validate('highlights.overrides', opts.highlights.overrides, 'table', true) + vim.validate('highlights.warn_max_lines', opts.highlights.warn_max_lines, 'boolean', true) vim.validate('highlights.context', opts.highlights.context, 'table', true) vim.validate('highlights.treesitter', opts.highlights.treesitter, 'table', true) vim.validate('highlights.vim', opts.highlights.vim, 'table', true) @@ -910,6 +915,7 @@ local function init() end local t0 = config.debug and vim.uv.hrtime() or nil local deferred_syntax = {} + local skipped_count = 0 local count = 0 for i = first, last do if not entry.highlighted[i] then @@ -923,6 +929,9 @@ local function init() highlight.highlight_hunk(bufnr, ns, hunk, fast_hl_opts) entry.highlighted[i] = true count = count + 1 + if hunk._skipped_max_lines then + skipped_count = skipped_count + 1 + end local has_syntax = hunk.lang and config.highlights.treesitter.enabled local needs_vim = not hunk.lang and hunk.ft and config.highlights.vim.enabled if has_syntax or needs_vim then @@ -930,6 +939,19 @@ local function init() end end end + if skipped_count > 0 and not entry.warned_max_lines and config.highlights.warn_max_lines then + entry.warned_max_lines = true + local n = skipped_count + vim.schedule(function() + vim.notify( + ( + '[diffs.nvim]: Syntax highlighting skipped for %d hunk(s) — too large.' + .. ' See :h diffs-max-lines to resolve or suppress this warning.' + ):format(n), + vim.log.levels.WARN + ) + end) + end if #deferred_syntax > 0 then local tick = entry.tick dbg('deferred syntax scheduled: %d hunks tick=%d', #deferred_syntax, tick) diff --git a/lua/diffs/parser.lua b/lua/diffs/parser.lua index aa63daf..8147e65 100644 --- a/lua/diffs/parser.lua +++ b/lua/diffs/parser.lua @@ -17,6 +17,8 @@ ---@field repo_root string? ---@field context_before string[]? ---@field context_after string[]? +---@field _hl_line_count integer? +---@field _skipped_max_lines boolean? local M = {} diff --git a/spec/highlight_spec.lua b/spec/highlight_spec.lua index 34101ea..22b884c 100644 --- a/spec/highlight_spec.lua +++ b/spec/highlight_spec.lua @@ -135,8 +135,8 @@ describe('highlight', function() local lines = { '@@ -1,100 +1,101 @@' } local hunk_lines = {} for i = 1, 600 do - table.insert(lines, ' line ' .. i) - table.insert(hunk_lines, ' line ' .. i) + table.insert(lines, '+line ' .. i) + table.insert(hunk_lines, '+line ' .. i) end local bufnr = create_buffer(lines)