@@ -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
373376end
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
404450end
405451
406- -- Navigate to next file
452+ -- Navigate to next file (auto-expands next commit at boundary)
407453function 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 )
444505end
445506
446- -- Navigate to previous file
507+ -- Navigate to previous file (auto-expands previous commit at boundary)
447508function 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 )
486561end
487562
488563-- Get all commit nodes from tree (for navigation in single-file mode)
0 commit comments