Skip to content
Merged
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,16 +166,40 @@ https://github.com/user-attachments/assets/64c41f01-dffe-4318-bce4-16eec8de356e
restore = "X", -- Discard changes (restore file)
toggle_changes = "gu", -- Toggle Changes (unstaged) group visibility
toggle_staged = "gs", -- Toggle Staged Changes group visibility
-- Fold keymaps (Vim-style)
fold_open = "zo", -- Open fold (expand current node)
fold_open_recursive = "zO", -- Open fold recursively (expand all descendants)
fold_close = "zc", -- Close fold (collapse current node)
fold_close_recursive = "zC", -- Close fold recursively (collapse all descendants)
fold_toggle = "za", -- Toggle fold (expand/collapse current node)
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
},
history = {
select = "<CR>", -- Select commit/file or toggle expand
toggle_view_mode = "i", -- Toggle between 'list' and 'tree' views
refresh = "R", -- Refresh history (re-fetch commits)
-- Fold keymaps (Vim-style, apply to directory nodes only)
fold_open = "zo", -- Open fold (expand current node)
fold_open_recursive = "zO", -- Open fold recursively (expand all descendants)
fold_close = "zc", -- Close fold (collapse current node)
fold_close_recursive = "zC", -- Close fold recursively (collapse all descendants)
fold_toggle = "za", -- Toggle fold (expand/collapse current node)
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
},
conflict = {
accept_incoming = "<leader>ct", -- Accept incoming (theirs/left) change
accept_current = "<leader>co", -- Accept current (ours/right) change
accept_both = "<leader>cb", -- Accept both changes (incoming first)
discard = "<leader>cx", -- Discard both, keep base
-- Accept all (whole file) - uppercase versions
accept_all_incoming = "<leader>cT", -- Accept ALL incoming changes
accept_all_current = "<leader>cO", -- Accept ALL current changes
accept_all_both = "<leader>cB", -- Accept ALL both changes
discard_all = "<leader>cX", -- Discard ALL, reset to base
next_conflict = "]x", -- Jump to next conflict
prev_conflict = "[x", -- Jump to previous conflict
diffget_incoming = "2do", -- Get hunk from incoming (left/theirs) buffer
Expand Down
16 changes: 16 additions & 0 deletions doc/codediff.txt
Original file line number Diff line number Diff line change
Expand Up @@ -227,10 +227,26 @@ Setup entry point:
restore = "X",
toggle_changes = "gu",
toggle_staged = "gs",
fold_open = "zo",
fold_open_recursive = "zO",
fold_close = "zc",
fold_close_recursive = "zC",
fold_toggle = "za",
fold_toggle_recursive = "zA",
fold_open_all = "zR",
fold_close_all = "zM",
},
history = {
select = "<CR>",
toggle_view_mode = "i",
fold_open = "zo",
fold_open_recursive = "zO",
fold_close = "zc",
fold_close_recursive = "zC",
fold_toggle = "za",
fold_toggle_recursive = "zA",
fold_open_all = "zR",
fold_close_all = "zM",
},
conflict = {
accept_incoming = "<leader>ct",
Expand Down
18 changes: 18 additions & 0 deletions lua/codediff/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,29 @@ M.defaults = {
restore = "X", -- Discard changes to file (restore to index/HEAD)
toggle_changes = "gu", -- Toggle Changes (unstaged) group visibility
toggle_staged = "gs", -- Toggle Staged Changes group visibility
-- Fold keymaps (Vim-style)
fold_open = "zo", -- Open fold (expand current node)
fold_open_recursive = "zO", -- Open fold recursively (expand current node and all descendants)
fold_close = "zc", -- Close fold (collapse current node)
fold_close_recursive = "zC", -- Close fold recursively (collapse current node and all descendants)
fold_toggle = "za", -- Toggle fold (expand/collapse current node)
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
},
history = {
select = "<CR>", -- Select commit/file or toggle expand
toggle_view_mode = "i", -- Toggle between 'list' and 'tree' views
refresh = "R", -- Refresh history (re-fetch commits)
-- Fold keymaps (Vim-style, apply to directory nodes only)
fold_open = "zo",
fold_open_recursive = "zO",
fold_close = "zc",
fold_close_recursive = "zC",
fold_toggle = "za",
fold_toggle_recursive = "zA",
fold_open_all = "zR",
fold_close_all = "zM",
},
-- Conflict mode keymaps (only active in merge conflict views)
conflict = {
Expand Down
8 changes: 8 additions & 0 deletions lua/codediff/ui/explorer/keymaps.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,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 M = {}

Expand Down Expand Up @@ -171,6 +172,13 @@ function M.setup(explorer)
end, vim.tbl_extend("force", map_options, { buffer = split.bufnr, desc = "Toggle Staged Changes visibility" }))
end

-- Fold keymaps (Vim-style: zo/zO/zc/zC/za/zA/zR/zM)
tree_utils.setup_fold_keymaps({
tree = tree,
keymaps = explorer_keymaps,
bufnr = split.bufnr,
})

-- 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
8 changes: 8 additions & 0 deletions lua/codediff/ui/history/keymaps.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
-- Keymaps for history panel
local config = require("codediff.config")
local tree_utils = require("codediff.ui.lib.tree_utils")

local M = {}

Expand Down Expand Up @@ -120,6 +121,13 @@ function M.setup(history, opts)
refresh_module.refresh(history)
end, vim.tbl_extend("force", map_options, { buffer = split.bufnr, desc = "Refresh history" }))
end

