mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-07-05 00:59:19 +02:00
Improve hunk selection mode in staging view (#4684)
- **PR Description** Hunk selection mode is one of the features that many people don't know about, because it is not very discoverable. You can switch to it from line selection mode by pressing `a` in the staging view. The problem with this mode is that it selects entire hunks, where hunks are defined to be sections of the diff starting with `@@`. Very often, hunks consist of multiple distinct blocks of changes, separated by context lines. For example, with the default diff context size of 3 it takes at least 6 unchanged lines between blocks of changes for them to be separated into distinct hunks; if there are 5 or less unchanged lines between them, they are grouped into one hunk. And of course, if you increase the diff context size by pressing `}`, you will get even fewer hunks. Now, most of the time I want to navigate between the individual blocks of changes in a diff, regardless of how git groups them into hunks. That's what this PR does: when pressing `a`, the selection is extended to just the current group of changes, separated by context lines; you can easily stage it by pressing space, and the selection will move on to the next block of changes. Actual hunks no longer play a role here. Also, in line selection mode the right/left arrow keys now move between blocks of changes rather than actual hunks. I find this new behavior so useful that I almost always switch to hunk mode right away after entering the staging view. It saves a lot of keystrokes, since it is very rare that I want to select only some lines of a block of adjacent changes. This makes me wonder whether we should enable hunk mode by default when entering staging, but that's going to be another PR.
This commit is contained in:
9
.vscode/tasks.json
vendored
9
.vscode/tasks.json
vendored
@ -24,22 +24,24 @@
|
||||
{
|
||||
"label": "Run current file integration test",
|
||||
"type": "shell",
|
||||
"command": "go generate pkg/integration/tests/tests.go && go run cmd/integration_test/main.go cli ${relativeFile}",
|
||||
"command": "go run cmd/integration_test/main.go cli ${relativeFile}",
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
},
|
||||
"presentation": {
|
||||
"clear": true,
|
||||
"focus": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Run current file integration test (slow)",
|
||||
"type": "shell",
|
||||
"command": "go generate pkg/integration/tests/tests.go && go run cmd/integration_test/main.go cli --slow ${relativeFile}",
|
||||
"command": "go run cmd/integration_test/main.go cli --slow ${relativeFile}",
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"clear": true,
|
||||
"kind": "test",
|
||||
},
|
||||
"presentation": {
|
||||
@ -49,12 +51,13 @@
|
||||
{
|
||||
"label": "Run current file integration test (sandbox)",
|
||||
"type": "shell",
|
||||
"command": "go generate pkg/integration/tests/tests.go && go run cmd/integration_test/main.go cli --sandbox ${relativeFile}",
|
||||
"command": "go run cmd/integration_test/main.go cli --sandbox ${relativeFile}",
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "test",
|
||||
},
|
||||
"presentation": {
|
||||
"clear": true,
|
||||
"focus": true
|
||||
}
|
||||
},
|
||||
|
@ -223,13 +223,13 @@ func (self *PatchExplorerController) HandleNextLineRange() error {
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) HandlePrevHunk() error {
|
||||
self.context.GetState().CycleHunk(false)
|
||||
self.context.GetState().SelectPreviousHunk()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) HandleNextHunk() error {
|
||||
self.context.GetState().CycleHunk(true)
|
||||
self.context.GetState().SelectNextHunk()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -125,6 +125,11 @@ func (s *State) ToggleSelectHunk() {
|
||||
s.selectMode = LINE
|
||||
} else {
|
||||
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) {
|
||||
if s.SelectingHunk() {
|
||||
s.CycleHunk(forward)
|
||||
if forward {
|
||||
s.SelectNextHunk()
|
||||
} else {
|
||||
s.SelectPreviousHunk()
|
||||
}
|
||||
} else {
|
||||
s.CycleLine(forward)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) CycleHunk(forward bool) {
|
||||
change := 1
|
||||
if !forward {
|
||||
change = -1
|
||||
func (s *State) SelectPreviousHunk() {
|
||||
patchLines := s.patch.Lines()
|
||||
patchLineIdx := s.patchLineIndices[s.selectedLineIdx]
|
||||
nextNonChangeLine := patchLineIdx
|
||||
for nextNonChangeLine >= 0 && patchLines[nextNonChangeLine].IsChange() {
|
||||
nextNonChangeLine--
|
||||
}
|
||||
|
||||
hunkIdx := s.patch.HunkContainingLine(s.patchLineIndices[s.selectedLineIdx])
|
||||
if hunkIdx != -1 {
|
||||
newHunkIdx := hunkIdx + change
|
||||
if newHunkIdx >= 0 && newHunkIdx < s.patch.HunkCount() {
|
||||
start := s.patch.HunkStartIdx(newHunkIdx)
|
||||
s.selectedLineIdx = s.viewLineIndices[s.patch.GetNextChangeIdx(start)]
|
||||
nextChangeLine := nextNonChangeLine
|
||||
for nextChangeLine >= 0 && !patchLines[nextChangeLine].IsChange() {
|
||||
nextChangeLine--
|
||||
}
|
||||
if nextChangeLine >= 0 {
|
||||
// Now we found a previous hunk, but we're on its last line. Skip to the beginning.
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
switch s.selectMode {
|
||||
case HUNK:
|
||||
start, end := s.CurrentHunkBounds()
|
||||
return s.viewLineIndices[start], s.viewLineIndices[end]
|
||||
return s.selectionRangeForCurrentBlockOfChanges()
|
||||
case RANGE:
|
||||
if s.rangeStartLineIdx > s.selectedLineIdx {
|
||||
return s.selectedLineIdx, s.rangeStartLineIdx
|
||||
|
@ -55,31 +55,13 @@ var SpecificSelection = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
).
|
||||
Press(keys.Main.ToggleSelectHunk).
|
||||
SelectedLines(
|
||||
Contains(`@@ -1,6 +1,6 @@`),
|
||||
Contains(`-1a`),
|
||||
Contains(`+aa`),
|
||||
Contains(` 1b`),
|
||||
Contains(`-1c`),
|
||||
Contains(`+cc`),
|
||||
Contains(` 1d`),
|
||||
Contains(` 1e`),
|
||||
Contains(` 1f`),
|
||||
).
|
||||
PressPrimaryAction().
|
||||
SelectedLines(
|
||||
Contains(`@@ -17,9 +17,9 @@`),
|
||||
Contains(` 1q`),
|
||||
Contains(` 1r`),
|
||||
Contains(` 1s`),
|
||||
Contains(`-1t`),
|
||||
Contains(`-1u`),
|
||||
Contains(`-1v`),
|
||||
Contains(`+tt`),
|
||||
Contains(`+uu`),
|
||||
Contains(`+vv`),
|
||||
Contains(` 1w`),
|
||||
Contains(` 1x`),
|
||||
Contains(` 1y`),
|
||||
Contains(`-1c`),
|
||||
Contains(`+cc`),
|
||||
).
|
||||
Tap(func() {
|
||||
t.Views().Information().Content(Contains("Building patch"))
|
||||
@ -154,8 +136,7 @@ var SpecificSelection = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Contains(`-1a`),
|
||||
Contains(`+aa`),
|
||||
Contains(` 1b`),
|
||||
Contains(`-1c`),
|
||||
Contains(`+cc`),
|
||||
Contains(` 1c`),
|
||||
Contains(` 1d`),
|
||||
Contains(` 1e`),
|
||||
Contains(` 1f`),
|
||||
|
@ -52,67 +52,40 @@ var DiffContextChange = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
IsFocused().
|
||||
Press(keys.Main.ToggleSelectHunk).
|
||||
SelectedLines(
|
||||
Contains(`@@ -1,6 +1,6 @@`),
|
||||
Contains(` 1a`),
|
||||
Contains(` 2a`),
|
||||
Contains(`-3a`),
|
||||
Contains(`+3b`),
|
||||
Contains(` 4a`),
|
||||
Contains(` 5a`),
|
||||
Contains(` 6a`),
|
||||
).
|
||||
Press(keys.Universal.IncreaseContextInDiffView).
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Changed diff context size to 4"))
|
||||
}).
|
||||
SelectedLines(
|
||||
Contains(`@@ -1,7 +1,7 @@`),
|
||||
Contains(` 1a`),
|
||||
Contains(` 2a`),
|
||||
Contains(`-3a`),
|
||||
Contains(`+3b`),
|
||||
Contains(` 4a`),
|
||||
Contains(` 5a`),
|
||||
Contains(` 6a`),
|
||||
Contains(` 7a`),
|
||||
).
|
||||
Press(keys.Universal.DecreaseContextInDiffView).
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Changed diff context size to 3"))
|
||||
}).
|
||||
SelectedLines(
|
||||
Contains(`@@ -1,6 +1,6 @@`),
|
||||
Contains(` 1a`),
|
||||
Contains(` 2a`),
|
||||
Contains(`-3a`),
|
||||
Contains(`+3b`),
|
||||
Contains(` 4a`),
|
||||
Contains(` 5a`),
|
||||
Contains(` 6a`),
|
||||
).
|
||||
Press(keys.Universal.DecreaseContextInDiffView).
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Changed diff context size to 2"))
|
||||
}).
|
||||
SelectedLines(
|
||||
Contains(`@@ -1,5 +1,5 @@`),
|
||||
Contains(` 1a`),
|
||||
Contains(` 2a`),
|
||||
Contains(`-3a`),
|
||||
Contains(`+3b`),
|
||||
Contains(` 4a`),
|
||||
Contains(` 5a`),
|
||||
).
|
||||
Press(keys.Universal.DecreaseContextInDiffView).
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Changed diff context size to 1"))
|
||||
}).
|
||||
SelectedLines(
|
||||
Contains(`@@ -2,3 +2,3 @@`),
|
||||
Contains(` 2a`),
|
||||
Contains(`-3a`),
|
||||
Contains(`+3b`),
|
||||
Contains(` 4a`),
|
||||
).
|
||||
PressPrimaryAction().
|
||||
Press(keys.Universal.TogglePanel)
|
||||
@ -121,18 +94,14 @@ var DiffContextChange = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
IsFocused().
|
||||
Press(keys.Main.ToggleSelectHunk).
|
||||
SelectedLines(
|
||||
Contains(`@@ -2,3 +2,3 @@`),
|
||||
Contains(` 2a`),
|
||||
Contains(`-3a`),
|
||||
Contains(`+3b`),
|
||||
Contains(` 4a`),
|
||||
).
|
||||
Press(keys.Universal.DecreaseContextInDiffView).
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Changed diff context size to 0"))
|
||||
}).
|
||||
SelectedLines(
|
||||
Contains(`@@ -3,1 +3 @@`),
|
||||
Contains(`-3a`),
|
||||
Contains(`+3b`),
|
||||
).
|
||||
@ -141,24 +110,16 @@ var DiffContextChange = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
t.ExpectToast(Equals("Changed diff context size to 1"))
|
||||
}).
|
||||
SelectedLines(
|
||||
Contains(`@@ -2,3 +2,3 @@`),
|
||||
Contains(` 2a`),
|
||||
Contains(`-3a`),
|
||||
Contains(`+3b`),
|
||||
Contains(` 4a`),
|
||||
).
|
||||
Press(keys.Universal.IncreaseContextInDiffView).
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Changed diff context size to 2"))
|
||||
}).
|
||||
SelectedLines(
|
||||
Contains(`@@ -1,5 +1,5 @@`),
|
||||
Contains(` 1a`),
|
||||
Contains(` 2a`),
|
||||
Contains(`-3a`),
|
||||
Contains(`+3b`),
|
||||
Contains(` 4a`),
|
||||
Contains(` 5a`),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
@ -11,11 +11,10 @@ var StageHunks = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
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\n9a\n10a\n11a\n12a\n13a\n14a\n15a")
|
||||
shell.CreateFileAndAdd("file1", "1a\n2a\n3a\n4a\n5a\n6a\n7a\n8a")
|
||||
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:
|
||||
// diff --git a/file1 b/file1
|
||||
@ -29,15 +28,10 @@ var StageHunks = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
// +3b
|
||||
// 4a
|
||||
// 5a
|
||||
// 6a
|
||||
// @@ -10,6 +10,6 @@
|
||||
// 10a
|
||||
// 11a
|
||||
// 12a
|
||||
// -13a
|
||||
// +13b
|
||||
// 14a
|
||||
// 15a
|
||||
// -6a
|
||||
// +6b
|
||||
// 7a
|
||||
// 8a
|
||||
// \ No newline at end of file
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
@ -55,43 +49,23 @@ var StageHunks = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
).
|
||||
Press(keys.Universal.NextBlock).
|
||||
SelectedLines(
|
||||
Contains("-13a"),
|
||||
Contains("-6a"),
|
||||
).
|
||||
Press(keys.Main.ToggleSelectHunk).
|
||||
SelectedLines(
|
||||
Contains("@@ -10,6 +10,6 @@"),
|
||||
Contains(" 10a"),
|
||||
Contains(" 11a"),
|
||||
Contains(" 12a"),
|
||||
Contains("-13a"),
|
||||
Contains("+13b"),
|
||||
Contains(" 14a"),
|
||||
Contains(" 15a"),
|
||||
Contains(`\ No newline at end of file`),
|
||||
Contains("-6a"),
|
||||
Contains("+6b"),
|
||||
).
|
||||
// when in hunk mode, pressing up/down moves us up/down by a hunk
|
||||
SelectPreviousItem().
|
||||
SelectedLines(
|
||||
Contains(`@@ -1,6 +1,6 @@`),
|
||||
Contains(` 1a`),
|
||||
Contains(` 2a`),
|
||||
Contains(`-3a`),
|
||||
Contains(`+3b`),
|
||||
Contains(` 4a`),
|
||||
Contains(` 5a`),
|
||||
Contains(` 6a`),
|
||||
).
|
||||
SelectNextItem().
|
||||
SelectedLines(
|
||||
Contains("@@ -10,6 +10,6 @@"),
|
||||
Contains(" 10a"),
|
||||
Contains(" 11a"),
|
||||
Contains(" 12a"),
|
||||
Contains("-13a"),
|
||||
Contains("+13b"),
|
||||
Contains(" 14a"),
|
||||
Contains(" 15a"),
|
||||
Contains(`\ No newline at end of file`),
|
||||
Contains("-6a"),
|
||||
Contains("+6b"),
|
||||
).
|
||||
// stage the second hunk
|
||||
PressPrimaryAction().
|
||||
@ -102,8 +76,8 @@ var StageHunks = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Tap(func() {
|
||||
t.Views().StagingSecondary().
|
||||
ContainsLines(
|
||||
Contains("-13a"),
|
||||
Contains("+13b"),
|
||||
Contains("-6a"),
|
||||
Contains("+6b"),
|
||||
)
|
||||
}).
|
||||
Press(keys.Universal.TogglePanel)
|
||||
@ -112,11 +86,11 @@ var StageHunks = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
IsFocused().
|
||||
// after toggling panel, we're back to only having selected a single line
|
||||
SelectedLines(
|
||||
Contains("-13a"),
|
||||
Contains("-6a"),
|
||||
).
|
||||
PressPrimaryAction().
|
||||
SelectedLines(
|
||||
Contains("+13b"),
|
||||
Contains("+6b"),
|
||||
).
|
||||
PressPrimaryAction().
|
||||
IsEmpty()
|
||||
@ -128,14 +102,8 @@ var StageHunks = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
).
|
||||
Press(keys.Main.ToggleSelectHunk).
|
||||
SelectedLines(
|
||||
Contains(`@@ -1,6 +1,6 @@`),
|
||||
Contains(` 1a`),
|
||||
Contains(` 2a`),
|
||||
Contains(`-3a`),
|
||||
Contains(`+3b`),
|
||||
Contains(` 4a`),
|
||||
Contains(` 5a`),
|
||||
Contains(` 6a`),
|
||||
).
|
||||
Press(keys.Universal.Remove).
|
||||
Tap(func() {
|
||||
@ -143,15 +111,8 @@ var StageHunks = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
}).
|
||||
Content(DoesNotContain("-3a").DoesNotContain("+3b")).
|
||||
SelectedLines(
|
||||
Contains("@@ -10,6 +10,6 @@"),
|
||||
Contains(" 10a"),
|
||||
Contains(" 11a"),
|
||||
Contains(" 12a"),
|
||||
Contains("-13a"),
|
||||
Contains("+13b"),
|
||||
Contains(" 14a"),
|
||||
Contains(" 15a"),
|
||||
Contains(`\ No newline at end of file`),
|
||||
Contains("-6a"),
|
||||
Contains("+6b"),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
Reference in New Issue
Block a user