diff --git a/README.md b/README.md index f0a04a8..583799e 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,7 @@ syntax highlighting. - 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 +- Configurable debouncing, max lines, and diff prefix concealment ## Requirements @@ -43,11 +40,12 @@ luarocks install diffs.nvim ## Known Limitations -- **Incomplete syntax context**: Treesitter parses each diff hunk in isolation. - To improve accuracy, `diffs.nvim` reads lines from disk before and after each - hunk for parsing context (`highlights.context`, enabled by default with 25 - lines). This resolves most boundary issues. Set - `highlights.context.enabled = false` to disable. +- **Incomplete syntax context**: Treesitter parses each diff hunk in isolation + without surrounding code context. When a hunk shows lines added to an existing + block (e.g., adding a plugin inside `return { ... }`), the parser doesn't see + the `return` statement and may produce incorrect highlighting. This is + inherent to parsing code fragments—no diff tooling solves this without + significant complexity. - **Syntax flashing**: `diffs.nvim` hooks into the `FileType fugitive` event triggered by `vim-fugitive`, at which point the buffer is preliminarily @@ -64,17 +62,12 @@ luarocks install diffs.nvim compatible, but both plugins modifying line highlights may produce unexpected results - [`git-conflict.nvim`](https://github.com/akinsho/git-conflict.nvim) - - `diffs.nvim` now includes built-in conflict resolution; disable one or the - other to avoid overlap + conflict marker highlighting may overlap with `diffs.nvim` # Acknowledgements - [`vim-fugitive`](https://github.com/tpope/vim-fugitive) - [`codediff.nvim`](https://github.com/esmuellert/codediff.nvim) - [`diffview.nvim`](https://github.com/sindrets/diffview.nvim) -- [`difftastic`](https://github.com/Wilfred/difftastic) -- [`mini.diff`](https://github.com/echasnovski/mini.diff) -- [`gitsigns.nvim`](https://github.com/lewis6991/gitsigns.nvim) -- [`git-conflict.nvim`](https://github.com/akinsho/git-conflict.nvim) - [@phanen](https://github.com/phanen) - diff header highlighting, unknown - filetype fix, shebang/modeline detection, treesitter injection support + filetype fix, shebang/modeline detection diff --git a/doc/diffs.nvim.txt b/doc/diffs.nvim.txt index 7663150..dacbfa6 100644 --- a/doc/diffs.nvim.txt +++ b/doc/diffs.nvim.txt @@ -20,7 +20,6 @@ Features: ~ - Blended diff background colors that preserve syntax visibility - Optional diff prefix (`+`/`-`/` `) concealment - Gutter (line number) highlighting -- Inline merge conflict marker detection, highlighting, and resolution ============================================================================== REQUIREMENTS *diffs-requirements* @@ -57,43 +56,24 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads: highlights = { background = true, gutter = true, - blend_alpha = 0.6, - context = { - enabled = true, - lines = 25, - }, treesitter = { enabled = true, max_lines = 500, }, vim = { enabled = false, - max_lines = 200, + max_lines = 500, }, intra = { enabled = true, - algorithm = 'default', + algorithm = 'auto', max_lines = 500, }, - overrides = {}, }, fugitive = { horizontal = 'du', vertical = 'dU', }, - conflict = { - enabled = true, - disable_diagnostics = true, - show_virtual_text = true, - keymaps = { - ours = 'doo', - theirs = 'dot', - both = 'dob', - none = 'don', - next = ']x', - prev = '[x', - }, - }, } < *diffs.Config* @@ -122,10 +102,6 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads: Fugitive status buffer keymap options. See |diffs.FugitiveConfig| for fields. - {conflict} (table, default: see below) - Inline merge conflict resolution options. - See |diffs.ConflictConfig| for fields. - *diffs.Highlights* Highlights table fields: ~ {background} (boolean, default: true) @@ -137,17 +113,6 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads: Highlight line numbers with matching colors. Only visible if line numbers are enabled. - {blend_alpha} (number, default: 0.6) - Alpha value for character-level blend intensity. - Controls how strongly changed characters stand - out from the line-level background. Must be - between 0 and 1 (inclusive). Higher values - produce more vivid character-level highlights. - - {context} (table, default: see below) - Syntax parsing context options. - See |diffs.ContextConfig| for fields. - {treesitter} (table, default: see below) Treesitter highlighting options. See |diffs.TreesitterConfig| for fields. @@ -160,27 +125,6 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads: Character-level (intra-line) diff highlighting. See |diffs.IntraConfig| for fields. - {overrides} (table, default: {}) - Map of highlight group names to highlight - definitions (see |nvim_set_hl()|). Applied - after all computed groups without `default`, - so overrides always win over both computed - defaults and colorscheme definitions. - - *diffs.ContextConfig* - Context config fields: ~ - {enabled} (boolean, default: true) - Read lines from disk before and after each hunk - to provide surrounding syntax context. Improves - accuracy at hunk boundaries where incomplete - constructs (e.g., a function definition with no - body) would otherwise confuse the parser. - - {lines} (integer, default: 25) - Number of context lines to read in each - direction. Lines are read with early exit — - cost scales with this value, not file size. - *diffs.TreesitterConfig* Treesitter config fields: ~ {enabled} (boolean, default: true) @@ -214,12 +158,12 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads: an intense background overlay while the rest of the line keeps the softer line-level background. - {algorithm} (string, default: 'default') + {algorithm} (string, default: 'auto') Diff algorithm for character-level analysis. - `'default'`: use |vim.diff()| with settings - inherited from |'diffopt'| (`algorithm` and - `linematch`). `'vscode'`: use libvscodediff FFI - (falls back to default if not available). + `'auto'`: use libvscodediff if available, else + native `vim.diff()`. `'native'`: always use + `vim.diff()`. `'vscode'`: require libvscodediff + (falls back to native if not available). {max_lines} (integer, default: 500) Skip character-level highlighting for hunks larger @@ -244,9 +188,6 @@ COMMANDS *diffs-commands* code language, plus diff header highlighting for `diff --git`, `---`, `+++`, and `@@` lines. - If a `diffs://` window already exists in the current tabpage, the new - diff replaces its buffer instead of creating another split. - Parameters: ~ {revision} (string, optional) Git revision to diff against. Defaults to HEAD. @@ -264,63 +205,6 @@ COMMANDS *diffs-commands* :Ghdiff [revision] *:Ghdiff* Like |:Gdiff| but explicitly opens in a horizontal split. -============================================================================== -MAPPINGS *diffs-mappings* - - *(diffs-gdiff)* -(diffs-gdiff) Show unified diff against HEAD in a horizontal - split. Equivalent to |:Gdiff| with no arguments. - - *(diffs-gvdiff)* -(diffs-gvdiff) Show unified diff against HEAD in a vertical - split. Equivalent to |:Gvdiff| with no arguments. - -Example configuration: >lua - vim.keymap.set('n', 'gd', '(diffs-gdiff)') - vim.keymap.set('n', 'gD', '(diffs-gvdiff)') -< - - *(diffs-conflict-ours)* -(diffs-conflict-ours) - Accept current (ours) change. Replaces the - conflict block with ours content. - - *(diffs-conflict-theirs)* -(diffs-conflict-theirs) - Accept incoming (theirs) change. Replaces the - conflict block with theirs content. - - *(diffs-conflict-both)* -(diffs-conflict-both) - Accept both changes (ours then theirs). - - *(diffs-conflict-none)* -(diffs-conflict-none) - Reject both changes (delete entire block). - - *(diffs-conflict-next)* -(diffs-conflict-next) - Jump to next conflict marker. Wraps around. - - *(diffs-conflict-prev)* -(diffs-conflict-prev) - Jump to previous conflict marker. Wraps around. - -Example configuration: >lua - vim.keymap.set('n', 'co', '(diffs-conflict-ours)') - vim.keymap.set('n', 'ct', '(diffs-conflict-theirs)') - vim.keymap.set('n', 'cb', '(diffs-conflict-both)') - vim.keymap.set('n', 'cn', '(diffs-conflict-none)') - vim.keymap.set('n', ']x', '(diffs-conflict-next)') - vim.keymap.set('n', '[x', '(diffs-conflict-prev)') -< - -Diff buffer mappings: ~ - *diffs-q* - q Close the diff window. Available in all `diffs://` - buffers created by |:Gdiff|, |:Gvdiff|, |:Ghdiff|, - or the fugitive status keymaps. - ============================================================================== FUGITIVE STATUS KEYMAPS *diffs-fugitive* @@ -370,94 +254,6 @@ Configuration: ~ Keymap for unified diff in vertical split. Set to `false` to disable. -============================================================================== -CONFLICT RESOLUTION *diffs-conflict* - -diffs.nvim detects inline merge conflict markers (`<<<<<<<`/`=======`/ -`>>>>>>>`) in working files and provides highlighting and resolution keymaps. -Both standard and diff3 (`|||||||`) formats are supported. - -Conflict regions are detected automatically on `BufReadPost` and re-scanned -on `TextChanged`. When all conflicts in a buffer are resolved, highlighting -is removed and diagnostics are re-enabled. - -Configuration: ~ - *diffs.ConflictConfig* ->lua - vim.g.diffs = { - conflict = { - enabled = true, - disable_diagnostics = true, - show_virtual_text = true, - keymaps = { - ours = 'doo', - theirs = 'dot', - both = 'dob', - none = 'don', - next = ']x', - prev = '[x', - }, - }, - } -< - Fields: ~ - {enabled} (boolean, default: true) - Enable conflict marker detection and - resolution. Set to `false` to disable - entirely. - - {disable_diagnostics} (boolean, default: true) - Suppress LSP diagnostics on buffers with - conflict markers. Markers produce syntax - errors that clutter the diagnostic list. - Diagnostics are re-enabled when all conflicts - are resolved. Set `false` to leave - diagnostics alone. - - {show_virtual_text} (boolean, default: true) - 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 - and navigation. Each value accepts a string - (custom key) or `false` (disabled). - - *diffs.ConflictKeymaps* - Keymap fields: ~ - {ours} (string|false, default: 'doo') - Accept current (ours) change. - - {theirs} (string|false, default: 'dot') - Accept incoming (theirs) change. - - {both} (string|false, default: 'dob') - Accept both changes (ours then theirs). - - {none} (string|false, default: 'don') - Reject both changes (delete entire block). - - {next} (string|false, default: ']x') - Jump to next conflict marker. Wraps around. - - {prev} (string|false, default: '[x') - Jump to previous conflict marker. Wraps - around. - -User events: ~ - *DiffsConflictResolved* - DiffsConflictResolved Fired when the last conflict in a buffer is - resolved. Useful for triggering custom actions - (e.g., auto-staging the file). >lua - vim.api.nvim_create_autocmd('User', { - pattern = 'DiffsConflictResolved', - callback = function() - print('all conflicts resolved!') - end, - }) -< - ============================================================================== API *diffs-api* @@ -489,8 +285,8 @@ 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 + - Background extmarks (`DiffsAdd`/`DiffsDelete`) at priority 198 + - `Normal` extmarks at priority 199 clear underlying diff foreground - Syntax highlights are applied as extmarks at priority 200 - Character-level diff extmarks (`DiffsAddText`/`DiffsDeleteText`) at priority 201 overlay changed characters with an intense background @@ -509,15 +305,14 @@ 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. +Treesitter parses each diff hunk in isolation without surrounding code +context. When a hunk shows lines added to an existing block (e.g., adding a +plugin inside `return { ... }`), the parser doesn't see the `return` +statement and may produce incorrect or unusual highlighting. -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. +This is inherent to parsing code fragments. No diff tooling solves this +problem without significant complexity—the parser simply doesn't have enough +information to understand the full syntactic structure. Syntax Highlighting Flash ~ *diffs-flash* @@ -555,9 +350,8 @@ conflict: modifying line highlights may produce unexpected results. - git-conflict.nvim (akinsho/git-conflict.nvim) - Provides conflict marker highlighting and resolution keymaps. - diffs.nvim now has built-in conflict resolution (see - |diffs-conflict|). Disable one or the other to avoid overlap. + Provides conflict marker highlighting that may overlap with + diffs.nvim's highlighting in conflict scenarios. If you experience visual conflicts, try disabling the conflicting plugin's diff-related features. @@ -565,15 +359,9 @@ diff-related features. ============================================================================== HIGHLIGHT GROUPS *diffs-highlights* -diffs.nvim defines custom highlight groups. All groups use `default = true`, -so colorschemes can override them by defining the group before the plugin -loads. - -All derived groups are computed by alpha-blending a source color into the -`Normal` background. Line-level groups blend at 40% alpha for a subtle tint; -character-level groups blend at 60% for more contrast. Line-number groups -combine both: background from the line-level blend, foreground from the -character-level blend. +diffs.nvim defines custom highlight groups. Fugitive unified diff groups use +`default = true`, so colorschemes can override them. Diff mode groups are +always derived from the corresponding `Diff*` groups. Fugitive unified diff highlights: ~ *DiffsAdd* @@ -586,54 +374,24 @@ Fugitive unified diff highlights: ~ *DiffsAddNr* DiffsAddNr Line number for `+` lines. Foreground from - `DiffsAddText`, background from `DiffsAdd`. + `diffAdded`, background from `DiffsAdd`. *DiffsDeleteNr* DiffsDeleteNr Line number for `-` lines. Foreground from - `DiffsDeleteText`, background from `DiffsDelete`. + `diffRemoved`, background from `DiffsDelete`. *DiffsAddText* DiffsAddText Character-level background for changed characters within `+` lines. Derived by blending `diffAdded` - foreground with `Normal` background at 60% alpha. - Only sets `bg`, so treesitter foreground colors show - through. + foreground with `Normal` background at 40% alpha. + Uses the same base color as `DiffsAddNr` foreground, + making changed characters clearly visible. Only sets + `bg`, so treesitter foreground colors show through. *DiffsDeleteText* DiffsDeleteText Character-level background for changed characters within `-` lines. Derived by blending `diffRemoved` - foreground with `Normal` background at 60% alpha. - -Conflict highlights: ~ - *DiffsConflictOurs* - DiffsConflictOurs Background for "ours" (current) content lines. - Derived by blending `DiffAdd` background with - `Normal` at 40% alpha (green tint). - - *DiffsConflictTheirs* - DiffsConflictTheirs Background for "theirs" (incoming) content lines. - Derived by blending `DiffChange` background with - `Normal` at 40% alpha. - - *DiffsConflictBase* - DiffsConflictBase Background for base (ancestor) content lines in - diff3 conflicts. Derived by blending `DiffText` - background with `Normal` at 30% alpha (muted). - - *DiffsConflictMarker* - DiffsConflictMarker Dimmed foreground with bold for `<<<<<<<`, - `=======`, `>>>>>>>`, and `|||||||` marker lines. - - *DiffsConflictOursNr* - DiffsConflictOursNr Line number for "ours" content lines. Foreground - from higher-alpha blend, background from line-level - blend. - - *DiffsConflictTheirsNr* - DiffsConflictTheirsNr Line number for "theirs" content lines. - - *DiffsConflictBaseNr* - DiffsConflictBaseNr Line number for base content lines (diff3). + foreground with `Normal` background at 40% alpha. Diff mode window highlights: ~ These are used for |winhighlight| remapping in `&diff` windows. @@ -659,16 +417,6 @@ To customize these in your colorscheme: >lua vim.api.nvim_set_hl(0, 'DiffsAdd', { bg = '#2e4a3a' }) vim.api.nvim_set_hl(0, 'DiffsDiffDelete', { link = 'DiffDelete' }) < -Or via `highlights.overrides` in config: >lua - vim.g.diffs = { - highlights = { - overrides = { - DiffsAdd = { bg = '#2e4a3a' }, - DiffsDiffDelete = { link = 'DiffDelete' }, - }, - }, - } -< ============================================================================== HEALTH CHECK *diffs-health* @@ -686,8 +434,7 @@ ACKNOWLEDGEMENTS *diffs-acknowledgements* - vim-fugitive (https://github.com/tpope/vim-fugitive) - codediff.nvim (https://github.com/esmuellert/codediff.nvim) - diffview.nvim (https://github.com/sindrets/diffview.nvim) -- @phanen (https://github.com/phanen) - diff header highlighting, - treesitter injection support +- @phanen (https://github.com/phanen) - diff header highlighting ============================================================================== vim:tw=78:ts=8:ft=help:norl: diff --git a/lua/diffs/commands.lua b/lua/diffs/commands.lua index dc6cfe2..b3f9b42 100644 --- a/lua/diffs/commands.lua +++ b/lua/diffs/commands.lua @@ -3,27 +3,6 @@ local M = {} local git = require('diffs.git') local dbg = require('diffs.log').dbg ----@return integer? -function M.find_diffs_window() - local tabpage = vim.api.nvim_get_current_tabpage() - for _, win in ipairs(vim.api.nvim_tabpage_list_wins(tabpage)) do - if vim.api.nvim_win_is_valid(win) then - local buf = vim.api.nvim_win_get_buf(win) - local name = vim.api.nvim_buf_get_name(buf) - if name:match('^diffs://') then - return win - end - end - end - return nil -end - ----@param bufnr integer -function M.setup_diff_buf(bufnr) - vim.diagnostic.enable(false, { bufnr = bufnr }) - vim.keymap.set('n', 'q', 'close', { buffer = bufnr }) -end - ---@param diff_lines string[] ---@param hunk_position { hunk_header: string, offset: integer } ---@return integer? @@ -107,9 +86,8 @@ function M.gdiff(revision, vertical) local diff_buf = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_lines(diff_buf, 0, -1, false, diff_lines) - vim.api.nvim_set_option_value('buftype', 'nowrite', { buf = diff_buf }) - vim.api.nvim_set_option_value('bufhidden', 'delete', { buf = diff_buf }) - vim.api.nvim_set_option_value('swapfile', false, { buf = diff_buf }) + vim.api.nvim_set_option_value('buftype', 'nofile', { buf = diff_buf }) + vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = diff_buf }) vim.api.nvim_set_option_value('modifiable', false, { buf = diff_buf }) vim.api.nvim_set_option_value('filetype', 'diff', { buf = diff_buf }) vim.api.nvim_buf_set_name(diff_buf, 'diffs://' .. revision .. ':' .. rel_path) @@ -117,16 +95,9 @@ function M.gdiff(revision, vertical) vim.api.nvim_buf_set_var(diff_buf, 'diffs_repo_root', repo_root) end - local existing_win = M.find_diffs_window() - if existing_win then - vim.api.nvim_set_current_win(existing_win) - vim.api.nvim_win_set_buf(existing_win, diff_buf) - else - vim.cmd(vertical and 'vsplit' or 'split') - vim.api.nvim_win_set_buf(0, diff_buf) - end + vim.cmd(vertical and 'vsplit' or 'split') + vim.api.nvim_win_set_buf(0, diff_buf) - M.setup_diff_buf(diff_buf) dbg('opened diff buffer %d for %s against %s', diff_buf, rel_path, revision) vim.schedule(function() @@ -205,27 +176,17 @@ function M.gdiff_file(filepath, opts) local diff_buf = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_lines(diff_buf, 0, -1, false, diff_lines) - vim.api.nvim_set_option_value('buftype', 'nowrite', { buf = diff_buf }) - vim.api.nvim_set_option_value('bufhidden', 'delete', { buf = diff_buf }) - vim.api.nvim_set_option_value('swapfile', false, { buf = diff_buf }) + vim.api.nvim_set_option_value('buftype', 'nofile', { buf = diff_buf }) + vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = diff_buf }) vim.api.nvim_set_option_value('modifiable', false, { buf = diff_buf }) vim.api.nvim_set_option_value('filetype', 'diff', { buf = diff_buf }) vim.api.nvim_buf_set_name(diff_buf, 'diffs://' .. diff_label .. ':' .. rel_path) if repo_root then vim.api.nvim_buf_set_var(diff_buf, 'diffs_repo_root', repo_root) end - if old_rel_path ~= rel_path then - vim.api.nvim_buf_set_var(diff_buf, 'diffs_old_filepath', old_rel_path) - end - local existing_win = M.find_diffs_window() - if existing_win then - vim.api.nvim_set_current_win(existing_win) - vim.api.nvim_win_set_buf(existing_win, diff_buf) - else - vim.cmd(opts.vertical and 'vsplit' or 'split') - vim.api.nvim_win_set_buf(0, diff_buf) - end + vim.cmd(opts.vertical and 'vsplit' or 'split') + vim.api.nvim_win_set_buf(0, diff_buf) if opts.hunk_position then local target_line = M.find_hunk_line(diff_lines, opts.hunk_position) @@ -235,7 +196,6 @@ function M.gdiff_file(filepath, opts) end end - M.setup_diff_buf(diff_buf) dbg('opened diff buffer %d for %s (%s)', diff_buf, rel_path, diff_label) vim.schedule(function() @@ -271,24 +231,16 @@ function M.gdiff_section(repo_root, opts) local diff_label = opts.staged and 'staged' or 'unstaged' local diff_buf = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_lines(diff_buf, 0, -1, false, result) - vim.api.nvim_set_option_value('buftype', 'nowrite', { buf = diff_buf }) - vim.api.nvim_set_option_value('bufhidden', 'delete', { buf = diff_buf }) - vim.api.nvim_set_option_value('swapfile', false, { buf = diff_buf }) + vim.api.nvim_set_option_value('buftype', 'nofile', { buf = diff_buf }) + vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = diff_buf }) vim.api.nvim_set_option_value('modifiable', false, { buf = diff_buf }) vim.api.nvim_set_option_value('filetype', 'diff', { buf = diff_buf }) vim.api.nvim_buf_set_name(diff_buf, 'diffs://' .. diff_label .. ':all') vim.api.nvim_buf_set_var(diff_buf, 'diffs_repo_root', repo_root) - local existing_win = M.find_diffs_window() - if existing_win then - vim.api.nvim_set_current_win(existing_win) - vim.api.nvim_win_set_buf(existing_win, diff_buf) - else - vim.cmd(opts.vertical and 'vsplit' or 'split') - vim.api.nvim_win_set_buf(0, diff_buf) - end + vim.cmd(opts.vertical and 'vsplit' or 'split') + vim.api.nvim_win_set_buf(0, diff_buf) - M.setup_diff_buf(diff_buf) dbg('opened section diff buffer %d (%s)', diff_buf, diff_label) vim.schedule(function() @@ -296,77 +248,6 @@ function M.gdiff_section(repo_root, opts) end) end ----@param bufnr integer -function M.read_buffer(bufnr) - local name = vim.api.nvim_buf_get_name(bufnr) - local url_body = name:match('^diffs://(.+)$') - if not url_body then - return - end - - local label, path = url_body:match('^([^:]+):(.+)$') - if not label or not path then - return - end - - local ok, repo_root = pcall(vim.api.nvim_buf_get_var, bufnr, 'diffs_repo_root') - if not ok or not repo_root then - return - end - - local diff_lines - - if path == 'all' then - local cmd = { 'git', '-C', repo_root, 'diff', '--no-ext-diff', '--no-color' } - if label == 'staged' then - table.insert(cmd, '--cached') - end - diff_lines = vim.fn.systemlist(cmd) - if vim.v.shell_error ~= 0 then - diff_lines = {} - end - else - local abs_path = repo_root .. '/' .. path - - local old_ok, old_rel_path = pcall(vim.api.nvim_buf_get_var, bufnr, 'diffs_old_filepath') - local old_abs_path = old_ok and old_rel_path and (repo_root .. '/' .. old_rel_path) or abs_path - local old_name = old_ok and old_rel_path or path - - local old_lines, new_lines - - if label == 'untracked' then - old_lines = {} - new_lines = git.get_working_content(abs_path) or {} - elseif label == 'staged' then - old_lines = git.get_file_content('HEAD', old_abs_path) or {} - new_lines = git.get_index_content(abs_path) or {} - elseif label == 'unstaged' then - old_lines = git.get_index_content(old_abs_path) - if not old_lines then - old_lines = git.get_file_content('HEAD', old_abs_path) or {} - end - new_lines = git.get_working_content(abs_path) or {} - else - old_lines = git.get_file_content(label, abs_path) or {} - new_lines = git.get_working_content(abs_path) or {} - end - - diff_lines = generate_unified_diff(old_lines, new_lines, old_name, path) - end - - vim.api.nvim_set_option_value('modifiable', true, { buf = bufnr }) - vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, diff_lines) - vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr }) - vim.api.nvim_set_option_value('buftype', 'nowrite', { buf = bufnr }) - vim.api.nvim_set_option_value('bufhidden', 'delete', { buf = bufnr }) - vim.api.nvim_set_option_value('swapfile', false, { buf = bufnr }) - vim.api.nvim_set_option_value('filetype', 'diff', { buf = bufnr }) - - dbg('reloaded diff buffer %d (%s:%s)', bufnr, label, path) - - require('diffs').attach(bufnr) -end - function M.setup() vim.api.nvim_create_user_command('Gdiff', function(opts) M.gdiff(opts.args ~= '' and opts.args or nil, false) diff --git a/lua/diffs/conflict.lua b/lua/diffs/conflict.lua deleted file mode 100644 index 9e62a15..0000000 --- a/lua/diffs/conflict.lua +++ /dev/null @@ -1,438 +0,0 @@ ----@class diffs.ConflictRegion ----@field marker_ours integer ----@field ours_start integer ----@field ours_end integer ----@field marker_base integer? ----@field base_start integer? ----@field base_end integer? ----@field marker_sep integer ----@field theirs_start integer ----@field theirs_end integer ----@field marker_theirs integer - -local M = {} - -local ns = vim.api.nvim_create_namespace('diffs-conflict') - ----@type table -local attached_buffers = {} - ----@type table -local diagnostics_suppressed = {} - -local PRIORITY_LINE_BG = 200 - ----@param lines string[] ----@return diffs.ConflictRegion[] -function M.parse(lines) - local regions = {} - local state = 'idle' - ---@type table? - local current = nil - - for i, line in ipairs(lines) do - local idx = i - 1 - - if state == 'idle' then - if line:match('^<<<<<<<') then - current = { marker_ours = idx, ours_start = idx + 1 } - state = 'in_ours' - end - elseif state == 'in_ours' then - if line:match('^|||||||') then - current.ours_end = idx - current.marker_base = idx - current.base_start = idx + 1 - state = 'in_base' - elseif line:match('^=======') then - current.ours_end = idx - current.marker_sep = idx - current.theirs_start = idx + 1 - state = 'in_theirs' - elseif line:match('^<<<<<<<') then - current = { marker_ours = idx, ours_start = idx + 1 } - elseif line:match('^>>>>>>>') then - current = nil - state = 'idle' - end - elseif state == 'in_base' then - if line:match('^=======') then - current.base_end = idx - current.marker_sep = idx - current.theirs_start = idx + 1 - state = 'in_theirs' - elseif line:match('^<<<<<<<') then - current = { marker_ours = idx, ours_start = idx + 1 } - state = 'in_ours' - elseif line:match('^>>>>>>>') then - current = nil - state = 'idle' - end - elseif state == 'in_theirs' then - if line:match('^>>>>>>>') then - current.theirs_end = idx - current.marker_theirs = idx - table.insert(regions, current) - current = nil - state = 'idle' - elseif line:match('^<<<<<<<') then - current = { marker_ours = idx, ours_start = idx + 1 } - state = 'in_ours' - end - end - end - - return regions -end - ----@param bufnr integer ----@return diffs.ConflictRegion[] -local function parse_buffer(bufnr) - local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - return M.parse(lines) -end - ----@param bufnr integer ----@param regions diffs.ConflictRegion[] ----@param config diffs.ConflictConfig -local function apply_highlights(bufnr, regions, config) - vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1) - - for _, region in ipairs(regions) do - pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, region.marker_ours, 0, { - end_row = region.marker_ours + 1, - hl_group = 'DiffsConflictMarker', - hl_eol = true, - priority = PRIORITY_LINE_BG, - }) - - if config.show_virtual_text then - 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 - pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, line, 0, { - end_row = line + 1, - hl_group = 'DiffsConflictOurs', - hl_eol = true, - priority = PRIORITY_LINE_BG, - }) - pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, line, 0, { - number_hl_group = 'DiffsConflictOursNr', - priority = PRIORITY_LINE_BG, - }) - end - - if region.marker_base then - pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, region.marker_base, 0, { - end_row = region.marker_base + 1, - hl_group = 'DiffsConflictMarker', - hl_eol = true, - priority = PRIORITY_LINE_BG, - }) - - for line = region.base_start, region.base_end - 1 do - pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, line, 0, { - end_row = line + 1, - hl_group = 'DiffsConflictBase', - hl_eol = true, - priority = PRIORITY_LINE_BG, - }) - pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, line, 0, { - number_hl_group = 'DiffsConflictBaseNr', - priority = PRIORITY_LINE_BG, - }) - end - end - - pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, region.marker_sep, 0, { - end_row = region.marker_sep + 1, - hl_group = 'DiffsConflictMarker', - hl_eol = true, - priority = PRIORITY_LINE_BG, - }) - - for line = region.theirs_start, region.theirs_end - 1 do - pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, line, 0, { - end_row = line + 1, - hl_group = 'DiffsConflictTheirs', - hl_eol = true, - priority = PRIORITY_LINE_BG, - }) - pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, line, 0, { - number_hl_group = 'DiffsConflictTheirsNr', - priority = PRIORITY_LINE_BG, - }) - end - - pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, region.marker_theirs, 0, { - end_row = region.marker_theirs + 1, - hl_group = 'DiffsConflictMarker', - hl_eol = true, - priority = PRIORITY_LINE_BG, - }) - - if config.show_virtual_text then - pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, region.marker_theirs, 0, { - virt_text = { { ' (incoming)', 'DiffsConflictMarker' } }, - virt_text_pos = 'eol', - }) - end - end -end - ----@param cursor_line integer ----@param regions diffs.ConflictRegion[] ----@return diffs.ConflictRegion? -local function find_conflict_at_cursor(cursor_line, regions) - for _, region in ipairs(regions) do - if cursor_line >= region.marker_ours and cursor_line <= region.marker_theirs then - return region - end - end - return nil -end - ----@param bufnr integer ----@param region diffs.ConflictRegion ----@param replacement string[] -local function replace_region(bufnr, region, replacement) - vim.api.nvim_buf_set_lines( - bufnr, - region.marker_ours, - region.marker_theirs + 1, - false, - replacement - ) -end - ----@param bufnr integer ----@param config diffs.ConflictConfig -local function refresh(bufnr, config) - local regions = parse_buffer(bufnr) - if #regions == 0 then - vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1) - if diagnostics_suppressed[bufnr] then - pcall(vim.diagnostic.reset, nil, bufnr) - pcall(vim.diagnostic.enable, true, { bufnr = bufnr }) - diagnostics_suppressed[bufnr] = nil - end - vim.api.nvim_exec_autocmds('User', { pattern = 'DiffsConflictResolved' }) - return - end - apply_highlights(bufnr, regions, config) - if config.disable_diagnostics and not diagnostics_suppressed[bufnr] then - pcall(vim.diagnostic.enable, false, { bufnr = bufnr }) - diagnostics_suppressed[bufnr] = true - end -end - ----@param bufnr integer ----@param config diffs.ConflictConfig -function M.resolve_ours(bufnr, config) - if not vim.api.nvim_get_option_value('modifiable', { buf = bufnr }) then - vim.notify('[diffs.nvim]: buffer is not modifiable', vim.log.levels.WARN) - return - end - local regions = parse_buffer(bufnr) - local cursor = vim.api.nvim_win_get_cursor(0) - local region = find_conflict_at_cursor(cursor[1] - 1, regions) - if not region then - return - end - local lines = vim.api.nvim_buf_get_lines(bufnr, region.ours_start, region.ours_end, false) - replace_region(bufnr, region, lines) - refresh(bufnr, config) -end - ----@param bufnr integer ----@param config diffs.ConflictConfig -function M.resolve_theirs(bufnr, config) - if not vim.api.nvim_get_option_value('modifiable', { buf = bufnr }) then - vim.notify('[diffs.nvim]: buffer is not modifiable', vim.log.levels.WARN) - return - end - local regions = parse_buffer(bufnr) - local cursor = vim.api.nvim_win_get_cursor(0) - local region = find_conflict_at_cursor(cursor[1] - 1, regions) - if not region then - return - end - local lines = vim.api.nvim_buf_get_lines(bufnr, region.theirs_start, region.theirs_end, false) - replace_region(bufnr, region, lines) - refresh(bufnr, config) -end - ----@param bufnr integer ----@param config diffs.ConflictConfig -function M.resolve_both(bufnr, config) - if not vim.api.nvim_get_option_value('modifiable', { buf = bufnr }) then - vim.notify('[diffs.nvim]: buffer is not modifiable', vim.log.levels.WARN) - return - end - local regions = parse_buffer(bufnr) - local cursor = vim.api.nvim_win_get_cursor(0) - local region = find_conflict_at_cursor(cursor[1] - 1, regions) - if not region then - return - end - local ours = vim.api.nvim_buf_get_lines(bufnr, region.ours_start, region.ours_end, false) - local theirs = vim.api.nvim_buf_get_lines(bufnr, region.theirs_start, region.theirs_end, false) - local combined = {} - for _, l in ipairs(ours) do - table.insert(combined, l) - end - for _, l in ipairs(theirs) do - table.insert(combined, l) - end - replace_region(bufnr, region, combined) - refresh(bufnr, config) -end - ----@param bufnr integer ----@param config diffs.ConflictConfig -function M.resolve_none(bufnr, config) - if not vim.api.nvim_get_option_value('modifiable', { buf = bufnr }) then - vim.notify('[diffs.nvim]: buffer is not modifiable', vim.log.levels.WARN) - return - end - local regions = parse_buffer(bufnr) - local cursor = vim.api.nvim_win_get_cursor(0) - local region = find_conflict_at_cursor(cursor[1] - 1, regions) - if not region then - return - end - replace_region(bufnr, region, {}) - refresh(bufnr, config) -end - ----@param bufnr integer -function M.goto_next(bufnr) - local regions = parse_buffer(bufnr) - if #regions == 0 then - return - end - local cursor = vim.api.nvim_win_get_cursor(0) - local cursor_line = cursor[1] - 1 - for _, region in ipairs(regions) do - if region.marker_ours > cursor_line then - vim.api.nvim_win_set_cursor(0, { region.marker_ours + 1, 0 }) - return - end - end - vim.api.nvim_win_set_cursor(0, { regions[1].marker_ours + 1, 0 }) -end - ----@param bufnr integer -function M.goto_prev(bufnr) - local regions = parse_buffer(bufnr) - if #regions == 0 then - return - end - local cursor = vim.api.nvim_win_get_cursor(0) - local cursor_line = cursor[1] - 1 - for i = #regions, 1, -1 do - if regions[i].marker_ours < cursor_line then - vim.api.nvim_win_set_cursor(0, { regions[i].marker_ours + 1, 0 }) - return - end - end - vim.api.nvim_win_set_cursor(0, { regions[#regions].marker_ours + 1, 0 }) -end - ----@param bufnr integer ----@param config diffs.ConflictConfig -local function setup_keymaps(bufnr, config) - local km = config.keymaps - - local maps = { - { km.ours, '(diffs-conflict-ours)' }, - { km.theirs, '(diffs-conflict-theirs)' }, - { km.both, '(diffs-conflict-both)' }, - { km.none, '(diffs-conflict-none)' }, - { km.next, '(diffs-conflict-next)' }, - { km.prev, '(diffs-conflict-prev)' }, - } - - for _, map in ipairs(maps) do - if map[1] then - vim.keymap.set('n', map[1], map[2], { buffer = bufnr }) - end - end -end - ----@param bufnr integer -function M.detach(bufnr) - vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1) - attached_buffers[bufnr] = nil - - if diagnostics_suppressed[bufnr] then - pcall(vim.diagnostic.reset, nil, bufnr) - pcall(vim.diagnostic.enable, true, { bufnr = bufnr }) - diagnostics_suppressed[bufnr] = nil - end -end - ----@param bufnr integer ----@param config diffs.ConflictConfig -function M.attach(bufnr, config) - if attached_buffers[bufnr] then - return - end - - local buftype = vim.api.nvim_get_option_value('buftype', { buf = bufnr }) - if buftype ~= '' then - return - end - - local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - local has_marker = false - for _, line in ipairs(lines) do - if line:match('^<<<<<<<') then - has_marker = true - break - end - end - if not has_marker then - return - end - - attached_buffers[bufnr] = true - - local regions = M.parse(lines) - apply_highlights(bufnr, regions, config) - setup_keymaps(bufnr, config) - - if config.disable_diagnostics then - pcall(vim.diagnostic.enable, false, { bufnr = bufnr }) - diagnostics_suppressed[bufnr] = true - end - - vim.api.nvim_create_autocmd({ 'TextChanged', 'TextChangedI' }, { - buffer = bufnr, - callback = function() - if not attached_buffers[bufnr] then - return true - end - refresh(bufnr, config) - end, - }) - - vim.api.nvim_create_autocmd('BufWipeout', { - buffer = bufnr, - callback = function() - attached_buffers[bufnr] = nil - diagnostics_suppressed[bufnr] = nil - end, - }) -end - ----@return integer -function M.get_namespace() - return ns -end - -return M diff --git a/lua/diffs/debug.lua b/lua/diffs/debug.lua index c234c32..5be95bc 100644 --- a/lua/diffs/debug.lua +++ b/lua/diffs/debug.lua @@ -18,16 +18,14 @@ function M.dump() end_col = details.end_col, hl_group = details.hl_group, priority = details.priority, - hl_eol = details.hl_eol, line_hl_group = details.line_hl_group, number_hl_group = details.number_hl_group, virt_text = details.virt_text, } - local key = tostring(row) - if not by_line[key] then - by_line[key] = { text = lines[row + 1] or '', marks = {} } + if not by_line[row] then + by_line[row] = { text = lines[row + 1] or '', marks = {} } end - table.insert(by_line[key].marks, entry) + table.insert(by_line[row].marks, entry) end local all_ns_marks = vim.api.nvim_buf_get_extmarks(bufnr, -1, 0, -1, { details = true }) diff --git a/lua/diffs/diff.lua b/lua/diffs/diff.lua index edf0275..65d3ac8 100644 --- a/lua/diffs/diff.lua +++ b/lua/diffs/diff.lua @@ -11,10 +11,6 @@ ---@field del_lines {idx: integer, text: string}[] ---@field add_lines {idx: integer, text: string}[] ----@class diffs.DiffOpts ----@field algorithm? string ----@field linematch? integer - local M = {} local dbg = require('diffs.log').dbg @@ -64,35 +60,11 @@ function M.extract_change_groups(hunk_lines) return groups end ----@return diffs.DiffOpts -local function parse_diffopt() - local opts = {} - for _, item in ipairs(vim.split(vim.o.diffopt, ',')) do - local key, val = item:match('^(%w+):(.+)$') - if key == 'algorithm' then - opts.algorithm = val - elseif key == 'linematch' then - opts.linematch = tonumber(val) - end - end - return opts -end - ---@param old_text string ---@param new_text string ----@param diff_opts? diffs.DiffOpts ---@return {old_start: integer, old_count: integer, new_start: integer, new_count: integer}[] -local function byte_diff(old_text, new_text, diff_opts) - local vim_opts = { result_type = 'indices' } - if diff_opts then - if diff_opts.algorithm then - vim_opts.algorithm = diff_opts.algorithm - end - if diff_opts.linematch then - vim_opts.linematch = diff_opts.linematch - end - end - local ok, result = pcall(vim.diff, old_text, new_text, vim_opts) +local function byte_diff(old_text, new_text) + local ok, result = pcall(vim.diff, old_text, new_text, { result_type = 'indices' }) if not ok or not result then return {} end @@ -123,9 +95,8 @@ end ---@param new_line string ---@param del_idx integer ---@param add_idx integer ----@param diff_opts? diffs.DiffOpts ---@return diffs.CharSpan[], diffs.CharSpan[] -local function char_diff_pair(old_line, new_line, del_idx, add_idx, diff_opts) +local function char_diff_pair(old_line, new_line, del_idx, add_idx) ---@type diffs.CharSpan[] local del_spans = {} ---@type diffs.CharSpan[] @@ -137,12 +108,7 @@ local function char_diff_pair(old_line, new_line, del_idx, add_idx, diff_opts) local old_text = table.concat(old_bytes, '\n') .. '\n' local new_text = table.concat(new_bytes, '\n') .. '\n' - local char_opts = diff_opts - if diff_opts and diff_opts.linematch then - char_opts = { algorithm = diff_opts.algorithm } - end - - local char_hunks = byte_diff(old_text, new_text, char_opts) + local char_hunks = byte_diff(old_text, new_text) for _, ch in ipairs(char_hunks) do if ch.old_count > 0 then @@ -166,9 +132,8 @@ local function char_diff_pair(old_line, new_line, del_idx, add_idx, diff_opts) end ---@param group diffs.ChangeGroup ----@param diff_opts? diffs.DiffOpts ---@return diffs.CharSpan[], diffs.CharSpan[] -local function diff_group_native(group, diff_opts) +local function diff_group_native(group) ---@type diffs.CharSpan[] local all_del = {} ---@type diffs.CharSpan[] @@ -182,8 +147,7 @@ local function diff_group_native(group, diff_opts) group.del_lines[1].text, group.add_lines[1].text, group.del_lines[1].idx, - group.add_lines[1].idx, - diff_opts + group.add_lines[1].idx ) vim.list_extend(all_del, ds) vim.list_extend(all_add, as) @@ -202,7 +166,7 @@ local function diff_group_native(group, diff_opts) local old_block = table.concat(old_texts, '\n') .. '\n' local new_block = table.concat(new_texts, '\n') .. '\n' - local line_hunks = byte_diff(old_block, new_block, diff_opts) + local line_hunks = byte_diff(old_block, new_block) ---@type table local old_to_new = {} @@ -220,8 +184,7 @@ local function diff_group_native(group, diff_opts) group.del_lines[old_i].text, group.add_lines[new_i].text, group.del_lines[old_i].idx, - group.add_lines[new_i].idx, - diff_opts + group.add_lines[new_i].idx ) vim.list_extend(all_del, ds) vim.list_extend(all_add, as) @@ -239,8 +202,7 @@ local function diff_group_native(group, diff_opts) group.del_lines[oi].text, group.add_lines[ni].text, group.del_lines[oi].idx, - group.add_lines[ni].idx, - diff_opts + group.add_lines[ni].idx ) vim.list_extend(all_del, ds) vim.list_extend(all_add, as) @@ -333,26 +295,16 @@ function M.compute_intra_hunks(hunk_lines, algorithm) return nil end - algorithm = algorithm or 'default' + algorithm = algorithm or 'auto' + local lib = require('diffs.lib') local vscode_handle = nil - if algorithm == 'vscode' then - vscode_handle = require('diffs.lib').load() - if not vscode_handle then - dbg('vscode algorithm requested but library not available, falling back to default') - end + if algorithm ~= 'native' then + vscode_handle = lib.load() end - ---@type diffs.DiffOpts? - local diff_opts = nil - if not vscode_handle then - diff_opts = parse_diffopt() - if diff_opts.algorithm then - dbg('diffopt algorithm: %s', diff_opts.algorithm) - end - if diff_opts.linematch then - dbg('diffopt linematch: %d', diff_opts.linematch) - end + if algorithm == 'vscode' and not vscode_handle then + dbg('vscode algorithm requested but library not available, falling back to native') end ---@type diffs.CharSpan[] @@ -373,7 +325,7 @@ function M.compute_intra_hunks(hunk_lines, algorithm) if vscode_handle then ds, as = diff_group_vscode(group, vscode_handle) else - ds, as = diff_group_native(group, diff_opts) + ds, as = diff_group_native(group) end dbg('group %d result: %d del spans, %d add spans', gi, #ds, #as) for _, s in ipairs(ds) do diff --git a/lua/diffs/highlight.lua b/lua/diffs/highlight.lua index 306b0e5..311a6f3 100644 --- a/lua/diffs/highlight.lua +++ b/lua/diffs/highlight.lua @@ -3,53 +3,15 @@ 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 ---@param col_offset integer ---@param text string ---@param lang string ----@param context_lines? string[] ---@return integer -local function highlight_text(bufnr, ns, hunk, col_offset, text, lang, context_lines) - local parse_text = text - if context_lines and #context_lines > 0 then - parse_text = text .. '\n' .. table.concat(context_lines, '\n') - end - - local ok, parser_obj = pcall(vim.treesitter.get_string_parser, parse_text, lang) +local function highlight_text(bufnr, ns, hunk, col_offset, text, lang) + local ok, parser_obj = pcall(vim.treesitter.get_string_parser, text, lang) if not ok or not parser_obj then return 0 end @@ -67,26 +29,24 @@ local function highlight_text(bufnr, ns, hunk, col_offset, text, lang, context_l local extmark_count = 0 local header_line = hunk.start_line - 1 - for id, node, metadata in query:iter_captures(trees[1]:root(), parse_text) do - local sr, sc, _, ec = node:range() - if sr == 0 then - local capture_name = '@' .. query.captures[id] .. '.' .. lang + for id, node, metadata in query:iter_captures(trees[1]:root(), text) do + local capture_name = '@' .. query.captures[id] .. '.' .. lang + local sr, sc, er, ec = node:range() - local buf_sr = header_line - local buf_er = header_line - local buf_sc = col_offset + sc - local buf_ec = col_offset + ec + local buf_sr = header_line + sr + local buf_er = header_line + er + 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 200 - pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_sr, buf_sc, { - end_row = buf_er, - end_col = buf_ec, - hl_group = capture_name, - priority = priority, - }) - extmark_count = extmark_count + 1 - end + pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_sr, buf_sc, { + end_row = buf_er, + end_col = buf_ec, + hl_group = capture_name, + priority = priority, + }) + extmark_count = extmark_count + 1 end return extmark_count @@ -98,21 +58,16 @@ end ---@param bufnr integer ---@param ns integer +---@param hunk diffs.Hunk ---@param code_lines string[] ----@param lang string ----@param line_map table ----@param col_offset integer ----@param covered_lines? table +---@param col_offset integer? ---@return integer -local function highlight_treesitter( - bufnr, - ns, - code_lines, - lang, - line_map, - col_offset, - covered_lines -) +local function highlight_treesitter(bufnr, ns, hunk, code_lines, col_offset) + local lang = hunk.lang + if not lang then + return 0 + end + local code = table.concat(code_lines, '\n') if code == '' then return 0 @@ -124,50 +79,54 @@ local function highlight_treesitter( return 0 end - local trees = parser_obj:parse(true) + local trees = parser_obj:parse() if not trees or #trees == 0 then dbg('parse returned no trees for lang: %s', lang) return 0 end + local query = vim.treesitter.query.get(lang, 'highlights') + if not query then + dbg('no highlights query for lang: %s', lang) + return 0 + end + + 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 = 'Normal', + priority = 199, + }) + local header_extmarks = + highlight_text(bufnr, ns, hunk, hunk.header_context_col, hunk.header_context, lang) + if header_extmarks > 0 then + dbg('header %s:%d applied %d extmarks', hunk.filename, hunk.start_line, header_extmarks) + end + end + + col_offset = col_offset or 1 + local extmark_count = 0 - parser_obj:for_each_tree(function(tree, ltree) - local tree_lang = ltree:lang() - local query = vim.treesitter.query.get(tree_lang, 'highlights') - if not query then - return - end + for id, node, metadata in query:iter_captures(trees[1]:root(), code) do + local capture_name = '@' .. query.captures[id] .. '.' .. lang + local sr, sc, er, ec = node:range() - for id, node, metadata in query:iter_captures(tree:root(), code) do - local capture = query.captures[id] - if capture ~= 'spell' and capture ~= 'nospell' then - local capture_name = '@' .. capture .. '.' .. tree_lang - local sr, sc, er, ec = node:range() + local buf_sr = hunk.start_line + sr + local buf_er = hunk.start_line + er + local buf_sc = sc + col_offset + local buf_ec = ec + col_offset - local buf_sr = line_map[sr] - if buf_sr then - local buf_er = line_map[er] or buf_sr + local priority = lang == 'diff' and (tonumber(metadata.priority) or 100) or 200 - local buf_sc = sc + col_offset - local buf_ec = ec + col_offset - - local priority = tree_lang == 'diff' and (tonumber(metadata.priority) or 100) - or PRIORITY_SYNTAX - - pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_sr, buf_sc, { - end_row = buf_er, - end_col = buf_ec, - hl_group = capture_name, - priority = priority, - }) - extmark_count = extmark_count + 1 - if covered_lines then - covered_lines[buf_sr] = true - end - end - end - end - end) + pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_sr, buf_sc, { + end_row = buf_er, + end_col = buf_ec, + hl_group = capture_name, + priority = priority, + }) + extmark_count = extmark_count + 1 + end return extmark_count end @@ -217,10 +176,8 @@ end ---@param ns integer ---@param hunk diffs.Hunk ---@param code_lines string[] ----@param covered_lines? table ----@param leading_offset? integer ---@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) local ft = hunk.ft if not ft then return 0 @@ -230,8 +187,6 @@ local function highlight_vim_syntax(bufnr, ns, hunk, code_lines, covered_lines, return 0 end - leading_offset = leading_offset or 0 - local scratch = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_lines(scratch, 0, -1, false, code_lines) vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = scratch }) @@ -258,22 +213,15 @@ local function highlight_vim_syntax(bufnr, ns, hunk, code_lines, covered_lines, vim.api.nvim_buf_delete(scratch, { force = true }) - local hunk_line_count = #hunk.lines local extmark_count = 0 for _, span in ipairs(spans) do - local adj = span.line - leading_offset - if adj >= 1 and adj <= hunk_line_count then - local buf_line = hunk.start_line + adj - 1 - 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, - }) - extmark_count = extmark_count + 1 - if covered_lines then - covered_lines[buf_line] = true - end - end + local buf_line = hunk.start_line + span.line - 1 + 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 = 200, + }) + extmark_count = extmark_count + 1 end return extmark_count @@ -300,101 +248,21 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) use_vim = false end - ---@type table - local covered_lines = {} + local apply_syntax = use_ts or use_vim - 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) + ---@type string[] + local code_lines = {} + if apply_syntax then + for _, line in ipairs(hunk.lines) do + table.insert(code_lines, line:sub(2)) 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[] - local new_code = {} - ---@type table - local new_map = {} - ---@type string[] - local old_code = {} - ---@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) - local buf_line = hunk.start_line + i - 1 - - if prefix == '+' then - new_map[#new_code] = buf_line - table.insert(new_code, stripped) - elseif prefix == '-' then - old_map[#old_code] = buf_line - table.insert(old_code, stripped) - else - new_map[#new_code] = buf_line - table.insert(new_code, stripped) - table.insert(old_code, stripped) - 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 = extmark_count - + highlight_treesitter(bufnr, ns, old_code, hunk.lang, old_map, 1, covered_lines) - - 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, - }) - local header_extmarks = highlight_text( - bufnr, - ns, - hunk, - hunk.header_context_col, - hunk.header_context, - hunk.lang, - new_code - ) - if header_extmarks > 0 then - dbg('header %s:%d applied %d extmarks', hunk.filename, hunk.start_line, header_extmarks) - end - extmark_count = extmark_count + header_extmarks - end + extmark_count = highlight_treesitter(bufnr, ns, hunk, code_lines) 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) end if @@ -403,15 +271,18 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) and #hunk.header_lines > 0 and opts.highlights.treesitter.enabled then - ---@type table - local header_map = {} - for i = 0, #hunk.header_lines - 1 do - 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, { + filename = hunk.filename, + start_line = hunk.header_start_line - 1, + lang = 'diff', + lines = hunk.header_lines, + header_lines = {}, + }, hunk.header_lines, 0) end + local syntax_applied = extmark_count > 0 + ---@type diffs.IntraChanges? local intra = nil local intra_cfg = opts.highlights.intra @@ -463,27 +334,22 @@ function M.highlight_hunk(bufnr, ns, hunk, opts) }) end - if line_len > 1 and covered_lines[buf_line] then + if line_len > 1 and syntax_applied then pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 1, { end_col = line_len, - hl_group = 'DiffsClear', - priority = PRIORITY_CLEAR, + hl_group = 'Normal', + priority = 198, }) end 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, + end_col = line_len, hl_group = line_hl, hl_eol = true, - priority = PRIORITY_LINE_BG, + number_hl_group = opts.highlights.gutter and number_hl or nil, + priority = 199, }) - 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, - }) - end end if char_spans_by_line[i] then @@ -501,7 +367,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 = 201, }) if not ok then dbg('char extmark FAILED: %s', err) diff --git a/lua/diffs/init.lua b/lua/diffs/init.lua index cdc29d1..d80cd2e 100644 --- a/lua/diffs/init.lua +++ b/lua/diffs/init.lua @@ -11,16 +11,9 @@ ---@field algorithm string ---@field max_lines integer ----@class diffs.ContextConfig ----@field enabled boolean ----@field lines integer - ---@class diffs.Highlights ---@field background boolean ---@field gutter boolean ----@field blend_alpha? number ----@field overrides? table ----@field context diffs.ContextConfig ---@field treesitter diffs.TreesitterConfig ---@field vim diffs.VimConfig ---@field intra diffs.IntraConfig @@ -29,27 +22,12 @@ ---@field horizontal string|false ---@field vertical string|false ----@class diffs.ConflictKeymaps ----@field ours string|false ----@field theirs string|false ----@field both string|false ----@field none string|false ----@field next string|false ----@field prev string|false - ----@class diffs.ConflictConfig ----@field enabled boolean ----@field disable_diagnostics boolean ----@field show_virtual_text boolean ----@field keymaps diffs.ConflictKeymaps - ---@class diffs.Config ---@field debug boolean ---@field debounce_ms integer ---@field hide_prefix boolean ---@field highlights diffs.Highlights ---@field fugitive diffs.FugitiveConfig ----@field conflict diffs.ConflictConfig ---@class diffs ---@field attach fun(bufnr?: integer) @@ -102,10 +80,6 @@ local default_config = { highlights = { background = true, gutter = true, - context = { - enabled = true, - lines = 25, - }, treesitter = { enabled = true, max_lines = 500, @@ -116,7 +90,7 @@ local default_config = { }, intra = { enabled = true, - algorithm = 'default', + algorithm = 'auto', max_lines = 500, }, }, @@ -124,19 +98,6 @@ local default_config = { horizontal = 'du', vertical = 'dU', }, - conflict = { - enabled = true, - disable_diagnostics = true, - show_virtual_text = true, - keymaps = { - ours = 'doo', - theirs = 'dot', - both = 'dob', - none = 'don', - next = ']x', - prev = '[x', - }, - }, } ---@type diffs.Config @@ -222,19 +183,13 @@ local function compute_highlight_groups() local blended_add = blend_color(add_bg, bg, 0.4) local blended_del = blend_color(del_bg, bg, 0.4) - local alpha = config.highlights.blend_alpha or 0.6 - local blended_add_text = blend_color(add_fg, bg, alpha) - local blended_del_text = blend_color(del_fg, bg, alpha) + local blended_add_text = blend_color(add_fg, bg, 0.7) + local blended_del_text = blend_color(del_fg, bg, 0.7) - vim.api.nvim_set_hl(0, 'DiffsClear', { default = true, fg = normal.fg or 0xc0c0c0 }) 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 }) - vim.api.nvim_set_hl( - 0, - 'DiffsDeleteNr', - { default = true, fg = blended_del_text, bg = blended_del } - ) + vim.api.nvim_set_hl(0, 'DiffsAddNr', { default = true, fg = add_fg, bg = blended_add }) + vim.api.nvim_set_hl(0, 'DiffsDeleteNr', { default = true, fg = del_fg, bg = blended_del }) vim.api.nvim_set_hl(0, 'DiffsAddText', { default = true, bg = blended_add_text }) vim.api.nvim_set_hl(0, 'DiffsDeleteText', { default = true, bg = blended_del_text }) @@ -250,51 +205,10 @@ local function compute_highlight_groups() local diff_change = resolve_hl('DiffChange') local diff_text = resolve_hl('DiffText') - vim.api.nvim_set_hl(0, 'DiffsDiffAdd', { default = true, bg = diff_add.bg }) - vim.api.nvim_set_hl( - 0, - 'DiffsDiffDelete', - { default = true, fg = diff_delete.fg, bg = diff_delete.bg } - ) - vim.api.nvim_set_hl(0, 'DiffsDiffChange', { default = true, bg = diff_change.bg }) - vim.api.nvim_set_hl(0, 'DiffsDiffText', { default = true, bg = diff_text.bg }) - - local change_bg = diff_change.bg or 0x3a3a4a - local text_bg = diff_text.bg or 0x4a4a5a - local change_fg = diff_change.fg or diff_text.fg or 0x80a0c0 - - local blended_ours = blend_color(add_bg, bg, 0.4) - local blended_theirs = blend_color(change_bg, bg, 0.4) - local blended_base = blend_color(text_bg, bg, 0.3) - local blended_ours_nr = blend_color(add_fg, bg, alpha) - local blended_theirs_nr = blend_color(change_fg, bg, alpha) - local blended_base_nr = blend_color(change_fg, bg, 0.4) - - vim.api.nvim_set_hl(0, 'DiffsConflictOurs', { default = true, bg = blended_ours }) - 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, - 'DiffsConflictOursNr', - { default = true, fg = blended_ours_nr, bg = blended_ours } - ) - vim.api.nvim_set_hl( - 0, - 'DiffsConflictTheirsNr', - { default = true, fg = blended_theirs_nr, bg = blended_theirs } - ) - vim.api.nvim_set_hl( - 0, - 'DiffsConflictBaseNr', - { default = true, fg = blended_base_nr, bg = blended_base } - ) - - if config.highlights.overrides then - for group, hl in pairs(config.highlights.overrides) do - vim.api.nvim_set_hl(0, group, hl) - end - end + vim.api.nvim_set_hl(0, 'DiffsDiffAdd', { bg = diff_add.bg }) + vim.api.nvim_set_hl(0, 'DiffsDiffDelete', { fg = diff_delete.fg, bg = diff_delete.bg }) + vim.api.nvim_set_hl(0, 'DiffsDiffChange', { bg = diff_change.bg }) + vim.api.nvim_set_hl(0, 'DiffsDiffText', { bg = diff_text.bg }) end local function init() @@ -316,21 +230,11 @@ local function init() vim.validate({ ['highlights.background'] = { opts.highlights.background, 'boolean', true }, ['highlights.gutter'] = { opts.highlights.gutter, 'boolean', true }, - ['highlights.blend_alpha'] = { opts.highlights.blend_alpha, 'number', true }, - ['highlights.overrides'] = { opts.highlights.overrides, 'table', true }, - ['highlights.context'] = { opts.highlights.context, 'table', true }, ['highlights.treesitter'] = { opts.highlights.treesitter, 'table', true }, ['highlights.vim'] = { opts.highlights.vim, 'table', true }, ['highlights.intra'] = { opts.highlights.intra, 'table', true }, }) - if opts.highlights.context then - vim.validate({ - ['highlights.context.enabled'] = { opts.highlights.context.enabled, 'boolean', true }, - ['highlights.context.lines'] = { opts.highlights.context.lines, 'number', true }, - }) - end - if opts.highlights.treesitter then vim.validate({ ['highlights.treesitter.enabled'] = { opts.highlights.treesitter.enabled, 'boolean', true }, @@ -355,9 +259,9 @@ local function init() ['highlights.intra.algorithm'] = { opts.highlights.intra.algorithm, function(v) - return v == nil or v == 'default' or v == 'vscode' + return v == nil or v == 'auto' or v == 'native' or v == 'vscode' end, - "'default' or 'vscode'", + "'auto', 'native', or 'vscode'", }, ['highlights.intra.max_lines'] = { opts.highlights.intra.max_lines, 'number', true }, }) @@ -383,41 +287,9 @@ local function init() }) end - if opts.conflict then - vim.validate({ - ['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.keymaps'] = { opts.conflict.keymaps, 'table', true }, - }) - - if opts.conflict.keymaps then - local keymap_validator = function(v) - return v == false or type(v) == 'string' - end - for _, key in ipairs({ 'ours', 'theirs', 'both', 'none', 'next', 'prev' }) do - vim.validate({ - ['conflict.keymaps.' .. key] = { - opts.conflict.keymaps[key], - keymap_validator, - 'string or false', - }, - }) - end - end - end - if opts.debounce_ms and opts.debounce_ms < 0 then error('diffs: debounce_ms must be >= 0') end - if - opts.highlights - and opts.highlights.context - and opts.highlights.context.lines - and opts.highlights.context.lines < 0 - then - error('diffs: highlights.context.lines must be >= 0') - end if opts.highlights and opts.highlights.treesitter @@ -442,13 +314,6 @@ local function init() then error('diffs: highlights.intra.max_lines must be >= 1') end - if - opts.highlights - and opts.highlights.blend_alpha - and (opts.highlights.blend_alpha < 0 or opts.highlights.blend_alpha > 1) - then - error('diffs: highlights.blend_alpha must be >= 0 and <= 1') - end config = vim.tbl_deep_extend('force', default_config, opts) log.set_enabled(config.debug) @@ -571,10 +436,4 @@ function M.get_fugitive_config() return config.fugitive end ----@return diffs.ConflictConfig -function M.get_conflict_config() - init() - return config.conflict -end - return M diff --git a/lua/diffs/parser.lua b/lua/diffs/parser.lua index 43eb1f6..52b2864 100644 --- a/lua/diffs/parser.lua +++ b/lua/diffs/parser.lua @@ -8,11 +8,6 @@ ---@field lines string[] ---@field header_start_line integer? ---@field header_lines string[]? ----@field file_old_start integer? ----@field file_old_count integer? ----@field file_new_start integer? ----@field file_new_count integer? ----@field repo_root string? local M = {} @@ -137,14 +132,6 @@ function M.parse_buffer(bufnr) local header_start = nil ---@type string[] local header_lines = {} - ---@type integer? - local file_old_start = nil - ---@type integer? - local file_old_count = nil - ---@type integer? - local file_new_start = nil - ---@type integer? - local file_new_count = nil local function flush_hunk() if hunk_start and #hunk_lines > 0 then @@ -156,11 +143,6 @@ function M.parse_buffer(bufnr) header_context = hunk_header_context, header_context_col = hunk_header_context_col, lines = hunk_lines, - file_old_start = file_old_start, - file_old_count = file_old_count, - file_new_start = file_new_start, - file_new_count = file_new_count, - repo_root = repo_root, } if hunk_count == 1 and header_start and #header_lines > 0 then hunk.header_start_line = header_start @@ -172,10 +154,6 @@ function M.parse_buffer(bufnr) hunk_header_context = nil hunk_header_context_col = nil hunk_lines = {} - file_old_start = nil - file_old_count = nil - file_new_start = nil - file_new_count = nil end for i, line in ipairs(lines) do @@ -196,13 +174,6 @@ function M.parse_buffer(bufnr) elseif line:match('^@@.-@@') then flush_hunk() hunk_start = i - local hs, hc, hs2, hc2 = line:match('^@@ %-(%d+),?(%d*) %+(%d+),?(%d*) @@') - if hs then - file_old_start = tonumber(hs) - file_old_count = tonumber(hc) or 1 - file_new_start = tonumber(hs2) - file_new_count = tonumber(hc2) or 1 - end local prefix, context = line:match('^(@@.-@@%s*)(.*)') if context and context ~= '' then hunk_header_context = context diff --git a/plugin/diffs.lua b/plugin/diffs.lua index 5d3c8b2..35aa223 100644 --- a/plugin/diffs.lua +++ b/plugin/diffs.lua @@ -23,22 +23,6 @@ vim.api.nvim_create_autocmd('FileType', { end, }) -vim.api.nvim_create_autocmd('BufReadCmd', { - pattern = 'diffs://*', - callback = function(args) - require('diffs.commands').read_buffer(args.buf) - end, -}) - -vim.api.nvim_create_autocmd('BufReadPost', { - callback = function(args) - local conflict_config = require('diffs').get_conflict_config() - if conflict_config.enabled then - require('diffs.conflict').attach(args.buf, conflict_config) - end - end, -}) - vim.api.nvim_create_autocmd('OptionSet', { pattern = 'diff', callback = function() @@ -49,36 +33,3 @@ vim.api.nvim_create_autocmd('OptionSet', { end end, }) - -local cmds = require('diffs.commands') -vim.keymap.set('n', '(diffs-gdiff)', function() - cmds.gdiff(nil, false) -end, { desc = 'Unified diff (horizontal)' }) -vim.keymap.set('n', '(diffs-gvdiff)', function() - cmds.gdiff(nil, true) -end, { desc = 'Unified diff (vertical)' }) - -local function conflict_action(fn) - local bufnr = vim.api.nvim_get_current_buf() - local config = require('diffs').get_conflict_config() - fn(bufnr, config) -end - -vim.keymap.set('n', '(diffs-conflict-ours)', function() - conflict_action(require('diffs.conflict').resolve_ours) -end, { desc = 'Accept current (ours) change' }) -vim.keymap.set('n', '(diffs-conflict-theirs)', function() - conflict_action(require('diffs.conflict').resolve_theirs) -end, { desc = 'Accept incoming (theirs) change' }) -vim.keymap.set('n', '(diffs-conflict-both)', function() - conflict_action(require('diffs.conflict').resolve_both) -end, { desc = 'Accept both changes' }) -vim.keymap.set('n', '(diffs-conflict-none)', function() - conflict_action(require('diffs.conflict').resolve_none) -end, { desc = 'Reject both changes' }) -vim.keymap.set('n', '(diffs-conflict-next)', function() - require('diffs.conflict').goto_next(vim.api.nvim_get_current_buf()) -end, { desc = 'Jump to next conflict' }) -vim.keymap.set('n', '(diffs-conflict-prev)', function() - require('diffs.conflict').goto_prev(vim.api.nvim_get_current_buf()) -end, { desc = 'Jump to previous conflict' }) diff --git a/spec/conflict_spec.lua b/spec/conflict_spec.lua deleted file mode 100644 index 75eac23..0000000 --- a/spec/conflict_spec.lua +++ /dev/null @@ -1,688 +0,0 @@ -local conflict = require('diffs.conflict') -local helpers = require('spec.helpers') - -local function default_config(overrides) - local cfg = { - enabled = true, - disable_diagnostics = false, - show_virtual_text = true, - keymaps = { - ours = 'doo', - theirs = 'dot', - both = 'dob', - none = 'don', - next = ']x', - prev = '[x', - }, - } - if overrides then - cfg = vim.tbl_deep_extend('force', cfg, overrides) - end - return cfg -end - -local function create_file_buffer(lines) - local bufnr = vim.api.nvim_create_buf(false, false) - vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines or {}) - return bufnr -end - -local function get_extmarks(bufnr) - return vim.api.nvim_buf_get_extmarks(bufnr, conflict.get_namespace(), 0, -1, { details = true }) -end - -describe('conflict', function() - describe('parse', function() - it('parses a single conflict', function() - local lines = { - '<<<<<<< HEAD', - 'local x = 1', - '=======', - 'local x = 2', - '>>>>>>> feature', - } - local regions = conflict.parse(lines) - assert.are.equal(1, #regions) - assert.are.equal(0, regions[1].marker_ours) - assert.are.equal(1, regions[1].ours_start) - assert.are.equal(2, regions[1].ours_end) - assert.are.equal(2, regions[1].marker_sep) - assert.are.equal(3, regions[1].theirs_start) - assert.are.equal(4, regions[1].theirs_end) - assert.are.equal(4, regions[1].marker_theirs) - end) - - it('parses multiple conflicts', function() - local lines = { - '<<<<<<< HEAD', - 'a', - '=======', - 'b', - '>>>>>>> feat', - 'normal line', - '<<<<<<< HEAD', - 'c', - '=======', - 'd', - '>>>>>>> feat', - } - local regions = conflict.parse(lines) - assert.are.equal(2, #regions) - assert.are.equal(0, regions[1].marker_ours) - assert.are.equal(6, regions[2].marker_ours) - end) - - it('parses diff3 format', function() - local lines = { - '<<<<<<< HEAD', - 'local x = 1', - '||||||| base', - 'local x = 0', - '=======', - 'local x = 2', - '>>>>>>> feature', - } - local regions = conflict.parse(lines) - assert.are.equal(1, #regions) - assert.are.equal(2, regions[1].marker_base) - assert.are.equal(3, regions[1].base_start) - assert.are.equal(4, regions[1].base_end) - end) - - it('handles empty ours section', function() - local lines = { - '<<<<<<< HEAD', - '=======', - 'local x = 2', - '>>>>>>> feature', - } - local regions = conflict.parse(lines) - assert.are.equal(1, #regions) - assert.are.equal(1, regions[1].ours_start) - assert.are.equal(1, regions[1].ours_end) - end) - - it('handles empty theirs section', function() - local lines = { - '<<<<<<< HEAD', - 'local x = 1', - '=======', - '>>>>>>> feature', - } - local regions = conflict.parse(lines) - assert.are.equal(1, #regions) - assert.are.equal(3, regions[1].theirs_start) - assert.are.equal(3, regions[1].theirs_end) - end) - - it('returns empty for no markers', function() - local lines = { 'local x = 1', 'local y = 2' } - local regions = conflict.parse(lines) - assert.are.equal(0, #regions) - end) - - it('discards malformed markers (no separator)', function() - local lines = { - '<<<<<<< HEAD', - 'local x = 1', - '>>>>>>> feature', - } - local regions = conflict.parse(lines) - assert.are.equal(0, #regions) - end) - - it('discards malformed markers (no end)', function() - local lines = { - '<<<<<<< HEAD', - 'local x = 1', - '=======', - 'local x = 2', - } - local regions = conflict.parse(lines) - assert.are.equal(0, #regions) - end) - - it('handles trailing text on marker lines', function() - local lines = { - '<<<<<<< HEAD (some text)', - 'local x = 1', - '======= extra', - 'local x = 2', - '>>>>>>> feature-branch/some-thing', - } - local regions = conflict.parse(lines) - assert.are.equal(1, #regions) - end) - - it('handles empty base in diff3', function() - local lines = { - '<<<<<<< HEAD', - 'local x = 1', - '||||||| base', - '=======', - 'local x = 2', - '>>>>>>> feature', - } - local regions = conflict.parse(lines) - assert.are.equal(1, #regions) - assert.are.equal(3, regions[1].base_start) - assert.are.equal(3, regions[1].base_end) - end) - end) - - describe('highlighting', function() - after_each(function() - conflict.detach(vim.api.nvim_get_current_buf()) - end) - - it('applies extmarks for conflict regions', function() - local bufnr = create_file_buffer({ - '<<<<<<< HEAD', - 'local x = 1', - '=======', - 'local x = 2', - '>>>>>>> feature', - }) - - conflict.attach(bufnr, default_config()) - - local extmarks = get_extmarks(bufnr) - assert.is_true(#extmarks > 0) - - local has_ours = false - local has_theirs = false - local has_marker = false - for _, mark in ipairs(extmarks) do - local hl = mark[4] and mark[4].hl_group - if hl == 'DiffsConflictOurs' then - has_ours = true - end - if hl == 'DiffsConflictTheirs' then - has_theirs = true - end - if hl == 'DiffsConflictMarker' then - has_marker = true - end - end - assert.is_true(has_ours) - assert.is_true(has_theirs) - assert.is_true(has_marker) - - helpers.delete_buffer(bufnr) - end) - - it('applies virtual text when enabled', function() - local bufnr = create_file_buffer({ - '<<<<<<< HEAD', - 'local x = 1', - '=======', - 'local x = 2', - '>>>>>>> feature', - }) - - conflict.attach(bufnr, default_config({ show_virtual_text = true })) - - 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(2, virt_text_count) - - 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', - 'local x = 1', - '=======', - 'local x = 2', - '>>>>>>> feature', - }) - - conflict.attach(bufnr, default_config()) - - local extmarks = get_extmarks(bufnr) - local has_ours_nr = false - local has_theirs_nr = false - for _, mark in ipairs(extmarks) do - local nr = mark[4] and mark[4].number_hl_group - if nr == 'DiffsConflictOursNr' then - has_ours_nr = true - end - if nr == 'DiffsConflictTheirsNr' then - has_theirs_nr = true - end - end - assert.is_true(has_ours_nr) - assert.is_true(has_theirs_nr) - - helpers.delete_buffer(bufnr) - end) - - it('highlights base region in diff3', function() - local bufnr = create_file_buffer({ - '<<<<<<< HEAD', - 'local x = 1', - '||||||| base', - 'local x = 0', - '=======', - 'local x = 2', - '>>>>>>> feature', - }) - - conflict.attach(bufnr, default_config()) - - local extmarks = get_extmarks(bufnr) - local has_base = false - for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].hl_group == 'DiffsConflictBase' then - has_base = true - break - end - end - assert.is_true(has_base) - - helpers.delete_buffer(bufnr) - end) - - it('clears extmarks on detach', function() - local bufnr = create_file_buffer({ - '<<<<<<< HEAD', - 'local x = 1', - '=======', - 'local x = 2', - '>>>>>>> feature', - }) - - conflict.attach(bufnr, default_config()) - assert.is_true(#get_extmarks(bufnr) > 0) - - conflict.detach(bufnr) - assert.are.equal(0, #get_extmarks(bufnr)) - - helpers.delete_buffer(bufnr) - end) - end) - - describe('resolution', function() - local function make_conflict_buffer() - local bufnr = create_file_buffer({ - '<<<<<<< HEAD', - 'local x = 1', - '=======', - 'local x = 2', - '>>>>>>> feature', - }) - vim.api.nvim_set_current_buf(bufnr) - return bufnr - end - - it('resolve_ours keeps ours content', function() - local bufnr = make_conflict_buffer() - vim.api.nvim_win_set_cursor(0, { 2, 0 }) - - conflict.resolve_ours(bufnr, default_config()) - - local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - assert.are.equal(1, #lines) - assert.are.equal('local x = 1', lines[1]) - - helpers.delete_buffer(bufnr) - end) - - it('resolve_theirs keeps theirs content', function() - local bufnr = make_conflict_buffer() - vim.api.nvim_win_set_cursor(0, { 2, 0 }) - - conflict.resolve_theirs(bufnr, default_config()) - - local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - assert.are.equal(1, #lines) - assert.are.equal('local x = 2', lines[1]) - - helpers.delete_buffer(bufnr) - end) - - it('resolve_both keeps ours then theirs', function() - local bufnr = make_conflict_buffer() - vim.api.nvim_win_set_cursor(0, { 2, 0 }) - - conflict.resolve_both(bufnr, default_config()) - - local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - assert.are.equal(2, #lines) - assert.are.equal('local x = 1', lines[1]) - assert.are.equal('local x = 2', lines[2]) - - helpers.delete_buffer(bufnr) - end) - - it('resolve_none removes entire block', function() - local bufnr = make_conflict_buffer() - vim.api.nvim_win_set_cursor(0, { 2, 0 }) - - conflict.resolve_none(bufnr, default_config()) - - local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - assert.are.equal(1, #lines) - assert.are.equal('', lines[1]) - - helpers.delete_buffer(bufnr) - end) - - it('does nothing when cursor is outside conflict', function() - local bufnr = create_file_buffer({ - 'normal line', - '<<<<<<< HEAD', - 'local x = 1', - '=======', - 'local x = 2', - '>>>>>>> feature', - }) - vim.api.nvim_set_current_buf(bufnr) - vim.api.nvim_win_set_cursor(0, { 1, 0 }) - - conflict.resolve_ours(bufnr, default_config()) - - local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - assert.are.equal(6, #lines) - - helpers.delete_buffer(bufnr) - end) - - it('resolves one conflict among multiple', function() - local bufnr = create_file_buffer({ - '<<<<<<< HEAD', - 'a', - '=======', - 'b', - '>>>>>>> feat', - 'middle', - '<<<<<<< HEAD', - 'c', - '=======', - 'd', - '>>>>>>> feat', - }) - vim.api.nvim_set_current_buf(bufnr) - vim.api.nvim_win_set_cursor(0, { 2, 0 }) - - conflict.resolve_ours(bufnr, default_config()) - - local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - assert.are.equal('a', lines[1]) - assert.are.equal('middle', lines[2]) - assert.are.equal('<<<<<<< HEAD', lines[3]) - - helpers.delete_buffer(bufnr) - end) - - it('resolve_ours with empty ours section', function() - local bufnr = create_file_buffer({ - '<<<<<<< HEAD', - '=======', - 'local x = 2', - '>>>>>>> feature', - }) - vim.api.nvim_set_current_buf(bufnr) - vim.api.nvim_win_set_cursor(0, { 1, 0 }) - - conflict.resolve_ours(bufnr, default_config()) - - local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - assert.are.equal(1, #lines) - assert.are.equal('', lines[1]) - - helpers.delete_buffer(bufnr) - end) - - it('handles diff3 resolution (ignores base)', function() - local bufnr = create_file_buffer({ - '<<<<<<< HEAD', - 'local x = 1', - '||||||| base', - 'local x = 0', - '=======', - 'local x = 2', - '>>>>>>> feature', - }) - vim.api.nvim_set_current_buf(bufnr) - vim.api.nvim_win_set_cursor(0, { 2, 0 }) - - conflict.resolve_theirs(bufnr, default_config()) - - local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - assert.are.equal(1, #lines) - assert.are.equal('local x = 2', lines[1]) - - helpers.delete_buffer(bufnr) - end) - end) - - describe('navigation', function() - it('goto_next jumps to next conflict', function() - local bufnr = create_file_buffer({ - 'normal', - '<<<<<<< HEAD', - 'a', - '=======', - 'b', - '>>>>>>> feat', - 'middle', - '<<<<<<< HEAD', - 'c', - '=======', - 'd', - '>>>>>>> feat', - }) - vim.api.nvim_set_current_buf(bufnr) - vim.api.nvim_win_set_cursor(0, { 1, 0 }) - - conflict.goto_next(bufnr) - assert.are.equal(2, vim.api.nvim_win_get_cursor(0)[1]) - - conflict.goto_next(bufnr) - assert.are.equal(8, vim.api.nvim_win_get_cursor(0)[1]) - - helpers.delete_buffer(bufnr) - end) - - it('goto_next wraps to first conflict', 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 }) - - conflict.goto_next(bufnr) - assert.are.equal(1, vim.api.nvim_win_get_cursor(0)[1]) - - helpers.delete_buffer(bufnr) - end) - - it('goto_prev jumps to previous conflict', function() - local bufnr = create_file_buffer({ - '<<<<<<< HEAD', - 'a', - '=======', - 'b', - '>>>>>>> feat', - 'middle', - '<<<<<<< HEAD', - 'c', - '=======', - 'd', - '>>>>>>> feat', - 'end', - }) - vim.api.nvim_set_current_buf(bufnr) - vim.api.nvim_win_set_cursor(0, { 12, 0 }) - - conflict.goto_prev(bufnr) - assert.are.equal(7, vim.api.nvim_win_get_cursor(0)[1]) - - conflict.goto_prev(bufnr) - assert.are.equal(1, vim.api.nvim_win_get_cursor(0)[1]) - - helpers.delete_buffer(bufnr) - end) - - it('goto_prev wraps to last conflict', 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 }) - - conflict.goto_prev(bufnr) - assert.are.equal(1, vim.api.nvim_win_get_cursor(0)[1]) - - 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) - vim.api.nvim_win_set_cursor(0, { 1, 0 }) - - conflict.goto_next(bufnr) - assert.are.equal(1, vim.api.nvim_win_get_cursor(0)[1]) - - helpers.delete_buffer(bufnr) - end) - end) - - describe('lifecycle', function() - it('attach is idempotent', function() - local bufnr = create_file_buffer({ - '<<<<<<< HEAD', - 'a', - '=======', - 'b', - '>>>>>>> feat', - }) - local cfg = default_config() - conflict.attach(bufnr, cfg) - local count1 = #get_extmarks(bufnr) - conflict.attach(bufnr, cfg) - local count2 = #get_extmarks(bufnr) - assert.are.equal(count1, count2) - conflict.detach(bufnr) - helpers.delete_buffer(bufnr) - end) - - it('skips non-file buffers', function() - local bufnr = helpers.create_buffer({ - '<<<<<<< HEAD', - 'a', - '=======', - 'b', - '>>>>>>> feat', - }) - vim.api.nvim_set_option_value('buftype', 'nofile', { buf = bufnr }) - - conflict.attach(bufnr, default_config()) - assert.are.equal(0, #get_extmarks(bufnr)) - - helpers.delete_buffer(bufnr) - end) - - it('skips buffers without conflict markers', function() - local bufnr = create_file_buffer({ 'local x = 1', 'local y = 2' }) - - conflict.attach(bufnr, default_config()) - assert.are.equal(0, #get_extmarks(bufnr)) - - helpers.delete_buffer(bufnr) - end) - - it('re-highlights when markers return after resolution', function() - local bufnr = create_file_buffer({ - '<<<<<<< HEAD', - 'local x = 1', - '=======', - 'local x = 2', - '>>>>>>> feature', - }) - vim.api.nvim_set_current_buf(bufnr) - local cfg = default_config() - conflict.attach(bufnr, cfg) - - assert.is_true(#get_extmarks(bufnr) > 0) - - vim.api.nvim_win_set_cursor(0, { 2, 0 }) - conflict.resolve_ours(bufnr, cfg) - assert.are.equal(0, #get_extmarks(bufnr)) - - vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { - '<<<<<<< HEAD', - 'local x = 1', - '=======', - 'local x = 2', - '>>>>>>> feature', - }) - vim.api.nvim_exec_autocmds('TextChanged', { buffer = bufnr }) - - assert.is_true(#get_extmarks(bufnr) > 0) - - conflict.detach(bufnr) - helpers.delete_buffer(bufnr) - end) - - it('detaches after last conflict resolved', function() - local bufnr = create_file_buffer({ - '<<<<<<< HEAD', - 'local x = 1', - '=======', - 'local x = 2', - '>>>>>>> feature', - }) - vim.api.nvim_set_current_buf(bufnr) - conflict.attach(bufnr, default_config()) - - assert.is_true(#get_extmarks(bufnr) > 0) - - vim.api.nvim_win_set_cursor(0, { 2, 0 }) - conflict.resolve_ours(bufnr, default_config()) - - assert.are.equal(0, #get_extmarks(bufnr)) - - helpers.delete_buffer(bufnr) - end) - end) -end) diff --git a/spec/diff_spec.lua b/spec/diff_spec.lua index 2cc22ac..80ad8a8 100644 --- a/spec/diff_spec.lua +++ b/spec/diff_spec.lua @@ -73,17 +73,17 @@ describe('diff', function() describe('compute_intra_hunks', function() it('returns nil for all-addition hunks', function() - local result = diff.compute_intra_hunks({ '+line1', '+line2' }, 'default') + local result = diff.compute_intra_hunks({ '+line1', '+line2' }, 'native') assert.is_nil(result) end) it('returns nil for all-deletion hunks', function() - local result = diff.compute_intra_hunks({ '-line1', '-line2' }, 'default') + local result = diff.compute_intra_hunks({ '-line1', '-line2' }, 'native') assert.is_nil(result) end) it('returns nil for context-only hunks', function() - local result = diff.compute_intra_hunks({ ' line1', ' line2' }, 'default') + local result = diff.compute_intra_hunks({ ' line1', ' line2' }, 'native') assert.is_nil(result) end) @@ -91,7 +91,7 @@ describe('diff', function() local result = diff.compute_intra_hunks({ '-local x = 1', '+local x = 2', - }, 'default') + }, 'native') assert.is_not_nil(result) assert.is_true(#result.del_spans > 0) assert.is_true(#result.add_spans > 0) @@ -101,7 +101,7 @@ describe('diff', function() local result = diff.compute_intra_hunks({ '-local x = 1', '+local x = 2', - }, 'default') + }, 'native') assert.is_not_nil(result) assert.are.equal(1, #result.del_spans) @@ -121,7 +121,7 @@ describe('diff', function() ' local b = 3', '-local c = 4', '+local c = 5', - }, 'default') + }, 'native') assert.is_not_nil(result) assert.is_true(#result.del_spans >= 2) assert.is_true(#result.add_spans >= 2) @@ -132,7 +132,7 @@ describe('diff', function() '-line one', '-line two', '+line combined', - }, 'default') + }, 'native') assert.is_not_nil(result) end) @@ -140,7 +140,7 @@ describe('diff', function() local result = diff.compute_intra_hunks({ '-local x = "héllo"', '+local x = "wörld"', - }, 'default') + }, 'native') assert.is_not_nil(result) assert.is_true(#result.del_spans > 0) assert.is_true(#result.add_spans > 0) @@ -150,7 +150,7 @@ describe('diff', function() local result = diff.compute_intra_hunks({ '-local x = 1', '+local x = 1', - }, 'default') + }, 'native') assert.is_nil(result) end) end) diff --git a/spec/helpers.lua b/spec/helpers.lua index 7774cf4..34128ac 100644 --- a/spec/helpers.lua +++ b/spec/helpers.lua @@ -12,7 +12,6 @@ local function ensure_parser(lang) end ensure_parser('lua') -ensure_parser('vim') local M = {} diff --git a/spec/highlight_spec.lua b/spec/highlight_spec.lua index 0b9051e..9d9080a 100644 --- a/spec/highlight_spec.lua +++ b/spec/highlight_spec.lua @@ -7,10 +7,8 @@ describe('highlight', function() before_each(function() ns = vim.api.nvim_create_namespace('diffs_test') - local normal = vim.api.nvim_get_hl(0, { name = 'Normal' }) local diff_add = vim.api.nvim_get_hl(0, { name = 'DiffAdd' }) local diff_delete = vim.api.nvim_get_hl(0, { name = 'DiffDelete' }) - vim.api.nvim_set_hl(0, 'DiffsClear', { fg = normal.fg or 0xc0c0c0 }) vim.api.nvim_set_hl(0, 'DiffsAdd', { bg = diff_add.bg }) vim.api.nvim_set_hl(0, 'DiffsDelete', { bg = diff_delete.bg }) end) @@ -37,7 +35,6 @@ describe('highlight', function() highlights = { background = false, gutter = false, - context = { enabled = false, lines = 0 }, treesitter = { enabled = true, max_lines = 500, @@ -48,7 +45,7 @@ describe('highlight', function() }, intra = { enabled = false, - algorithm = 'default', + algorithm = 'native', max_lines = 500, }, }, @@ -85,7 +82,7 @@ describe('highlight', function() delete_buffer(bufnr) end) - it('applies DiffsClear extmarks to clear diff colors', function() + it('applies Normal extmarks to clear diff colors', function() local bufnr = create_buffer({ '@@ -1,1 +1,2 @@', ' local x = 1', @@ -102,46 +99,14 @@ describe('highlight', function() highlight.highlight_hunk(bufnr, ns, hunk, default_opts()) local extmarks = get_extmarks(bufnr) - local has_clear = false + local has_normal = false for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].hl_group == 'DiffsClear' then - has_clear = true + if mark[4] and mark[4].hl_group == 'Normal' then + has_normal = true break end end - assert.is_true(has_clear) - delete_buffer(bufnr) - end) - - it('produces treesitter captures on all lines with split parsing', function() - local bufnr = create_buffer({ - '@@ -1,3 +1,3 @@', - ' local x = 1', - '-local y = 2', - '+local y = 3', - ' return x', - }) - - local hunk = { - filename = 'test.lua', - lang = 'lua', - start_line = 1, - lines = { ' local x = 1', '-local y = 2', '+local y = 3', ' return x' }, - } - - highlight.highlight_hunk(bufnr, ns, hunk, default_opts()) - - local extmarks = get_extmarks(bufnr) - local lines_with_ts = {} - for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].hl_group and mark[4].hl_group:match('^@.*%.lua$') then - lines_with_ts[mark[2]] = true - end - end - assert.is_true(lines_with_ts[1] ~= nil) - assert.is_true(lines_with_ts[2] ~= nil) - assert.is_true(lines_with_ts[3] ~= nil) - assert.is_true(lines_with_ts[4] ~= nil) + assert.is_true(has_normal) delete_buffer(bufnr) end) @@ -220,40 +185,6 @@ describe('highlight', function() delete_buffer(bufnr) end) - it('highlights function keyword in header context', function() - local bufnr = create_buffer({ - '@@ -5,3 +5,4 @@ function M.setup()', - ' local x = 1', - '+local y = 2', - ' return x', - }) - - local hunk = { - filename = 'test.lua', - lang = 'lua', - start_line = 1, - header_context = 'function M.setup()', - header_context_col = 18, - lines = { ' local x = 1', '+local y = 2', ' return x' }, - } - - highlight.highlight_hunk(bufnr, ns, hunk, default_opts()) - - local extmarks = get_extmarks(bufnr) - local has_keyword_function = false - for _, mark in ipairs(extmarks) do - if mark[2] == 0 and mark[4] and mark[4].hl_group then - local hl = mark[4].hl_group - if hl == '@keyword.function.lua' or hl == '@keyword.lua' then - has_keyword_function = true - break - end - end - end - assert.is_true(has_keyword_function) - delete_buffer(bufnr) - end) - it('does not highlight header when no header_context', function() local bufnr = create_buffer({ '@@ -10,3 +10,4 @@', @@ -645,7 +576,7 @@ describe('highlight', function() 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 + if mark[4] and mark[4].hl_group and mark[4].hl_group ~= 'Normal' then has_syntax_hl = true break end @@ -679,7 +610,7 @@ describe('highlight', function() 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 + if mark[4] and mark[4].hl_group and mark[4].hl_group ~= 'Normal' then has_syntax_hl = true break end @@ -751,7 +682,7 @@ describe('highlight', function() delete_buffer(bufnr) end) - it('applies DiffsClear blanking for vim fallback hunks', function() + it('applies Normal blanking for vim fallback hunks', function() local orig_synID = vim.fn.synID local orig_synIDtrans = vim.fn.synIDtrans local orig_synIDattr = vim.fn.synIDattr @@ -791,14 +722,14 @@ describe('highlight', function() vim.fn.synIDattr = orig_synIDattr local extmarks = get_extmarks(bufnr) - local has_clear = false + local has_normal = false for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].hl_group == 'DiffsClear' then - has_clear = true + if mark[4] and mark[4].hl_group == 'Normal' then + has_normal = true break end end - assert.is_true(has_clear) + assert.is_true(has_normal) delete_buffer(bufnr) end) @@ -834,7 +765,7 @@ describe('highlight', function() delete_buffer(bufnr) end) - it('hl_eol background extmarks are multiline so hl_eol takes effect', function() + it('line bg priority > Normal priority', function() local bufnr = create_buffer({ '@@ -1,2 +1,1 @@', '-local x = 1', @@ -856,86 +787,20 @@ describe('highlight', function() ) local extmarks = get_extmarks(bufnr) - for _, mark in ipairs(extmarks) do - local d = mark[4] - if d and (d.hl_group == 'DiffsAdd' or d.hl_group == 'DiffsDelete') then - assert.is_true(d.end_row > mark[2]) - end - end - delete_buffer(bufnr) - end) - - it('number_hl_group does not bleed to adjacent lines', function() - local bufnr = create_buffer({ - '@@ -1,3 +1,3 @@', - ' local a = 0', - '-local x = 1', - '+local y = 2', - ' local b = 3', - }) - - local hunk = { - filename = 'test.lua', - lang = 'lua', - start_line = 1, - lines = { ' local a = 0', '-local x = 1', '+local y = 2', ' local b = 3' }, - } - - highlight.highlight_hunk( - bufnr, - ns, - hunk, - default_opts({ highlights = { background = true, gutter = true } }) - ) - - local extmarks = get_extmarks(bufnr) - for _, mark in ipairs(extmarks) do - local d = mark[4] - if d and d.number_hl_group then - local start_row = mark[2] - local end_row = d.end_row or start_row - assert.are.equal(start_row, end_row) - end - end - 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 normal_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 + if d and d.hl_group == 'Normal' then + normal_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(normal_priority) assert.is_not_nil(line_bg_priority) - assert.is_true(line_bg_priority > clear_priority) + assert.is_true(line_bg_priority > normal_priority) delete_buffer(bufnr) end) @@ -963,7 +828,7 @@ describe('highlight', function() default_opts({ highlights = { background = true, - intra = { enabled = true, algorithm = 'default', max_lines = 500 }, + intra = { enabled = true, algorithm = 'native', max_lines = 500 }, }, }) ) @@ -1008,7 +873,7 @@ describe('highlight', function() ns, hunk, default_opts({ - highlights = { intra = { enabled = true, algorithm = 'default', max_lines = 500 } }, + highlights = { intra = { enabled = true, algorithm = 'native', max_lines = 500 } }, }) ) @@ -1048,7 +913,7 @@ describe('highlight', function() ns, hunk, default_opts({ - highlights = { intra = { enabled = false, algorithm = 'default', max_lines = 500 } }, + highlights = { intra = { enabled = false, algorithm = 'native', max_lines = 500 } }, }) ) @@ -1082,7 +947,7 @@ describe('highlight', function() ns, hunk, default_opts({ - highlights = { intra = { enabled = true, algorithm = 'default', max_lines = 500 } }, + highlights = { intra = { enabled = true, algorithm = 'native', max_lines = 500 } }, }) ) @@ -1095,7 +960,7 @@ describe('highlight', function() delete_buffer(bufnr) end) - it('enforces priority order: DiffsClear < syntax < line bg < char bg', function() + it('enforces priority order: Normal < line bg < syntax < char bg', function() vim.api.nvim_set_hl(0, 'DiffsAddText', { bg = 0x00FF00 }) vim.api.nvim_set_hl(0, 'DiffsDeleteText', { bg = 0xFF0000 }) @@ -1119,237 +984,43 @@ describe('highlight', function() default_opts({ highlights = { background = true, - intra = { enabled = true, algorithm = 'default', max_lines = 500 }, + intra = { enabled = true, algorithm = 'native', max_lines = 500 }, }, }) ) local extmarks = get_extmarks(bufnr) - local priorities = { clear = {}, line_bg = {}, syntax = {}, char_bg = {} } + local priorities = { normal = {}, line_bg = {}, syntax = {}, char_bg = {} } for _, mark in ipairs(extmarks) do local d = mark[4] - 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' 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) - elseif d.hl_group and d.hl_group:match('^@.*%.lua$') then - table.insert(priorities.syntax, d.priority) - end + if not d then + goto continue end + if d.hl_group == 'Normal' then + table.insert(priorities.normal, d.priority) + elseif d.hl_group == 'DiffsAdd' or d.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) + elseif d.hl_group and d.hl_group:match('^@.*%.lua$') then + table.insert(priorities.syntax, d.priority) + end + ::continue:: end - assert.is_true(#priorities.clear > 0) + assert.is_true(#priorities.normal > 0) assert.is_true(#priorities.line_bg > 0) assert.is_true(#priorities.syntax > 0) assert.is_true(#priorities.char_bg > 0) - local max_clear = math.max(unpack(priorities.clear)) + local max_normal = math.max(unpack(priorities.normal)) local min_line_bg = math.min(unpack(priorities.line_bg)) local min_syntax = math.min(unpack(priorities.syntax)) local min_char_bg = math.min(unpack(priorities.char_bg)) - assert.is_true(max_clear < min_syntax) - assert.is_true(min_syntax < min_line_bg) - assert.is_true(min_line_bg < min_char_bg) - 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 @@', - ' 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_lua = false - local has_vim = false - for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].hl_group then - if mark[4].hl_group:match('^@.*%.lua$') then - has_lua = true - end - if mark[4].hl_group:match('^@.*%.vim$') then - has_vim = true - end - end - end - assert.is_true(has_lua) - assert.is_true(has_vim) - delete_buffer(bufnr) - end) - - it('filters @spell and @nospell captures from 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) - for _, mark in ipairs(extmarks) do - if mark[4] and mark[4].hl_group then - assert.is_falsy(mark[4].hl_group:match('@spell')) - assert.is_falsy(mark[4].hl_group:match('@nospell')) - end - end + assert.is_true(max_normal < min_line_bg) + assert.is_true(min_line_bg < min_syntax) + assert.is_true(min_syntax < min_char_bg) delete_buffer(bufnr) end) end) @@ -1383,7 +1054,6 @@ describe('highlight', function() highlights = { background = false, gutter = false, - context = { enabled = false, lines = 0 }, treesitter = { enabled = true, max_lines = 500 }, vim = { enabled = false, max_lines = 200 }, }, @@ -1540,14 +1210,13 @@ describe('highlight', function() highlights = { background = false, gutter = false, - context = { enabled = false, lines = 0 }, treesitter = { enabled = true, max_lines = 500 }, vim = { enabled = false, max_lines = 200 }, }, } end - it('uses priority 199 for code languages', function() + it('uses priority 200 for code languages', function() local bufnr = create_buffer({ '@@ -1,1 +1,2 @@', ' local x = 1', @@ -1564,16 +1233,16 @@ describe('highlight', function() highlight.highlight_hunk(bufnr, ns, hunk, default_opts()) local extmarks = get_extmarks(bufnr) - local has_priority_199 = false + local has_priority_200 = 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 + if mark[4].priority == 200 then + has_priority_200 = true break end end end - assert.is_true(has_priority_199) + assert.is_true(has_priority_200) delete_buffer(bufnr) end) @@ -1611,7 +1280,7 @@ describe('highlight', function() end assert.is_true(#diff_extmark_priorities > 0) for _, priority in ipairs(diff_extmark_priorities) do - assert.is_true(priority < 199) + assert.is_true(priority < 200) end delete_buffer(bufnr) end) diff --git a/spec/parser_spec.lua b/spec/parser_spec.lua index 11ac3be..89d0ac8 100644 --- a/spec/parser_spec.lua +++ b/spec/parser_spec.lua @@ -421,84 +421,5 @@ describe('parser', function() 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', - '@@ -1,3 +1,4 @@', - ' local M = {}', - '+local new = true', - ' return M', - }) - local hunks = parser.parse_buffer(bufnr) - - assert.are.equal(1, #hunks) - assert.are.equal(1, hunks[1].file_old_start) - assert.are.equal(3, hunks[1].file_old_count) - assert.are.equal(1, hunks[1].file_new_start) - assert.are.equal(4, hunks[1].file_new_count) - 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', - '@@ -1 +1 @@', - ' local M = {}', - }) - local hunks = parser.parse_buffer(bufnr) - - assert.are.equal(1, #hunks) - assert.are.equal(1, hunks[1].file_old_start) - assert.are.equal(1, hunks[1].file_old_count) - assert.are.equal(1, hunks[1].file_new_start) - assert.are.equal(1, hunks[1].file_new_count) - delete_buffer(bufnr) - end) - - it('stores repo_root on hunk when available', function() - local bufnr = create_buffer({ - 'M lua/test.lua', - '@@ -1,3 +1,4 @@', - ' local M = {}', - '+local new = true', - ' return M', - }) - vim.api.nvim_buf_set_var(bufnr, 'diffs_repo_root', '/tmp/test-repo') - local hunks = parser.parse_buffer(bufnr) - - assert.are.equal(1, #hunks) - 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) diff --git a/spec/read_buffer_spec.lua b/spec/read_buffer_spec.lua deleted file mode 100644 index f571d97..0000000 --- a/spec/read_buffer_spec.lua +++ /dev/null @@ -1,400 +0,0 @@ -require('spec.helpers') - -local commands = require('diffs.commands') -local diffs = require('diffs') -local git = require('diffs.git') - -local saved_git = {} -local saved_systemlist -local test_buffers = {} - -local function mock_git(overrides) - overrides = overrides or {} - saved_git.get_file_content = git.get_file_content - saved_git.get_index_content = git.get_index_content - saved_git.get_working_content = git.get_working_content - - git.get_file_content = overrides.get_file_content - or function() - return { 'local M = {}', 'return M' } - end - git.get_index_content = overrides.get_index_content - or function() - return { 'local M = {}', 'return M' } - end - git.get_working_content = overrides.get_working_content - or function() - return { 'local M = {}', 'local x = 1', 'return M' } - end -end - -local function mock_systemlist(fn) - saved_systemlist = vim.fn.systemlist - vim.fn.systemlist = function(cmd) - local result = fn(cmd) - saved_systemlist({ 'true' }) - return result - end -end - -local function restore_mocks() - for k, v in pairs(saved_git) do - git[k] = v - end - saved_git = {} - if saved_systemlist then - vim.fn.systemlist = saved_systemlist - saved_systemlist = nil - end -end - ----@param name string ----@param vars? table ----@return integer -local function create_diffs_buffer(name, vars) - local existing = vim.fn.bufnr(name) - if existing ~= -1 then - vim.api.nvim_buf_delete(existing, { force = true }) - end - local bufnr = vim.api.nvim_create_buf(false, true) - vim.api.nvim_buf_set_name(bufnr, name) - vars = vars or {} - for k, v in pairs(vars) do - vim.api.nvim_buf_set_var(bufnr, k, v) - end - table.insert(test_buffers, bufnr) - return bufnr -end - -local function cleanup_buffers() - for _, bufnr in ipairs(test_buffers) do - if vim.api.nvim_buf_is_valid(bufnr) then - vim.api.nvim_buf_delete(bufnr, { force = true }) - end - end - test_buffers = {} -end - -describe('read_buffer', function() - after_each(function() - restore_mocks() - cleanup_buffers() - end) - - describe('early returns', function() - it('does nothing on non-diffs:// buffer', function() - local bufnr = vim.api.nvim_create_buf(false, true) - table.insert(test_buffers, bufnr) - assert.has_no.errors(function() - commands.read_buffer(bufnr) - end) - assert.are.same({ '' }, vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)) - end) - - it('does nothing on malformed url without colon separator', function() - local bufnr = create_diffs_buffer('diffs://nocolonseparator') - vim.api.nvim_buf_set_var(bufnr, 'diffs_repo_root', '/tmp') - local lines_before = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - assert.has_no.errors(function() - commands.read_buffer(bufnr) - end) - assert.are.same(lines_before, vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)) - end) - - it('does nothing when diffs_repo_root is missing', function() - local bufnr = create_diffs_buffer('diffs://staged:missing_root.lua') - assert.has_no.errors(function() - commands.read_buffer(bufnr) - end) - assert.are.same({ '' }, vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)) - end) - end) - - describe('buffer options', function() - it('sets buftype, bufhidden, swapfile, modifiable, filetype', function() - mock_git() - local bufnr = create_diffs_buffer('diffs://staged:options_test.lua', { - diffs_repo_root = '/tmp', - }) - - commands.read_buffer(bufnr) - - assert.are.equal('nowrite', vim.api.nvim_get_option_value('buftype', { buf = bufnr })) - assert.are.equal('delete', vim.api.nvim_get_option_value('bufhidden', { buf = bufnr })) - assert.is_false(vim.api.nvim_get_option_value('swapfile', { buf = bufnr })) - assert.is_false(vim.api.nvim_get_option_value('modifiable', { buf = bufnr })) - assert.are.equal('diff', vim.api.nvim_get_option_value('filetype', { buf = bufnr })) - end) - end) - - describe('dispatch', function() - it('calls get_file_content + get_index_content for staged label', function() - local called_get_file = false - local called_get_index = false - mock_git({ - get_file_content = function() - called_get_file = true - return { 'old' } - end, - get_index_content = function() - called_get_index = true - return { 'new' } - end, - }) - - local bufnr = create_diffs_buffer('diffs://staged:dispatch_staged.lua', { - diffs_repo_root = '/tmp', - }) - commands.read_buffer(bufnr) - - assert.is_true(called_get_file) - assert.is_true(called_get_index) - end) - - it('calls get_index_content + get_working_content for unstaged label', function() - local called_get_index = false - local called_get_working = false - mock_git({ - get_index_content = function() - called_get_index = true - return { 'index' } - end, - get_working_content = function() - called_get_working = true - return { 'working' } - end, - }) - - local bufnr = create_diffs_buffer('diffs://unstaged:dispatch_unstaged.lua', { - diffs_repo_root = '/tmp', - }) - commands.read_buffer(bufnr) - - assert.is_true(called_get_index) - assert.is_true(called_get_working) - end) - - it('calls only get_working_content for untracked label', function() - local called_get_file = false - local called_get_working = false - mock_git({ - get_file_content = function() - called_get_file = true - return {} - end, - get_working_content = function() - called_get_working = true - return { 'new file' } - end, - }) - - local bufnr = create_diffs_buffer('diffs://untracked:dispatch_untracked.lua', { - diffs_repo_root = '/tmp', - }) - commands.read_buffer(bufnr) - - assert.is_false(called_get_file) - assert.is_true(called_get_working) - end) - - it('calls get_file_content + get_working_content for revision label', function() - local captured_rev - local called_get_working = false - mock_git({ - get_file_content = function(rev) - captured_rev = rev - return { 'old' } - end, - get_working_content = function() - called_get_working = true - return { 'new' } - end, - }) - - local bufnr = create_diffs_buffer('diffs://HEAD~3:dispatch_rev.lua', { - diffs_repo_root = '/tmp', - }) - commands.read_buffer(bufnr) - - assert.are.equal('HEAD~3', captured_rev) - assert.is_true(called_get_working) - end) - - it('falls back from index to HEAD for unstaged when index returns nil', function() - local call_order = {} - mock_git({ - get_index_content = function() - table.insert(call_order, 'index') - return nil - end, - get_file_content = function() - table.insert(call_order, 'head') - return { 'head content' } - end, - get_working_content = function() - return { 'working content' } - end, - }) - - local bufnr = create_diffs_buffer('diffs://unstaged:dispatch_fallback.lua', { - diffs_repo_root = '/tmp', - }) - commands.read_buffer(bufnr) - - assert.are.same({ 'index', 'head' }, call_order) - end) - - it('runs git diff for section diffs with path=all', function() - local captured_cmd - mock_systemlist(function(cmd) - captured_cmd = cmd - return { - 'diff --git a/file.lua b/file.lua', - '--- a/file.lua', - '+++ b/file.lua', - '@@ -1 +1 @@', - '-old', - '+new', - } - end) - - local bufnr = create_diffs_buffer('diffs://unstaged:all', { - diffs_repo_root = '/home/test/repo', - }) - commands.read_buffer(bufnr) - - assert.is_not_nil(captured_cmd) - assert.are.equal('git', captured_cmd[1]) - assert.are.equal('/home/test/repo', captured_cmd[3]) - assert.are.equal('diff', captured_cmd[4]) - - local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - assert.are.equal('diff --git a/file.lua b/file.lua', lines[1]) - end) - - it('passes --cached for staged section diffs', function() - local captured_cmd - mock_systemlist(function(cmd) - captured_cmd = cmd - return { 'diff --git a/f.lua b/f.lua', '@@ -1 +1 @@', '-a', '+b' } - end) - - local bufnr = create_diffs_buffer('diffs://staged:all', { - diffs_repo_root = '/tmp', - }) - commands.read_buffer(bufnr) - - assert.is_truthy(vim.tbl_contains(captured_cmd, '--cached')) - end) - end) - - describe('content', function() - it('generates valid unified diff header with correct paths', function() - mock_git({ - get_file_content = function() - return { 'old' } - end, - get_working_content = function() - return { 'new' } - end, - }) - - local bufnr = create_diffs_buffer('diffs://HEAD:lua/diffs/init.lua', { - diffs_repo_root = '/tmp', - }) - commands.read_buffer(bufnr) - - local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - assert.are.equal('diff --git a/lua/diffs/init.lua b/lua/diffs/init.lua', lines[1]) - assert.are.equal('--- a/lua/diffs/init.lua', lines[2]) - assert.are.equal('+++ b/lua/diffs/init.lua', lines[3]) - end) - - it('uses old_filepath for diff header in renames', function() - mock_git({ - get_file_content = function(_, path) - assert.are.equal('/tmp/old_name.lua', path) - return { 'old content' } - end, - get_index_content = function() - return { 'new content' } - end, - }) - - local bufnr = create_diffs_buffer('diffs://staged:new_name.lua', { - diffs_repo_root = '/tmp', - diffs_old_filepath = 'old_name.lua', - }) - commands.read_buffer(bufnr) - - local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - assert.are.equal('diff --git a/old_name.lua b/new_name.lua', lines[1]) - assert.are.equal('--- a/old_name.lua', lines[2]) - assert.are.equal('+++ b/new_name.lua', lines[3]) - end) - - it('produces empty buffer when old and new are identical', function() - mock_git({ - get_file_content = function() - return { 'identical' } - end, - get_working_content = function() - return { 'identical' } - end, - }) - - local bufnr = create_diffs_buffer('diffs://HEAD:nodiff.lua', { - diffs_repo_root = '/tmp', - }) - commands.read_buffer(bufnr) - - local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - assert.are.same({ '' }, lines) - end) - - it('replaces existing buffer content on reload', function() - mock_git({ - get_file_content = function() - return { 'old' } - end, - get_working_content = function() - return { 'new' } - end, - }) - - local bufnr = create_diffs_buffer('diffs://HEAD:replace_test.lua', { - diffs_repo_root = '/tmp', - }) - vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { 'stale', 'content', 'from', 'before' }) - - commands.read_buffer(bufnr) - - local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - assert.are.equal('diff --git a/replace_test.lua b/replace_test.lua', lines[1]) - for _, line in ipairs(lines) do - assert.is_not_equal('stale', line) - end - end) - end) - - describe('attach integration', function() - it('calls attach on the buffer', function() - mock_git() - - local attach_called_with - local original_attach = diffs.attach - diffs.attach = function(bufnr) - attach_called_with = bufnr - end - - local bufnr = create_diffs_buffer('diffs://staged:attach_test.lua', { - diffs_repo_root = '/tmp', - }) - commands.read_buffer(bufnr) - - assert.are.equal(bufnr, attach_called_with) - - diffs.attach = original_attach - end) - end) -end) diff --git a/spec/ux_spec.lua b/spec/ux_spec.lua deleted file mode 100644 index 9cf0110..0000000 --- a/spec/ux_spec.lua +++ /dev/null @@ -1,135 +0,0 @@ -local commands = require('diffs.commands') -local helpers = require('spec.helpers') - -local counter = 0 - -local function create_diffs_buffer(name) - counter = counter + 1 - local bufnr = vim.api.nvim_create_buf(false, true) - vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { - 'diff --git a/file.lua b/file.lua', - '--- a/file.lua', - '+++ b/file.lua', - '@@ -1,1 +1,2 @@', - ' local x = 1', - '+local y = 2', - }) - vim.api.nvim_set_option_value('buftype', 'nowrite', { buf = bufnr }) - vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = bufnr }) - vim.api.nvim_set_option_value('swapfile', false, { buf = bufnr }) - vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr }) - vim.api.nvim_set_option_value('filetype', 'diff', { buf = bufnr }) - vim.api.nvim_buf_set_name(bufnr, name or ('diffs://unstaged:file_' .. counter .. '.lua')) - return bufnr -end - -describe('ux', function() - describe('diagnostics', function() - it('disables diagnostics on diff buffers', function() - local bufnr = create_diffs_buffer() - commands.setup_diff_buf(bufnr) - - assert.is_false(vim.diagnostic.is_enabled({ bufnr = bufnr })) - vim.api.nvim_buf_delete(bufnr, { force = true }) - end) - - it('does not affect other buffers', function() - local diff_buf = create_diffs_buffer() - local normal_buf = helpers.create_buffer({ 'hello' }) - - commands.setup_diff_buf(diff_buf) - - assert.is_true(vim.diagnostic.is_enabled({ bufnr = normal_buf })) - vim.api.nvim_buf_delete(diff_buf, { force = true }) - helpers.delete_buffer(normal_buf) - end) - end) - - describe('q keymap', function() - it('sets q keymap on diff buffer', function() - local bufnr = create_diffs_buffer() - commands.setup_diff_buf(bufnr) - - local keymaps = vim.api.nvim_buf_get_keymap(bufnr, 'n') - local has_q = false - for _, km in ipairs(keymaps) do - if km.lhs == 'q' then - has_q = true - break - end - end - assert.is_true(has_q) - vim.api.nvim_buf_delete(bufnr, { force = true }) - end) - - it('q closes the window', function() - local bufnr = create_diffs_buffer() - commands.setup_diff_buf(bufnr) - - vim.cmd('split') - local win = vim.api.nvim_get_current_win() - vim.api.nvim_win_set_buf(win, bufnr) - - local win_count_before = #vim.api.nvim_tabpage_list_wins(0) - - vim.api.nvim_buf_call(bufnr, function() - vim.cmd('normal q') - end) - - local win_count_after = #vim.api.nvim_tabpage_list_wins(0) - assert.equals(win_count_before - 1, win_count_after) - end) - end) - - describe('window reuse', function() - it('returns nil when no diffs window exists', function() - local win = commands.find_diffs_window() - assert.is_nil(win) - end) - - it('finds existing diffs:// window', function() - local bufnr = create_diffs_buffer() - vim.cmd('split') - local expected_win = vim.api.nvim_get_current_win() - vim.api.nvim_win_set_buf(expected_win, bufnr) - - local found = commands.find_diffs_window() - assert.equals(expected_win, found) - - vim.api.nvim_win_close(expected_win, true) - end) - - it('ignores non-diffs buffers', function() - local normal_buf = helpers.create_buffer({ 'hello' }) - vim.cmd('split') - local win = vim.api.nvim_get_current_win() - vim.api.nvim_win_set_buf(win, normal_buf) - - local found = commands.find_diffs_window() - assert.is_nil(found) - - vim.api.nvim_win_close(win, true) - helpers.delete_buffer(normal_buf) - end) - - it('returns first diffs window when multiple exist', function() - local buf1 = create_diffs_buffer() - local buf2 = create_diffs_buffer() - - vim.cmd('split') - local win1 = vim.api.nvim_get_current_win() - vim.api.nvim_win_set_buf(win1, buf1) - - vim.cmd('split') - local win2 = vim.api.nvim_get_current_win() - vim.api.nvim_win_set_buf(win2, buf2) - - local found = commands.find_diffs_window() - assert.is_not_nil(found) - assert.is_true(found == win1 or found == win2) - - vim.api.nvim_win_close(win1, true) - vim.api.nvim_win_close(win2, true) - end) - end) -end)