Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<C-b>", -- Scroll diff buffers up half page
scroll_down_half_page = "<C-f>", -- Scroll diff buffers down half page
},
history = {
select = "<CR>", -- Select commit/file or toggle expand
Expand Down
2 changes: 2 additions & 0 deletions doc/codediff.txt
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,8 @@ Setup entry point:
fold_toggle_recursive = "zA",
fold_open_all = "zR",
fold_close_all = "zM",
scroll_up_half_page = "<C-b>",
scroll_down_half_page = "<C-f>",
},
history = {
select = "<CR>",
Expand Down
3 changes: 3 additions & 0 deletions lua/codediff/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<C-b>", -- Scroll diff buffers up half page
scroll_down_half_page = "<C-f>", -- Scroll diff buffers down half page
},
history = {
select = "<CR>", -- Select commit/file or toggle expand
Expand Down
15 changes: 15 additions & 0 deletions lua/codediff/ui/explorer/keymaps.lua
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,21 @@ function M.setup(explorer)
bufnr = split.bufnr,
})

-- Scroll diff buffers from explorer (<C-b>/<C-f>)
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
Expand Down
2 changes: 2 additions & 0 deletions lua/codediff/ui/keymap_help.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 37 additions & 0 deletions lua/codediff/ui/view/navigation.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<C-u>",
down = "<C-d>",
}

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
293 changes: 293 additions & 0 deletions tests/ui/explorer/scroll_keymaps_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
-- Tests for explorer scroll keymaps
-- Validates that <C-b> and <C-f> 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 (<C-b> and <C-f>)
-- Note: Vim stores these as <C-B> and <C-F> (uppercase)
local scroll_keymaps = { "<C-B>", "<C-F>" }
for _, key in ipairs(scroll_keymaps) do
assert.is_true(keymap_found[key],
"Keymap " .. key .. " should be registered on explorer")
end
end)

-- Test 2: <C-f> scrolls diff buffers down from explorer
it("<C-f> 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("<CR>", 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: <C-b> scrolls diff buffers up from explorer
it("<C-b> 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("<CR>", 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)