diff --git a/README.md b/README.md index 996ee27..f0a04a8 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,18 @@ syntax highlighting. ## Features -- Treesitter syntax highlighting in fugitive diffs and commit views -- Character-level intra-line diff highlighting -- `:Gdiff` unified diff against any revision -- Background-only diff colors for `&diff` buffers -- Inline merge conflict detection, highlighting, and resolution -- Vim syntax fallback, context padding, configurable blend/debounce +- Treesitter syntax highlighting in `:Git` diffs and commit views +- Diff header highlighting (`diff --git`, `index`, `---`, `+++`) +- `:Gdiffsplit` / `:Gvdiffsplit` syntax through diff backgrounds +- `:Gdiff` unified diff against any git revision with syntax highlighting +- Fugitive status buffer keymaps (`du`/`dU`) for unified diffs +- Background-only diff colors for any `&diff` buffer (`:diffthis`, `vimdiff`) +- Vim syntax fallback for languages without a treesitter parser +- Hunk header context highlighting (`@@ ... @@ function foo()`) +- Character-level (intra-line) diff highlighting for changed characters +- Inline merge conflict detection, highlighting, and resolution keymaps +- Configurable debouncing, max lines, diff prefix concealment, blend alpha, and + highlight overrides ## Requirements diff --git a/doc/diffs.nvim.txt b/doc/diffs.nvim.txt index f199d31..6bd848c 100644 --- a/doc/diffs.nvim.txt +++ b/doc/diffs.nvim.txt @@ -85,7 +85,6 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads: enabled = true, disable_diagnostics = true, show_virtual_text = true, - show_actions = false, keymaps = { ours = 'doo', theirs = 'dot', @@ -417,7 +416,6 @@ Configuration: ~ enabled = true, disable_diagnostics = true, show_virtual_text = true, - show_actions = false, keymaps = { ours = 'doo', theirs = 'dot', @@ -444,32 +442,9 @@ Configuration: ~ diagnostics alone. {show_virtual_text} (boolean, default: true) - Show `(current)` and `(incoming)` labels at - the end of `<<<<<<<` and `>>>>>>>` marker - lines. Also controls hunk hints in merge - diff views. - - {format_virtual_text} (function|nil, default: nil) - Custom formatter for virtual text labels. - Receives `(side, keymap)` where `side` is - `"ours"` or `"theirs"` and `keymap` is the - configured keymap string or `false`. Return - a string (label text without parens) or - `nil` to hide the label. Example: >lua - format_virtual_text = function(side, keymap) - if keymap then - return side .. ' [' .. keymap .. ']' - end - return side - end -< - - {show_actions} (boolean, default: false) - Show a codelens-style action line above each - `<<<<<<<` marker listing available resolution - keymaps. Renders as virtual lines using the - `DiffsConflictActions` highlight group. - Only keymaps that are not `false` appear. + Show virtual text labels (" current" and + " incoming") at the end of `<<<<<<<` and + `>>>>>>>` marker lines. {keymaps} (table, default: see above) Buffer-local keymaps for conflict resolution @@ -712,10 +687,6 @@ Conflict highlights: ~ *DiffsConflictBaseNr* DiffsConflictBaseNr Line number for base content lines (diff3). - *DiffsConflictActions* - DiffsConflictActions Dimmed foreground (no bold) for the codelens-style - action line shown when `show_actions` is true. - Diff mode window highlights: ~ These are used for |winhighlight| remapping in `&diff` windows. diff --git a/lua/diffs/conflict.lua b/lua/diffs/conflict.lua index ce3618d..894c0b8 100644 --- a/lua/diffs/conflict.lua +++ b/lua/diffs/conflict.lua @@ -92,17 +92,6 @@ local function parse_buffer(bufnr) return M.parse(lines) end ----@param side string ----@param config diffs.ConflictConfig ----@return string? -local function get_virtual_text_label(side, config) - if config.format_virtual_text then - local keymap = side == 'ours' and config.keymaps.ours or config.keymaps.theirs - return config.format_virtual_text(side, keymap) - end - return side == 'ours' and 'current' or 'incoming' -end - ---@param bufnr integer ---@param regions diffs.ConflictRegion[] ---@param config diffs.ConflictConfig @@ -118,37 +107,10 @@ local function apply_highlights(bufnr, regions, config) }) if config.show_virtual_text then - local ours_label = get_virtual_text_label('ours', config) - if ours_label then - pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, region.marker_ours, 0, { - virt_text = { { ' (' .. ours_label .. ')', 'DiffsConflictMarker' } }, - virt_text_pos = 'eol', - }) - end - end - - if config.show_actions then - local parts = {} - local actions = { - { 'Current', config.keymaps.ours }, - { 'Incoming', config.keymaps.theirs }, - { 'Both', config.keymaps.both }, - { 'None', config.keymaps.none }, - } - for _, action in ipairs(actions) do - if action[2] then - if #parts > 0 then - table.insert(parts, { ' \226\148\130 ', 'DiffsConflictActions' }) - end - table.insert(parts, { ('%s (%s)'):format(action[1], action[2]), 'DiffsConflictActions' }) - end - end - if #parts > 0 then - pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, region.marker_ours, 0, { - virt_lines = { parts }, - virt_lines_above = true, - }) - end + pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, region.marker_ours, 0, { + virt_text = { { ' (current)', 'DiffsConflictMarker' } }, + virt_text_pos = 'eol', + }) end for line = region.ours_start, region.ours_end - 1 do @@ -214,13 +176,10 @@ local function apply_highlights(bufnr, regions, config) }) if config.show_virtual_text then - local theirs_label = get_virtual_text_label('theirs', config) - if theirs_label then - pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, region.marker_theirs, 0, { - virt_text = { { ' (' .. theirs_label .. ')', 'DiffsConflictMarker' } }, - virt_text_pos = 'eol', - }) - end + pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, region.marker_theirs, 0, { + virt_text = { { ' (incoming)', 'DiffsConflictMarker' } }, + virt_text_pos = 'eol', + }) end end end diff --git a/lua/diffs/git.lua b/lua/diffs/git.lua index 1aa6328..7695283 100644 --- a/lua/diffs/git.lua +++ b/lua/diffs/git.lua @@ -1,19 +1,13 @@ local M = {} -local repo_root_cache = {} - ---@param filepath string ---@return string? function M.get_repo_root(filepath) local dir = vim.fn.fnamemodify(filepath, ':h') - if repo_root_cache[dir] ~= nil then - return repo_root_cache[dir] - end local result = vim.fn.systemlist({ 'git', '-C', dir, 'rev-parse', '--show-toplevel' }) if vim.v.shell_error ~= 0 then return nil end - repo_root_cache[dir] = result[1] return result[1] end diff --git a/lua/diffs/highlight.lua b/lua/diffs/highlight.lua index 6cdfe92..306b0e5 100644 --- a/lua/diffs/highlight.lua +++ b/lua/diffs/highlight.lua @@ -238,7 +238,7 @@ local function highlight_vim_syntax(bufnr, ns, hunk, code_lines, covered_lines, local spans = {} - pcall(vim.api.nvim_buf_call, scratch, function() + vim.api.nvim_buf_call(scratch, function() vim.cmd('setlocal syntax=' .. ft) vim.cmd('redraw') @@ -256,7 +256,7 @@ local function highlight_vim_syntax(bufnr, ns, hunk, code_lines, covered_lines, spans = M.coalesce_syntax_spans(query_fn, code_lines) end) - pcall(vim.api.nvim_buf_delete, scratch, { force = true }) + vim.api.nvim_buf_delete(scratch, { force = true }) local hunk_line_count = #hunk.lines local extmark_count = 0 diff --git a/lua/diffs/init.lua b/lua/diffs/init.lua index 85dc879..cdc29d1 100644 --- a/lua/diffs/init.lua +++ b/lua/diffs/init.lua @@ -41,8 +41,6 @@ ---@field enabled boolean ---@field disable_diagnostics boolean ---@field show_virtual_text boolean ----@field format_virtual_text? fun(side: string, keymap: string|false): string? ----@field show_actions boolean ---@field keymaps diffs.ConflictKeymaps ---@class diffs.Config @@ -130,7 +128,6 @@ local default_config = { enabled = true, disable_diagnostics = true, show_virtual_text = true, - show_actions = false, keymaps = { ours = 'doo', theirs = 'dot', @@ -203,9 +200,7 @@ local function create_debounced_highlight(bufnr) timer = nil t:close() end - if vim.api.nvim_buf_is_valid(bufnr) then - highlight_buffer(bufnr) - end + highlight_buffer(bufnr) end) ) end @@ -279,7 +274,6 @@ local function compute_highlight_groups() vim.api.nvim_set_hl(0, 'DiffsConflictTheirs', { default = true, bg = blended_theirs }) vim.api.nvim_set_hl(0, 'DiffsConflictBase', { default = true, bg = blended_base }) vim.api.nvim_set_hl(0, 'DiffsConflictMarker', { default = true, fg = 0x808080, bold = true }) - vim.api.nvim_set_hl(0, 'DiffsConflictActions', { default = true, fg = 0x808080 }) vim.api.nvim_set_hl( 0, 'DiffsConflictOursNr', @@ -394,8 +388,6 @@ local function init() ['conflict.enabled'] = { opts.conflict.enabled, 'boolean', true }, ['conflict.disable_diagnostics'] = { opts.conflict.disable_diagnostics, 'boolean', true }, ['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.keymaps'] = { opts.conflict.keymaps, 'table', true }, }) diff --git a/lua/diffs/lib.lua b/lua/diffs/lib.lua index 8376925..5b3254b 100644 --- a/lua/diffs/lib.lua +++ b/lua/diffs/lib.lua @@ -8,9 +8,6 @@ local cached_handle = nil ---@type boolean local download_in_progress = false ----@type fun(handle: table?)[] -local pending_callbacks = {} - ---@return string local function get_os() local os_name = jit.os:lower() @@ -167,10 +164,9 @@ function M.ensure(callback) return end - table.insert(pending_callbacks, callback) - if download_in_progress then - dbg('download already in progress, queued callback') + dbg('download already in progress') + callback(nil) return end @@ -196,25 +192,21 @@ function M.ensure(callback) vim.system(cmd, {}, function(result) download_in_progress = false vim.schedule(function() - local handle = nil if result.code ~= 0 then vim.notify('[diffs] failed to download libvscode_diff', vim.log.levels.WARN) dbg('curl failed: %s', result.stderr or '') - else - local f = io.open(version_path(), 'w') - if f then - f:write(EXPECTED_VERSION) - f:close() - end - vim.notify('[diffs] libvscode_diff downloaded', vim.log.levels.INFO) - handle = M.load() + callback(nil) + return end - local cbs = pending_callbacks - pending_callbacks = {} - for _, cb in ipairs(cbs) do - cb(handle) + local f = io.open(version_path(), 'w') + if f then + f:write(EXPECTED_VERSION) + f:close() end + + vim.notify('[diffs] libvscode_diff downloaded', vim.log.levels.INFO) + callback(M.load()) end) end) end diff --git a/lua/diffs/merge.lua b/lua/diffs/merge.lua index 4332220..9bb9d68 100644 --- a/lua/diffs/merge.lua +++ b/lua/diffs/merge.lua @@ -338,43 +338,6 @@ function M.goto_prev(bufnr) vim.api.nvim_win_set_cursor(0, { candidates[#candidates].start_line + 1, 0 }) end ----@param bufnr integer ----@param config diffs.ConflictConfig -local function apply_hunk_hints(bufnr, config) - if not config.show_virtual_text then - return - end - - local hunks = M.parse_hunks(bufnr) - for _, hunk in ipairs(hunks) do - if M.is_resolved(bufnr, hunk.index) then - add_resolved_virtual_text(bufnr, hunk) - else - local parts = {} - local actions = { - { 'current', config.keymaps.ours }, - { 'incoming', config.keymaps.theirs }, - { 'both', config.keymaps.both }, - { 'none', config.keymaps.none }, - } - for _, action in ipairs(actions) do - if action[2] then - if #parts > 0 then - table.insert(parts, { ' | ', 'Comment' }) - end - table.insert(parts, { ('%s: %s'):format(action[2], action[1]), 'Comment' }) - end - end - if #parts > 0 then - pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, hunk.start_line, 0, { - virt_text = parts, - virt_text_pos = 'eol', - }) - end - end - end -end - ---@param bufnr integer ---@param config diffs.ConflictConfig function M.setup_keymaps(bufnr, config) @@ -395,8 +358,6 @@ function M.setup_keymaps(bufnr, config) end end - apply_hunk_hints(bufnr, config) - vim.api.nvim_create_autocmd('BufWipeout', { buffer = bufnr, callback = function() diff --git a/spec/conflict_spec.lua b/spec/conflict_spec.lua index b163960..75eac23 100644 --- a/spec/conflict_spec.lua +++ b/spec/conflict_spec.lua @@ -6,7 +6,6 @@ local function default_config(overrides) enabled = true, disable_diagnostics = false, show_virtual_text = true, - show_actions = false, keymaps = { ours = 'doo', theirs = 'dot', @@ -686,158 +685,4 @@ describe('conflict', function() helpers.delete_buffer(bufnr) end) end) - - describe('virtual text formatting', function() - after_each(function() - conflict.detach(vim.api.nvim_get_current_buf()) - end) - - it('default labels show current and incoming without keymaps', function() - local bufnr = create_file_buffer({ - '<<<<<<< HEAD', - 'local x = 1', - '=======', - 'local x = 2', - '>>>>>>> feature', - }) - - conflict.attach(bufnr, default_config()) - - local extmarks = get_extmarks(bufnr) - local labels = {} - for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].virt_text then - table.insert(labels, mark[4].virt_text[1][1]) - end - end - assert.are.equal(2, #labels) - assert.are.equal(' (current)', labels[1]) - assert.are.equal(' (incoming)', labels[2]) - - helpers.delete_buffer(bufnr) - end) - - it('uses custom format_virtual_text function', function() - local bufnr = create_file_buffer({ - '<<<<<<< HEAD', - 'local x = 1', - '=======', - 'local x = 2', - '>>>>>>> feature', - }) - - conflict.attach( - bufnr, - default_config({ - format_virtual_text = function(side) - return side == 'ours' and 'OURS' or 'THEIRS' - end, - }) - ) - - local extmarks = get_extmarks(bufnr) - local labels = {} - for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].virt_text then - table.insert(labels, mark[4].virt_text[1][1]) - end - end - assert.are.equal(2, #labels) - assert.are.equal(' (OURS)', labels[1]) - assert.are.equal(' (THEIRS)', labels[2]) - - helpers.delete_buffer(bufnr) - end) - - it('hides label when format_virtual_text returns nil', function() - local bufnr = create_file_buffer({ - '<<<<<<< HEAD', - 'local x = 1', - '=======', - 'local x = 2', - '>>>>>>> feature', - }) - - conflict.attach( - bufnr, - default_config({ - format_virtual_text = function() - return nil - end, - }) - ) - - 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) - end) - - describe('action lines', function() - after_each(function() - conflict.detach(vim.api.nvim_get_current_buf()) - end) - - it('adds virt_lines when show_actions is true', function() - local bufnr = create_file_buffer({ - '<<<<<<< HEAD', - 'local x = 1', - '=======', - 'local x = 2', - '>>>>>>> feature', - }) - - conflict.attach(bufnr, default_config({ show_actions = true })) - - local extmarks = get_extmarks(bufnr) - local virt_lines_count = 0 - for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].virt_lines then - virt_lines_count = virt_lines_count + 1 - end - end - assert.are.equal(1, virt_lines_count) - - helpers.delete_buffer(bufnr) - end) - - it('omits disabled keymaps from action line', function() - local bufnr = create_file_buffer({ - '<<<<<<< HEAD', - 'local x = 1', - '=======', - 'local x = 2', - '>>>>>>> feature', - }) - - conflict.attach( - bufnr, - default_config({ show_actions = true, keymaps = { both = false, none = false } }) - ) - - local extmarks = get_extmarks(bufnr) - for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].virt_lines then - local line = mark[4].virt_lines[1] - local text = '' - for _, chunk in ipairs(line) do - text = text .. chunk[1] - end - assert.is_truthy(text:find('Current')) - assert.is_truthy(text:find('Incoming')) - assert.is_falsy(text:find('Both')) - assert.is_falsy(text:find('None')) - end - end - - helpers.delete_buffer(bufnr) - end) - end) end) diff --git a/spec/merge_spec.lua b/spec/merge_spec.lua index 7d5019b..2f788a6 100644 --- a/spec/merge_spec.lua +++ b/spec/merge_spec.lua @@ -6,7 +6,6 @@ local function default_config(overrides) enabled = true, disable_diagnostics = false, show_virtual_text = true, - show_actions = false, keymaps = { ours = 'doo', theirs = 'dot', @@ -618,65 +617,6 @@ describe('merge', function() end) end) - describe('hunk hints', function() - it('adds keymap hints on hunk header lines', function() - 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', - }) - - merge.setup_keymaps(d_bufnr, default_config()) - - local extmarks = - vim.api.nvim_buf_get_extmarks(d_bufnr, merge.get_namespace(), 0, -1, { details = true }) - local hint_marks = {} - for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].virt_text then - local text = '' - for _, chunk in ipairs(mark[4].virt_text) do - text = text .. chunk[1] - end - table.insert(hint_marks, { line = mark[2], text = text }) - end - end - assert.are.equal(1, #hint_marks) - assert.are.equal(3, hint_marks[1].line) - assert.is_truthy(hint_marks[1].text:find('doo')) - assert.is_truthy(hint_marks[1].text:find('dot')) - - helpers.delete_buffer(d_bufnr) - end) - - 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', - '+++ b/file.lua', - '@@ -1,1 +1,1 @@', - '-local x = 1', - '+local x = 2', - }) - - merge.setup_keymaps(d_bufnr, default_config({ show_virtual_text = false })) - - local extmarks = - vim.api.nvim_buf_get_extmarks(d_bufnr, merge.get_namespace(), 0, -1, { details = true }) - 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(d_bufnr) - end) - end) - describe('fugitive integration', function() it('parse_file_line returns status for unmerged files', function() local fugitive = require('diffs.fugitive')