Skip to content

Commit 52f8027

Browse files
committed
feat(ProjectDiffScreen): Add next/previous nav
Generalized navigation keybinds (default J/K): - In diff pane: navigate between hunks across all files - In file list: navigate between files (skipping folders) Both wrap around and jump to first/last hunk when crossing files.
1 parent 4c582d9 commit 52f8027

2 files changed

Lines changed: 144 additions & 10 deletions

File tree

lua/vgit/features/screens/ProjectDiffScreen/init.lua

Lines changed: 136 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ function ProjectDiffScreen:constructor(opts)
3535
{ 'Stage hunk', keymaps['buffer_hunk_stage'] },
3636
{ 'Unstage hunk', keymaps['buffer_hunk_unstage'] },
3737
{ 'Reset hunk', keymaps['buffer_hunk_reset'] },
38+
{ 'Next', keymaps['next'] },
39+
{ 'Previous', keymaps['previous'] },
3840
{ 'Stage all', keymaps['stage_all'] },
3941
{ 'Unstage all', keymaps['unstage_all'] },
4042
{ 'Reset all', keymaps['reset_all'] },
@@ -81,16 +83,6 @@ function ProjectDiffScreen:constructor(opts)
8183
}
8284
end
8385

84-
function ProjectDiffScreen:hunk_up()
85-
local hunk_alignment = project_diff_preview_setting:get('hunk_alignment')
86-
self.diff_view:prev(hunk_alignment)
87-
end
88-
89-
function ProjectDiffScreen:hunk_down()
90-
local hunk_alignment = project_diff_preview_setting:get('hunk_alignment')
91-
self.diff_view:next(hunk_alignment)
92-
end
93-
9486
function ProjectDiffScreen:move_to(query_fn)
9587
return self.status_list_view:move_to(query_fn)
9688
end
@@ -422,6 +414,100 @@ function ProjectDiffScreen:handle_list_move()
422414
self.diff_view:move_to_hunk(nil, hunk_alignment)
423415
end
424416

