From 8171eadc9007f72e249ca6284e4d914e061a44ff Mon Sep 17 00:00:00 2001 From: bxhart Date: Tue, 14 Apr 2026 00:00:26 +0200 Subject: [PATCH 1/2] Allow keymaps to be a single key (string) or multiple (table) --- lua/codediff/ui/conflict/keymaps.lua | 30 ++++++++++--------- lua/codediff/ui/explorer/keymaps.lua | 21 ++++++------- lua/codediff/ui/history/keymaps.lua | 9 +++--- lua/codediff/ui/keymap_help.lua | 3 +- lua/codediff/ui/lib/tree_utils.lua | 3 +- lua/codediff/ui/lifecycle/accessors.lua | 39 +++++++++++++++++++++---- lua/codediff/ui/view/keymaps.lua | 9 +++--- 7 files changed, 75 insertions(+), 39 deletions(-) diff --git a/lua/codediff/ui/conflict/keymaps.lua b/lua/codediff/ui/conflict/keymaps.lua index 9511179f..c20946c8 100644 --- a/lua/codediff/ui/conflict/keymaps.lua +++ b/lua/codediff/ui/conflict/keymaps.lua @@ -7,6 +7,8 @@ local tracking = require("codediff.ui.conflict.tracking") local actions = require("codediff.ui.conflict.actions") local diffget = require("codediff.ui.conflict.diffget") local navigation = require("codediff.ui.conflict.navigation") +local set_keymap = require("codediff.ui.lifecycle.accessors").set_keymap +local del_keymap = require("codediff.ui.lifecycle.accessors").del_keymap --- Setup conflict keymaps for a session --- @param tabpage number @@ -28,15 +30,15 @@ function M.setup_keymaps(tabpage) if bufnr and vim.api.nvim_buf_is_valid(bufnr) then -- Unbind normal mode do/dp from view keymaps (they don't apply in merge conflict mode) if view_keymaps.diff_get then - pcall(vim.keymap.del, "n", view_keymaps.diff_get, { buffer = bufnr }) + pcall(keymap_del, "n", view_keymaps.diff_get, { buffer = bufnr }) end if view_keymaps.diff_put then - pcall(vim.keymap.del, "n", view_keymaps.diff_put, { buffer = bufnr }) + pcall(keymap_del, "n", view_keymaps.diff_put, { buffer = bufnr }) end -- Accept incoming if keymaps.accept_incoming then - vim.keymap.set( + set_keymap( "n", keymaps.accept_incoming, tracking.make_repeatable(function() @@ -48,7 +50,7 @@ function M.setup_keymaps(tabpage) -- Accept current if keymaps.accept_current then - vim.keymap.set( + set_keymap( "n", keymaps.accept_current, tracking.make_repeatable(function() @@ -60,7 +62,7 @@ function M.setup_keymaps(tabpage) -- Accept both if keymaps.accept_both then - vim.keymap.set( + set_keymap( "n", keymaps.accept_both, tracking.make_repeatable(function() @@ -72,7 +74,7 @@ function M.setup_keymaps(tabpage) -- Discard if keymaps.discard then - vim.keymap.set( + set_keymap( "n", keymaps.discard, tracking.make_repeatable(function() @@ -84,48 +86,48 @@ function M.setup_keymaps(tabpage) -- Accept ALL incoming if keymaps.accept_all_incoming then - vim.keymap.set("n", keymaps.accept_all_incoming, function() + set_keymap("n", keymaps.accept_all_incoming, function() actions.accept_all_incoming(tabpage) end, vim.tbl_extend("force", base_opts, { buffer = bufnr, desc = "Accept ALL incoming changes" })) end -- Accept ALL current if keymaps.accept_all_current then - vim.keymap.set("n", keymaps.accept_all_current, function() + set_keymap("n", keymaps.accept_all_current, function() actions.accept_all_current(tabpage) end, vim.tbl_extend("force", base_opts, { buffer = bufnr, desc = "Accept ALL current changes" })) end -- Accept ALL both if keymaps.accept_all_both then - vim.keymap.set("n", keymaps.accept_all_both, function() + set_keymap("n", keymaps.accept_all_both, function() actions.accept_all_both(tabpage) end, vim.tbl_extend("force", base_opts, { buffer = bufnr, desc = "Accept ALL both changes" })) end -- Discard ALL if keymaps.discard_all then - vim.keymap.set("n", keymaps.discard_all, function() + set_keymap("n", keymaps.discard_all, function() actions.discard_all(tabpage) end, vim.tbl_extend("force", base_opts, { buffer = bufnr, desc = "Discard ALL, reset to base" })) end -- Navigation if keymaps.next_conflict then - vim.keymap.set("n", keymaps.next_conflict, function() + set_keymap("n", keymaps.next_conflict, function() navigation.navigate_next_conflict(tabpage) end, vim.tbl_extend("force", base_opts, { buffer = bufnr, desc = "Next conflict" })) end if keymaps.prev_conflict then - vim.keymap.set("n", keymaps.prev_conflict, function() + set_keymap("n", keymaps.prev_conflict, function() navigation.navigate_prev_conflict(tabpage) end, vim.tbl_extend("force", base_opts, { buffer = bufnr, desc = "Previous conflict" })) end -- Vimdiff-style diffget from incoming (2do) - only on result buffer if keymaps.diffget_incoming and bufnr == session.result_bufnr then - vim.keymap.set( + set_keymap( "n", keymaps.diffget_incoming, tracking.make_repeatable(function() @@ -137,7 +139,7 @@ function M.setup_keymaps(tabpage) -- Vimdiff-style diffget from current (3do) - only on result buffer if keymaps.diffget_current and bufnr == session.result_bufnr then - vim.keymap.set( + set_keymap( "n", keymaps.diffget_current, tracking.make_repeatable(function() diff --git a/lua/codediff/ui/explorer/keymaps.lua b/lua/codediff/ui/explorer/keymaps.lua index 4256c023..4dac0293 100644 --- a/lua/codediff/ui/explorer/keymaps.lua +++ b/lua/codediff/ui/explorer/keymaps.lua @@ -3,6 +3,7 @@ local config = require("codediff.config") local actions_module = require("codediff.ui.explorer.actions") local refresh_module = require("codediff.ui.explorer.refresh") local tree_utils = require("codediff.ui.lib.tree_utils") +local set_keymap = require("codediff.ui.lifecycle.accessors").set_keymap local M = {} @@ -18,7 +19,7 @@ function M.setup(explorer) -- Toggle expand/collapse or select file if explorer_keymaps.select then - vim.keymap.set("n", explorer_keymaps.select, function() + set_keymap("n", explorer_keymaps.select, function() local node = tree:get_node() if not node then return @@ -52,7 +53,7 @@ function M.setup(explorer) end -- Double click also works for files - vim.keymap.set("n", "<2-LeftMouse>", function() + set_keymap("n", "<2-LeftMouse>", function() local node = tree:get_node() if not node or not node.data or node.data.type == "group" or node.data.type == "directory" then return @@ -63,7 +64,7 @@ function M.setup(explorer) -- Hover to show full path (K key, like LSP hover) local hover_win = nil if explorer_keymaps.hover then - vim.keymap.set("n", explorer_keymaps.hover, function() + set_keymap("n", explorer_keymaps.hover, function() -- Close existing hover window if hover_win and vim.api.nvim_win_is_valid(hover_win) then vim.api.nvim_win_close(hover_win, true) @@ -125,49 +126,49 @@ function M.setup(explorer) -- Refresh explorer (R key) if explorer_keymaps.refresh then - vim.keymap.set("n", explorer_keymaps.refresh, function() + set_keymap("n", explorer_keymaps.refresh, function() refresh_module.refresh(explorer) end, vim.tbl_extend("force", map_options, { buffer = split.bufnr, desc = "Refresh explorer" })) end -- Toggle view mode (i key) - switch between 'list' and 'tree' if explorer_keymaps.toggle_view_mode then - vim.keymap.set("n", explorer_keymaps.toggle_view_mode, function() + set_keymap("n", explorer_keymaps.toggle_view_mode, function() actions_module.toggle_view_mode(explorer) end, vim.tbl_extend("force", map_options, { buffer = split.bufnr, desc = "Toggle list/tree view" })) end -- Stage all files (S key) if explorer_keymaps.stage_all then - vim.keymap.set("n", explorer_keymaps.stage_all, function() + set_keymap("n", explorer_keymaps.stage_all, function() actions_module.stage_all(explorer) end, vim.tbl_extend("force", map_options, { buffer = split.bufnr, desc = "Stage all files" })) end -- Unstage all files (U key) if explorer_keymaps.unstage_all then - vim.keymap.set("n", explorer_keymaps.unstage_all, function() + set_keymap("n", explorer_keymaps.unstage_all, function() actions_module.unstage_all(explorer) end, vim.tbl_extend("force", map_options, { buffer = split.bufnr, desc = "Unstage all files" })) end -- Restore/discard changes (X key) if explorer_keymaps.restore then - vim.keymap.set("n", explorer_keymaps.restore, function() + set_keymap("n", explorer_keymaps.restore, function() actions_module.restore_entry(explorer, tree) end, vim.tbl_extend("force", map_options, { buffer = split.bufnr, desc = "Restore/discard changes" })) end -- Toggle Changes (unstaged) group visibility if explorer_keymaps.toggle_changes then - vim.keymap.set("n", explorer_keymaps.toggle_changes, function() + set_keymap("n", explorer_keymaps.toggle_changes, function() actions_module.toggle_group(explorer, "unstaged") end, vim.tbl_extend("force", map_options, { buffer = split.bufnr, desc = "Toggle Changes visibility" })) end -- Toggle Staged Changes group visibility if explorer_keymaps.toggle_staged then - vim.keymap.set("n", explorer_keymaps.toggle_staged, function() + set_keymap("n", explorer_keymaps.toggle_staged, function() actions_module.toggle_group(explorer, "staged") end, vim.tbl_extend("force", map_options, { buffer = split.bufnr, desc = "Toggle Staged Changes visibility" })) end diff --git a/lua/codediff/ui/history/keymaps.lua b/lua/codediff/ui/history/keymaps.lua index 5622d646..f0ee2d00 100644 --- a/lua/codediff/ui/history/keymaps.lua +++ b/lua/codediff/ui/history/keymaps.lua @@ -1,6 +1,7 @@ -- Keymaps for history panel local config = require("codediff.config") local tree_utils = require("codediff.ui.lib.tree_utils") +local set_keymap = require("codediff.ui.lifecycle.accessors").set_keymap local M = {} @@ -18,7 +19,7 @@ function M.setup(history, opts) -- Toggle expand/collapse or select file if history_keymaps.select then - vim.keymap.set("n", history_keymaps.select, function() + set_keymap("n", history_keymaps.select, function() local node = tree:get_node() if not node then return @@ -54,7 +55,7 @@ function M.setup(history, opts) end -- Double-click support - vim.keymap.set("n", "<2-LeftMouse>", function() + set_keymap("n", "<2-LeftMouse>", function() local node = tree:get_node() if not node then return @@ -91,7 +92,7 @@ function M.setup(history, opts) -- Toggle view mode between list and tree if history_keymaps.toggle_view_mode then - vim.keymap.set("n", history_keymaps.toggle_view_mode, function() + set_keymap("n", history_keymaps.toggle_view_mode, function() local history_config = config.options.history or {} local current_mode = history_config.view_mode or "list" local new_mode = (current_mode == "list") and "tree" or "list" @@ -116,7 +117,7 @@ function M.setup(history, opts) -- Refresh (R key) - re-fetch commits if history_keymaps.refresh then - vim.keymap.set("n", history_keymaps.refresh, function() + set_keymap("n", history_keymaps.refresh, function() local refresh_module = require("codediff.ui.history.refresh") refresh_module.refresh(history) end, vim.tbl_extend("force", map_options, { buffer = split.bufnr, desc = "Refresh history" })) diff --git a/lua/codediff/ui/keymap_help.lua b/lua/codediff/ui/keymap_help.lua index 15ffe82c..f975f0cb 100644 --- a/lua/codediff/ui/keymap_help.lua +++ b/lua/codediff/ui/keymap_help.lua @@ -250,8 +250,9 @@ function M.toggle(tabpage) -- Close keymaps local show_help_key = keymaps.view.show_help or "g?" + local set_keymap = require("codediff.ui.lifecycle.accessors").set_keymap for _, key in ipairs({ "q", "", show_help_key }) do - vim.keymap.set("n", key, function() + set_keymap("n", key, function() if vim.api.nvim_win_is_valid(win) then vim.api.nvim_win_close(win, true) end diff --git a/lua/codediff/ui/lib/tree_utils.lua b/lua/codediff/ui/lib/tree_utils.lua index 236d4dc2..62d51dbc 100644 --- a/lua/codediff/ui/lib/tree_utils.lua +++ b/lua/codediff/ui/lib/tree_utils.lua @@ -151,7 +151,8 @@ function M.setup_fold_keymaps(opts) for _, binding in ipairs(fold_bindings) do local key = keymaps[binding.key] if key then - vim.keymap.set("n", key, binding.fn, vim.tbl_extend("force", map_options, { buffer = bufnr, desc = binding.desc })) + local set_keymap = require("codediff.ui.lifecycle.accessors").set_keymap + set_keymap("n", key, binding.fn, vim.tbl_extend("force", map_options, { buffer = bufnr, desc = binding.desc })) end end end diff --git a/lua/codediff/ui/lifecycle/accessors.lua b/lua/codediff/ui/lifecycle/accessors.lua index fe52d68c..511e38fa 100644 --- a/lua/codediff/ui/lifecycle/accessors.lua +++ b/lua/codediff/ui/lifecycle/accessors.lua @@ -439,29 +439,58 @@ function M.set_tab_keymap(tabpage, mode, lhs, rhs, opts) local base_opts = { noremap = true, silent = true, nowait = true } if vim.api.nvim_buf_is_valid(sess.original_bufnr) then - vim.keymap.set(mode, lhs, rhs, vim.tbl_extend("force", base_opts, opts, { buffer = sess.original_bufnr })) + M.set_keymap(mode, lhs, rhs, vim.tbl_extend("force", base_opts, opts, { buffer = sess.original_bufnr })) sess.keymap_buffers[sess.original_bufnr] = true end if vim.api.nvim_buf_is_valid(sess.modified_bufnr) then - vim.keymap.set(mode, lhs, rhs, vim.tbl_extend("force", base_opts, opts, { buffer = sess.modified_bufnr })) + M.set_keymap(mode, lhs, rhs, vim.tbl_extend("force", base_opts, opts, { buffer = sess.modified_bufnr })) sess.keymap_buffers[sess.modified_bufnr] = true end local explorer = sess.explorer if explorer and explorer.bufnr and vim.api.nvim_buf_is_valid(explorer.bufnr) then - vim.keymap.set(mode, lhs, rhs, vim.tbl_extend("force", base_opts, opts, { buffer = explorer.bufnr })) + M.set_keymap(mode, lhs, rhs, vim.tbl_extend("force", base_opts, opts, { buffer = explorer.bufnr })) sess.keymap_buffers[explorer.bufnr] = true end if sess.result_bufnr and vim.api.nvim_buf_is_valid(sess.result_bufnr) then - vim.keymap.set(mode, lhs, rhs, vim.tbl_extend("force", base_opts, opts, { buffer = sess.result_bufnr })) + M.set_keymap(mode, lhs, rhs, vim.tbl_extend("force", base_opts, opts, { buffer = sess.result_bufnr })) sess.keymap_buffers[sess.result_bufnr] = true end return true end +--- Keymap set wrapper to allow lists of keys +---@param modes string|string[] Mode "short-name" (see |nvim_set_keymap()|), or a list thereof. +---@param lhs string|string[] Left-hand side |{lhs}| of the mapping, can also be a list. +---@param rhs string|function Right-hand side |{rhs}| of the mapping, can be a Lua function. +---@param opts? vim.keymap.set.Opts +function M.set_keymap(modes, lhs, rhs, opts) + if type(lhs) == "string" then + vim.keymap.set(modes, lhs, rhs, opts) + else + for _, key in ipairs(lhs) do + vim.keymap.set(modes, key, rhs, opts) + end + end +end + +--- Keymap del wrapper to delete lists of keys +---@param modes string|string[] +---@param lhs string|string[] +---@param opts? vim.keymap.del.Opts +function M.del_keymap(mode, lhs, opts) + if type(lhs) == "string" then + vim.keymap.del(mode, lhs, opts) + else + for _, key in ipairs(lhs) do + vim.keymap.del(mode, key, opts) + end + end +end + --- Remove codediff keymaps from a session's buffers function M.clear_tab_keymaps(tabpage) local active_diffs = get_active_diffs() @@ -476,7 +505,7 @@ function M.clear_tab_keymaps(tabpage) end for _, key in pairs(keys) do if key then - pcall(vim.keymap.del, "n", key, { buffer = bufnr }) + pcall(M.del_keymap, "n", key, { buffer = bufnr }) end end end diff --git a/lua/codediff/ui/view/keymaps.lua b/lua/codediff/ui/view/keymaps.lua index 546bef0c..ca765abe 100644 --- a/lua/codediff/ui/view/keymaps.lua +++ b/lua/codediff/ui/view/keymaps.lua @@ -6,6 +6,7 @@ local auto_refresh = require("codediff.ui.auto_refresh") local config = require("codediff.config") local navigation = require("codediff.ui.view.navigation") local render = require("codediff.ui.view.render") +local set_keymap = require("codediff.ui.lifecycle.accessors").set_keymap -- Centralized keymap setup for all diff view keymaps -- This function sets up ALL keymaps in one place for better maintainability @@ -644,13 +645,13 @@ function M.setup_all_keymaps(tabpage, original_bufnr, modified_bufnr, is_explore end for _, bufnr in ipairs(diff_bufs) do if keymaps.stage_hunk then - vim.keymap.set("n", keymaps.stage_hunk, stage_hunk, vim.tbl_extend("force", hunk_opts, { buffer = bufnr, desc = "Stage hunk under cursor" })) + set_keymap("n", keymaps.stage_hunk, stage_hunk, vim.tbl_extend("force", hunk_opts, { buffer = bufnr, desc = "Stage hunk under cursor" })) end if keymaps.unstage_hunk then - vim.keymap.set("n", keymaps.unstage_hunk, unstage_hunk, vim.tbl_extend("force", hunk_opts, { buffer = bufnr, desc = "Unstage hunk under cursor" })) + set_keymap("n", keymaps.unstage_hunk, unstage_hunk, vim.tbl_extend("force", hunk_opts, { buffer = bufnr, desc = "Unstage hunk under cursor" })) end if keymaps.discard_hunk then - vim.keymap.set("n", keymaps.discard_hunk, discard_hunk, vim.tbl_extend("force", hunk_opts, { buffer = bufnr, desc = "Discard hunk under cursor" })) + set_keymap("n", keymaps.discard_hunk, discard_hunk, vim.tbl_extend("force", hunk_opts, { buffer = bufnr, desc = "Discard hunk under cursor" })) end -- Hunk textobject (ih) - select hunk lines in visual/operator-pending mode @@ -674,7 +675,7 @@ function M.setup_all_keymaps(tabpage, original_bufnr, modified_bufnr, is_explore vim.cmd("normal! " .. start_line .. "GV" .. (end_line - 1) .. "G") end - vim.keymap.set({ "o", "x" }, keymaps.hunk_textobject, select_hunk, vim.tbl_extend("force", hunk_opts, { buffer = bufnr, desc = "Hunk textobject" })) + set_keymap({ "o", "x" }, keymaps.hunk_textobject, select_hunk, vim.tbl_extend("force", hunk_opts, { buffer = bufnr, desc = "Hunk textobject" })) end end From 2257b6c62288ad562a2c2006138faada0e448ddc Mon Sep 17 00:00:00 2001 From: bxhart Date: Tue, 14 Apr 2026 11:41:10 +0200 Subject: [PATCH 2/2] Update README --- README.md | 4 ++-- lua/codediff/config.lua | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 61a6cd68..6627d078 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ https://github.com/user-attachments/assets/64c41f01-dffe-4318-bce4-16eec8de356e view_mode = "list", -- "list" or "tree" for files under commits }, - -- Keymaps in diff view + -- Keymaps in diff view (can also be a table of keys) keymaps = { view = { quit = "q", -- Close diff tab @@ -159,7 +159,7 @@ https://github.com/user-attachments/assets/64c41f01-dffe-4318-bce4-16eec8de356e toggle_layout = "t", -- Toggle between side-by-side and inline layout }, explorer = { - select = "", -- Open diff for selected file + select = { "", "o" }, -- Open diff for selected file hover = "K", -- Show file diff preview refresh = "R", -- Refresh git status toggle_view_mode = "i", -- Toggle between 'list' and 'tree' views diff --git a/lua/codediff/config.lua b/lua/codediff/config.lua index da6fb325..42db7778 100644 --- a/lua/codediff/config.lua +++ b/lua/codediff/config.lua @@ -103,7 +103,7 @@ M.defaults = { show_help = "g?", -- Show floating window with available keymaps }, explorer = { - select = "", + select = { "", "o" }, hover = "K", refresh = "R", toggle_view_mode = "i", -- Toggle between 'list' and 'tree' views