diff --git a/doc/diffs.nvim.txt b/doc/diffs.nvim.txt index 391ad05..d0298b9 100644 --- a/doc/diffs.nvim.txt +++ b/doc/diffs.nvim.txt @@ -226,6 +226,9 @@ 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. @@ -259,6 +262,12 @@ Example configuration: >lua vim.keymap.set('n', 'gD', '(diffs-gvdiff)') < +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* diff --git a/lua/diffs/commands.lua b/lua/diffs/commands.lua index 4fc795b..dc6cfe2 100644 --- a/lua/diffs/commands.lua +++ b/lua/diffs/commands.lua @@ -3,6 +3,27 @@ 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? @@ -96,9 +117,16 @@ function M.gdiff(revision, vertical) vim.api.nvim_buf_set_var(diff_buf, 'diffs_repo_root', repo_root) end - vim.cmd(vertical and 'vsplit' or 'split') - vim.api.nvim_win_set_buf(0, diff_buf) + 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 + M.setup_diff_buf(diff_buf) dbg('opened diff buffer %d for %s against %s', diff_buf, rel_path, revision) vim.schedule(function() @@ -190,8 +218,14 @@ function M.gdiff_file(filepath, opts) vim.api.nvim_buf_set_var(diff_buf, 'diffs_old_filepath', old_rel_path) end - vim.cmd(opts.vertical and 'vsplit' or 'split') - vim.api.nvim_win_set_buf(0, diff_buf) + 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 if opts.hunk_position then local target_line = M.find_hunk_line(diff_lines, opts.hunk_position) @@ -201,6 +235,7 @@ 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() @@ -244,9 +279,16 @@ function M.gdiff_section(repo_root, opts) 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) - vim.cmd(opts.vertical and 'vsplit' or 'split') - vim.api.nvim_win_set_buf(0, diff_buf) + 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 + M.setup_diff_buf(diff_buf) dbg('opened section diff buffer %d (%s)', diff_buf, diff_label) vim.schedule(function() diff --git a/spec/ux_spec.lua b/spec/ux_spec.lua new file mode 100644 index 0000000..9cf0110 --- /dev/null +++ b/spec/ux_spec.lua @@ -0,0 +1,135 @@ +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)