417+
function ProjectDiffScreen:get_current_mark_index()
418+
loop.free_textlock()
419+
local diff = self.model:get_diff()
420+
if not diff or not diff.marks or #diff.marks == 0 then
421+
return nil, 0
422+
end
423+
424+
local marks = diff.marks
425+
local lnum = self.diff_view.scene:get('current'):get_lnum()
426+
427+
for i, mark in ipairs(marks) do
428+
if lnum >= mark.top and lnum <= mark.bot then
429+
return i, #marks
430+
elseif mark.top > lnum then
431+
return math.max(1, i - 1), #marks
432+
end
433+
end
434+
435+
return #marks, #marks
436+
end
437+
438+
function ProjectDiffScreen:move_to_next_file()
439+
loop.free_textlock()
440+
local component = self.status_list_view.scene:get('list')
441+
local current_lnum = component:get_lnum()
442+
local count = component:get_line_count()
443+
444+
-- Find next file entry (skip folders)
445+
for offset = 1, count do
446+
local target_lnum = current_lnum + offset
447+
if target_lnum > count then target_lnum = target_lnum - count end
448+
449+
local item = self.status_list_view:get_list_item(target_lnum)
450+
if item and item.entry and item.entry.status then
451+
component:unlock():set_lnum(target_lnum):lock()
452+
return item
453+
end
454+
end
455+
return nil
456+
end
457+
458+
function ProjectDiffScreen:move_to_prev_file()
459+
loop.free_textlock()
460+
local component = self.status_list_view.scene:get('list')
461+
local current_lnum = component:get_lnum()
462+
local count = component:get_line_count()
463+
464+
-- Find previous file entry (skip folders)
465+
for offset = 1, count do
466+
local target_lnum = current_lnum - offset
467+
if target_lnum < 1 then target_lnum = target_lnum + count end
468+
469+
local item = self.status_list_view:get_list_item(target_lnum)
470+
if item and item.entry and item.entry.status then
471+
component:unlock():set_lnum(target_lnum):lock()
472+
return item
473+
end
474+
end
475+
return nil
476+
end
477+
478+
function ProjectDiffScreen:next_hunk()
479+
local current_index, total_hunks = self:get_current_mark_index()
480+
local hunk_alignment = project_diff_preview_setting:get('hunk_alignment')
481+
482+
if not current_index or total_hunks == 0 or current_index >= total_hunks then
483+
-- At last hunk or no hunks - move to next file
484+
local list_item = self:move_to_next_file()
485+
if not list_item then return end
486+
self.model:set_entry_id(list_item.id)
487+
self.diff_view:render()
488+
self.diff_view:move_to_hunk(1, hunk_alignment)
489+
else
490+
self.diff_view:next(hunk_alignment)
491+
end
492+
end
493+
494+
function ProjectDiffScreen:prev_hunk()
495+
local current_index, total_hunks = self:get_current_mark_index()
496+
local hunk_alignment = project_diff_preview_setting:get('hunk_alignment')
497+
498+
if not current_index or total_hunks == 0 or current_index <= 1 then
499+
-- At first hunk or no hunks - move to previous file's last hunk
500+
local list_item = self:move_to_prev_file()
501+
if not list_item then return end
502+
self.model:set_entry_id(list_item.id)
503+
self.diff_view:render()
504+
-- Pass 0 to go to last hunk (move_to_hunk clamps <1 to #marks)
505+
self.diff_view:move_to_hunk(0, hunk_alignment)
506+
else
507+
self.diff_view:prev(hunk_alignment)
508+
end
509+
end
510+
425511
function ProjectDiffScreen:focus_relative_buffer_entry(buffer)
426512
local filename = buffer:get_relative_name()
427513

@@ -523,6 +609,30 @@ function ProjectDiffScreen:setup_list_keymaps()
523609
self:toggle_focus()
524610
end,
525611
},
612+
{
613+
mode = 'n',
614+
mapping = keymaps.next,
615+
handler = loop.debounce_coroutine(function()
616+
local list_item = self:move_to_next_file()
617+
if not list_item then return end
618+
self.model:set_entry_id(list_item.id)
619+
local hunk_alignment = project_diff_preview_setting:get('hunk_alignment')
620+
self.diff_view:render()
621+
self.diff_view:move_to_hunk(1, hunk_alignment)
622+
end, 15),
623+
},
624+
{
625+
mode = 'n',
626+
mapping = keymaps.previous,
627+
handler = loop.debounce_coroutine(function()
628+
local list_item = self:move_to_prev_file()
629+
if not list_item then return end
630+
self.model:set_entry_id(list_item.id)
631+
local hunk_alignment = project_diff_preview_setting:get('hunk_alignment')
632+
self.diff_view:render()
633+
self.diff_view:move_to_hunk(0, hunk_alignment)
634+
end, 15),
635+
},
526636
})
527637
end
528638

@@ -564,6 +674,12 @@ function ProjectDiffScreen:setup_diff_keymaps()
564674
enter = loop.coroutine(function()
565675
self:enter_view()
566676
end),
677+
next_hunk = loop.debounce_coroutine(function()
678+
self:next_hunk()
679+
end, 15),
680+
prev_hunk = loop.debounce_coroutine(function()
681+
self:prev_hunk()
682+
end, 15),
567683
}
568684

569685
self.diff_keymaps = handlers
@@ -626,6 +742,16 @@ function ProjectDiffScreen:setup_diff_keymaps()
626742
self:toggle_focus()
627743
end,
628744
},
745+
{
746+
mode = 'n',
747+
mapping = keymaps.next,
748+
handler = handlers.next_hunk,
749+
},
750+
{
751+
mode = 'n',
752+
mapping = keymaps.previous,
753+
handler = handlers.prev_hunk,
754+
},
629755
{
630756
mode = 'n',
631757
mapping = {

lua/vgit/settings/project_diff_preview.lua

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,13 @@ return Config({
4848
key = '<Tab>',
4949
desc = 'Switch focus between file list and diff preview'
5050
},
51+
next = {
52+
key = 'J',
53+
desc = 'Next'
54+
},
55+
previous = {
56+
key = 'K',
57+
desc = 'Previous'
58+
},
5159
},
5260
})

0 commit comments

Comments
 (0)