1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-07-31 14:24:25 +03:00

In hunk staging mode, select blocks of changes rather than actual hunks

Also, pressing right or left arrow moves between blocks of changes rather than
actual hunks. I find this to be much more useful.
This commit is contained in:
Stefan Haller
2025-07-04 11:55:41 +02:00
parent 08ad8a0b2d
commit a6096f4702
5 changed files with 88 additions and 133 deletions

View File

@ -223,13 +223,13 @@ func (self *PatchExplorerController) HandleNextLineRange() error {
} }
func (self *PatchExplorerController) HandlePrevHunk() error { func (self *PatchExplorerController) HandlePrevHunk() error {
self.context.GetState().CycleHunk(false) self.context.GetState().SelectPreviousHunk()
return nil return nil
} }
func (self *PatchExplorerController) HandleNextHunk() error { func (self *PatchExplorerController) HandleNextHunk() error {
self.context.GetState().CycleHunk(true) self.context.GetState().SelectNextHunk()
return nil return nil
} }

View File

@ -125,6 +125,11 @@ func (s *State) ToggleSelectHunk() {
s.selectMode = LINE s.selectMode = LINE
} else { } else {
s.selectMode = HUNK s.selectMode = HUNK
// If we are not currently on a change line, select the next one (or the
// previous one if there is no next one):
s.selectedLineIdx = s.viewLineIndices[s.patch.GetNextChangeIdx(
s.patchLineIndices[s.selectedLineIdx])]
} }
} }
@ -203,25 +208,49 @@ func (s *State) DragSelectLine(newSelectedLineIdx int) {
func (s *State) CycleSelection(forward bool) { func (s *State) CycleSelection(forward bool) {
if s.SelectingHunk() { if s.SelectingHunk() {
s.CycleHunk(forward) if forward {
s.SelectNextHunk()
} else {
s.SelectPreviousHunk()
}
} else { } else {
s.CycleLine(forward) s.CycleLine(forward)
} }
} }
func (s *State) CycleHunk(forward bool) { func (s *State) SelectPreviousHunk() {
change := 1 patchLines := s.patch.Lines()
if !forward { patchLineIdx := s.patchLineIndices[s.selectedLineIdx]
change = -1 nextNonChangeLine := patchLineIdx
for nextNonChangeLine >= 0 && patchLines[nextNonChangeLine].IsChange() {
nextNonChangeLine--
} }
nextChangeLine := nextNonChangeLine
hunkIdx := s.patch.HunkContainingLine(s.patchLineIndices[s.selectedLineIdx]) for nextChangeLine >= 0 && !patchLines[nextChangeLine].IsChange() {
if hunkIdx != -1 { nextChangeLine--
newHunkIdx := hunkIdx + change }
if newHunkIdx >= 0 && newHunkIdx < s.patch.HunkCount() { if nextChangeLine >= 0 {
start := s.patch.HunkStartIdx(newHunkIdx) // Now we found a previous hunk, but we're on its last line. Skip to the beginning.
s.selectedLineIdx = s.viewLineIndices[s.patch.GetNextChangeIdx(start)] for nextChangeLine > 0 && patchLines[nextChangeLine-1].IsChange() {
nextChangeLine--
} }
s.selectedLineIdx = s.viewLineIndices[nextChangeLine]
}
}
func (s *State) SelectNextHunk() {
patchLines := s.patch.Lines()
patchLineIdx := s.patchLineIndices[s.selectedLineIdx]
nextNonChangeLine := patchLineIdx
for nextNonChangeLine < len(patchLines) && patchLines[nextNonChangeLine].IsChange() {
nextNonChangeLine++
}
nextChangeLine := nextNonChangeLine
for nextChangeLine < len(patchLines) && !patchLines[nextChangeLine].IsChange() {
nextChangeLine++
}
if nextChangeLine < len(patchLines) {
s.selectedLineIdx = s.viewLineIndices[nextChangeLine]
} }
} }
@ -259,11 +288,34 @@ func (s *State) CurrentHunkBounds() (int, int) {
return start, end return start, end
} }
func (s *State) selectionRangeForCurrentBlockOfChanges() (int, int) {
patchLines := s.patch.Lines()
patchLineIdx := s.patchLineIndices[s.selectedLineIdx]
patchStart := patchLineIdx
for patchStart > 0 && patchLines[patchStart-1].IsChange() {
patchStart--
}
patchEnd := patchLineIdx
for patchEnd < len(patchLines)-1 && patchLines[patchEnd+1].IsChange() {
patchEnd++
}
viewStart, viewEnd := s.viewLineIndices[patchStart], s.viewLineIndices[patchEnd]
// Increase viewEnd in case the last patch line is wrapped to more than one view line.
for viewEnd < len(s.patchLineIndices)-1 && s.patchLineIndices[viewEnd] == s.patchLineIndices[viewEnd+1] {
viewEnd++
}
return viewStart, viewEnd
}
func (s *State) SelectedViewRange() (int, int) { func (s *State) SelectedViewRange() (int, int) {
switch s.selectMode { switch s.selectMode {
case HUNK: case HUNK:
start, end := s.CurrentHunkBounds() return s.selectionRangeForCurrentBlockOfChanges()
return s.viewLineIndices[start], s.viewLineIndices[end]
case RANGE: case RANGE:
if s.rangeStartLineIdx > s.selectedLineIdx { if s.rangeStartLineIdx > s.selectedLineIdx {
return s.selectedLineIdx, s.rangeStartLineIdx return s.selectedLineIdx, s.rangeStartLineIdx

View File

@ -55,31 +55,13 @@ var SpecificSelection = NewIntegrationTest(NewIntegrationTestArgs{
). ).
Press(keys.Main.ToggleSelectHunk). Press(keys.Main.ToggleSelectHunk).
SelectedLines( SelectedLines(
Contains(`@@ -1,6 +1,6 @@`),
Contains(`-1a`), Contains(`-1a`),
Contains(`+aa`), Contains(`+aa`),
Contains(` 1b`),
Contains(`-1c`),
Contains(`+cc`),
Contains(` 1d`),
Contains(` 1e`),
Contains(` 1f`),
). ).
PressPrimaryAction(). PressPrimaryAction().
SelectedLines( SelectedLines(
Contains(`@@ -17,9 +17,9 @@`), Contains(`-1c`),
Contains(` 1q`), Contains(`+cc`),
Contains(` 1r`),
Contains(` 1s`),
Contains(`-1t`),
Contains(`-1u`),
Contains(`-1v`),
Contains(`+tt`),
Contains(`+uu`),
Contains(`+vv`),
Contains(` 1w`),
Contains(` 1x`),
Contains(` 1y`),
). ).
Tap(func() { Tap(func() {
t.Views().Information().Content(Contains("Building patch")) t.Views().Information().Content(Contains("Building patch"))
@ -154,8 +136,7 @@ var SpecificSelection = NewIntegrationTest(NewIntegrationTestArgs{
Contains(`-1a`), Contains(`-1a`),
Contains(`+aa`), Contains(`+aa`),
Contains(` 1b`), Contains(` 1b`),
Contains(`-1c`), Contains(` 1c`),
Contains(`+cc`),
Contains(` 1d`), Contains(` 1d`),
Contains(` 1e`), Contains(` 1e`),
Contains(` 1f`), Contains(` 1f`),

View File

@ -52,67 +52,40 @@ var DiffContextChange = NewIntegrationTest(NewIntegrationTestArgs{
IsFocused(). IsFocused().
Press(keys.Main.ToggleSelectHunk). Press(keys.Main.ToggleSelectHunk).
SelectedLines( SelectedLines(
Contains(`@@ -1,6 +1,6 @@`),
Contains(` 1a`),
Contains(` 2a`),
Contains(`-3a`), Contains(`-3a`),
Contains(`+3b`), Contains(`+3b`),
Contains(` 4a`),
Contains(` 5a`),
Contains(` 6a`),
). ).
Press(keys.Universal.IncreaseContextInDiffView). Press(keys.Universal.IncreaseContextInDiffView).
Tap(func() { Tap(func() {
t.ExpectToast(Equals("Changed diff context size to 4")) t.ExpectToast(Equals("Changed diff context size to 4"))
}). }).
SelectedLines( SelectedLines(
Contains(`@@ -1,7 +1,7 @@`),
Contains(` 1a`),
Contains(` 2a`),
Contains(`-3a`), Contains(`-3a`),
Contains(`+3b`), Contains(`+3b`),
Contains(` 4a`),
Contains(` 5a`),
Contains(` 6a`),
Contains(` 7a`),
). ).
Press(keys.Universal.DecreaseContextInDiffView). Press(keys.Universal.DecreaseContextInDiffView).
Tap(func() { Tap(func() {
t.ExpectToast(Equals("Changed diff context size to 3")) t.ExpectToast(Equals("Changed diff context size to 3"))
}). }).
SelectedLines( SelectedLines(
Contains(`@@ -1,6 +1,6 @@`),
Contains(` 1a`),
Contains(` 2a`),
Contains(`-3a`), Contains(`-3a`),
Contains(`+3b`), Contains(`+3b`),
Contains(` 4a`),
Contains(` 5a`),
Contains(` 6a`),
). ).
Press(keys.Universal.DecreaseContextInDiffView). Press(keys.Universal.DecreaseContextInDiffView).
Tap(func() { Tap(func() {
t.ExpectToast(Equals("Changed diff context size to 2")) t.ExpectToast(Equals("Changed diff context size to 2"))
}). }).
SelectedLines( SelectedLines(
Contains(`@@ -1,5 +1,5 @@`),
Contains(` 1a`),
Contains(` 2a`),
Contains(`-3a`), Contains(`-3a`),
Contains(`+3b`), Contains(`+3b`),
Contains(` 4a`),
Contains(` 5a`),
). ).
Press(keys.Universal.DecreaseContextInDiffView). Press(keys.Universal.DecreaseContextInDiffView).
Tap(func() { Tap(func() {
t.ExpectToast(Equals("Changed diff context size to 1")) t.ExpectToast(Equals("Changed diff context size to 1"))
}). }).
SelectedLines( SelectedLines(
Contains(`@@ -2,3 +2,3 @@`),
Contains(` 2a`),
Contains(`-3a`), Contains(`-3a`),
Contains(`+3b`), Contains(`+3b`),
Contains(` 4a`),
). ).
PressPrimaryAction(). PressPrimaryAction().
Press(keys.Universal.TogglePanel) Press(keys.Universal.TogglePanel)
@ -121,18 +94,14 @@ var DiffContextChange = NewIntegrationTest(NewIntegrationTestArgs{
IsFocused(). IsFocused().
Press(keys.Main.ToggleSelectHunk). Press(keys.Main.ToggleSelectHunk).
SelectedLines( SelectedLines(
Contains(`@@ -2,3 +2,3 @@`),
Contains(` 2a`),
Contains(`-3a`), Contains(`-3a`),
Contains(`+3b`), Contains(`+3b`),
Contains(` 4a`),
). ).
Press(keys.Universal.DecreaseContextInDiffView). Press(keys.Universal.DecreaseContextInDiffView).
Tap(func() { Tap(func() {
t.ExpectToast(Equals("Changed diff context size to 0")) t.ExpectToast(Equals("Changed diff context size to 0"))
}). }).
SelectedLines( SelectedLines(
Contains(`@@ -3,1 +3 @@`),
Contains(`-3a`), Contains(`-3a`),
Contains(`+3b`), Contains(`+3b`),
). ).
@ -141,24 +110,16 @@ var DiffContextChange = NewIntegrationTest(NewIntegrationTestArgs{
t.ExpectToast(Equals("Changed diff context size to 1")) t.ExpectToast(Equals("Changed diff context size to 1"))
}). }).
SelectedLines( SelectedLines(
Contains(`@@ -2,3 +2,3 @@`),
Contains(` 2a`),
Contains(`-3a`), Contains(`-3a`),
Contains(`+3b`), Contains(`+3b`),
Contains(` 4a`),
). ).
Press(keys.Universal.IncreaseContextInDiffView). Press(keys.Universal.IncreaseContextInDiffView).
Tap(func() { Tap(func() {
t.ExpectToast(Equals("Changed diff context size to 2")) t.ExpectToast(Equals("Changed diff context size to 2"))
}). }).
SelectedLines( SelectedLines(
Contains(`@@ -1,5 +1,5 @@`),
Contains(` 1a`),
Contains(` 2a`),
Contains(`-3a`), Contains(`-3a`),
Contains(`+3b`), Contains(`+3b`),
Contains(` 4a`),
Contains(` 5a`),
) )
}, },
}) })

View File

@ -11,11 +11,10 @@ var StageHunks = NewIntegrationTest(NewIntegrationTestArgs{
Skip: false, Skip: false,
SetupConfig: func(config *config.AppConfig) {}, SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) { SetupRepo: func(shell *Shell) {
// need to be working with a few lines so that git perceives it as two separate hunks shell.CreateFileAndAdd("file1", "1a\n2a\n3a\n4a\n5a\n6a\n7a\n8a")
shell.CreateFileAndAdd("file1", "1a\n2a\n3a\n4a\n5a\n6a\n7a\n8a\n9a\n10a\n11a\n12a\n13a\n14a\n15a")
shell.Commit("one") shell.Commit("one")
shell.UpdateFile("file1", "1a\n2a\n3b\n4a\n5a\n6a\n7a\n8a\n9a\n10a\n11a\n12a\n13b\n14a\n15a") shell.UpdateFile("file1", "1a\n2a\n3b\n4a\n5a\n6b\n7a\n8a")
// hunk looks like: // hunk looks like:
// diff --git a/file1 b/file1 // diff --git a/file1 b/file1
@ -29,15 +28,10 @@ var StageHunks = NewIntegrationTest(NewIntegrationTestArgs{
// +3b // +3b
// 4a // 4a
// 5a // 5a
// 6a // -6a
// @@ -10,6 +10,6 @@ // +6b
// 10a // 7a
// 11a // 8a
// 12a
// -13a
// +13b
// 14a
// 15a
// \ No newline at end of file // \ No newline at end of file
}, },
Run: func(t *TestDriver, keys config.KeybindingConfig) { Run: func(t *TestDriver, keys config.KeybindingConfig) {
@ -55,43 +49,23 @@ var StageHunks = NewIntegrationTest(NewIntegrationTestArgs{
). ).
Press(keys.Universal.NextBlock). Press(keys.Universal.NextBlock).
SelectedLines( SelectedLines(
Contains("-13a"), Contains("-6a"),
). ).
Press(keys.Main.ToggleSelectHunk). Press(keys.Main.ToggleSelectHunk).
SelectedLines( SelectedLines(
Contains("@@ -10,6 +10,6 @@"), Contains("-6a"),
Contains(" 10a"), Contains("+6b"),
Contains(" 11a"),
Contains(" 12a"),
Contains("-13a"),
Contains("+13b"),
Contains(" 14a"),
Contains(" 15a"),
Contains(`\ No newline at end of file`),
). ).
// when in hunk mode, pressing up/down moves us up/down by a hunk // when in hunk mode, pressing up/down moves us up/down by a hunk
SelectPreviousItem(). SelectPreviousItem().
SelectedLines( SelectedLines(
Contains(`@@ -1,6 +1,6 @@`),
Contains(` 1a`),
Contains(` 2a`),
Contains(`-3a`), Contains(`-3a`),
Contains(`+3b`), Contains(`+3b`),
Contains(` 4a`),
Contains(` 5a`),
Contains(` 6a`),
). ).
SelectNextItem(). SelectNextItem().
SelectedLines( SelectedLines(
Contains("@@ -10,6 +10,6 @@"), Contains("-6a"),
Contains(" 10a"), Contains("+6b"),
Contains(" 11a"),
Contains(" 12a"),
Contains("-13a"),
Contains("+13b"),
Contains(" 14a"),
Contains(" 15a"),
Contains(`\ No newline at end of file`),
). ).
// stage the second hunk // stage the second hunk
PressPrimaryAction(). PressPrimaryAction().
@ -102,8 +76,8 @@ var StageHunks = NewIntegrationTest(NewIntegrationTestArgs{
Tap(func() { Tap(func() {
t.Views().StagingSecondary(). t.Views().StagingSecondary().
ContainsLines( ContainsLines(
Contains("-13a"), Contains("-6a"),
Contains("+13b"), Contains("+6b"),
) )
}). }).
Press(keys.Universal.TogglePanel) Press(keys.Universal.TogglePanel)
@ -112,11 +86,11 @@ var StageHunks = NewIntegrationTest(NewIntegrationTestArgs{
IsFocused(). IsFocused().
// after toggling panel, we're back to only having selected a single line // after toggling panel, we're back to only having selected a single line
SelectedLines( SelectedLines(
Contains("-13a"), Contains("-6a"),
). ).
PressPrimaryAction(). PressPrimaryAction().
SelectedLines( SelectedLines(
Contains("+13b"), Contains("+6b"),
). ).
PressPrimaryAction(). PressPrimaryAction().
IsEmpty() IsEmpty()
@ -128,14 +102,8 @@ var StageHunks = NewIntegrationTest(NewIntegrationTestArgs{
). ).
Press(keys.Main.ToggleSelectHunk). Press(keys.Main.ToggleSelectHunk).
SelectedLines( SelectedLines(
Contains(`@@ -1,6 +1,6 @@`),
Contains(` 1a`),
Contains(` 2a`),
Contains(`-3a`), Contains(`-3a`),
Contains(`+3b`), Contains(`+3b`),
Contains(` 4a`),
Contains(` 5a`),
Contains(` 6a`),
). ).
Press(keys.Universal.Remove). Press(keys.Universal.Remove).
Tap(func() { Tap(func() {
@ -143,15 +111,8 @@ var StageHunks = NewIntegrationTest(NewIntegrationTestArgs{
}). }).
Content(DoesNotContain("-3a").DoesNotContain("+3b")). Content(DoesNotContain("-3a").DoesNotContain("+3b")).
SelectedLines( SelectedLines(
Contains("@@ -10,6 +10,6 @@"), Contains("-6a"),
Contains(" 10a"), Contains("+6b"),
Contains(" 11a"),
Contains(" 12a"),
Contains("-13a"),
Contains("+13b"),
Contains(" 14a"),
Contains(" 15a"),
Contains(`\ No newline at end of file`),
) )
}, },
}) })