@@ -345,8 +345,8 @@ function M.create(commits, git_root, tabpage, width, opts)
345345 on_file_select (file_data , opts )
346346 end
347347
348- -- Store load_commit_files for refresh to re-expand commits
349- history ._load_commit_files = load_commit_files
348+ -- Store load_commit_files for refresh and navigation
349+ history .load_commit_files = load_commit_files
350350
351351 -- Setup keymaps
352352 keymaps_module .setup (history , {
@@ -440,131 +440,199 @@ function M.rerender_current(history)
440440 return false
441441end
442442
443- -- Get all file nodes from tree (for navigation )
444- function M . get_all_files (tree )
443+ -- Collect all files from a commit node (handles tree mode with nested directories )
444+ local function collect_commit_files (tree , commit_node )
445445 local files = {}
446446
447- local function collect_files (parent_node )
448- if not parent_node :has_children () then
449- return
450- end
451- if not parent_node :is_expanded () then
452- return
447+ local function collect_recursive (node_ids )
448+ for _ , node_id in ipairs (node_ids ) do
449+ local node = tree :get_node (node_id )
450+ if node and node .data then
451+ if node .data .type == " file" then
452+ table.insert (files , { node = node , data = node .data })
453+ elseif node .data .type == " directory" then
454+ collect_recursive (node :get_child_ids () or {})
455+ end
456+ end
453457 end
458+ end
459+
460+ if commit_node :has_children () then
461+ collect_recursive (commit_node :get_child_ids () or {})
462+ end
454463
455- for _ , child_id in ipairs (parent_node :get_child_ids ()) do
456- local node = tree :get_node (child_id )
457- if node and node .data and node .data .type == " file" then
458- table.insert (files , {
459- node = node ,
460- data = node .data ,
461- })
464+ return files
465+ end
466+
467+ -- Get all file nodes from expanded commits (for external navigation)
468+ function M .get_all_files (tree )
469+ local files = {}
470+ for _ , node in ipairs (tree :get_nodes ()) do
471+ if node .data and node .data .type == " commit" and node :is_expanded () then
472+ for _ , file in ipairs (collect_commit_files (tree , node )) do
473+ table.insert (files , file )
462474 end
463475 end
464476 end
477+ return files
478+ end
465479
466- local nodes = tree :get_nodes ()
467- for _ , commit_node in ipairs (nodes ) do
468- collect_files (commit_node )
480+ -- Update cursor position in history panel
481+ local function update_cursor (history , node )
482+ local current_win = vim .api .nvim_get_current_win ()
483+ if vim .api .nvim_win_is_valid (history .winid ) then
484+ vim .api .nvim_set_current_win (history .winid )
485+ vim .api .nvim_win_set_cursor (history .winid , { node ._line or 1 , 0 })
486+ vim .api .nvim_set_current_win (current_win )
469487 end
488+ end
470489
471- return files
490+ -- Find current position: returns commit_idx, file_idx, commits list
491+ local function find_current_position (history )
492+ local commits = {}
493+ for _ , node in ipairs (history .tree :get_nodes ()) do
494+ if node .data and node .data .type == " commit" then
495+ table.insert (commits , node )
496+ end
497+ end
498+
499+ if # commits == 0 then
500+ return nil , nil , commits
501+ end
502+
503+ for commit_idx , commit_node in ipairs (commits ) do
504+ if commit_node .data .hash == history .current_commit and commit_node :is_expanded () then
505+ local files = collect_commit_files (history .tree , commit_node )
506+ for file_idx , file in ipairs (files ) do
507+ if file .data .path == history .current_file then
508+ return commit_idx , file_idx , commits
509+ end
510+ end
511+ end
512+ end
513+
514+ return nil , nil , commits
472515end
473516
474- -- Navigate to next file
517+ -- Navigate to next file (auto-expands next commit at boundary)
475518function M .navigate_next (history )
476- local all_files = M .get_all_files (history .tree )
477- if # all_files == 0 then
478- vim .notify (" No files in history" , vim .log .levels .WARN )
519+ local commit_idx , file_idx , commits = find_current_position (history )
520+
521+ if # commits == 0 then
522+ vim .notify (" No commits in history" , vim .log .levels .WARN )
479523 return
480524 end
481525
482- local current_commit = history .current_commit
483- local current_file = history .current_file
484-
485- if not current_commit or not current_file then
486- local first_file = all_files [1 ]
487- history .on_file_select (first_file .data )
526+ -- No current selection: select first file of first expanded commit
527+ if not commit_idx then
528+ for _ , commit_node in ipairs (commits ) do
529+ if commit_node :is_expanded () then
530+ local files = collect_commit_files (history .tree , commit_node )
531+ if # files > 0 then
532+ update_cursor (history , files [1 ].node )
533+ history .on_file_select (files [1 ].data )
534+ return
535+ end
536+ end
537+ end
538+ vim .notify (" No files in history" , vim .log .levels .WARN )
488539 return
489540 end
490541
491- -- Find current index
492- local current_index = 0
493- for i , file in ipairs (all_files ) do
494- if file .data .commit_hash == current_commit and file .data .path == current_file then
495- current_index = i
496- break
497- end
542+ local current_commit = commits [commit_idx ]
543+ local files = collect_commit_files (history .tree , current_commit )
544+
545+ -- Not at boundary: go to next file in same commit
546+ if file_idx < # files then
547+ local next_file = files [file_idx + 1 ]
548+ update_cursor (history , next_file .node )
549+ history .on_file_select (next_file .data )
550+ return
498551 end
499552
500- if current_index >= # all_files and not config .options .diff .cycle_next_file then
501- vim .api .nvim_echo ({ { string.format (" Last file (%d of %d)" , # all_files , # all_files ), " WarningMsg" } }, false , {})
553+ -- At boundary: go to next commit
554+ if commit_idx >= # commits and not config .options .diff .cycle_next_file then
555+ vim .api .nvim_echo ({ { string.format (" Last file (%d of %d commits)" , # commits , # commits ), " WarningMsg" } }, false , {})
502556 return
503- else
504- vim .api .nvim_echo ({}, false , {})
505557 end
506558
507- local next_index = current_index % # all_files + 1
508- local next_file = all_files [ next_index ]
559+ local next_commit_idx = commit_idx % # commits + 1
560+ local next_commit = commits [ next_commit_idx ]
509561
510- -- Update cursor position
511- local current_win = vim . api . nvim_get_current_win ( )
512- if vim . api . nvim_win_is_valid ( history . winid ) then
513- vim . api . nvim_set_current_win (history . winid )
514- vim . api . nvim_win_set_cursor ( history . winid , { next_file . node . _line or 1 , 0 } )
515- vim . api . nvim_set_current_win ( current_win )
562+ local function select_first_file ()
563+ local next_files = collect_commit_files ( history . tree , next_commit )
564+ if # next_files > 0 then
565+ update_cursor (history , next_files [ 1 ]. node )
566+ history . on_file_select ( next_files [ 1 ]. data )
567+ end
516568 end
517569
518- history .on_file_select (next_file .data )
570+ if next_commit :is_expanded () then
571+ select_first_file ()
572+ elseif history .load_commit_files then
573+ history .load_commit_files (next_commit , select_first_file )
574+ end
519575end
520576
521- -- Navigate to previous file
577+ -- Navigate to previous file (auto-expands previous commit at boundary)
522578function M .navigate_prev (history )
523- local all_files = M .get_all_files (history .tree )
524- if # all_files == 0 then
525- vim .notify (" No files in history" , vim .log .levels .WARN )
526- return
527- end
528-
529- local current_commit = history .current_commit
530- local current_file = history .current_file
579+ local commit_idx , file_idx , commits = find_current_position (history )
531580
532- if not current_commit or not current_file then
533- local last_file = all_files [# all_files ]
534- history .on_file_select (last_file .data )
581+ if # commits == 0 then
582+ vim .notify (" No commits in history" , vim .log .levels .WARN )
535583 return
536584 end
537585
538- local current_index = 0
539- for i , file in ipairs (all_files ) do
540- if file .data .commit_hash == current_commit and file .data .path == current_file then
541- current_index = i
542- break
586+ -- No current selection: select last file of last expanded commit
587+ if not commit_idx then
588+ for i = # commits , 1 , - 1 do
589+ local commit_node = commits [i ]
590+ if commit_node :is_expanded () then
591+ local files = collect_commit_files (history .tree , commit_node )
592+ if # files > 0 then
593+ update_cursor (history , files [# files ].node )
594+ history .on_file_select (files [# files ].data )
595+ return
596+ end
597+ end
543598 end
599+ vim .notify (" No files in history" , vim .log .levels .WARN )
600+ return
544601 end
545602
546- if current_index <= 1 and not config .options .diff .cycle_next_file then
547- vim .api .nvim_echo ({ { string.format (" First file (1 of %d)" , # all_files ), " WarningMsg" } }, false , {})
603+ local current_commit = commits [commit_idx ]
604+ local files = collect_commit_files (history .tree , current_commit )
605+
606+ -- Not at boundary: go to previous file in same commit
607+ if file_idx > 1 then
608+ local prev_file = files [file_idx - 1 ]
609+ update_cursor (history , prev_file .node )
610+ history .on_file_select (prev_file .data )
548611 return
549- else
550- vim .api .nvim_echo ({}, false , {})
551612 end
552613
553- local prev_index = current_index - 2
554- if prev_index < 0 then
555- prev_index = # all_files + prev_index
614+ -- At boundary: go to previous commit
615+ if commit_idx <= 1 and not config .options .diff .cycle_next_file then
616+ vim .api .nvim_echo ({ { string.format (" First file (1 of %d commits)" , # commits ), " WarningMsg" } }, false , {})
617+ return
556618 end
557- prev_index = prev_index % # all_files + 1
558- local prev_file = all_files [prev_index ]
559619
560- local current_win = vim .api .nvim_get_current_win ()
561- if vim .api .nvim_win_is_valid (history .winid ) then
562- vim .api .nvim_set_current_win (history .winid )
563- vim .api .nvim_win_set_cursor (history .winid , { prev_file .node ._line or 1 , 0 })
564- vim .api .nvim_set_current_win (current_win )
620+ local prev_commit_idx = (commit_idx - 2 ) % # commits + 1
621+ local prev_commit = commits [prev_commit_idx ]
622+
623+ local function select_last_file ()
624+ local prev_files = collect_commit_files (history .tree , prev_commit )
625+ if # prev_files > 0 then
626+ update_cursor (history , prev_files [# prev_files ].node )
627+ history .on_file_select (prev_files [# prev_files ].data )
628+ end
565629 end
566630
567- history .on_file_select (prev_file .data )
631+ if prev_commit :is_expanded () then
632+ select_last_file ()
633+ elseif history .load_commit_files then
634+ history .load_commit_files (prev_commit , select_last_file )
635+ end
568636end
569637
570638-- Get all commit nodes from tree (for navigation in single-file mode)
0 commit comments