feat(highlight): warn when hunks exceed max_lines (#184)

This commit is contained in:
Barrett Ruth 2026-03-10 17:36:36 -04:00 committed by GitHub
parent 600d3757f2
commit de04381298
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 99 additions and 22 deletions

View file

@ -93,6 +93,7 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
background = true, background = true,
gutter = true, gutter = true,
blend_alpha = 0.6, blend_alpha = 0.6,
warn_max_lines = true,
context = { context = {
enabled = true, enabled = true,
lines = 25, 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 (inclusive). Higher values produce more vivid
backgrounds. 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) {context} (table, default: see below)
Syntax parsing context options. Syntax parsing context options.
See |diffs.ContextConfig| for fields. 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. Apply treesitter syntax highlighting to code.
{max_lines} (integer, default: 500) {max_lines} (integer, default: 500)
Skip treesitter highlighting for hunks larger than Skip treesitter highlighting for hunks with more
this many lines. Prevents lag on massive diffs. highlighted lines (`+`/`-`) than this threshold.
Context lines are not counted. Prevents lag on
massive diffs.
*diffs.VimConfig* *diffs.VimConfig*
Vim config fields: ~ 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). parser installed (e.g., COBOL, Fortran).
{max_lines} (integer, default: 200) {max_lines} (integer, default: 200)
Skip vim syntax highlighting for hunks larger than Skip vim syntax highlighting for hunks with more
this many lines. Lower than the treesitter default highlighted lines (`+`/`-`) than this threshold.
due to the per-character cost of |synID()|. Context lines are not counted. Lower than the
treesitter default due to the per-character cost
of |synID()|.
*diffs.IntraConfig* *diffs.IntraConfig*
Intra config fields: ~ 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). (falls back to default if not available).
{max_lines} (integer, default: 500) {max_lines} (integer, default: 500)
Skip character-level highlighting for hunks larger Skip character-level highlighting for hunks with
than this many lines. more highlighted lines (`+`/`-`) than this
threshold. Context lines are not counted.
Note: Header context (e.g., `@@ -10,3 +10,4 @@ func()`) is always Note: Header context (e.g., `@@ -10,3 +10,4 @@ func()`) is always
highlighted with treesitter when a parser is available. 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 or register treesitter parsers for custom filetypes, use
|vim.filetype.add()| and |vim.treesitter.language.register()|. |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* COMMANDS *diffs-commands*

View file

