diff --git a/doc/diffs.nvim.txt b/doc/diffs.nvim.txt index f199d31..d1df476 100644 --- a/doc/diffs.nvim.txt +++ b/doc/diffs.nvim.txt @@ -75,6 +75,12 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads: algorithm = 'default', max_lines = 500, }, + priorities = { + clear = 198, + syntax = 199, + line_bg = 200, + char_bg = 201, + }, overrides = {}, }, fugitive = { @@ -161,6 +167,10 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads: Character-level (intra-line) diff highlighting. See |diffs.IntraConfig| for fields. + {priorities} (table, default: see below) + Extmark priority values. + See |diffs.PrioritiesConfig| for fields. + {overrides} (table, default: {}) Map of highlight group names to highlight definitions (see |nvim_set_hl()|). Applied @@ -182,6 +192,28 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads: direction. Lines are read with early exit — cost scales with this value, not file size. + *diffs.PrioritiesConfig* + Priorities config fields: ~ + {clear} (integer, default: 198) + Priority for `DiffsClear` extmarks that reset + underlying diff foreground colors. Must be + below {syntax}. + + {syntax} (integer, default: 199) + Priority for treesitter and vim syntax extmarks. + Must be below {line_bg} so that colorscheme + backgrounds on syntax groups do not obscure + line-level diff backgrounds. + + {line_bg} (integer, default: 200) + Priority for `DiffsAdd`/`DiffsDelete` line + background extmarks. Must be below {char_bg}. + + {char_bg} (integer, default: 201) + Priority for `DiffsAddText`/`DiffsDeleteText` + character-level background extmarks. Highest + priority so changed characters stand out. + *diffs.TreesitterConfig* Treesitter config fields: ~ {enabled} (boolean, default: true) @@ -418,6 +450,7 @@ Configuration: ~ disable_diagnostics = true, show_virtual_text = true, show_actions = false, + priority = 200, keymaps = { ours = 'doo', theirs = 'dot', @@ -471,6 +504,11 @@ Configuration: ~ `DiffsConflictActions` highlight group. Only keymaps that are not `false` appear. + {priority} (integer, default: 200) + Extmark priority for conflict region + backgrounds and markers. Adjust if other + plugins use the same priority range. + {keymaps} (table, default: see above) Buffer-local keymaps for conflict resolution and navigation. Each value accepts a string @@ -566,12 +604,13 @@ Summary / commit detail views: ~ - 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 + - `DiffsClear` extmarks at priority 198 clear underlying diff foreground + - Syntax highlights are applied as extmarks at priority 199 + - Background extmarks (`DiffsAdd`/`DiffsDelete`) 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 + - All priorities are configurable via |diffs.PrioritiesConfig| 4. Re-highlighting occurs on `TextChanged` (debounced) and `Syntax` events Diff mode views: ~ @@ -586,15 +625,10 @@ 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. +Treesitter parses each diff hunk in isolation. Context lines within the hunk +(lines with a ` ` prefix) provide syntactic context for the parser. In rare +cases, hunks that start or end mid-expression may produce imperfect highlights +due to treesitter error recovery. Syntax Highlighting Flash ~ *diffs-flash* diff --git a/lua/diffs/conflict.lua b/lua/diffs/conflict.lua index ce3618d..5ed8009 100644 --- a/lua/diffs/conflict.lua +++ b/lua/diffs/conflict.lua @@ -20,8 +20,6 @@ local attached_buffers = {} ---@type table local diagnostics_suppressed = {} -local PRIORITY_LINE_BG = 200 - ---@param lines string[] ---@return diffs.ConflictRegion[] function M.parse(lines) @@ -114,7 +112,7 @@ local function apply_highlights(bufnr, regions, config) end_row = region.marker_ours + 1, hl_group = 'DiffsConflictMarker', hl_eol = true, - priority = PRIORITY_LINE_BG, + priority = config.priority, }) if config.show_virtual_text then @@ -156,11 +154,11 @@ local function apply_highlights(bufnr, regions, config) end_row = line + 1, hl_group = 'DiffsConflictOurs', hl_eol = true, - priority = PRIORITY_LINE_BG, + priority = config.priority, }) pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, line, 0, { number_hl_group = 'DiffsConflictOursNr', - priority = PRIORITY_LINE_BG, + priority = config.priority, }) end @@ -169,7 +167,7 @@ local function apply_highlights(bufnr, regions, config) end_row = region.marker_base + 1, hl_group = 'DiffsConflictMarker', hl_eol = true, - priority = PRIORITY_LINE_BG, + priority = config.priority, }) for line = region.base_start, region.base_end - 1 do @@ -177,11 +175,11 @@ local function apply_highlights(bufnr, regions, config) end_row = line + 1, hl_group = 'DiffsConflictBase', hl_eol = true, - priority = PRIORITY_LINE_BG, + priority = config.priority, }) pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, line, 0, { number_hl_group = 'DiffsConflictBaseNr', - priority = PRIORITY_LINE_BG, + priority = config.priority, }) end end @@ -190,7 +188,7 @@ local function apply_highlights(bufnr, regions, config) end_row = region.marker_sep + 1, hl_group = 'DiffsConflictMarker', hl_eol = true, - priority = PRIORITY_LINE_BG, + priority = config.priority, }) for line = region.theirs_start, region.theirs_end - 1 do @@ -198,11 +196,11 @@ local function apply_highlights(bufnr, regions, config) end_row = line + 1, hl_group = 'DiffsConflictTheirs', hl_eol = true, - priority = PRIORITY_LINE_BG, + priority = config.priority, }) pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, line, 0, { number_hl_group = 'DiffsConflictTheirsNr', - priority = PRIORITY_LINE_BG, + priority = config.priority, }) end @@ -210,7 +208,7 @@ local function apply_highlights(bufnr, regions, config) end_row = region.marker_theirs + 1, hl_group = 'DiffsConflictMarker', hl_eol = true, - priority = PRIORITY_LINE_BG, + priority = config.priority, }) if config.show_virtual_text then @@ -364,6 +362,7 @@ function M.goto_next(bufnr) return end end + vim.notify('[diffs.nvim]: wrapped to first conflict', vim.log.levels.INFO) vim.api.nvim_win_set_cursor(0, { regions[1].marker_ours + 1, 0 }) end @@ -381,6 +380,7 @@ function M.goto_prev(bufnr) return end end + vim.notify('[diffs.nvim]: wrapped to last conflict', vim.log.levels.INFO) vim.api.nvim_win_set_cursor(0, { regions[#regions].marker_ours + 1, 0 }) end diff --git a/lua/diffs/fugitive.lua b/lua/diffs/fugitive.lua index b11985c..d4c3782 100644 --- a/lua/diffs/fugitive.lua +++ b/lua/diffs/fugitive.lua @@ -26,17 +26,62 @@ function M.get_section_at_line(bufnr, lnum) return nil end +---@param s string +---@return string +local function unquote(s) + if s:sub(1, 1) ~= '"' then + return s + end + local inner = s:sub(2, -2) + local result = {} + local i = 1 + while i <= #inner do + if inner:sub(i, i) == '\\' and i < #inner then + local next_char = inner:sub(i + 1, i + 1) + if next_char == 'n' then + table.insert(result, '\n') + i = i + 2 + elseif next_char == 't' then + table.insert(result, '\t') + i = i + 2 + elseif next_char == '"' then + table.insert(result, '"') + i = i + 2 + elseif next_char == '\\' then + table.insert(result, '\\') + i = i + 2 + elseif next_char:match('%d') then + local oct = inner:match('^(%d%d%d)', i + 1) + if oct then + table.insert(result, string.char(tonumber(oct, 8))) + i = i + 4 + else + table.insert(result, next_char) + i = i + 2 + end + else + table.insert(result, next_char) + i = i + 2 + end + else + table.insert(result, inner:sub(i, i)) + i = i + 1 + end + end + return table.concat(result) +end + ---@param line string ---@return string?, string?, string? local function parse_file_line(line) local old, new = line:match('^R%d*%s+(.-)%s+->%s+(.+)$') if old and new then - return vim.trim(new), vim.trim(old), 'R' + return unquote(vim.trim(new)), unquote(vim.trim(old)), 'R' end local status, filename = line:match('^([MADRCU?])[MADRCU%s]*%s+(.+)$') if status and filename then - return vim.trim(filename), nil, status + return unquote(vim.trim(filename)), nil, status end return nil, nil, nil diff --git a/lua/diffs/highlight.lua b/lua/diffs/highlight.lua index 6cdfe92..d2f65e9 100644 --- a/lua/diffs/highlight.lua +++ b/lua/diffs/highlight.lua @@ -3,38 +3,6 @@ local M = {} local dbg = require('diffs.log').dbg local diff = require('diffs.diff') ----@param filepath string ----@param from_line integer ----@param count integer ----@return string[] -local function read_line_range(filepath, from_line, count) - if count <= 0 then - return {} - end - local f = io.open(filepath, 'r') - if not f then - return {} - end - local result = {} - local line_num = 0 - for line in f:lines() do - line_num = line_num + 1 - if line_num >= from_line then - table.insert(result, line) - if #result >= count then - break - end - end - end - f:close() - return result -end - -local PRIORITY_CLEAR = 198 -local PRIORITY_SYNTAX = 199 -local PRIORITY_LINE_BG = 200 -local PRIORITY_CHAR_BG = 201 - ---@param bufnr integer ---@param ns integer ---@param hunk diffs.Hunk @@ -42,8 +10,9 @@ local PRIORITY_CHAR_BG = 201 ---@param text string ---@param lang string ---@param context_lines? string[] +---@param priorities diffs.PrioritiesConfig ---@return integer -local function highlight_text(bufnr, ns, hunk, col_offset, text, lang, context_lines) +local function highlight_text(bufnr, ns, hunk, col_offset, text, lang, context_lines, priorities) local parse_text = text if context_lines and #context_lines > 0 then parse_text = text .. '\n' .. table.concat(context_lines, '\n') @@ -77,7 +46,7 @@ local function highlight_text(bufnr, ns, hunk, col_offset, text, lang, context_l local buf_sc = col_offset + sc local buf_ec = col_offset + ec - local priority = lang == 'diff' and (tonumber(metadata.priority) or 100) or PRIORITY_SYNTAX + local priority = lang == 'diff' and (tonumber(metadata.priority) or 100) or priorities.syntax pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_sr, buf_sc, { end_row = buf_er, @@ -103,6 +72,7 @@ end ---@param line_map table ---@param col_offset integer ---@param covered_lines? table +---@param priorities diffs.PrioritiesConfig ---@return integer local function highlight_treesitter( bufnr, @@ -111,7 +81,8 @@ local function highlight_treesitter( lang, line_map, col_offset, - covered_lines + covered_lines, + priorities ) local code = table.concat(code_lines, '\n') if code == '' then @@ -152,7 +123,7 @@ local function highlight_treesitter( local buf_ec = ec + col_offset local priority = tree_lang == 'diff' and (tonumber(metadata.priority) or 100) - or PRIORITY_SYNTAX + or priorities.syntax pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_sr, buf_sc, { end_row = buf_er, @@ -219,8 +190,17 @@ end ---@param code_lines string[] ---@param covered_lines? table ---@param leading_offset? integer +---@param priorities diffs.PrioritiesConfig ---@return integer -local function highlight_vim_syntax(bufnr, ns, hunk, code_lines, covered_lines, leading_offset) +local function highlight_vim_syntax( + bufnr, + ns, + hunk, + code_lines, + covered_lines, + leading_offset, + priorities +) local ft = hunk.ft if not ft then return 0 @@ -267,7 +247,7 @@ local function highlight_vim_syntax(bufnr, ns, hunk, code_lines, covered_lines, pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, span.col_start, { end_col = span.col_end, hl_group = span.hl_name, - priority = PRIORITY_SYNTAX, + priority = priorities.syntax, }) extmark_count = extmark_count + 1 if covered_lines then @@ -284,6 +264,7 @@ end ---@param hunk diffs.Hunk ---@param opts diffs.HunkOpts function M.highlight_hunk(bufnr, ns, hunk, opts) + local p = opts.highlights.priorities local use_ts = hunk.lang and opts.highlights.treesitter.enabled local use_vim = not use_ts and hunk.ft and opts.highlights.vim.enabled @@ -303,21 +284,6 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) ---@type table local covered_lines = {} - local ctx_cfg = opts.highlights.context - local context = (ctx_cfg and ctx_cfg.enabled) and ctx_cfg.lines or 0 - local leading = {} - local trailing = {} - if (use_ts or use_vim) and context > 0 and hunk.file_new_start and hunk.repo_root then - local filepath = vim.fs.joinpath(hunk.repo_root, hunk.filename) - local lead_from = math.max(1, hunk.file_new_start - context) - local lead_count = hunk.file_new_start - lead_from - if lead_count > 0 then - leading = read_line_range(filepath, lead_from, lead_count) - end - local trail_from = hunk.file_new_start + (hunk.file_new_count or 0) - trailing = read_line_range(filepath, trail_from, context) - end - local extmark_count = 0 if use_ts then ---@type string[] @@ -329,11 +295,6 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) ---@type table local old_map = {} - for _, pad_line in ipairs(leading) do - table.insert(new_code, pad_line) - table.insert(old_code, pad_line) - end - for i, line in ipairs(hunk.lines) do local prefix = line:sub(1, 1) local stripped = line:sub(2) @@ -352,21 +313,17 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) end end - for _, pad_line in ipairs(trailing) do - table.insert(new_code, pad_line) - table.insert(old_code, pad_line) - end - - extmark_count = highlight_treesitter(bufnr, ns, new_code, hunk.lang, new_map, 1, covered_lines) + extmark_count = + highlight_treesitter(bufnr, ns, new_code, hunk.lang, new_map, 1, covered_lines, p) extmark_count = extmark_count - + highlight_treesitter(bufnr, ns, old_code, hunk.lang, old_map, 1, covered_lines) + + highlight_treesitter(bufnr, ns, old_code, hunk.lang, old_map, 1, 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 = PRIORITY_CLEAR, + priority = p.clear, }) local header_extmarks = highlight_text( bufnr, @@ -375,7 +332,8 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) hunk.header_context_col, hunk.header_context, hunk.lang, - new_code + new_code, + p ) if header_extmarks > 0 then dbg('header %s:%d applied %d extmarks', hunk.filename, hunk.start_line, header_extmarks) @@ -385,16 +343,10 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) elseif use_vim then ---@type string[] local code_lines = {} - for _, pad_line in ipairs(leading) do - table.insert(code_lines, pad_line) - end for _, line in ipairs(hunk.lines) do table.insert(code_lines, line:sub(2)) end - for _, pad_line in ipairs(trailing) do - table.insert(code_lines, pad_line) - end - extmark_count = highlight_vim_syntax(bufnr, ns, hunk, code_lines, covered_lines, #leading) + extmark_count = highlight_vim_syntax(bufnr, ns, hunk, code_lines, covered_lines, 0, p) end if @@ -409,7 +361,7 @@ 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) + + highlight_treesitter(bufnr, ns, hunk.header_lines, 'diff', header_map, 0, nil, p) end ---@type diffs.IntraChanges? @@ -467,7 +419,7 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 1, { end_col = line_len, hl_group = 'DiffsClear', - priority = PRIORITY_CLEAR, + priority = p.clear, }) end @@ -476,12 +428,12 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) end_row = buf_line + 1, hl_group = line_hl, hl_eol = true, - priority = PRIORITY_LINE_BG, + 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 = PRIORITY_LINE_BG, + priority = p.line_bg, }) end end @@ -501,7 +453,7 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) local ok, err = pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, span.col_start, { end_col = span.col_end, hl_group = char_hl, - priority = PRIORITY_CHAR_BG, + priority = p.char_bg, }) if not ok then dbg('char extmark FAILED: %s', err) diff --git a/lua/diffs/init.lua b/lua/diffs/init.lua index 85dc879..00dddca 100644 --- a/lua/diffs/init.lua +++ b/lua/diffs/init.lua @@ -15,6 +15,12 @@ ---@field enabled boolean ---@field lines integer +---@class diffs.PrioritiesConfig +---@field clear integer +---@field syntax integer +---@field line_bg integer +---@field char_bg integer + ---@class diffs.Highlights ---@field background boolean ---@field gutter boolean @@ -24,6 +30,7 @@ ---@field treesitter diffs.TreesitterConfig ---@field vim diffs.VimConfig ---@field intra diffs.IntraConfig +---@field priorities diffs.PrioritiesConfig ---@class diffs.FugitiveConfig ---@field horizontal string|false @@ -43,6 +50,7 @@ ---@field show_virtual_text boolean ---@field format_virtual_text? fun(side: string, keymap: string|false): string? ---@field show_actions boolean +---@field priority integer ---@field keymaps diffs.ConflictKeymaps ---@class diffs.Config @@ -121,6 +129,12 @@ local default_config = { algorithm = 'default', max_lines = 500, }, + priorities = { + clear = 198, + syntax = 199, + line_bg = 200, + char_bg = 201, + }, }, fugitive = { horizontal = 'du', @@ -131,6 +145,7 @@ local default_config = { disable_diagnostics = true, show_virtual_text = true, show_actions = false, + priority = 200, keymaps = { ours = 'doo', theirs = 'dot', @@ -328,6 +343,7 @@ local function init() ['highlights.treesitter'] = { opts.highlights.treesitter, 'table', true }, ['highlights.vim'] = { opts.highlights.vim, 'table', true }, ['highlights.intra'] = { opts.highlights.intra, 'table', true }, + ['highlights.priorities'] = { opts.highlights.priorities, 'table', true }, }) if opts.highlights.context then @@ -368,6 +384,15 @@ local function init() ['highlights.intra.max_lines'] = { opts.highlights.intra.max_lines, 'number', true }, }) end + + if opts.highlights.priorities then + vim.validate({ + ['highlights.priorities.clear'] = { opts.highlights.priorities.clear, 'number', true }, + ['highlights.priorities.syntax'] = { opts.highlights.priorities.syntax, 'number', true }, + ['highlights.priorities.line_bg'] = { opts.highlights.priorities.line_bg, 'number', true }, + ['highlights.priorities.char_bg'] = { opts.highlights.priorities.char_bg, 'number', true }, + }) + end end if opts.fugitive then @@ -396,6 +421,7 @@ local function init() ['conflict.show_virtual_text'] = { opts.conflict.show_virtual_text, 'boolean', true }, ['conflict.format_virtual_text'] = { opts.conflict.format_virtual_text, 'function', true }, ['conflict.show_actions'] = { opts.conflict.show_actions, 'boolean', true }, + ['conflict.priority'] = { opts.conflict.priority, 'number', true }, ['conflict.keymaps'] = { opts.conflict.keymaps, 'table', true }, }) @@ -457,6 +483,17 @@ local function init() then error('diffs: highlights.blend_alpha must be >= 0 and <= 1') end + if opts.highlights and opts.highlights.priorities then + for _, key in ipairs({ 'clear', 'syntax', 'line_bg', 'char_bg' }) do + local v = opts.highlights.priorities[key] + if v and v < 0 then + error('diffs: highlights.priorities.' .. key .. ' must be >= 0') + end + end + end + if opts.conflict and opts.conflict.priority and opts.conflict.priority < 0 then + error('diffs: conflict.priority must be >= 0') + end config = vim.tbl_deep_extend('force', default_config, opts) log.set_enabled(config.debug) diff --git a/lua/diffs/merge.lua b/lua/diffs/merge.lua index 4332220..daf72ac 100644 --- a/lua/diffs/merge.lua +++ b/lua/diffs/merge.lua @@ -298,6 +298,7 @@ function M.goto_next(bufnr) end end + vim.notify('[diffs.nvim]: wrapped to first hunk', vim.log.levels.INFO) vim.api.nvim_win_set_cursor(0, { candidates[1].start_line + 1, 0 }) end @@ -335,6 +336,7 @@ function M.goto_prev(bufnr) end end + vim.notify('[diffs.nvim]: wrapped to last hunk', vim.log.levels.INFO) vim.api.nvim_win_set_cursor(0, { candidates[#candidates].start_line + 1, 0 }) end @@ -378,6 +380,9 @@ end ---@param bufnr integer ---@param config diffs.ConflictConfig function M.setup_keymaps(bufnr, config) + resolved_hunks[bufnr] = nil + vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1) + local km = config.keymaps local maps = { diff --git a/spec/conflict_spec.lua b/spec/conflict_spec.lua index b163960..dd190c9 100644 --- a/spec/conflict_spec.lua +++ b/spec/conflict_spec.lua @@ -235,29 +235,6 @@ describe('conflict', function() helpers.delete_buffer(bufnr) end) - it('does not apply virtual text when disabled', function() - local bufnr = create_file_buffer({ - '<<<<<<< HEAD', - 'local x = 1', - '=======', - 'local x = 2', - '>>>>>>> feature', - }) - - conflict.attach(bufnr, default_config({ show_virtual_text = false })) - - local extmarks = get_extmarks(bufnr) - local virt_text_count = 0 - for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].virt_text then - virt_text_count = virt_text_count + 1 - end - end - assert.are.equal(0, virt_text_count) - - helpers.delete_buffer(bufnr) - end) - it('applies number_hl_group to content lines', function() local bufnr = create_file_buffer({ '<<<<<<< HEAD', @@ -532,6 +509,33 @@ describe('conflict', function() helpers.delete_buffer(bufnr) end) + it('goto_next notifies on wrap-around', function() + local bufnr = create_file_buffer({ + '<<<<<<< HEAD', + 'a', + '=======', + 'b', + '>>>>>>> feat', + }) + vim.api.nvim_set_current_buf(bufnr) + vim.api.nvim_win_set_cursor(0, { 5, 0 }) + + local notified = false + local orig_notify = vim.notify + vim.notify = function(msg) + if msg:match('wrapped to first conflict') then + notified = true + end + end + + conflict.goto_next(bufnr) + vim.notify = orig_notify + + assert.is_true(notified) + + helpers.delete_buffer(bufnr) + end) + it('goto_prev jumps to previous conflict', function() local bufnr = create_file_buffer({ '<<<<<<< HEAD', @@ -576,6 +580,33 @@ describe('conflict', function() helpers.delete_buffer(bufnr) end) + it('goto_prev notifies on wrap-around', function() + local bufnr = create_file_buffer({ + '<<<<<<< HEAD', + 'a', + '=======', + 'b', + '>>>>>>> feat', + }) + vim.api.nvim_set_current_buf(bufnr) + vim.api.nvim_win_set_cursor(0, { 1, 0 }) + + local notified = false + local orig_notify = vim.notify + vim.notify = function(msg) + if msg:match('wrapped to last conflict') then + notified = true + end + end + + conflict.goto_prev(bufnr) + vim.notify = orig_notify + + assert.is_true(notified) + + helpers.delete_buffer(bufnr) + end) + it('goto_next does nothing with no conflicts', function() local bufnr = create_file_buffer({ 'normal line' }) vim.api.nvim_set_current_buf(bufnr) diff --git a/spec/fugitive_spec.lua b/spec/fugitive_spec.lua index 244e1b7..95b40a3 100644 --- a/spec/fugitive_spec.lua +++ b/spec/fugitive_spec.lua @@ -87,28 +87,6 @@ describe('fugitive', function() vim.api.nvim_buf_delete(buf, { force = true }) end) - it('parses added file', function() - local buf = create_status_buffer({ - 'Staged (1)', - 'A newfile.lua', - }) - local filename, section = fugitive.get_file_at_line(buf, 2) - assert.equals('newfile.lua', filename) - assert.equals('staged', section) - vim.api.nvim_buf_delete(buf, { force = true }) - end) - - it('parses deleted file', function() - local buf = create_status_buffer({ - 'Staged (1)', - 'D oldfile.lua', - }) - local filename, section = fugitive.get_file_at_line(buf, 2) - assert.equals('oldfile.lua', filename) - assert.equals('staged', section) - vim.api.nvim_buf_delete(buf, { force = true }) - end) - it('parses renamed file and returns both names', function() local buf = create_status_buffer({ 'Staged (1)', @@ -157,28 +135,6 @@ describe('fugitive', function() vim.api.nvim_buf_delete(buf, { force = true }) end) - it('handles renamed file in subdirectory', function() - local buf = create_status_buffer({ - 'Staged (1)', - 'R src/old.lua -> src/new.lua', - }) - local filename, _, _, old_filename = fugitive.get_file_at_line(buf, 2) - assert.equals('src/new.lua', filename) - assert.equals('src/old.lua', old_filename) - vim.api.nvim_buf_delete(buf, { force = true }) - end) - - it('handles renamed file moved to different directory', function() - local buf = create_status_buffer({ - 'Staged (1)', - 'R old/file.lua -> new/file.lua', - }) - local filename, _, _, old_filename = fugitive.get_file_at_line(buf, 2) - assert.equals('new/file.lua', filename) - assert.equals('old/file.lua', old_filename) - vim.api.nvim_buf_delete(buf, { force = true }) - end) - it('KNOWN LIMITATION: filename containing arrow parsed incorrectly', function() local buf = create_status_buffer({ 'Staged (1)', @@ -190,77 +146,54 @@ describe('fugitive', function() vim.api.nvim_buf_delete(buf, { force = true }) end) - it('handles double extensions', function() + it('unquotes git-quoted filenames with spaces', function() + local buf = create_status_buffer({ + 'Unstaged (1)', + 'M "path with spaces/file.lua"', + }) + local filename = fugitive.get_file_at_line(buf, 2) + assert.equals('path with spaces/file.lua', filename) + vim.api.nvim_buf_delete(buf, { force = true }) + end) + + it('unquotes escaped quotes in filenames', function() + local buf = create_status_buffer({ + 'Unstaged (1)', + 'M "file\\"name.lua"', + }) + local filename = fugitive.get_file_at_line(buf, 2) + assert.equals('file"name.lua', filename) + vim.api.nvim_buf_delete(buf, { force = true }) + end) + + it('unquotes octal escapes in filenames', function() + local buf = create_status_buffer({ + 'Unstaged (1)', + 'M "\\303\\251le.lua"', + }) + local filename = fugitive.get_file_at_line(buf, 2) + assert.equals('\195\169le.lua', filename) + vim.api.nvim_buf_delete(buf, { force = true }) + end) + + it('passes through unquoted filenames unchanged', function() + local buf = create_status_buffer({ + 'Unstaged (1)', + 'M normal.lua', + }) + local filename = fugitive.get_file_at_line(buf, 2) + assert.equals('normal.lua', filename) + vim.api.nvim_buf_delete(buf, { force = true }) + end) + + it('unquotes renamed files with quotes', function() local buf = create_status_buffer({ 'Staged (1)', - 'M test.spec.lua', + 'R100 "old name.lua" -> "new name.lua"', }) local filename, _, _, old_filename = fugitive.get_file_at_line(buf, 2) - assert.equals('test.spec.lua', filename) - assert.is_nil(old_filename) - vim.api.nvim_buf_delete(buf, { force = true }) - end) - - it('handles hyphenated filenames', function() - local buf = create_status_buffer({ - 'Unstaged (1)', - 'M my-component-test.lua', - }) - local filename, section = fugitive.get_file_at_line(buf, 2) - assert.equals('my-component-test.lua', filename) - assert.equals('unstaged', section) - vim.api.nvim_buf_delete(buf, { force = true }) - end) - - it('handles underscores and numbers', function() - local buf = create_status_buffer({ - 'Staged (1)', - 'A test_file_123.lua', - }) - local filename = fugitive.get_file_at_line(buf, 2) - assert.equals('test_file_123.lua', filename) - vim.api.nvim_buf_delete(buf, { force = true }) - end) - - it('handles dotfiles', function() - local buf = create_status_buffer({ - 'Unstaged (1)', - 'M .gitignore', - }) - local filename = fugitive.get_file_at_line(buf, 2) - assert.equals('.gitignore', filename) - vim.api.nvim_buf_delete(buf, { force = true }) - end) - - it('handles renamed with complex names', function() - local buf = create_status_buffer({ - 'Staged (1)', - 'R src/old-file.spec.lua -> src/new-file.spec.lua', - }) - local filename, _, _, old_filename = fugitive.get_file_at_line(buf, 2) - assert.equals('src/new-file.spec.lua', filename) - assert.equals('src/old-file.spec.lua', old_filename) - vim.api.nvim_buf_delete(buf, { force = true }) - end) - - it('handles deeply nested paths', function() - local buf = create_status_buffer({ - 'Unstaged (1)', - 'M lua/diffs/ui/components/diff-view.lua', - }) - local filename = fugitive.get_file_at_line(buf, 2) - assert.equals('lua/diffs/ui/components/diff-view.lua', filename) - vim.api.nvim_buf_delete(buf, { force = true }) - end) - - it('parses untracked file', function() - local buf = create_status_buffer({ - 'Untracked (1)', - '? untracked.lua', - }) - local filename, section = fugitive.get_file_at_line(buf, 2) - assert.equals('untracked.lua', filename) - assert.equals('untracked', section) + assert.equals('new name.lua', filename) + assert.equals('old name.lua', old_filename) vim.api.nvim_buf_delete(buf, { force = true }) end) @@ -321,30 +254,6 @@ describe('fugitive', function() vim.api.nvim_buf_delete(buf, { force = true }) end) - it('detects section header for Unstaged', function() - local buf = create_status_buffer({ - 'Unstaged (3)', - 'M file1.lua', - }) - local filename, section, is_header = fugitive.get_file_at_line(buf, 1) - assert.is_nil(filename) - assert.equals('unstaged', section) - assert.is_true(is_header) - vim.api.nvim_buf_delete(buf, { force = true }) - end) - - it('detects section header for Untracked', function() - local buf = create_status_buffer({ - 'Untracked (1)', - '? newfile.lua', - }) - local filename, section, is_header = fugitive.get_file_at_line(buf, 1) - assert.is_nil(filename) - assert.equals('untracked', section) - assert.is_true(is_header) - vim.api.nvim_buf_delete(buf, { force = true }) - end) - it('returns is_header=false for file lines', function() local buf = create_status_buffer({ 'Staged (1)', @@ -406,22 +315,6 @@ describe('fugitive', function() vim.api.nvim_buf_delete(buf, { force = true }) end) - it('returns hunk header and offset for - line', function() - local buf = create_status_buffer({ - 'Unstaged (1)', - 'M file.lua', - '@@ -1,3 +1,3 @@', - ' local M = {}', - '-local old = false', - ' return M', - }) - local pos = fugitive.get_hunk_position(buf, 5) - assert.is_not_nil(pos) - assert.equals('@@ -1,3 +1,3 @@', pos.hunk_header) - assert.equals(2, pos.offset) - vim.api.nvim_buf_delete(buf, { force = true }) - end) - it('returns hunk header and offset for context line', function() local buf = create_status_buffer({ 'Unstaged (1)', diff --git a/spec/highlight_spec.lua b/spec/highlight_spec.lua index 0b9051e..4064857 100644 --- a/spec/highlight_spec.lua +++ b/spec/highlight_spec.lua @@ -51,6 +51,12 @@ describe('highlight', function() algorithm = 'default', max_lines = 500, }, + priorities = { + clear = 198, + syntax = 199, + line_bg = 200, + char_bg = 201, + }, }, } if overrides then @@ -64,27 +70,6 @@ describe('highlight', function() return opts end - it('applies extmarks for lua code', 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()) - - local extmarks = get_extmarks(bufnr) - assert.is_true(#extmarks > 0) - delete_buffer(bufnr) - end) - it('applies DiffsClear extmarks to clear diff colors', function() local bufnr = create_buffer({ '@@ -1,1 +1,2 @@', @@ -190,36 +175,6 @@ describe('highlight', function() delete_buffer(bufnr) end) - it('highlights header context when enabled', function() - local bufnr = create_buffer({ - '@@ -10,3 +10,4 @@ function hello()', - ' local x = 1', - '+local y = 2', - }) - - local hunk = { - filename = 'test.lua', - lang = 'lua', - start_line = 1, - header_context = 'function hello()', - header_context_col = 18, - lines = { ' local x = 1', '+local y = 2' }, - } - - highlight.highlight_hunk(bufnr, ns, hunk, default_opts()) - - local extmarks = get_extmarks(bufnr) - local has_header_extmark = false - for _, mark in ipairs(extmarks) do - if mark[2] == 0 then - has_header_extmark = true - break - end - end - assert.is_true(has_header_extmark) - delete_buffer(bufnr) - end) - it('highlights function keyword in header context', function() local bufnr = create_buffer({ '@@ -5,3 +5,4 @@ function M.setup()', @@ -280,44 +235,6 @@ describe('highlight', function() delete_buffer(bufnr) end) - it('handles empty hunk lines', function() - local bufnr = create_buffer({ - '@@ -1,0 +1,0 @@', - }) - - local hunk = { - filename = 'test.lua', - lang = 'lua', - start_line = 1, - lines = {}, - } - - assert.has_no.errors(function() - highlight.highlight_hunk(bufnr, ns, hunk, default_opts()) - end) - delete_buffer(bufnr) - end) - - it('handles code that is just whitespace', function() - local bufnr = create_buffer({ - '@@ -1,1 +1,2 @@', - ' ', - '+ ', - }) - - local hunk = { - filename = 'test.lua', - lang = 'lua', - start_line = 1, - lines = { ' ', '+ ' }, - } - - assert.has_no.errors(function() - highlight.highlight_hunk(bufnr, ns, hunk, default_opts()) - end) - delete_buffer(bufnr) - end) - it('applies overlay extmarks when hide_prefix enabled', function() local bufnr = create_buffer({ '@@ -1,1 +1,2 @@', @@ -345,33 +262,6 @@ describe('highlight', function() delete_buffer(bufnr) end) - it('does not apply overlay extmarks when hide_prefix disabled', 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({ hide_prefix = false })) - - local extmarks = get_extmarks(bufnr) - local overlay_count = 0 - for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].virt_text_pos == 'overlay' then - overlay_count = overlay_count + 1 - end - end - assert.are.equal(0, overlay_count) - delete_buffer(bufnr) - end) - it('applies DiffAdd background to + lines when background enabled', function() local bufnr = create_buffer({ '@@ -1,1 +1,2 @@', @@ -438,39 +328,6 @@ describe('highlight', function() delete_buffer(bufnr) end) - it('does not apply background when background disabled', 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 = false } }) - ) - - local extmarks = get_extmarks(bufnr) - local has_line_hl = false - for _, mark in ipairs(extmarks) do - if mark[4] and (mark[4].hl_group == 'DiffsAdd' or mark[4].hl_group == 'DiffsDelete') then - has_line_hl = true - break - end - end - assert.is_false(has_line_hl) - delete_buffer(bufnr) - end) - it('applies number_hl_group when gutter enabled', function() local bufnr = create_buffer({ '@@ -1,1 +1,2 @@', @@ -504,72 +361,6 @@ describe('highlight', function() delete_buffer(bufnr) end) - it('does not apply number_hl_group when gutter disabled', 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, gutter = false } }) - ) - - local extmarks = get_extmarks(bufnr) - local has_number_hl = false - for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].number_hl_group then - has_number_hl = true - break - end - end - assert.is_false(has_number_hl) - delete_buffer(bufnr) - end) - - it('skips treesitter highlights when treesitter disabled', 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 = { treesitter = { enabled = false }, background = true } }) - ) - - local extmarks = get_extmarks(bufnr) - local has_ts_highlight = false - for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].hl_group and mark[4].hl_group:match('^@') then - has_ts_highlight = true - break - end - end - assert.is_false(has_ts_highlight) - delete_buffer(bufnr) - end) - it('still applies background when treesitter disabled', function() local bufnr = create_buffer({ '@@ -1,1 +1,2 @@', @@ -654,40 +445,6 @@ describe('highlight', function() delete_buffer(bufnr) end) - it('skips vim fallback when vim.enabled is false', function() - local bufnr = create_buffer({ - '@@ -1,1 +1,2 @@', - ' local x = 1', - '+local y = 2', - }) - - local hunk = { - filename = 'test.lua', - ft = 'abap', - lang = nil, - start_line = 1, - lines = { ' local x = 1', '+local y = 2' }, - } - - highlight.highlight_hunk( - bufnr, - ns, - hunk, - default_opts({ highlights = { vim = { enabled = false } } }) - ) - - local extmarks = get_extmarks(bufnr) - local has_syntax_hl = false - for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].hl_group and mark[4].hl_group ~= 'DiffsClear' then - has_syntax_hl = true - break - end - end - assert.is_false(has_syntax_hl) - delete_buffer(bufnr) - end) - it('respects vim.max_lines', function() local lines = { '@@ -1,100 +1,101 @@' } local hunk_lines = {} @@ -900,92 +657,6 @@ describe('highlight', function() delete_buffer(bufnr) end) - it('line bg priority > DiffsClear priority', function() - local bufnr = create_buffer({ - '@@ -1,2 +1,1 @@', - '-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) - local clear_priority = nil - local line_bg_priority = nil - for _, mark in ipairs(extmarks) do - local d = mark[4] - if d and d.hl_group == 'DiffsClear' then - clear_priority = d.priority - end - if d and (d.hl_group == 'DiffsAdd' or d.hl_group == 'DiffsDelete') then - line_bg_priority = d.priority - end - end - assert.is_not_nil(clear_priority) - assert.is_not_nil(line_bg_priority) - assert.is_true(line_bg_priority > clear_priority) - delete_buffer(bufnr) - end) - - it('char-level extmarks have higher priority than line bg', function() - vim.api.nvim_set_hl(0, 'DiffsAddText', { bg = 0x00FF00 }) - vim.api.nvim_set_hl(0, 'DiffsDeleteText', { bg = 0xFF0000 }) - - local bufnr = create_buffer({ - '@@ -1,2 +1,2 @@', - '-local x = 1', - '+local x = 2', - }) - - local hunk = { - filename = 'test.lua', - lang = 'lua', - start_line = 1, - lines = { '-local x = 1', '+local x = 2' }, - } - - highlight.highlight_hunk( - bufnr, - ns, - hunk, - default_opts({ - highlights = { - background = true, - intra = { enabled = true, algorithm = 'default', max_lines = 500 }, - }, - }) - ) - - local extmarks = get_extmarks(bufnr) - local line_bg_priority = nil - local char_bg_priority = nil - for _, mark in ipairs(extmarks) do - local d = mark[4] - if d and (d.hl_group == 'DiffsAdd' or d.hl_group == 'DiffsDelete') then - line_bg_priority = d.priority - end - if d and (d.hl_group == 'DiffsAddText' or d.hl_group == 'DiffsDeleteText') then - char_bg_priority = d.priority - end - end - assert.is_not_nil(line_bg_priority) - assert.is_not_nil(char_bg_priority) - assert.is_true(char_bg_priority > line_bg_priority) - delete_buffer(bufnr) - end) - it('creates char-level extmarks for changed characters', function() vim.api.nvim_set_hl(0, 'DiffsAddText', { bg = 0x00FF00 }) vim.api.nvim_set_hl(0, 'DiffsDeleteText', { bg = 0xFF0000 }) @@ -1029,38 +700,6 @@ describe('highlight', function() delete_buffer(bufnr) end) - it('does not create char-level extmarks when intra disabled', function() - local bufnr = create_buffer({ - '@@ -1,2 +1,2 @@', - '-local x = 1', - '+local x = 2', - }) - - local hunk = { - filename = 'test.lua', - lang = 'lua', - start_line = 1, - lines = { '-local x = 1', '+local x = 2' }, - } - - highlight.highlight_hunk( - bufnr, - ns, - hunk, - default_opts({ - highlights = { intra = { enabled = false, algorithm = 'default', max_lines = 500 } }, - }) - ) - - local extmarks = get_extmarks(bufnr) - for _, mark in ipairs(extmarks) do - local d = mark[4] - assert.is_not_equal('DiffsAddText', d and d.hl_group) - assert.is_not_equal('DiffsDeleteText', d and d.hl_group) - end - delete_buffer(bufnr) - end) - it('does not create char-level extmarks for pure additions', function() vim.api.nvim_set_hl(0, 'DiffsAddText', { bg = 0x00FF00 }) @@ -1157,142 +796,6 @@ describe('highlight', function() delete_buffer(bufnr) end) - it('context padding produces no extmarks on padding lines', function() - local repo_root = '/tmp/diffs-test-context' - vim.fn.mkdir(repo_root, 'p') - - local f = io.open(repo_root .. '/test.lua', 'w') - f:write('local M = {}\n') - f:write('function M.hello()\n') - f:write(' return "hi"\n') - f:write('end\n') - f:write('return M\n') - f:close() - - local bufnr = create_buffer({ - '@@ -3,1 +3,2 @@', - ' return "hi"', - '+"bye"', - }) - - local hunk = { - filename = 'test.lua', - lang = 'lua', - start_line = 1, - lines = { ' return "hi"', '+"bye"' }, - file_old_start = 3, - file_old_count = 1, - file_new_start = 3, - file_new_count = 2, - repo_root = repo_root, - } - - highlight.highlight_hunk( - bufnr, - ns, - hunk, - default_opts({ highlights = { context = { enabled = true, lines = 25 } } }) - ) - - local extmarks = get_extmarks(bufnr) - for _, mark in ipairs(extmarks) do - local row = mark[2] - assert.is_true(row >= 1 and row <= 2) - end - - delete_buffer(bufnr) - os.remove(repo_root .. '/test.lua') - vim.fn.delete(repo_root, 'rf') - end) - - it('context disabled matches behavior without padding', 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' }, - file_new_start = 1, - file_new_count = 2, - repo_root = '/nonexistent', - } - - highlight.highlight_hunk( - bufnr, - ns, - hunk, - default_opts({ highlights = { context = { enabled = false, lines = 0 } } }) - ) - - local extmarks = get_extmarks(bufnr) - assert.is_true(#extmarks > 0) - delete_buffer(bufnr) - end) - - it('gracefully handles missing file for context padding', 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' }, - file_new_start = 1, - file_new_count = 2, - repo_root = '/nonexistent/path', - } - - assert.has_no.errors(function() - highlight.highlight_hunk( - bufnr, - ns, - hunk, - default_opts({ highlights = { context = { enabled = true, lines = 25 } } }) - ) - end) - - local extmarks = get_extmarks(bufnr) - assert.is_true(#extmarks > 0) - delete_buffer(bufnr) - end) - - it('highlights treesitter injections', function() - local bufnr = create_buffer({ - '@@ -1,1 +1,2 @@', - ' local x = 1', - '+vim.cmd([[ echo 1 ]])', - }) - - local hunk = { - filename = 'test.lua', - lang = 'lua', - start_line = 1, - lines = { ' local x = 1', '+vim.cmd([[ echo 1 ]])' }, - } - - highlight.highlight_hunk(bufnr, ns, hunk, default_opts()) - - local extmarks = get_extmarks(bufnr) - local has_vim_capture = false - for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].hl_group and mark[4].hl_group:match('^@.*%.vim$') then - has_vim_capture = true - break - end - end - assert.is_true(has_vim_capture) - delete_buffer(bufnr) - end) - it('includes captures from both base and injected languages', function() local bufnr = create_buffer({ '@@ -1,1 +1,2 @@', @@ -1386,6 +889,7 @@ describe('highlight', function() context = { enabled = false, lines = 0 }, treesitter = { enabled = true, max_lines = 500 }, vim = { enabled = false, max_lines = 200 }, + priorities = { clear = 198, syntax = 199, line_bg = 200, char_bg = 201 }, }, } end @@ -1468,47 +972,6 @@ describe('highlight', function() assert.are.equal(0, header_extmarks) delete_buffer(bufnr) end) - - it('does not apply header highlights when treesitter disabled', 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', - }, - } - - local opts = default_opts() - opts.highlights.treesitter.enabled = false - - highlight.highlight_hunk(bufnr, ns, hunk, opts) - - local extmarks = get_extmarks(bufnr) - local header_extmarks = 0 - for _, mark in ipairs(extmarks) do - if mark[2] < 4 and mark[4] and mark[4].hl_group and mark[4].hl_group:match('^@') then - header_extmarks = header_extmarks + 1 - end - end - assert.are.equal(0, header_extmarks) - delete_buffer(bufnr) - end) end) describe('extmark priority', function() @@ -1543,40 +1006,11 @@ describe('highlight', function() context = { enabled = false, lines = 0 }, treesitter = { enabled = true, max_lines = 500 }, vim = { enabled = false, max_lines = 200 }, + priorities = { clear = 198, syntax = 199, line_bg = 200, char_bg = 201 }, }, } end - it('uses priority 199 for code languages', 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()) - - local extmarks = get_extmarks(bufnr) - local has_priority_199 = false - for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].hl_group and mark[4].hl_group:match('^@.*%.lua$') then - if mark[4].priority == 199 then - has_priority_199 = true - break - end - end - end - assert.is_true(has_priority_199) - delete_buffer(bufnr) - end) - it('uses treesitter priority for diff language', function() local bufnr = create_buffer({ 'diff --git a/test.lua b/test.lua', diff --git a/spec/merge_spec.lua b/spec/merge_spec.lua index 7d5019b..6cb9f7e 100644 --- a/spec/merge_spec.lua +++ b/spec/merge_spec.lua @@ -509,6 +509,44 @@ describe('merge', function() helpers.delete_buffer(w_bufnr) end) + it('goto_next notifies on wrap-around', function() + local working_path = '/tmp/diffs_test_wrap_notify.lua' + local w_bufnr = create_working_buffer({ + '<<<<<<< HEAD', + 'local x = 1', + '=======', + 'local x = 2', + '>>>>>>> feature', + }, working_path) + + local d_bufnr = create_diff_buffer({ + 'diff --git a/file.lua b/file.lua', + '--- a/file.lua', + '+++ b/file.lua', + '@@ -1,1 +1,1 @@', + '-local x = 1', + '+local x = 2', + }, working_path) + vim.api.nvim_set_current_buf(d_bufnr) + vim.api.nvim_win_set_cursor(0, { 6, 0 }) + + local notified = false + local orig_notify = vim.notify + vim.notify = function(msg) + if msg:match('wrapped to first hunk') then + notified = true + end + end + + merge.goto_next(d_bufnr) + vim.notify = orig_notify + + assert.is_true(notified) + + helpers.delete_buffer(d_bufnr) + helpers.delete_buffer(w_bufnr) + end) + it('goto_prev jumps to previous conflict hunk', function() local working_path = '/tmp/diffs_test_prev.lua' local w_bufnr = create_working_buffer({ @@ -577,6 +615,44 @@ describe('merge', function() helpers.delete_buffer(w_bufnr) end) + it('goto_prev notifies on wrap-around', function() + local working_path = '/tmp/diffs_test_prev_wrap_notify.lua' + local w_bufnr = create_working_buffer({ + '<<<<<<< HEAD', + 'local x = 1', + '=======', + 'local x = 2', + '>>>>>>> feature', + }, working_path) + + local d_bufnr = create_diff_buffer({ + 'diff --git a/file.lua b/file.lua', + '--- a/file.lua', + '+++ b/file.lua', + '@@ -1,1 +1,1 @@', + '-local x = 1', + '+local x = 2', + }, working_path) + vim.api.nvim_set_current_buf(d_bufnr) + vim.api.nvim_win_set_cursor(0, { 1, 0 }) + + local notified = false + local orig_notify = vim.notify + vim.notify = function(msg) + if msg:match('wrapped to last hunk') then + notified = true + end + end + + merge.goto_prev(d_bufnr) + vim.notify = orig_notify + + assert.is_true(notified) + + helpers.delete_buffer(d_bufnr) + helpers.delete_buffer(w_bufnr) + end) + it('skips resolved hunks', function() local working_path = '/tmp/diffs_test_skip_resolved.lua' local w_bufnr = create_working_buffer({ @@ -650,8 +726,19 @@ describe('merge', function() helpers.delete_buffer(d_bufnr) end) + end) + + describe('setup_keymaps', function() + it('clears resolved state on re-init', function() + local working_path = '/tmp/diffs_test_reinit.lua' + local w_bufnr = create_working_buffer({ + '<<<<<<< HEAD', + 'local x = 1', + '=======', + 'local x = 2', + '>>>>>>> feature', + }, working_path) - it('does not add hints when show_virtual_text is false', function() local d_bufnr = create_diff_buffer({ 'diff --git a/file.lua b/file.lua', '--- a/file.lua', @@ -659,21 +746,37 @@ describe('merge', function() '@@ -1,1 +1,1 @@', '-local x = 1', '+local x = 2', - }) + }, working_path) + vim.api.nvim_set_current_buf(d_bufnr) + vim.api.nvim_win_set_cursor(0, { 5, 0 }) - merge.setup_keymaps(d_bufnr, default_config({ show_virtual_text = false })) + local cfg = default_config() + merge.resolve_ours(d_bufnr, cfg) + assert.is_true(merge.is_resolved(d_bufnr, 1)) local extmarks = vim.api.nvim_buf_get_extmarks(d_bufnr, merge.get_namespace(), 0, -1, { details = true }) - local virt_text_count = 0 + assert.is_true(#extmarks > 0) + + merge.setup_keymaps(d_bufnr, cfg) + + assert.is_false(merge.is_resolved(d_bufnr, 1)) + extmarks = + vim.api.nvim_buf_get_extmarks(d_bufnr, merge.get_namespace(), 0, -1, { details = true }) + local resolved_count = 0 for _, mark in ipairs(extmarks) do if mark[4] and mark[4].virt_text then - virt_text_count = virt_text_count + 1 + for _, chunk in ipairs(mark[4].virt_text) do + if chunk[1]:match('resolved') then + resolved_count = resolved_count + 1 + end + end end end - assert.are.equal(0, virt_text_count) + assert.are.equal(0, resolved_count) helpers.delete_buffer(d_bufnr) + helpers.delete_buffer(w_bufnr) end) end) @@ -694,18 +797,6 @@ describe('merge', function() vim.api.nvim_buf_delete(buf, { force = true }) end) - it('parse_file_line returns status for modified files', function() - local fugitive = require('diffs.fugitive') - local buf = vim.api.nvim_create_buf(false, true) - vim.api.nvim_buf_set_lines(buf, 0, -1, false, { - 'Unstaged (1)', - 'M file.lua', - }) - local _, _, _, _, status = fugitive.get_file_at_line(buf, 2) - assert.are.equal('M', status) - vim.api.nvim_buf_delete(buf, { force = true }) - end) - it('walkback from hunk line propagates status', function() local fugitive = require('diffs.fugitive') local buf = vim.api.nvim_create_buf(false, true) diff --git a/spec/parser_spec.lua b/spec/parser_spec.lua index 11ac3be..32a3001 100644 --- a/spec/parser_spec.lua +++ b/spec/parser_spec.lua @@ -391,37 +391,6 @@ describe('parser', function() vim.fn.delete(repo_root, 'rf') end) - it('detects python from shebang without open buffer', function() - local repo_root = '/tmp/diffs-test-shebang-py' - vim.fn.mkdir(repo_root, 'p') - - local file_path = repo_root .. '/deploy' - local f = io.open(file_path, 'w') - f:write('#!/usr/bin/env python3\n') - f:write('import sys\n') - f:write('print("hi")\n') - f:close() - - local diff_buf = create_buffer({ - 'M deploy', - '@@ -1,2 +1,3 @@', - ' #!/usr/bin/env python3', - '+import sys', - ' print("hi")', - }) - vim.api.nvim_buf_set_var(diff_buf, 'diffs_repo_root', repo_root) - - local hunks = parser.parse_buffer(diff_buf) - - assert.are.equal(1, #hunks) - assert.are.equal('deploy', hunks[1].filename) - assert.are.equal('python', hunks[1].ft) - - delete_buffer(diff_buf) - os.remove(file_path) - vim.fn.delete(repo_root, 'rf') - end) - it('extracts file line numbers from @@ header', function() local bufnr = create_buffer({ 'M lua/test.lua', @@ -440,22 +409,6 @@ describe('parser', function() delete_buffer(bufnr) end) - it('extracts large line numbers from @@ header', function() - local bufnr = create_buffer({ - 'M lua/test.lua', - '@@ -100,20 +200,30 @@', - ' local M = {}', - }) - local hunks = parser.parse_buffer(bufnr) - - assert.are.equal(1, #hunks) - assert.are.equal(100, hunks[1].file_old_start) - assert.are.equal(20, hunks[1].file_old_count) - assert.are.equal(200, hunks[1].file_new_start) - assert.are.equal(30, hunks[1].file_new_count) - delete_buffer(bufnr) - end) - it('defaults count to 1 when omitted in @@ header', function() local bufnr = create_buffer({ 'M lua/test.lua', @@ -487,18 +440,5 @@ describe('parser', function() assert.are.equal('/tmp/test-repo', hunks[1].repo_root) delete_buffer(bufnr) end) - - it('repo_root is nil when not available', function() - local bufnr = create_buffer({ - 'M lua/test.lua', - '@@ -1,3 +1,4 @@', - ' local M = {}', - }) - local hunks = parser.parse_buffer(bufnr) - - assert.are.equal(1, #hunks) - assert.is_nil(hunks[1].repo_root) - delete_buffer(bufnr) - end) end) end)