-- Fold keymaps (Vim-style: zo/zO/zc/zC/za/zA/zR/zM — directory nodes only)
tree_utils.setup_fold_keymaps({
tree = tree,
keymaps = history_keymaps,
bufnr = split.bufnr,
})
end

return M
16 changes: 16 additions & 0 deletions lua/codediff/ui/keymap_help.lua
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ local function build_sections(keymaps, is_explorer, is_history, is_conflict)
{ ekm.restore, "Discard changes to file" },
{ ekm.toggle_changes, "Toggle Changes visibility" },
{ ekm.toggle_staged, "Toggle Staged visibility" },
{ ekm.fold_open, "Open fold" },
{ ekm.fold_open_recursive, "Open fold recursively" },
{ ekm.fold_close, "Close fold" },
{ ekm.fold_close_recursive, "Close fold recursively" },
{ ekm.fold_toggle, "Toggle fold" },
{ ekm.fold_toggle_recursive, "Toggle fold recursively" },
{ ekm.fold_open_all, "Open all folds" },
{ ekm.fold_close_all, "Close all folds" },
})
)
end
Expand All @@ -93,6 +101,14 @@ local function build_sections(keymaps, is_explorer, is_history, is_conflict)
{ hkm.select, "Select commit/file or toggle" },
{ hkm.toggle_view_mode, "Toggle list/tree view" },
{ hkm.refresh, "Refresh history" },
{ hkm.fold_open, "Open fold" },
{ hkm.fold_open_recursive, "Open fold recursively" },
{ hkm.fold_close, "Close fold" },
{ hkm.fold_close_recursive, "Close fold recursively" },
{ hkm.fold_toggle, "Toggle fold" },
{ hkm.fold_toggle_recursive, "Toggle fold recursively" },
{ hkm.fold_open_all, "Open all folds" },
{ hkm.fold_close_all, "Close all folds" },
})
)
end
Expand Down
21 changes: 21 additions & 0 deletions lua/codediff/ui/lib/tree.lua
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ end

-- Node methods

local function set_expanded_recursively(node, expanded)
if node:is_foldable() then
node._expanded = expanded
end
for _, child in ipairs(node._children) do
set_expanded_recursively(child, expanded)
end
end