@ -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 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 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 if use_ts or use_vim then
dbg( local hl_count = 0
'skipping hunk %s:%d (%d lines > %d max)', for _, line in ipairs(hunk.lines) do
hunk.filename, local c = line:sub(1, 1)
hunk.start_line, if c == '+' or c == '-' then
#hunk.lines, hl_count = hl_count + 1
max_lines end
) end
use_ts = false hunk._hl_line_count = hl_count
use_vim = false 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 end
if use_vim and opts.defer_vim_syntax then 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
and intra_cfg.enabled and intra_cfg.enabled
and pw == 1 and pw == 1
and #hunk.lines <= intra_cfg.max_lines and (hunk._hl_line_count or #hunk.lines) <= intra_cfg.max_lines
then then
dbg('computing intra for hunk %s:%d (%d lines)', hunk.filename, hunk.start_line, #hunk.lines) 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) intra = diff.compute_intra_hunks(hunk.lines, intra_cfg.algorithm)
@ -467,8 +478,12 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
end end
elseif intra_cfg and not intra_cfg.enabled then elseif intra_cfg and not intra_cfg.enabled then
dbg('intra disabled by config') dbg('intra disabled by config')
elseif intra_cfg and #hunk.lines > intra_cfg.max_lines then elseif intra_cfg and (hunk._hl_line_count or #hunk.lines) > intra_cfg.max_lines then
dbg('intra skipped: %d lines > %d max', #hunk.lines, intra_cfg.max_lines) dbg(
'intra skipped: %d highlighted lines > %d max',
hunk._hl_line_count or #hunk.lines,
intra_cfg.max_lines
)
end end
---@type table<integer, diffs.CharSpan[]> ---@type table<integer, diffs.CharSpan[]>

View file

@ -26,6 +26,7 @@
---@field gutter boolean ---@field gutter boolean
---@field blend_alpha? number ---@field blend_alpha? number
---@field overrides? table<string, table> ---@field overrides? table<string, table>
---@field warn_max_lines boolean
---@field context diffs.ContextConfig ---@field context diffs.ContextConfig
---@field treesitter diffs.TreesitterConfig ---@field treesitter diffs.TreesitterConfig
---@field vim diffs.VimConfig ---@field vim diffs.VimConfig
@ -132,6 +133,7 @@ local default_config = {
highlights = { highlights = {
background = true, background = true,
gutter = true, gutter = true,
warn_max_lines = true,
context = { context = {
enabled = true, enabled = true,
lines = 25, lines = 25,
@ -203,6 +205,7 @@ local diff_windows = {}
---@field tick integer ---@field tick integer
---@field highlighted table<integer, true> ---@field highlighted table<integer, true>
---@field pending_clear boolean ---@field pending_clear boolean
---@field warned_max_lines boolean
---@field line_count integer ---@field line_count integer
---@field byte_count integer ---@field byte_count integer
@ -423,6 +426,7 @@ local function ensure_cache(bufnr)
tick = tick, tick = tick,
highlighted = carried or {}, highlighted = carried or {},
pending_clear = not carried, pending_clear = not carried,
warned_max_lines = false,
line_count = lc, line_count = lc,
byte_count = bc, byte_count = bc,
} }
@ -703,6 +707,7 @@ local function init()
vim.validate('highlights.gutter', opts.highlights.gutter, 'boolean', true) vim.validate('highlights.gutter', opts.highlights.gutter, 'boolean', true)
vim.validate('highlights.blend_alpha', opts.highlights.blend_alpha, 'number', true) vim.validate('highlights.blend_alpha', opts.highlights.blend_alpha, 'number', true)
vim.validate('highlights.overrides', opts.highlights.overrides, 'table', 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.context', opts.highlights.context, 'table', true)
vim.validate('highlights.treesitter', opts.highlights.treesitter, 'table', true) vim.validate('highlights.treesitter', opts.highlights.treesitter, 'table', true)
vim.validate('highlights.vim', opts.highlights.vim, 'table', true) vim.validate('highlights.vim', opts.highlights.vim, 'table', true)
@ -910,6 +915,7 @@ local function init()
end end
local t0 = config.debug and vim.uv.hrtime() or nil local t0 = config.debug and vim.uv.hrtime() or nil
local deferred_syntax = {} local deferred_syntax = {}
local skipped_count = 0
local count = 0 local count = 0
for i = first, last do for i = first, last do
if not entry.highlighted[i] then if not entry.highlighted[i] then
@ -923,6 +929,9 @@ local function init()
highlight.highlight_hunk(bufnr, ns, hunk, fast_hl_opts) highlight.highlight_hunk(bufnr, ns, hunk, fast_hl_opts)
entry.highlighted[i] = true entry.highlighted[i] = true
count = count + 1 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 has_syntax = hunk.lang and config.highlights.treesitter.enabled
local needs_vim = not hunk.lang and hunk.ft and config.highlights.vim.enabled local needs_vim = not hunk.lang and hunk.ft and config.highlights.vim.enabled
if has_syntax or needs_vim then if has_syntax or needs_vim then
@ -930,6 +939,19 @@ local function init()
end end
end 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 if #deferred_syntax > 0 then
local tick = entry.tick local tick = entry.tick
dbg('deferred syntax scheduled: %d hunks tick=%d', #deferred_syntax, tick) dbg('deferred syntax scheduled: %d hunks tick=%d', #deferred_syntax, tick)

View file

@ -17,6 +17,8 @@
---@field repo_root string? ---@field repo_root string?
---@field context_before string[]? ---@field context_before string[]?
---@field context_after string[]? ---@field context_after string[]?
---@field _hl_line_count integer?
---@field _skipped_max_lines boolean?
local M = {} local M = {}

View file

@ -135,8 +135,8 @@ describe('highlight', function()
local lines = { '@@ -1,100 +1,101 @@' } local lines = { '@@ -1,100 +1,101 @@' }
local hunk_lines = {} local hunk_lines = {}
for i = 1, 600 do for i = 1, 600 do
table.insert(lines, ' line ' .. i) table.insert(lines, '+line ' .. i)
table.insert(hunk_lines, ' line ' .. i) table.insert(hunk_lines, '+line ' .. i)
end end
local bufnr = create_buffer(lines) local bufnr = create_buffer(lines)