From 9edccb192cec3410b76b349641c6d99eee414d74 Mon Sep 17 00:00:00 2001 From: ruicsh Date: Wed, 18 Mar 2026 19:27:29 +0000 Subject: [PATCH] feat(explorer) scroll diff buffers --- README.md | 3 + doc/codediff.txt | 2 + lua/codediff/config.lua | 3 + lua/codediff/ui/explorer/keymaps.lua | 15 ++ lua/codediff/ui/keymap_help.lua | 2 + lua/codediff/ui/view/navigation.lua | 37 +++ tests/ui/explorer/scroll_keymaps_spec.lua | 293 ++++++++++++++++++++++ 7 files changed, 355 insertions(+) create mode 100644 tests/ui/explorer/scroll_keymaps_spec.lua diff --git a/README.md b/README.md index 61a6cd68..edcb0c28 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,9 @@ https://github.com/user-attachments/assets/64c41f01-dffe-4318-bce4-16eec8de356e fold_toggle_recursive = "zA", -- Toggle fold recursively fold_open_all = "zR", -- Open all folds in tree fold_close_all = "zM", -- Close all folds in tree + -- Scroll diff buffers from explorer + scroll_up_half_page = "", -- Scroll diff buffers up half page + scroll_down_half_page = "", -- Scroll diff buffers down half page }, history = { select = "", -- Select commit/file or toggle expand diff --git a/doc/codediff.txt b/doc/codediff.txt index a01055ea..69044572 100644 --- a/doc/codediff.txt +++ b/doc/codediff.txt @@ -253,6 +253,8 @@ Setup entry point: fold_toggle_recursive = "zA", fold_open_all = "zR", fold_close_all = "zM", + scroll_up_half_page = "", + scroll_down_half_page = "", }, history = { select = "", diff --git a/lua/codediff/config.lua b/lua/codediff/config.lua index da6fb325..9c48cc16 100644 --- a/lua/codediff/config.lua +++ b/lua/codediff/config.lua @@ -121,6 +121,9 @@ M.defaults = { fold_toggle_recursive = "zA", -- Toggle fold recursively fold_open_all = "zR", -- Open all folds in tree fold_close_all = "zM", -- Close all folds in tree + -- Scroll diff buffers from explorer + scroll_up_half_page = "", -- Scroll diff buffers up half page + scroll_down_half_page = "", -- Scroll diff buffers down half page }, history = { select = "", -- Select commit/file or toggle expand diff --git a/lua/codediff/ui/explorer/keymaps.lua b/lua/codediff/ui/explorer/keymaps.lua index 4256c023..b3b0cdfd 100644 --- a/lua/codediff/ui/explorer/keymaps.lua +++ b/lua/codediff/ui/explorer/keymaps.lua @@ -179,6 +179,21 @@ function M.setup(explorer) bufnr = split.bufnr, }) + -- Scroll diff buffers from explorer (/) + local navigation = require("codediff.ui.view.navigation") + + if explorer_keymaps.scroll_up_half_page then + vim.keymap.set("n", explorer_keymaps.scroll_up_half_page, function() + navigation.scroll_diff_windows("up") + end, vim.tbl_extend("force", map_options, { buffer = split.bufnr, desc = "Scroll diff buffers up half page" })) + end + + if explorer_keymaps.scroll_down_half_page then + vim.keymap.set("n", explorer_keymaps.scroll_down_half_page, function() + navigation.scroll_diff_windows("down") + end, vim.tbl_extend("force", map_options, { buffer = split.bufnr, desc = "Scroll diff buffers down half page" })) + end + -- Note: next_file/prev_file keymaps are set via view/keymaps.lua:setup_all_keymaps() -- which uses set_tab_keymap to set them on all buffers including explorer end diff --git a/lua/codediff/ui/keymap_help.lua b/lua/codediff/ui/keymap_help.lua index c065b217..41c8e0af 100644 --- a/lua/codediff/ui/keymap_help.lua +++ b/lua/codediff/ui/keymap_help.lua @@ -88,6 +88,8 @@ local function build_sections(keymaps, is_explorer, is_history, is_conflict) { ekm.fold_toggle_recursive, "Toggle fold recursively" }, { ekm.fold_open_all, "Open all folds" }, { ekm.fold_close_all, "Close all folds" }, + { ekm.scroll_up_half_page, "Scroll diff buffers up half page" }, + { ekm.scroll_down_half_page, "Scroll diff buffers down half page" }, }) ) end diff --git a/lua/codediff/ui/view/navigation.lua b/lua/codediff/ui/view/navigation.lua index 745b20c3..a426499e 100644 --- a/lua/codediff/ui/view/navigation.lua +++ b/lua/codediff/ui/view/navigation.lua @@ -199,4 +199,41 @@ function M.prev_file() return true end +-- Scroll diff windows by half page +-- @param direction "up" or "down" +function M.scroll_diff_windows(direction) + local tabpage = vim.api.nvim_get_current_tabpage() + local session = lifecycle.get_session(tabpage) + if not session then + return false + end + + local mod_win = session.modified_win + + if not mod_win or not vim.api.nvim_win_is_valid(mod_win) then + return false + end + + -- Map direction to Vim half-page scroll commands + local scroll_keys = { + up = "", + down = "", + } + + local scroll_key = scroll_keys[direction] + if not scroll_key then + return false + end + + -- Execute scroll command by temporarily switching to modified window + -- Scrollbind will automatically sync the original window + local current_win = vim.api.nvim_get_current_win() + vim.api.nvim_set_current_win(mod_win) + local keys = vim.api.nvim_replace_termcodes(scroll_key, true, false, true) + vim.api.nvim_feedkeys(keys, "x", false) + vim.api.nvim_set_current_win(current_win) + + return true +end + return M diff --git a/tests/ui/explorer/scroll_keymaps_spec.lua b/tests/ui/explorer/scroll_keymaps_spec.lua new file mode 100644 index 00000000..704a471a --- /dev/null +++ b/tests/ui/explorer/scroll_keymaps_spec.lua @@ -0,0 +1,293 @@ +-- Tests for explorer scroll keymaps +-- Validates that and scroll the diff buffers from explorer + +local h = dofile('tests/helpers.lua') +local navigation = require('codediff.ui.view.navigation') +local lifecycle = require('codediff.ui.lifecycle') + +-- Setup CodeDiff command for tests +local function setup_command() + local commands = require("codediff.commands") + vim.api.nvim_create_user_command("CodeDiff", function(opts) + commands.vscode_diff(opts) + end, { + nargs = "*", + bang = true, + complete = function() + return { "file", "install" } + end, + }) +end + +describe("Explorer scroll keymaps", function() + local temp_dir + local original_cwd + + before_each(function() + require("codediff").setup({ + diff = { layout = "side-by-side" }, + }) + setup_command() + original_cwd = vim.fn.getcwd() + + -- Create temporary git repository + temp_dir = vim.fn.tempname() + vim.fn.mkdir(temp_dir, "p") + vim.fn.chdir(temp_dir) + + h.git_cmd(temp_dir, "init") + h.git_cmd(temp_dir, "branch -m main") + h.git_cmd(temp_dir, 'config user.email "test@example.com"') + h.git_cmd(temp_dir, 'config user.name "Test User"') + + -- Create a file with enough lines for scrolling + local lines = {} + for i = 1, 100 do + table.insert(lines, "line " .. i) + end + vim.fn.writefile(lines, temp_dir .. "/scroll_test.txt") + h.git_cmd(temp_dir, "add scroll_test.txt") + h.git_cmd(temp_dir, 'commit -m "Initial commit"') + + -- Modify the file + local modified_lines = {} + for i = 1, 100 do + if i >= 50 and i <= 60 then + table.insert(modified_lines, "modified line " .. i) + else + table.insert(modified_lines, "line " .. i) + end + end + vim.fn.writefile(modified_lines, temp_dir .. "/scroll_test.txt") + end) + + after_each(function() + vim.cmd("tabnew") + vim.cmd("tabonly") + vim.fn.chdir(original_cwd) + vim.wait(200) + if temp_dir and vim.fn.isdirectory(temp_dir) == 1 then + vim.fn.delete(temp_dir, "rf") + end + end) + + -- Helper: Find explorer window + local function find_explorer_window() + for i = 1, vim.fn.winnr('$') do + local winid = vim.fn.win_getid(i) + local bufnr = vim.api.nvim_win_get_buf(winid) + if vim.bo[bufnr].filetype == "codediff-explorer" then + return winid, bufnr + end + end + return nil, nil + end + + -- Helper: Find modified window + local function find_modified_window() + local tabpage = vim.api.nvim_get_current_tabpage() + local session = lifecycle.get_session(tabpage) + if session and session.modified_win and vim.api.nvim_win_is_valid(session.modified_win) then + return session.modified_win + end + return nil + end + + -- Test 1: Scroll keymaps are registered on explorer buffer + it("Registers scroll keymaps on explorer buffer", function() + vim.cmd("edit " .. temp_dir .. "/scroll_test.txt") + vim.cmd("CodeDiff") + + vim.wait(3000, function() + local _, buf = find_explorer_window() + return buf ~= nil + end) + + local _, explorer_buf = find_explorer_window() + if not explorer_buf then + pending("Explorer not created in time") + return + end + + -- Check keymaps exist + local maps = vim.api.nvim_buf_get_keymap(explorer_buf, "n") + local keymap_found = {} + for _, m in ipairs(maps) do + keymap_found[m.lhs] = true + end + + -- Verify scroll keymaps are registered ( and ) + -- Note: Vim stores these as and (uppercase) + local scroll_keymaps = { "", "" } + for _, key in ipairs(scroll_keymaps) do + assert.is_true(keymap_found[key], + "Keymap " .. key .. " should be registered on explorer") + end + end) + + -- Test 2: scrolls diff buffers down from explorer + it(" scrolls diff buffers down half page from explorer", function() + vim.cmd("edit " .. temp_dir .. "/scroll_test.txt") + vim.cmd("CodeDiff") + + vim.wait(3000, function() + local winid, _ = find_explorer_window() + local mod_win = find_modified_window() + return winid ~= nil and mod_win ~= nil + end) + + local explorer_win, explorer_buf = find_explorer_window() + local modified_win = find_modified_window() + + if not explorer_win or not modified_win then + pending("Windows not ready in time") + return + end + + -- Select the file in explorer to load it into diff buffers + vim.api.nvim_set_current_win(explorer_win) + -- Find the file entry in explorer (should be under "Changes" group) + local lines = vim.api.nvim_buf_get_lines(explorer_buf, 0, -1, false) + local file_line = nil + for i, line in ipairs(lines) do + if line:match("scroll_test") then + file_line = i + break + end + end + + if file_line then + vim.api.nvim_win_set_cursor(explorer_win, { file_line, 0 }) + vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", true, false, true), "x", false) + -- Wait for file to load + vim.wait(1000) + else + pending("File not found in explorer") + return + end + + -- Verify the file loaded by checking buffer line count + local mod_buf = vim.api.nvim_win_get_buf(modified_win) + local line_count = vim.api.nvim_buf_line_count(mod_buf) + if line_count < 10 then + pending("File content not loaded properly (only " .. line_count .. " lines)") + return + end + + -- Get initial scroll position (top line visible) + vim.api.nvim_set_current_win(modified_win) + vim.api.nvim_win_set_cursor(modified_win, { 1, 0 }) + local initial_top = vim.fn.line("w0", modified_win) + + -- Switch back to explorer + vim.api.nvim_set_current_win(explorer_win) + + -- Execute scroll down + navigation.scroll_diff_windows("down") + vim.wait(100) -- Give it a moment to process + + -- Check that modified window scrolled (check top visible line) + local new_top = vim.fn.line("w0", modified_win) + assert.is_true(new_top > initial_top, + "Modified window should have scrolled down (top line from " .. initial_top .. " to " .. new_top .. ")") + end) + + -- Test 3: scrolls diff buffers up from explorer + it(" scrolls diff buffers up half page from explorer", function() + vim.cmd("edit " .. temp_dir .. "/scroll_test.txt") + vim.cmd("CodeDiff") + + vim.wait(3000, function() + local winid, _ = find_explorer_window() + local mod_win = find_modified_window() + return winid ~= nil and mod_win ~= nil + end) + + local explorer_win, explorer_buf = find_explorer_window() + local modified_win = find_modified_window() + + if not explorer_win or not modified_win then + pending("Windows not ready in time") + return + end + + -- Select the file in explorer to load it into diff buffers + vim.api.nvim_set_current_win(explorer_win) + -- Find the file entry in explorer (should be under "Changes" group) + local lines = vim.api.nvim_buf_get_lines(explorer_buf, 0, -1, false) + local file_line = nil + for i, line in ipairs(lines) do + if line:match("scroll_test") then + file_line = i + break + end + end + + if file_line then + vim.api.nvim_win_set_cursor(explorer_win, { file_line, 0 }) + vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", true, false, true), "x", false) + -- Wait for file to load + vim.wait(1000) + end + + -- First scroll down to have room to scroll up + vim.api.nvim_set_current_win(modified_win) + vim.api.nvim_win_set_cursor(modified_win, { 50, 0 }) + vim.cmd("normal! zt") -- Move current line to top + local initial_top = vim.fn.line("w0", modified_win) + + -- Switch back to explorer + vim.api.nvim_set_current_win(explorer_win) + + -- Execute scroll up + navigation.scroll_diff_windows("up") + vim.wait(100) -- Give it a moment to process + + -- Check that modified window scrolled up (check top visible line) + local new_top = vim.fn.line("w0", modified_win) + assert.is_true(new_top < initial_top, + "Modified window should have scrolled up (top line from " .. initial_top .. " to " .. new_top .. ")") + end) + + -- Test 4: Returns false when no session + it("Returns false when no active session", function() + -- Ensure we're not in a diff session + while vim.fn.tabpagenr('$') > 1 do + vim.cmd('tabclose!') + end + vim.cmd('enew') + + local result = navigation.scroll_diff_windows("down") + assert.is_false(result, "Should return false when no session") + end) + + -- Test 5: Scroll commands preserve explorer focus + it("Preserves explorer focus after scrolling", function() + vim.cmd("edit " .. temp_dir .. "/scroll_test.txt") + vim.cmd("CodeDiff") + + vim.wait(3000, function() + local winid, _ = find_explorer_window() + return winid ~= nil + end) + + local explorer_win, _ = find_explorer_window() + local modified_win = find_modified_window() + + if not explorer_win or not modified_win then + pending("Windows not ready in time") + return + end + + -- Ensure we're in explorer + vim.api.nvim_set_current_win(explorer_win) + + -- Execute scroll + navigation.scroll_diff_windows("down") + + -- Verify we're still in explorer window + local current_win = vim.api.nvim_get_current_win() + assert.equal(explorer_win, current_win, + "Should remain in explorer window after scroll") + end) +end) \ No newline at end of file