function Node:get_id()
return self._id
end
Expand All @@ -49,14 +58,26 @@ function Node:is_expanded()
return self._expanded
end

function Node:is_foldable()
return self.data and (self.data.type == "group" or self.data.type == "directory") or false
end

function Node:expand()
self._expanded = true
end

function Node:expand_recursively()
set_expanded_recursively(self, true)
end

function Node:collapse()
self._expanded = false
end

function Node:collapse_recursively()
set_expanded_recursively(self, false)
end

function Node:has_children()
return #self._children > 0
end
Expand Down
159 changes: 159 additions & 0 deletions lua/codediff/ui/lib/tree_utils.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
local M = {}

function M.find_foldable_node(node, tree)
if node:is_foldable() then
return node
end

if node._parent_id then
local parent = tree:get_node(node._parent_id)
if parent and parent:is_foldable() then
return parent
end
end
return nil
end

function M.find_foldable_at_cursor(tree)
local node = tree:get_node()
if not node then
return nil
end
return M.find_foldable_node(node, tree)
end

function M.get_root_node(tree)
local node = tree:get_node()
if not node then
return
end

local function find_root(n)
if not n._parent_id then
return n
end
local parent = tree:get_node(n._parent_id)
if parent then
return find_root(parent)
else
return n
end
end
return find_root(node)
end

-- Setup all fold-related keymaps on a tree buffer.
-- @param opts table { tree, keymaps, bufnr }
function M.setup_fold_keymaps(opts)
local tree = opts.tree
local keymaps = opts.keymaps
local bufnr = opts.bufnr

local function update_tree_view(node)
tree:render()
local winid = vim.fn.bufwinid(bufnr)
if node._line and winid ~= -1 then
vim.api.nvim_win_set_cursor(winid, { node._line, 0 })
end
end

local function fold_open()
local node = M.find_foldable_at_cursor(tree)
if not node then
return
end
node:expand()
update_tree_view(node)
end

local function fold_open_recursive()
local node = M.find_foldable_at_cursor(tree)
if not node then
return
end
node:expand_recursively()
update_tree_view(node)
end

local function fold_close()
local node = M.find_foldable_at_cursor(tree)
if not node then
return
end
node:collapse()
update_tree_view(node)
end

local function fold_close_recursive()
local node = M.find_foldable_at_cursor(tree)
if not node then
return
end
node:collapse_recursively()
update_tree_view(node)
end

local function fold_toggle()
local node = M.find_foldable_at_cursor(tree)
if not node then
return
end
if node:is_expanded() then
node:collapse()
else
node:expand()
end
update_tree_view(node)
end

local function fold_toggle_recursive()
local node = M.find_foldable_at_cursor(tree)
if not node then
return
end
if node:is_expanded() then
node:collapse_recursively()
else
node:expand_recursively()
end
update_tree_view(node)
end

local function fold_open_all()
local root = M.get_root_node(tree)
if not root then
return
end
root:expand_recursively()
update_tree_view(root)
end

local function fold_close_all()
local root = M.get_root_node(tree)
if not root then
return
end
root:collapse_recursively()
update_tree_view(root)
end

local fold_bindings = {
{ key = "fold_open", fn = fold_open, desc = "Open fold" },
{ key = "fold_open_recursive", fn = fold_open_recursive, desc = "Open fold recursively" },
{ key = "fold_close", fn = fold_close, desc = "Close fold" },
{ key = "fold_close_recursive", fn = fold_close_recursive, desc = "Close fold recursively" },
{ key = "fold_toggle", fn = fold_toggle, desc = "Toggle fold" },
{ key = "fold_toggle_recursive", fn = fold_toggle_recursive, desc = "Toggle fold recursively" },
{ key = "fold_open_all", fn = fold_open_all, desc = "Open all folds" },
{ key = "fold_close_all", fn = fold_close_all, desc = "Close all folds" },
}
local map_options = { noremap = true, silent = true, nowait = true }
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 }))
end
end
end

return M
Loading