From 7106bcc291d5ec77ce848521e15a24297ba84368 Mon Sep 17 00:00:00 2001 From: Barrett Ruth <62671086+barrettruth@users.noreply.github.com> Date: Thu, 5 Mar 2026 09:01:22 -0500 Subject: [PATCH] refactor(highlight): unified per-line extmark builder (#144) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem `highlight_hunk` applied DiffsClear extmarks across 5 scattered sites with ad-hoc column arithmetic. This fragmentation produced the 1-column DiffsClear gap on email-quoted body context lines (#142 issue 1). A redundant `highlight_hunk_vim_syntax` function duplicated the inline vim syntax path, and the deferred pass in init.lua double-called it, creating duplicate scratch buffers and extmarks. ## Solution Reorganize `highlight_hunk` into two clean phases: - **Phase 1** — multi-line syntax computation (treesitter, vim syntax, diff grammar, header context text). Sets syntax extmarks only, no DiffsClear. - **Phase 2** — per-line chrome (DiffsClear, backgrounds, gutter, overlays, intra-line). All non-syntax extmarks consolidated in one pass. Hoist `new_code` to function scope (needed by `highlight_text` outside the `use_ts` block). Hoist `at_raw_line` so Phase 1d and Phase 2b share one `nvim_buf_get_lines` call. Delete `highlight_hunk_vim_syntax` (redundant with inline path). Remove the double-call from the deferred pass in init.lua. Extend body prefix DiffsClear `end_col` from `qw` to `pw + qw`, fixing the 1-column gap where native treesitter background bled through on context lines in email-quoted diffs (#142 issue 1). ### Email-quoted diff support The parser now strips `> ` (and `>> `, etc.) email quote prefixes before pattern matching, enabling syntax highlighting for diffs embedded in email replies and `git-send-email` / sourcehut-style patch review threads. Each hunk stores `quote_width` so the highlight pipeline can apply `DiffsClear` at the correct column offsets to suppress native treesitter on quoted regions. Closes #141 ### #142 status after this PR | Sub-issue | Status | |-----------|--------| | 1. Col gap on context lines | Fixed | | 2. Bare `>` context lines | Improved, edge case remains | | 3. Diff prefix marker fg | Not addressed (follow-up) | --- .luarc.json | 9 +- .styluaignore | 1 + README.md | 2 + flake.nix | 39 ++- lua/diffs/highlight.lua | 157 ++++++---- lua/diffs/init.lua | 25 +- lua/diffs/parser.lua | 24 +- scripts/ci.sh | 10 + spec/highlight_spec.lua | 604 +++++++++++++++++++++++++++++++++++++- spec/integration_spec.lua | 120 +++++++- spec/parser_spec.lua | 30 +- 11 files changed, 919 insertions(+), 102 deletions(-) create mode 100644 .styluaignore create mode 100755 scripts/ci.sh diff --git a/.luarc.json b/.luarc.json index 23646d3..bfbf500 100644 --- a/.luarc.json +++ b/.luarc.json @@ -2,7 +2,14 @@ "runtime.version": "LuaJIT", "runtime.path": ["lua/?.lua", "lua/?/init.lua"], "diagnostics.globals": ["vim", "jit"], - "workspace.library": ["$VIMRUNTIME/lua", "${3rd}/luv/library"], + "workspace.library": [ + "$VIMRUNTIME/lua", + "${3rd}/luv/library", + "${3rd}/busted/library", + "${3rd}/luassert/library" + ], "workspace.checkThirdParty": false, + "diagnostics.libraryFiles": "Disable", + "workspace.ignoreDir": [".direnv"], "completion.callSnippet": "Replace" } diff --git a/.styluaignore b/.styluaignore new file mode 100644 index 0000000..9b42106 --- /dev/null +++ b/.styluaignore @@ -0,0 +1 @@ +.direnv/ diff --git a/README.md b/README.md index 5d0022f..eee728d 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ with language-aware syntax highlighting. - `:Gdiff` unified diff against any revision - Background-only diff colors for `&diff` buffers - Inline merge conflict detection, highlighting, and resolution +- Email-quoted diff highlighting (`> diff ...` prefixes, arbitrary nesting + depth) - Vim syntax fallback, configurable blend/priorities ## Requirements diff --git a/flake.nix b/flake.nix index 0c5f5d8..2221bdf 100644 --- a/flake.nix +++ b/flake.nix @@ -13,24 +13,41 @@ ... }: let - forEachSystem = f: nixpkgs.lib.genAttrs (import systems) (system: f nixpkgs.legacyPackages.${system}); + forEachSystem = + f: nixpkgs.lib.genAttrs (import systems) (system: f nixpkgs.legacyPackages.${system}); in { + formatter = forEachSystem (pkgs: pkgs.nixfmt-tree); + devShells = forEachSystem (pkgs: { - default = pkgs.mkShell { - packages = [ - (pkgs.luajit.withPackages ( + default = + let + ts-plugin = pkgs.vimPlugins.nvim-treesitter.withPlugins (p: [ p.diff ]); + diff-grammar = pkgs.vimPlugins.nvim-treesitter-parsers.diff; + luaEnv = pkgs.luajit.withPackages ( ps: with ps; [ busted nlua ] - )) - pkgs.prettier - pkgs.stylua - pkgs.selene - pkgs.lua-language-server - ]; - }; + ); + busted-with-grammar = pkgs.writeShellScriptBin "busted" '' + nvim_bin=$(which nvim) + tmpdir=$(mktemp -d) + trap 'rm -rf "$tmpdir"' EXIT + printf '#!/bin/sh\nexec "%s" --cmd "set rtp+=${ts-plugin}/runtime" --cmd "set rtp+=${diff-grammar}" "$@"\n' "$nvim_bin" > "$tmpdir/nvim" + chmod +x "$tmpdir/nvim" + PATH="$tmpdir:$PATH" exec ${luaEnv}/bin/busted "$@" + ''; + in + pkgs.mkShell { + packages = [ + busted-with-grammar + pkgs.prettier + pkgs.stylua + pkgs.selene + pkgs.lua-language-server + ]; + }; }); }; } diff --git a/lua/diffs/highlight.lua b/lua/diffs/highlight.lua index 3b50055..806fb0a 100644 --- a/lua/diffs/highlight.lua +++ b/lua/diffs/highlight.lua @@ -74,6 +74,7 @@ end ---@param col_offset integer ---@param covered_lines? table ---@param priorities diffs.PrioritiesConfig +---@param force_high_priority? boolean ---@return integer local function highlight_treesitter( bufnr, @@ -83,7 +84,8 @@ local function highlight_treesitter( line_map, col_offset, covered_lines, - priorities + priorities, + force_high_priority ) local code = table.concat(code_lines, '\n') if code == '' then @@ -123,7 +125,9 @@ local function highlight_treesitter( local buf_sc = sc + col_offset local buf_ec = ec + col_offset - local priority = tree_lang == 'diff' and (tonumber(metadata.priority) or 100) + local meta_prio = tonumber(metadata.priority) or 100 + local priority = tree_lang == 'diff' + and ((col_offset > 0 or force_high_priority) and (priorities.syntax + meta_prio - 100) or meta_prio) or priorities.syntax pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_sr, buf_sc, { @@ -221,7 +225,7 @@ local function highlight_vim_syntax( pcall(vim.api.nvim_buf_call, scratch, function() vim.cmd('setlocal syntax=' .. ft) - vim.cmd('redraw') + vim.cmd.redraw() ---@param line integer ---@param col integer @@ -292,9 +296,10 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) local covered_lines = {} local extmark_count = 0 + ---@type string[] + local new_code = {} + if use_ts then - ---@type string[] - local new_code = {} ---@type table local new_map = {} ---@type string[] @@ -328,12 +333,6 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) + highlight_treesitter(bufnr, ns, old_code, hunk.lang, old_map, pw, covered_lines, p) if hunk.header_context and hunk.header_context_col then - local header_line = hunk.start_line - 1 - pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, header_line, hunk.header_context_col, { - end_col = hunk.header_context_col + #hunk.header_context, - hl_group = 'DiffsClear', - priority = p.clear, - }) local header_extmarks = highlight_text( bufnr, ns, @@ -370,7 +369,13 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) header_map[i] = hunk.header_start_line - 1 + i end extmark_count = extmark_count - + highlight_treesitter(bufnr, ns, hunk.header_lines, 'diff', header_map, 0, nil, p) + + highlight_treesitter(bufnr, ns, hunk.header_lines, 'diff', header_map, 0, nil, p, pw > 1) + end + + local at_raw_line + if pw > 1 and opts.highlights.treesitter.enabled then + local at_buf_line = hunk.start_line - 1 + at_raw_line = vim.api.nvim_buf_get_lines(bufnr, at_buf_line, at_buf_line + 1, false)[1] end ---@type diffs.IntraChanges? @@ -407,6 +412,69 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) end end + if + pw > 1 + and hunk.header_start_line + and hunk.header_lines + and #hunk.header_lines > 0 + and opts.highlights.treesitter.enabled + then + for i = 0, #hunk.header_lines - 1 do + local buf_line = hunk.header_start_line - 1 + i + pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, { + end_col = #hunk.header_lines[i + 1], + hl_group = 'DiffsClear', + priority = p.clear, + }) + + local hline = hunk.header_lines[i + 1] + if hline:match('^index ') then + pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, { + end_col = 5, + hl_group = '@keyword.diff', + priority = p.syntax, + }) + local dot_pos = hline:find('%.%.', 1, false) + if dot_pos then + local rest = hline:sub(dot_pos + 2) + local hash = rest:match('^(%x+)') + if hash then + pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, dot_pos + 1, { + end_col = dot_pos + 1 + #hash, + hl_group = '@constant.diff', + priority = p.syntax, + }) + end + end + end + end + end + + if pw > 1 and at_raw_line then + local at_buf_line = hunk.start_line - 1 + pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, at_buf_line, 0, { + end_col = #at_raw_line, + hl_group = 'DiffsClear', + priority = p.clear, + }) + if opts.highlights.treesitter.enabled then + pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, at_buf_line, 0, { + end_col = #at_raw_line, + hl_group = '@attribute.diff', + priority = p.syntax, + }) + end + end + + if use_ts and hunk.header_context and hunk.header_context_col then + local header_line = hunk.start_line - 1 + pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, header_line, hunk.header_context_col, { + end_col = hunk.header_context_col + #hunk.header_context, + hl_group = 'DiffsClear', + priority = p.clear, + }) + end + for i, line in ipairs(hunk.lines) do local buf_line = hunk.start_line + i - 1 local line_len = #line @@ -434,6 +502,24 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) }) end + if pw > 1 then + pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, { + end_col = pw, + hl_group = 'DiffsClear', + priority = p.clear, + }) + for ci = 0, pw - 1 do + local ch = line:sub(ci + 1, ci + 1) + if ch == '+' or ch == '-' then + pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, ci, { + end_col = ci + 1, + hl_group = ch == '+' and '@diff.plus' or '@diff.minus', + priority = p.syntax, + }) + end + end + end + if line_len > pw and covered_lines[buf_line] then pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, pw, { end_col = line_len, @@ -444,17 +530,10 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) if opts.highlights.background and is_diff_line then pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, { - end_row = buf_line + 1, - hl_group = line_hl, - hl_eol = true, + line_hl_group = line_hl, + number_hl_group = opts.highlights.gutter and number_hl or nil, priority = p.line_bg, }) - if opts.highlights.gutter then - pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, { - number_hl_group = number_hl, - priority = p.line_bg, - }) - end end if is_marker and line_len > pw then @@ -493,40 +572,4 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) dbg('hunk %s:%d applied %d extmarks', hunk.filename, hunk.start_line, extmark_count) end ----@param bufnr integer ----@param ns integer ----@param hunk diffs.Hunk ----@param opts diffs.HunkOpts -function M.highlight_hunk_vim_syntax(bufnr, ns, hunk, opts) - local p = opts.highlights.priorities - local pw = hunk.prefix_width or 1 - - if not hunk.ft or #hunk.lines == 0 then - return - end - - if #hunk.lines > opts.highlights.vim.max_lines then - return - end - - local code_lines = {} - for _, line in ipairs(hunk.lines) do - table.insert(code_lines, line:sub(pw + 1)) - end - - local covered_lines = {} - highlight_vim_syntax(bufnr, ns, hunk, code_lines, covered_lines, 0, p) - - for buf_line in pairs(covered_lines) do - local line = hunk.lines[buf_line - hunk.start_line + 1] - if line and #line > pw then - pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, pw, { - end_col = #line, - hl_group = 'DiffsClear', - priority = p.clear, - }) - end - end -end - return M diff --git a/lua/diffs/init.lua b/lua/diffs/init.lua index e64bf40..6af7e1a 100644 --- a/lua/diffs/init.lua +++ b/lua/diffs/init.lua @@ -169,6 +169,9 @@ local fast_hl_opts = {} ---@type diffs.HunkOpts ---@type table local attached_buffers = {} +---@type table +local ft_retry_pending = {} + ---@type table local diff_windows = {} @@ -334,12 +337,15 @@ local function ensure_cache(bufnr) has_nil_ft = true end end - if has_nil_ft and vim.fn.did_filetype() ~= 0 then + if has_nil_ft and vim.fn.did_filetype() ~= 0 and not ft_retry_pending[bufnr] then + ft_retry_pending[bufnr] = true vim.schedule(function() if vim.api.nvim_buf_is_valid(bufnr) and hunk_cache[bufnr] then dbg('retrying filetype detection for buffer %d (was blocked by did_filetype)', bufnr) invalidate_cache(bufnr) + vim.cmd('redraw!') end + ft_retry_pending[bufnr] = nil end) end end @@ -411,7 +417,7 @@ local function compute_highlight_groups() local blended_add_text = blend_color(add_fg, bg, alpha) local blended_del_text = blend_color(del_fg, bg, alpha) - vim.api.nvim_set_hl(0, 'DiffsClear', { default = true, fg = normal.fg or 0xc0c0c0 }) + vim.api.nvim_set_hl(0, 'DiffsClear', { default = true, fg = normal.fg or 0xc0c0c0, bg = bg }) vim.api.nvim_set_hl(0, 'DiffsAdd', { default = true, bg = blended_add }) vim.api.nvim_set_hl(0, 'DiffsDelete', { default = true, bg = blended_del }) vim.api.nvim_set_hl(0, 'DiffsAddNr', { default = true, fg = blended_add_text, bg = blended_add }) @@ -759,7 +765,7 @@ local function init() if not entry.highlighted[i] then local hunk = entry.hunks[i] local clear_start = hunk.start_line - 1 - local clear_end = clear_start + #hunk.lines + local clear_end = hunk.start_line + #hunk.lines if hunk.header_start_line then clear_start = hunk.header_start_line - 1 end @@ -776,12 +782,18 @@ local function init() end if #deferred_syntax > 0 then local tick = entry.tick + dbg('deferred syntax scheduled: %d hunks tick=%d', #deferred_syntax, tick) vim.schedule(function() if not vim.api.nvim_buf_is_valid(bufnr) then return end local cur = hunk_cache[bufnr] if not cur or cur.tick ~= tick then + dbg( + 'deferred syntax stale: cur.tick=%s captured=%d', + cur and tostring(cur.tick) or 'nil', + tick + ) return end local t1 = config.debug and vim.uv.hrtime() or nil @@ -791,15 +803,12 @@ local function init() } for _, hunk in ipairs(deferred_syntax) do local start_row = hunk.start_line - 1 - local end_row = start_row + #hunk.lines + local end_row = hunk.start_line + #hunk.lines if hunk.header_start_line then start_row = hunk.header_start_line - 1 end vim.api.nvim_buf_clear_namespace(bufnr, ns, start_row, end_row) highlight.highlight_hunk(bufnr, ns, hunk, full_opts) - if not hunk.lang and hunk.ft then - highlight.highlight_hunk_vim_syntax(bufnr, ns, hunk, full_opts) - end end if t1 then dbg('deferred pass: %d hunks in %.2fms', #deferred_syntax, (vim.uv.hrtime() - t1) / 1e6) @@ -866,6 +875,7 @@ function M.attach(bufnr) callback = function() attached_buffers[bufnr] = nil hunk_cache[bufnr] = nil + ft_retry_pending[bufnr] = nil if neogit_augroup then pcall(vim.api.nvim_del_augroup_by_id, neogit_augroup) end @@ -947,6 +957,7 @@ M._test = { invalidate_cache = invalidate_cache, hunks_eq = hunks_eq, process_pending_clear = process_pending_clear, + ft_retry_pending = ft_retry_pending, } return M diff --git a/lua/diffs/parser.lua b/lua/diffs/parser.lua index fa1bcc0..a32e563 100644 --- a/lua/diffs/parser.lua +++ b/lua/diffs/parser.lua @@ -60,6 +60,15 @@ local function get_ft_from_filename(filename, repo_root) end local ft = vim.filetype.match({ filename = filename }) + if not ft and vim.fn.did_filetype() ~= 0 then + dbg('retrying filetype match for %s (clearing did_filetype)', filename) + local saved = rawget(vim.fn, 'did_filetype') + rawset(vim.fn, 'did_filetype', function() + return 0 + end) + ft = vim.filetype.match({ filename = filename }) + rawset(vim.fn, 'did_filetype', saved) + end if ft then dbg('filetype from filename: %s', ft) return ft @@ -125,6 +134,7 @@ end function M.parse_buffer(bufnr) local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) local repo_root = get_repo_root(bufnr) + ---@type diffs.Hunk[] local hunks = {} @@ -162,7 +172,6 @@ function M.parse_buffer(bufnr) local old_remaining = nil ---@type integer? local new_remaining = nil - local is_unified_diff = false local function flush_hunk() if hunk_start and #hunk_lines > 0 then @@ -201,6 +210,8 @@ function M.parse_buffer(bufnr) for i, line in ipairs(lines) do local diff_git_file = line:match('^diff %-%-git a/.+ b/(.+)$') + or line:match('^diff %-%-combined (.+)$') + or line:match('^diff %-%-cc (.+)$') local neogit_file = line:match('^modified%s+(.+)$') or (not line:match('^new file mode') and line:match('^new file%s+(.+)$')) or (not line:match('^deleted file mode') and line:match('^deleted%s+(.+)$')) @@ -209,7 +220,6 @@ function M.parse_buffer(bufnr) local bare_file = not hunk_start and line:match('^([^%s]+%.[^%s]+)$') local filename = line:match('^[MADRCU%?!]%s+(.+)$') or diff_git_file or neogit_file or bare_file if filename then - is_unified_diff = diff_git_file ~= nil flush_hunk() current_filename = filename local cache_key = (repo_root or '') .. '\0' .. filename @@ -249,10 +259,17 @@ function M.parse_buffer(bufnr) new_remaining = file_new_count end else + local hs, hc = line:match('%-(%d+),?(%d*)') + if hs then + file_old_start = tonumber(hs) + file_old_count = tonumber(hc) or 1 + old_remaining = file_old_count + end local hs2, hc2 = line:match('%+(%d+),?(%d*) @@') if hs2 then file_new_start = tonumber(hs2) file_new_count = tonumber(hc2) or 1 + new_remaining = file_new_count end end local at_end, context = line:match('^(@@+.-@@+%s*)(.*)') @@ -275,13 +292,12 @@ function M.parse_buffer(bufnr) end elseif line == '' - and is_unified_diff and old_remaining and old_remaining > 0 and new_remaining and new_remaining > 0 then - table.insert(hunk_lines, ' ') + table.insert(hunk_lines, string.rep(' ', hunk_prefix_width)) old_remaining = old_remaining - 1 new_remaining = new_remaining - 1 elseif diff --git a/scripts/ci.sh b/scripts/ci.sh new file mode 100755 index 0000000..e06bf09 --- /dev/null +++ b/scripts/ci.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -eu + +nix develop --command stylua --check . +git ls-files '*.lua' | xargs nix develop --command selene --display-style quiet +nix develop --command prettier --check . +nix fmt +git diff --exit-code -- '*.nix' +nix develop --command lua-language-server --check . --checklevel=Warning +nix develop --command busted diff --git a/spec/highlight_spec.lua b/spec/highlight_spec.lua index 00a0774..5d4b94e 100644 --- a/spec/highlight_spec.lua +++ b/spec/highlight_spec.lua @@ -287,7 +287,7 @@ describe('highlight', function() local extmarks = get_extmarks(bufnr) local has_diff_add = false for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].hl_group == 'DiffsAdd' then + if mark[4] and mark[4].line_hl_group == 'DiffsAdd' then has_diff_add = true break end @@ -320,7 +320,7 @@ describe('highlight', function() local extmarks = get_extmarks(bufnr) local has_diff_delete = false for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].hl_group == 'DiffsDelete' then + if mark[4] and mark[4].line_hl_group == 'DiffsDelete' then has_diff_delete = true break end @@ -362,6 +362,122 @@ describe('highlight', function() delete_buffer(bufnr) end) + it('line bg uses line_hl_group not hl_group with end_row', function() + local bufnr = create_buffer({ + '@@ -1,1 +1,2 @@', + ' local x = 1', + '+local y = 2', + }) + + local hunk = { + filename = 'test.lua', + lang = 'lua', + start_line = 1, + lines = { ' local x = 1', '+local y = 2' }, + } + + highlight.highlight_hunk( + bufnr, + ns, + hunk, + default_opts({ highlights = { background = true } }) + ) + + local extmarks = get_extmarks(bufnr) + for _, mark in ipairs(extmarks) do + local d = mark[4] + assert.is_not_equal('DiffsAdd', d and d.hl_group) + assert.is_not_equal('DiffsDelete', d and d.hl_group) + end + delete_buffer(bufnr) + end) + + it('line bg extmark survives adjacent clear_namespace starting at next row', function() + local bufnr = create_buffer({ + 'diff --git a/foo.py b/foo.py', + '@@ -1,2 +1,2 @@', + '-old', + '+new', + }) + + local hunk = { + filename = 'foo.py', + header_start_line = 1, + start_line = 2, + lines = { '-old', '+new' }, + prefix_width = 1, + } + + highlight.highlight_hunk( + bufnr, + ns, + hunk, + default_opts({ highlights = { background = true, treesitter = { enabled = false } } }) + ) + + local last_body_row = hunk.start_line + #hunk.lines - 1 + vim.api.nvim_buf_clear_namespace(bufnr, ns, last_body_row + 1, last_body_row + 10) + + local marks = vim.api.nvim_buf_get_extmarks( + bufnr, + ns, + { last_body_row, 0 }, + { last_body_row, -1 }, + { details = true } + ) + local has_line_bg = false + for _, mark in ipairs(marks) do + if mark[4] and mark[4].line_hl_group == 'DiffsAdd' then + has_line_bg = true + end + end + assert.is_true(has_line_bg) + delete_buffer(bufnr) + end) + + it('clear range covers last body line of hunk with header', function() + local bufnr = create_buffer({ + 'diff --git a/foo.py b/foo.py', + 'index abc..def 100644', + '--- a/foo.py', + '+++ b/foo.py', + '@@ -1,3 +1,3 @@', + ' ctx', + '-old', + '+new', + }) + + local hunk = { + filename = 'foo.py', + header_start_line = 1, + start_line = 5, + lines = { ' ctx', '-old', '+new' }, + prefix_width = 1, + } + + highlight.highlight_hunk( + bufnr, + ns, + hunk, + default_opts({ highlights = { background = true, treesitter = { enabled = false } } }) + ) + + local last_body_row = hunk.start_line + #hunk.lines - 1 + local clear_start = hunk.header_start_line - 1 + local clear_end = hunk.start_line + #hunk.lines + vim.api.nvim_buf_clear_namespace(bufnr, ns, clear_start, clear_end) + + local marks = vim.api.nvim_buf_get_extmarks( + bufnr, + ns, + { last_body_row, 0 }, + { last_body_row, -1 }, + { details = false } + ) + assert.are.equal(0, #marks) + delete_buffer(bufnr) + end) + it('still applies background when treesitter disabled', function() local bufnr = create_buffer({ '@@ -1,1 +1,2 @@', @@ -386,7 +502,7 @@ describe('highlight', function() local extmarks = get_extmarks(bufnr) local has_diff_add = false for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].hl_group == 'DiffsAdd' then + if mark[4] and mark[4].line_hl_group == 'DiffsAdd' then has_diff_add = true break end @@ -500,7 +616,7 @@ describe('highlight', function() local extmarks = get_extmarks(bufnr) local has_diff_add = false for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].hl_group == 'DiffsAdd' then + if mark[4] and mark[4].line_hl_group == 'DiffsAdd' then has_diff_add = true break end @@ -585,7 +701,7 @@ describe('highlight', function() local found = false for _, mark in ipairs(extmarks) do local d = mark[4] - if d and (d.hl_group == 'DiffsAdd' or d.hl_group == 'DiffsDelete') and d.hl_eol then + if d and (d.line_hl_group == 'DiffsAdd' or d.line_hl_group == 'DiffsDelete') then found = true end end @@ -741,7 +857,7 @@ describe('highlight', function() if d then if d.hl_group == 'DiffsClear' then table.insert(priorities.clear, d.priority) - elseif (d.hl_group == 'DiffsAdd' or d.hl_group == 'DiffsDelete') and d.hl_eol then + elseif d.line_hl_group == 'DiffsAdd' or d.line_hl_group == 'DiffsDelete' then table.insert(priorities.line_bg, d.priority) elseif d.hl_group == 'DiffsAddText' or d.hl_group == 'DiffsDeleteText' then table.insert(priorities.char_bg, d.priority) @@ -842,8 +958,8 @@ describe('highlight', function() local line_bgs = {} for _, mark in ipairs(extmarks) do local d = mark[4] - if d and (d.hl_group == 'DiffsAdd' or d.hl_group == 'DiffsDelete') and d.hl_eol then - line_bgs[mark[2]] = d.hl_group + if d and (d.line_hl_group == 'DiffsAdd' or d.line_hl_group == 'DiffsDelete') then + line_bgs[mark[2]] = d.line_hl_group end end assert.is_nil(line_bgs[1]) @@ -946,11 +1062,16 @@ describe('highlight', function() highlight.highlight_hunk(bufnr, ns, hunk, default_opts()) local extmarks = get_extmarks(bufnr) + local content_clear_count = 0 for _, mark in ipairs(extmarks) do if mark[4] and mark[4].hl_group == 'DiffsClear' then - assert.are.equal(2, mark[3]) + assert.is_true(mark[3] == 0 or mark[3] == 2, 'DiffsClear at unexpected col ' .. mark[3]) + if mark[3] == 2 then + content_clear_count = content_clear_count + 1 + end end end + assert.are.equal(2, content_clear_count) delete_buffer(bufnr) end) @@ -1034,8 +1155,8 @@ describe('highlight', function() local marker_text = {} for _, mark in ipairs(extmarks) do local d = mark[4] - if d and (d.hl_group == 'DiffsAdd' or d.hl_group == 'DiffsDelete') and d.hl_eol then - line_bgs[mark[2]] = d.hl_group + if d and (d.line_hl_group == 'DiffsAdd' or d.line_hl_group == 'DiffsDelete') then + line_bgs[mark[2]] = d.line_hl_group end if d and d.number_hl_group then gutter_hls[mark[2]] = d.number_hl_group @@ -1245,6 +1366,467 @@ describe('highlight', function() assert.are.equal(0, header_extmarks) delete_buffer(bufnr) end) + + it('does not apply DiffsClear to header lines for non-quoted diffs', function() + local bufnr = create_buffer({ + 'diff --git a/parser.lua b/parser.lua', + 'index 3e8afa0..018159c 100644', + '--- a/parser.lua', + '+++ b/parser.lua', + '@@ -1,2 +1,3 @@', + ' local M = {}', + '+local x = 1', + }) + + local hunk = { + filename = 'parser.lua', + lang = 'lua', + start_line = 5, + lines = { ' local M = {}', '+local x = 1' }, + header_start_line = 1, + header_lines = { + 'diff --git a/parser.lua b/parser.lua', + 'index 3e8afa0..018159c 100644', + '--- a/parser.lua', + '+++ b/parser.lua', + }, + } + + highlight.highlight_hunk(bufnr, ns, hunk, default_opts()) + + local extmarks = get_extmarks(bufnr) + for _, mark in ipairs(extmarks) do + local d = mark[4] + if d and d.hl_group == 'DiffsClear' and mark[3] == 0 and mark[2] < 4 then + error('unexpected DiffsClear on header row ' .. mark[2] .. ' for non-quoted diff') + end + end + delete_buffer(bufnr) + end) + + it('preserves diff grammar treesitter on headers for non-quoted diffs', function() + local bufnr = create_buffer({ + 'diff --git a/parser.lua b/parser.lua', + '--- a/parser.lua', + '+++ b/parser.lua', + '@@ -1,2 +1,3 @@', + ' local M = {}', + '+local x = 1', + }) + + local hunk = { + filename = 'parser.lua', + lang = 'lua', + start_line = 4, + lines = { ' local M = {}', '+local x = 1' }, + header_start_line = 1, + header_lines = { + 'diff --git a/parser.lua b/parser.lua', + '--- a/parser.lua', + '+++ b/parser.lua', + }, + } + + highlight.highlight_hunk(bufnr, ns, hunk, default_opts()) + + local extmarks = get_extmarks(bufnr) + local header_ts_count = 0 + for _, mark in ipairs(extmarks) do + local d = mark[4] + if mark[2] < 3 and d and d.hl_group and d.hl_group:match('^@.*%.diff$') then + header_ts_count = header_ts_count + 1 + end + end + assert.is_true(header_ts_count > 0, 'expected diff grammar treesitter on header lines') + delete_buffer(bufnr) + end) + + it('applies syntax extmarks to combined diff body lines', function() + local bufnr = create_buffer({ + '@@@ -1,2 -1,2 +1,3 @@@', + ' local M = {}', + '+ local x = 1', + ' -local y = 2', + }) + + local hunk = { + filename = 'test.lua', + lang = 'lua', + prefix_width = 2, + start_line = 1, + lines = { ' local M = {}', '+ local x = 1', ' -local y = 2' }, + } + + highlight.highlight_hunk(bufnr, ns, hunk, default_opts()) + + local extmarks = get_extmarks(bufnr) + local syntax_on_body = 0 + for _, mark in ipairs(extmarks) do + local d = mark[4] + if mark[2] >= 1 and d and d.hl_group and d.hl_group:match('^@.*%.lua$') then + syntax_on_body = syntax_on_body + 1 + end + end + assert.is_true(syntax_on_body > 0, 'expected lua treesitter syntax on combined diff body') + delete_buffer(bufnr) + end) + + it('applies DiffsClear and per-char diff fg to combined diff body prefixes', function() + local bufnr = create_buffer({ + '@@@', + ' unchanged', + '+ added', + ' -removed', + '++both', + }) + + local hunk = { + filename = 'test.lua', + lang = 'lua', + prefix_width = 2, + start_line = 1, + lines = { ' unchanged', '+ added', ' -removed', '++both' }, + } + + highlight.highlight_hunk(bufnr, ns, hunk, default_opts()) + + local extmarks = get_extmarks(bufnr) + local prefix_clears = {} + local plus_marks = {} + local minus_marks = {} + for _, mark in ipairs(extmarks) do + local d = mark[4] + if mark[2] >= 1 and d then + if d.hl_group == 'DiffsClear' and mark[3] == 0 and d.end_col == 2 then + prefix_clears[mark[2]] = true + end + if d.hl_group == '@diff.plus' and d.priority == 199 then + if not plus_marks[mark[2]] then + plus_marks[mark[2]] = {} + end + table.insert(plus_marks[mark[2]], mark[3]) + end + if d.hl_group == '@diff.minus' and d.priority == 199 then + if not minus_marks[mark[2]] then + minus_marks[mark[2]] = {} + end + table.insert(minus_marks[mark[2]], mark[3]) + end + end + end + + assert.is_true(prefix_clears[1] ~= nil, 'DiffsClear on context prefix') + assert.is_true(prefix_clears[2] ~= nil, 'DiffsClear on add prefix') + assert.is_true(prefix_clears[3] ~= nil, 'DiffsClear on del prefix') + assert.is_true(prefix_clears[4] ~= nil, 'DiffsClear on both-add prefix') + + assert.is_true(plus_marks[2] ~= nil, '@diff.plus on + in "+ added"') + assert.are.equal(0, plus_marks[2][1]) + + assert.is_true(minus_marks[3] ~= nil, '@diff.minus on - in " -removed"') + assert.are.equal(1, minus_marks[3][1]) + + assert.is_true(plus_marks[4] ~= nil, '@diff.plus on ++ in "++both"') + assert.are.equal(2, #plus_marks[4]) + + assert.is_nil(plus_marks[1], 'no @diff.plus on context " unchanged"') + assert.is_nil(minus_marks[1], 'no @diff.minus on context " unchanged"') + delete_buffer(bufnr) + end) + + it('applies DiffsClear to headers for combined diffs', function() + local bufnr = create_buffer({ + 'diff --combined lua/merge/target.lua', + 'index abc1234,def5678..ghi9012', + '--- a/lua/merge/target.lua', + '+++ b/lua/merge/target.lua', + '@@@ -1,2 -1,2 +1,3 @@@', + ' local M = {}', + '+ local x = 1', + }) + + local hunk = { + filename = 'lua/merge/target.lua', + lang = 'lua', + prefix_width = 2, + start_line = 5, + lines = { ' local M = {}', '+ local x = 1' }, + header_start_line = 1, + header_lines = { + 'diff --combined lua/merge/target.lua', + 'index abc1234,def5678..ghi9012', + '--- a/lua/merge/target.lua', + '+++ b/lua/merge/target.lua', + }, + } + + highlight.highlight_hunk(bufnr, ns, hunk, default_opts()) + + local extmarks = get_extmarks(bufnr) + local clear_lines = {} + for _, mark in ipairs(extmarks) do + local d = mark[4] + if d and d.hl_group == 'DiffsClear' and mark[3] == 0 and mark[2] < 4 then + clear_lines[mark[2]] = true + end + end + assert.is_true(clear_lines[0] ~= nil, 'DiffsClear on diff --combined line') + assert.is_true(clear_lines[1] ~= nil, 'DiffsClear on index line') + assert.is_true(clear_lines[2] ~= nil, 'DiffsClear on --- line') + assert.is_true(clear_lines[3] ~= nil, 'DiffsClear on +++ line') + delete_buffer(bufnr) + end) + + it('applies @attribute.diff at syntax priority to @@@ line for combined diffs', function() + local bufnr = create_buffer({ + '@@@ -1,2 -1,2 +1,3 @@@', + ' local M = {}', + '+ local x = 1', + }) + + local hunk = { + filename = 'test.lua', + lang = 'lua', + prefix_width = 2, + start_line = 1, + lines = { ' local M = {}', '+ local x = 1' }, + } + + highlight.highlight_hunk(bufnr, ns, hunk, default_opts()) + + local extmarks = get_extmarks(bufnr) + local has_attr = false + for _, mark in ipairs(extmarks) do + local d = mark[4] + if mark[2] == 0 and d and d.hl_group == '@attribute.diff' and (d.priority or 0) >= 199 then + has_attr = true + end + end + assert.is_true(has_attr, '@attribute.diff at p>=199 on @@@ line') + delete_buffer(bufnr) + end) + + it('applies DiffsClear to @@@ line for combined diffs', function() + local bufnr = create_buffer({ + '@@@ -1,2 -1,2 +1,3 @@@', + ' local M = {}', + '+ local x = 1', + }) + + local hunk = { + filename = 'test.lua', + lang = 'lua', + prefix_width = 2, + start_line = 1, + lines = { ' local M = {}', '+ local x = 1' }, + } + + highlight.highlight_hunk(bufnr, ns, hunk, default_opts()) + + local extmarks = get_extmarks(bufnr) + local has_at_clear = false + for _, mark in ipairs(extmarks) do + local d = mark[4] + if mark[2] == 0 and d and d.hl_group == 'DiffsClear' and mark[3] == 0 then + has_at_clear = true + end + end + assert.is_true(has_at_clear, 'DiffsClear on @@@ line') + delete_buffer(bufnr) + end) + + it('applies header diff grammar at syntax priority for combined diffs', function() + local bufnr = create_buffer({ + 'diff --combined lua/merge/target.lua', + 'index abc1234,def5678..ghi9012', + '--- a/lua/merge/target.lua', + '+++ b/lua/merge/target.lua', + '@@@ -1,2 -1,2 +1,3 @@@', + ' local M = {}', + '+ local x = 1', + }) + + local hunk = { + filename = 'lua/merge/target.lua', + lang = 'lua', + prefix_width = 2, + start_line = 5, + lines = { ' local M = {}', '+ local x = 1' }, + header_start_line = 1, + header_lines = { + 'diff --combined lua/merge/target.lua', + 'index abc1234,def5678..ghi9012', + '--- a/lua/merge/target.lua', + '+++ b/lua/merge/target.lua', + }, + } + + highlight.highlight_hunk(bufnr, ns, hunk, default_opts()) + + local extmarks = get_extmarks(bufnr) + local high_prio_diff = {} + for _, mark in ipairs(extmarks) do + local d = mark[4] + if + mark[2] < 4 + and d + and d.hl_group + and d.hl_group:match('^@.*%.diff$') + and (d.priority or 0) >= 199 + then + high_prio_diff[mark[2]] = true + end + end + assert.is_true(high_prio_diff[2] ~= nil, 'diff grammar at p>=199 on --- line') + assert.is_true(high_prio_diff[3] ~= nil, 'diff grammar at p>=199 on +++ line') + delete_buffer(bufnr) + end) + + it('@diff.minus wins over @punctuation.special on combined diff headers', function() + local bufnr = create_buffer({ + 'diff --combined lua/merge/target.lua', + 'index abc1234,def5678..ghi9012', + '--- a/lua/merge/target.lua', + '+++ b/lua/merge/target.lua', + '@@@ -1,2 -1,2 +1,3 @@@', + ' local M = {}', + '+ local x = 1', + }) + + local hunk = { + filename = 'lua/merge/target.lua', + lang = 'lua', + prefix_width = 2, + start_line = 5, + lines = { ' local M = {}', '+ local x = 1' }, + header_start_line = 1, + header_lines = { + 'diff --combined lua/merge/target.lua', + 'index abc1234,def5678..ghi9012', + '--- a/lua/merge/target.lua', + '+++ b/lua/merge/target.lua', + }, + } + + highlight.highlight_hunk(bufnr, ns, hunk, default_opts()) + + local extmarks = get_extmarks(bufnr) + local minus_prio, punct_prio_minus = 0, 0 + local plus_prio, punct_prio_plus = 0, 0 + for _, mark in ipairs(extmarks) do + local d = mark[4] + if d and d.hl_group then + if mark[2] == 2 then + if d.hl_group == '@diff.minus.diff' then + minus_prio = math.max(minus_prio, d.priority or 0) + elseif d.hl_group == '@punctuation.special.diff' then + punct_prio_minus = math.max(punct_prio_minus, d.priority or 0) + end + elseif mark[2] == 3 then + if d.hl_group == '@diff.plus.diff' then + plus_prio = math.max(plus_prio, d.priority or 0) + elseif d.hl_group == '@punctuation.special.diff' then + punct_prio_plus = math.max(punct_prio_plus, d.priority or 0) + end + end + end + end + assert.is_true( + minus_prio > punct_prio_minus, + '@diff.minus.diff should beat @punctuation.special.diff on --- line' + ) + assert.is_true( + plus_prio > punct_prio_plus, + '@diff.plus.diff should beat @punctuation.special.diff on +++ line' + ) + delete_buffer(bufnr) + end) + + it('applies @keyword.diff on index word for combined diffs', function() + local bufnr = create_buffer({ + 'diff --combined lua/merge/target.lua', + 'index abc1234,def5678..ghi9012', + '--- a/lua/merge/target.lua', + '+++ b/lua/merge/target.lua', + '@@@ -1,2 -1,2 +1,3 @@@', + ' local M = {}', + '+ local x = 1', + }) + + local hunk = { + filename = 'lua/merge/target.lua', + lang = 'lua', + prefix_width = 2, + start_line = 5, + lines = { ' local M = {}', '+ local x = 1' }, + header_start_line = 1, + header_lines = { + 'diff --combined lua/merge/target.lua', + 'index abc1234,def5678..ghi9012', + '--- a/lua/merge/target.lua', + '+++ b/lua/merge/target.lua', + }, + } + + highlight.highlight_hunk(bufnr, ns, hunk, default_opts()) + + local extmarks = get_extmarks(bufnr) + local has_keyword = false + for _, mark in ipairs(extmarks) do + local d = mark[4] + if + mark[2] == 1 + and d + and d.hl_group == '@keyword.diff' + and mark[3] == 0 + and (d.end_col or 0) == 5 + then + has_keyword = true + end + end + assert.is_true(has_keyword, '@keyword.diff at row 1, cols 0-5') + delete_buffer(bufnr) + end) + + it('applies @constant.diff on result hash for combined diffs', function() + local bufnr = create_buffer({ + 'diff --combined lua/merge/target.lua', + 'index abc1234,def5678..ghi9012', + '--- a/lua/merge/target.lua', + '+++ b/lua/merge/target.lua', + '@@@ -1,2 -1,2 +1,3 @@@', + ' local M = {}', + '+ local x = 1', + }) + + local hunk = { + filename = 'lua/merge/target.lua', + lang = 'lua', + prefix_width = 2, + start_line = 5, + lines = { ' local M = {}', '+ local x = 1' }, + header_start_line = 1, + header_lines = { + 'diff --combined lua/merge/target.lua', + 'index abc1234,def5678..ghi9012', + '--- a/lua/merge/target.lua', + '+++ b/lua/merge/target.lua', + }, + } + + highlight.highlight_hunk(bufnr, ns, hunk, default_opts()) + + local extmarks = get_extmarks(bufnr) + local has_constant = false + for _, mark in ipairs(extmarks) do + local d = mark[4] + if mark[2] == 1 and d and d.hl_group == '@constant.diff' and (d.priority or 0) >= 199 then + has_constant = true + end + end + assert.is_true(has_constant, '@constant.diff on result hash') + delete_buffer(bufnr) + end) end) describe('extmark priority', function() diff --git a/spec/integration_spec.lua b/spec/integration_spec.lua index 7468e8f..23e870b 100644 --- a/spec/integration_spec.lua +++ b/spec/integration_spec.lua @@ -127,6 +127,110 @@ describe('integration', function() end) end) + describe('ft_retry_pending', function() + before_each(function() + rawset(vim.fn, 'did_filetype', function() + return 1 + end) + require('diffs.parser')._test.ft_lang_cache = {} + end) + + after_each(function() + rawset(vim.fn, 'did_filetype', nil) + end) + + it('sets ft_retry_pending when nil-ft hunks detected under did_filetype', function() + local bufnr = create_buffer({ + 'diff --git a/app.conf b/app.conf', + '@@ -1,2 +1,2 @@', + ' server {', + '- listen 80;', + '+ listen 8080;', + }) + diffs.attach(bufnr) + local entry = diffs._test.hunk_cache[bufnr] + assert.is_not_nil(entry) + assert.is_nil(entry.hunks[1].ft) + assert.is_true(diffs._test.ft_retry_pending[bufnr] == true) + delete_buffer(bufnr) + end) + + it('clears ft_retry_pending after scheduled callback fires', function() + local bufnr = create_buffer({ + 'diff --git a/app.conf b/app.conf', + '@@ -1,2 +1,2 @@', + ' server {', + '- listen 80;', + '+ listen 8080;', + }) + diffs.attach(bufnr) + assert.is_true(diffs._test.ft_retry_pending[bufnr] == true) + + local done = false + vim.schedule(function() + done = true + end) + vim.wait(1000, function() + return done + end) + + assert.is_nil(diffs._test.ft_retry_pending[bufnr]) + delete_buffer(bufnr) + end) + + it('invalidates cache after scheduled callback fires', function() + local bufnr = create_buffer({ + 'diff --git a/app.conf b/app.conf', + '@@ -1,2 +1,2 @@', + ' server {', + '- listen 80;', + '+ listen 8080;', + }) + diffs.attach(bufnr) + local tick_after_attach = diffs._test.hunk_cache[bufnr].tick + assert.is_true(tick_after_attach >= 0) + + local done = false + vim.schedule(function() + done = true + end) + vim.wait(1000, function() + return done + end) + + local entry = diffs._test.hunk_cache[bufnr] + assert.are.equal(-1, entry.tick) + assert.is_true(entry.pending_clear) + delete_buffer(bufnr) + end) + + it('does not set ft_retry_pending when did_filetype() is zero', function() + rawset(vim.fn, 'did_filetype', nil) + local bufnr = create_buffer({ + 'diff --git a/test.sh b/test.sh', + '@@ -1,2 +1,3 @@', + ' #!/usr/bin/env bash', + '-old line', + '+new line', + }) + diffs.attach(bufnr) + assert.is_falsy(diffs._test.ft_retry_pending[bufnr]) + delete_buffer(bufnr) + end) + + it('does not set ft_retry_pending for files with resolvable ft', function() + local bufnr = create_buffer({ + 'M test.lua', + '@@ -1,1 +1,2 @@', + ' local x = 1', + '+local y = 2', + }) + diffs.attach(bufnr) + assert.is_falsy(diffs._test.ft_retry_pending[bufnr]) + delete_buffer(bufnr) + end) + end) + describe('extmarks from highlight pipeline', function() it('DiffsAdd background applied to + lines', function() local bufnr = create_buffer({ @@ -145,7 +249,7 @@ describe('integration', function() local extmarks = get_extmarks(bufnr, ns) local has_diff_add = false for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].hl_group == 'DiffsAdd' then + if mark[4] and mark[4].line_hl_group == 'DiffsAdd' then has_diff_add = true break end @@ -171,7 +275,7 @@ describe('integration', function() local extmarks = get_extmarks(bufnr, ns) local has_diff_delete = false for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].hl_group == 'DiffsDelete' then + if mark[4] and mark[4].line_hl_group == 'DiffsDelete' then has_diff_delete = true break end @@ -198,10 +302,10 @@ describe('integration', function() local has_add = false local has_delete = false for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].hl_group == 'DiffsAdd' then + if mark[4] and mark[4].line_hl_group == 'DiffsAdd' then has_add = true end - if mark[4] and mark[4].hl_group == 'DiffsDelete' then + if mark[4] and mark[4].line_hl_group == 'DiffsDelete' then has_delete = true end end @@ -230,8 +334,8 @@ describe('integration', function() local line_bgs = {} for _, mark in ipairs(extmarks) do local d = mark[4] - if d and (d.hl_group == 'DiffsAdd' or d.hl_group == 'DiffsDelete') and d.hl_eol then - line_bgs[mark[2]] = d.hl_group + if d and (d.line_hl_group == 'DiffsAdd' or d.line_hl_group == 'DiffsDelete') then + line_bgs[mark[2]] = d.line_hl_group end end assert.is_nil(line_bgs[1]) @@ -313,10 +417,10 @@ describe('integration', function() local del_lines = {} for _, mark in ipairs(extmarks) do local d = mark[4] - if d and d.hl_group == 'DiffsAdd' and d.hl_eol then + if d and d.line_hl_group == 'DiffsAdd' then add_lines[mark[2]] = true end - if d and d.hl_group == 'DiffsDelete' and d.hl_eol then + if d and d.line_hl_group == 'DiffsDelete' then del_lines[mark[2]] = true end end diff --git a/spec/parser_spec.lua b/spec/parser_spec.lua index 5613784..adbbd37 100644 --- a/spec/parser_spec.lua +++ b/spec/parser_spec.lua @@ -163,10 +163,10 @@ describe('parser', function() end end) - it('stops hunk at blank line', function() + it('stops hunk at blank line when remaining counts exhausted', function() local bufnr = create_buffer({ 'M test.lua', - '@@ -1,2 +1,3 @@', + '@@ -1,1 +1,2 @@', ' local x = 1', '+local y = 2', '', @@ -391,6 +391,29 @@ describe('parser', function() vim.fn.delete(repo_root, 'rf') end) + it('detects filetype for .sh files when did_filetype() is non-zero', function() + rawset(vim.fn, 'did_filetype', function() + return 1 + end) + + parser._test.ft_lang_cache = {} + local bufnr = create_buffer({ + 'diff --git a/test.sh b/test.sh', + '@@ -1,3 +1,4 @@', + ' #!/usr/bin/env bash', + ' set -euo pipefail', + '-echo "running tests..."', + '+echo "running tests with coverage..."', + }) + local hunks = parser.parse_buffer(bufnr) + + assert.are.equal(1, #hunks) + assert.are.equal('test.sh', hunks[1].filename) + assert.are.equal('sh', hunks[1].ft) + delete_buffer(bufnr) + rawset(vim.fn, 'did_filetype', nil) + end) + it('extracts file line numbers from @@ header', function() local bufnr = create_buffer({ 'M lua/test.lua', @@ -506,7 +529,8 @@ describe('parser', function() assert.are.equal(1, #hunks) assert.are.equal(1, hunks[1].file_new_start) assert.are.equal(9, hunks[1].file_new_count) - assert.is_nil(hunks[1].file_old_start) + assert.are.equal(1, hunks[1].file_old_start) + assert.are.equal(3, hunks[1].file_old_count) delete_buffer(bufnr) end)