Skip to content

Commit 0918d10

Browse files
feat(history): auto-expand commits on file navigation boundary
When navigating files in history mode with ]f/[f, reaching the boundary of a commit now automatically expands the adjacent commit: - navigate_next at last file → expands next commit, selects first file - navigate_prev at first file → expands previous commit, selects last file - wrap-around supported (last commit → first, first → last) Implementation uses shared helpers to reduce code duplication: - collect_commit_files: recursively collects files (handles tree mode) - find_current_position: locates commit/file indices for boundary detection - update_cursor: updates history panel cursor position Store load_commit_files on history object for access in navigation functions, enabling async commit expansion with callback pattern.
1 parent fc6157e commit 0918d10

1 file changed

Lines changed: 146 additions & 71 deletions

File tree

lua/codediff/ui/history/render.lua

Lines changed: 146 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,9 @@ function M.create(commits, git_root, tabpage, width, opts)
297297
on_file_select(file_data)
298298
end
299299

300+
-- Store load_commit_files for navigation functions
301+
history.load_commit_files = load_commit_files
302+
300303
-- Setup keymaps
301304
keymaps_module.setup(history, {
302305
is_single_file_mode = is_single_file_mode,
@@ -372,117 +375,189 @@ function M.create(commits, git_root, tabpage, width, opts)
372375
return history
373376
end
374377

375-
-- Get all file nodes from tree (for navigation)
376-
function M.get_all_files(tree)
378+
-- Collect all files from a commit node (handles tree mode with nested directories)
379+
local function collect_commit_files(tree, commit_node)
377380
local files = {}
378381

379-
local function collect_files(parent_node)
380-
if not parent_node:has_children() then
381-
return
382-
end
383-
if not parent_node:is_expanded() then
384-
return
382+
local function collect_recursive(node_ids)
383+
for _, node_id in ipairs(node_ids) do
384+
local node = tree:get_node(node_id)
385+
if node and node.data then
386+
if node.data.type == "file" then
387+
table.insert(files, { node = node, data = node.data })
388+
elseif node.data.type == "directory" then
389+
collect_recursive(node:get_child_ids() or {})
390+
end
391+
end
385392
end
393+
end
394+
395+
if commit_node:has_children() then
396+
collect_recursive(commit_node:get_child_ids() or {})
397+
end
386398

387-
for _, child_id in ipairs(parent_node:get_child_ids()) do
388-
local node = tree:get_node(child_id)
389-
if node and node.data and node.data.type == "file" then
390-
table.insert(files, {
391-
node = node,
392-
data = node.data,
393-
})
399+
return files
400+
end
401+
402+
-- Get all file nodes from expanded commits (for navigation)
403+
function M.get_all_files(tree)
404+
local files = {}
405+
for _, node in ipairs(tree:get_nodes()) do
406+
if node.data and node.data.type == "commit" and node:is_expanded() then
407+
for _, file in ipairs(collect_commit_files(tree, node)) do
408+
table.insert(files, file)
394409
end
395410
end
396411
end
412+
return files
413+
end
397414

398-
local nodes = tree:get_nodes()
399-
for _, commit_node in ipairs(nodes) do
400-
collect_files(commit_node)
415+
-- Update cursor position in history panel
416+
local function update_cursor(history, node)
417+
local current_win = vim.api.nvim_get_current_win()
418+
if vim.api.nvim_win_is_valid(history.winid) then
419+
vim.api.nvim_set_current_win(history.winid)
420+
vim.api.nvim_win_set_cursor(history.winid, { node._line or 1, 0 })
421+
vim.api.nvim_set_current_win(current_win)
401422
end
423+
end
402424

403-
return files
425+
-- Find current position: returns commit_idx, file_idx, commits list
426+
local function find_current_position(history)
427+
local commits = {}
428+
for _, node in ipairs(history.tree:get_nodes()) do
429+
if node.data and node.data.type == "commit" then
430+
table.insert(commits, node)
431+
end
432+
end
433+
434+
if #commits == 0 then
435+
return nil, nil, commits
436+
end
437+
438+
for commit_idx, commit_node in ipairs(commits) do
439+
if commit_node.data.hash == history.current_commit and commit_node:is_expanded() then
440+
local files = collect_commit_files(history.tree, commit_node)
441+
for file_idx, file in ipairs(files) do
442+
if file.data.path == history.current_file then
443+
return commit_idx, file_idx, commits
444+
end
445+
end
446+
end
447+
end
448+
449+
return nil, nil, commits
404450
end
405451

406-
-- Navigate to next file
452+
-- Navigate to next file (auto-expands next commit at boundary)
407453
function M.navigate_next(history)
408-
local all_files = M.get_all_files(history.tree)
409-
if #all_files == 0 then
454+
local commit_idx, file_idx, commits = find_current_position(history)
455+
456+
if #commits == 0 then
457+
vim.notify("No commits in history", vim.log.levels.WARN)
458+
return
459+
end
460+
461+
-- No current selection: select first file of first expanded commit
462+
if not commit_idx then
463+
for _, commit_node in ipairs(commits) do
464+
if commit_node:is_expanded() then
465+
local files = collect_commit_files(history.tree, commit_node)
466+
if #files > 0 then
467+
update_cursor(history, files[1].node)
468+
history.on_file_select(files[1].data)
469+
return
470+
end
471+
end
472+
end
410473
vim.notify("No files in history", vim.log.levels.WARN)
411474
return
412475
end
413476

414-
local current_commit = history.current_commit
415-
local current_file = history.current_file
477+
local current_commit = commits[commit_idx]
478+
local files = collect_commit_files(history.tree, current_commit)
416479

417-
if not current_commit or not current_file then
418-
local first_file = all_files[1]
419-
history.on_file_select(first_file.data)
480+
-- Not at boundary: go to next file in same commit
481+
if file_idx < #files then
482+
local next_file = files[file_idx + 1]
483+
update_cursor(history, next_file.node)
484+
history.on_file_select(next_file.data)
420485
return
421486
end
422487

423-
-- Find current index
424-
local current_index = 0
425-
for i, file in ipairs(all_files) do
426-
if file.data.commit_hash == current_commit and file.data.path == current_file then
427-
current_index = i
428-
break
488+
-- At boundary: go to next commit
489+
local next_commit_idx = commit_idx % #commits + 1
490+
local next_commit = commits[next_commit_idx]
491+
492+
local function select_first_file()
493+
local next_files = collect_commit_files(history.tree, next_commit)
494+
if #next_files > 0 then
495+
update_cursor(history, next_files[1].node)
496+
history.on_file_select(next_files[1].data)
429497
end
430498
end
431499

432-
local next_index = current_index % #all_files + 1
433-
local next_file = all_files[next_index]
434-
435-
-- Update cursor position
436-
local current_win = vim.api.nvim_get_current_win()
437-
if vim.api.nvim_win_is_valid(history.winid) then
438-
vim.api.nvim_set_current_win(history.winid)
439-
vim.api.nvim_win_set_cursor(history.winid, { next_file.node._line or 1, 0 })
440-
vim.api.nvim_set_current_win(current_win)
500+
if next_commit:is_expanded() then
501+
select_first_file()
502+
elseif history.load_commit_files then
503+
history.load_commit_files(next_commit, select_first_file)
441504
end
442-
443-
history.on_file_select(next_file.data)
444505
end
445506

446-
-- Navigate to previous file
507+
-- Navigate to previous file (auto-expands previous commit at boundary)
447508
function M.navigate_prev(history)
448-
local all_files = M.get_all_files(history.tree)
449-
if #all_files == 0 then
509+
local commit_idx, file_idx, commits = find_current_position(history)
510+
511+
if #commits == 0 then
512+
vim.notify("No commits in history", vim.log.levels.WARN)
513+
return
514+
end
515+
516+
-- No current selection: select last file of last expanded commit
517+
if not commit_idx then
518+
for i = #commits, 1, -1 do
519+
local commit_node = commits[i]
520+
if commit_node:is_expanded() then
521+
local files = collect_commit_files(history.tree, commit_node)
522+
if #files > 0 then
523+
update_cursor(history, files[#files].node)
524+
history.on_file_select(files[#files].data)
525+
return
526+
end
527+
end
528+
end
450529
vim.notify("No files in history", vim.log.levels.WARN)
451530
return
452531
end
453532

454-
local current_commit = history.current_commit
455-
local current_file = history.current_file
533+
local current_commit = commits[commit_idx]
534+
local files = collect_commit_files(history.tree, current_commit)
456535

457-
if not current_commit or not current_file then
458-
local last_file = all_files[#all_files]
459-
history.on_file_select(last_file.data)
536+
-- Not at boundary: go to previous file in same commit
537+
if file_idx > 1 then
538+
local prev_file = files[file_idx - 1]
539+
update_cursor(history, prev_file.node)
540+
history.on_file_select(prev_file.data)
460541
return
461542
end
462543

463-
local current_index = 0
464-
for i, file in ipairs(all_files) do
465-
if file.data.commit_hash == current_commit and file.data.path == current_file then
466-
current_index = i
467-
break
468-
end
469-
end
544+
-- At boundary: go to previous commit
545+
local prev_commit_idx = (commit_idx - 2) % #commits + 1
546+
local prev_commit = commits[prev_commit_idx]
470547

471-
local prev_index = current_index - 2
472-
if prev_index < 0 then
473-
prev_index = #all_files + prev_index
548+
local function select_last_file()
549+
local prev_files = collect_commit_files(history.tree, prev_commit)
550+
if #prev_files > 0 then
551+
update_cursor(history, prev_files[#prev_files].node)
552+
history.on_file_select(prev_files[#prev_files].data)
553+
end
474554
end
475-
prev_index = prev_index % #all_files + 1
476-
local prev_file = all_files[prev_index]
477555

478-
local current_win = vim.api.nvim_get_current_win()
479-
if vim.api.nvim_win_is_valid(history.winid) then
480-
vim.api.nvim_set_current_win(history.winid)
481-
vim.api.nvim_win_set_cursor(history.winid, { prev_file.node._line or 1, 0 })
482-
vim.api.nvim_set_current_win(current_win)
556+
if prev_commit:is_expanded() then
557+
select_last_file()
558+
elseif history.load_commit_files then
559+
history.load_commit_files(prev_commit, select_last_file)
483560
end
484-
485-
history.on_file_select(prev_file.data)
486561
end
487562

488563
-- Get all commit nodes from tree (for navigation in single-file mode)

0 commit comments

Comments
 (0)