Skip to content

Commit 61ff8a4

Browse files
authored
Move to next stageable line when adding a line to a custom patch (#4675)
- **PR Description** While it's true that the behavior is a little different from the staging panel, where the staged lines are actually removed from the view and in many cases the selection stays more or less in the same place, it is still very useful to move to the next stageable thing in the custom patch building view too. Also, we change the visualization of what's included in the patch to mark only the + and - lines of the patch; for all other lines it doesn't make a difference whether they are included. And finally, we make it so that only + and - lines are considered when pressing space; previously it would also look at selected context lines, which doesn't make much sense. This improves the experience for mouse users who like to generously select hunks by dragging across them, including some context lines above and below.
2 parents a8e9859 + 039831a commit 61ff8a4

File tree

8 files changed

+95
-54
lines changed

8 files changed

+95
-54
lines changed

pkg/commands/patch/format.go

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -72,30 +72,22 @@ func (self *patchPresenter) format() string {
7272

7373
lineIdx++
7474
}
75-
appendFormattedLine := func(line string, style style.TextStyle) {
76-
formattedLine := self.formatLine(
77-
line,
78-
style,
79-
lineIdx,
80-
)
81-
82-
appendLine(formattedLine)
83-
}
8475

8576
for _, line := range self.patch.header {
86-
appendFormattedLine(line, theme.DefaultTextColor.SetBold())
77+
// always passing false for 'included' here because header lines are not part of the patch
78+
appendLine(self.formatLineAux(line, theme.DefaultTextColor.SetBold(), false))
8779
}
8880

8981
for _, hunk := range self.patch.hunks {
9082
appendLine(
91-
self.formatLine(
83+
self.formatLineAux(
9284
hunk.formatHeaderStart(),
9385
style.FgCyan,
94-
lineIdx,
86+
false,
9587
) +
9688
// we're splitting the line into two parts: the diff header and the context
97-
// We explicitly pass 'included' as false here so that we're only tagging the
98-
// first half of the line as included if the line is indeed included.
89+
// We explicitly pass 'included' as false for both because these are not part
90+
// of the actual patch
9991
self.formatLineAux(
10092
hunk.headerContext,
10193
theme.DefaultTextColor,
@@ -104,7 +96,12 @@ func (self *patchPresenter) format() string {
10496
)
10597

10698
for _, line := range hunk.bodyLines {
107-
appendFormattedLine(line.Content, self.patchLineStyle(line))
99+
style := self.patchLineStyle(line)
100+
if line.IsChange() {
101+
appendLine(self.formatLine(line.Content, style, lineIdx))
102+
} else {
103+
appendLine(self.formatLineAux(line.Content, style, false))
104+
}
108105
}
109106
}
110107

pkg/commands/patch/patch.go

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -115,29 +115,41 @@ func (self *Patch) HunkContainingLine(idx int) int {
115115
return -1
116116
}
117117

118-
// Returns the patch line index of the next change (i.e. addition or deletion).
119-
func (self *Patch) GetNextChangeIdx(idx int) int {
118+
// Returns the patch line index of the next change (i.e. addition or deletion)
119+
// that matches the same "included" state, given the includedLines. If you don't
120+
// care about included states, pass nil for includedLines and false for included.
121+
func (self *Patch) GetNextChangeIdxOfSameIncludedState(idx int, includedLines []int, included bool) (int, bool) {
120122
idx = lo.Clamp(idx, 0, self.LineCount()-1)
121123

122124
lines := self.Lines()
123125

126+
isMatch := func(i int, line *PatchLine) bool {
127+
sameIncludedState := lo.Contains(includedLines, i) == included
128+
return line.IsChange() && sameIncludedState
129+
}
130+
124131
for i, line := range lines[idx:] {
125-
if line.isChange() {
126-
return i + idx
132+
if isMatch(i+idx, line) {
133+
return i + idx, true
127134
}
128135
}
129136

130137
// there are no changes from the cursor onwards so we'll instead
131138
// return the index of the last change
132139
for i := len(lines) - 1; i >= 0; i-- {
133140
line := lines[i]
134-
if line.isChange() {
135-
return i
141+
if isMatch(i, line) {
142+
return i, true
136143
}
137144
}
138145

139-
// should not be possible
140-
return 0
146+
return 0, false
147+
}
148+
149+
// Returns the patch line index of the next change (i.e. addition or deletion).
150+
func (self *Patch) GetNextChangeIdx(idx int) int {
151+
result, _ := self.GetNextChangeIdxOfSameIncludedState(idx, nil, false)
152+
return result
141153
}
142154

143155
// Returns the length of the patch in lines

pkg/commands/patch/patch_builder.go

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -124,14 +124,6 @@ func (p *PatchBuilder) RemoveFile(filename string) error {
124124
return nil
125125
}
126126

127-
func getIndicesForRange(first, last int) []int {
128-
indices := []int{}
129-
for i := first; i <= last; i++ {
130-
indices = append(indices, i)
131-
}
132-
return indices
133-
}
134-
135127
func (p *PatchBuilder) getFileInfo(filename string) (*fileInfo, error) {
136128
info, ok := p.fileInfoMap[filename]
137129
if ok {
@@ -152,24 +144,24 @@ func (p *PatchBuilder) getFileInfo(filename string) (*fileInfo, error) {
152144
return info, nil
153145
}
154146

155-
func (p *PatchBuilder) AddFileLineRange(filename string, firstLineIdx, lastLineIdx int) error {
147+
func (p *PatchBuilder) AddFileLineRange(filename string, lineIndices []int) error {
156148
info, err := p.getFileInfo(filename)
157149
if err != nil {
158150
return err
159151
}
160152
info.mode = PART
161-
info.includedLineIndices = lo.Union(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
153+
info.includedLineIndices = lo.Union(info.includedLineIndices, lineIndices)
162154

163155
return nil
164156
}
165157

166-
func (p *PatchBuilder) RemoveFileLineRange(filename string, firstLineIdx, lastLineIdx int) error {
158+
func (p *PatchBuilder) RemoveFileLineRange(filename string, lineIndices []int) error {
167159
info, err := p.getFileInfo(filename)
168160
if err != nil {
169161
return err
170162
}
171163
info.mode = PART
172-
info.includedLineIndices, _ = lo.Difference(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
164+
info.includedLineIndices, _ = lo.Difference(info.includedLineIndices, lineIndices)
173165
if len(info.includedLineIndices) == 0 {
174166
p.removeFile(info)
175167
}

pkg/commands/patch/patch_line.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ type PatchLine struct {
1818
Content string // something like '+ hello' (note the first character is not removed)
1919
}
2020

21-
func (self *PatchLine) isChange() bool {
21+
func (self *PatchLine) IsChange() bool {
2222
return self.Kind == ADDITION || self.Kind == DELETION
2323
}
2424

pkg/gui/controllers/patch_building_controller.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,27 +126,33 @@ func (self *PatchBuildingController) toggleSelection() error {
126126
self.context().GetMutex().Lock()
127127
defer self.context().GetMutex().Unlock()
128128

129-
toggleFunc := self.c.Git().Patch.PatchBuilder.AddFileLineRange
130129
filename := self.c.Contexts().CommitFiles.GetSelectedPath()
131130
if filename == "" {
132131
return nil
133132
}
134133

135134
state := self.context().GetState()
136135

136+
// Get added/deleted lines in the selected patch range
137+
lineIndicesToToggle := state.ChangeLinesInSelectedPatchRange()
138+
if len(lineIndicesToToggle) == 0 {
139+
// Only context lines or header lines selected, so nothing to do
140+
return nil
141+
}
142+
137143
includedLineIndices, err := self.c.Git().Patch.PatchBuilder.GetFileIncLineIndices(filename)
138144
if err != nil {
139145
return err
140146
}
141-
currentLineIsStaged := lo.Contains(includedLineIndices, state.GetSelectedPatchLineIdx())
142-
if currentLineIsStaged {
147+
148+
toggleFunc := self.c.Git().Patch.PatchBuilder.AddFileLineRange
149+
firstSelectedChangeLineIsStaged := lo.Contains(includedLineIndices, lineIndicesToToggle[0])
150+
if firstSelectedChangeLineIsStaged {
143151
toggleFunc = self.c.Git().Patch.PatchBuilder.RemoveFileLineRange
144152
}
145153

146154
// add range of lines to those set for the file
147-
firstLineIdx, lastLineIdx := state.SelectedPatchRange()
148-
149-
if err := toggleFunc(filename, firstLineIdx, lastLineIdx); err != nil {
155+
if err := toggleFunc(filename, lineIndicesToToggle); err != nil {
150156
// might actually want to return an error here
151157
self.c.Log.Error(err)
152158
}
@@ -155,6 +161,8 @@ func (self *PatchBuildingController) toggleSelection() error {
155161
state.SetLineSelectMode()
156162
}
157163

164+
state.SelectNextStageableLineOfSameIncludedState(self.context().GetIncludedLineIndices(), firstSelectedChangeLineIsStaged)
165+
158166
return nil
159167
}
160168

pkg/gui/patch_exploring/state.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,20 @@ func (s *State) SelectedPatchRange() (int, int) {
282282
return s.patchLineIndices[start], s.patchLineIndices[end]
283283
}
284284

285+
// Returns the line indices of the selected patch range that are changes (i.e. additions or deletions)
286+
func (s *State) ChangeLinesInSelectedPatchRange() []int {
287+
viewStart, viewEnd := s.SelectedViewRange()
288+
patchStart, patchEnd := s.patchLineIndices[viewStart], s.patchLineIndices[viewEnd]
289+
lines := s.patch.Lines()
290+
indices := []int{}
291+
for i := patchStart; i <= patchEnd; i++ {
292+
if lines[i].IsChange() {
293+
indices = append(indices, i)
294+
}
295+
}
296+
return indices
297+
}
298+
285299
func (s *State) CurrentLineNumber() int {
286300
return s.patch.LineNumberOfLine(s.patchLineIndices[s.selectedLineIdx])
287301
}
@@ -324,3 +338,11 @@ func wrapPatchLines(diff string, view *gocui.View) ([]int, []int) {
324338
view.Wrap, view.Editable, strings.TrimSuffix(diff, "\n"), view.InnerWidth(), view.TabWidth)
325339
return viewLineIndices, patchLineIndices
326340
}
341+
342+
func (s *State) SelectNextStageableLineOfSameIncludedState(includedLines []int, included bool) {
343+
_, lastLineIdx := s.SelectedPatchRange()
344+
patchLineIdx, found := s.patch.GetNextChangeIdxOfSameIncludedState(lastLineIdx+1, includedLines, included)
345+
if found {
346+
s.SelectLine(s.viewLineIndices[patchLineIdx])
347+
}
348+
}

pkg/integration/tests/patch_building/move_to_index_partial.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ var MoveToIndexPartial = NewIntegrationTest(NewIntegrationTestArgs{
4848
Contains(`+third line2`),
4949
).
5050
PressPrimaryAction().
51-
SelectNextItem().
5251
PressPrimaryAction().
5352
Tap(func() {
5453
t.Views().Information().Content(Contains("Building patch"))

pkg/integration/tests/patch_building/specific_selection.go

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,18 +66,20 @@ var SpecificSelection = NewIntegrationTest(NewIntegrationTestArgs{
6666
Contains(` 1f`),
6767
).
6868
PressPrimaryAction().
69-
// unlike in the staging panel, we don't remove lines from the patch building panel
70-
// upon 'adding' them. So the same lines will be selected
7169
SelectedLines(
72-
Contains(`@@ -1,6 +1,6 @@`),
73-
Contains(`-1a`),
74-
Contains(`+aa`),
75-
Contains(` 1b`),
76-
Contains(`-1c`),
77-
Contains(`+cc`),
78-
Contains(` 1d`),
79-
Contains(` 1e`),
80-
Contains(` 1f`),
70+
Contains(`@@ -17,9 +17,9 @@`),
71+
Contains(` 1q`),
72+
Contains(` 1r`),
73+
Contains(` 1s`),
74+
Contains(`-1t`),
75+
Contains(`-1u`),
76+
Contains(`-1v`),
77+
Contains(`+tt`),
78+
Contains(`+uu`),
79+
Contains(`+vv`),
80+
Contains(` 1w`),
81+
Contains(` 1x`),
82+
Contains(` 1y`),
8183
).
8284
Tap(func() {
8385
t.Views().Information().Content(Contains("Building patch"))
@@ -106,12 +108,21 @@ var SpecificSelection = NewIntegrationTest(NewIntegrationTestArgs{
106108
Contains("+2a"),
107109
).
108110
PressPrimaryAction().
111+
SelectedLines(
112+
Contains("+2b"),
113+
).
109114
NavigateToLine(Contains("+2c")).
110115
Press(keys.Universal.ToggleRangeSelect).
111116
NavigateToLine(Contains("+2e")).
112117
PressPrimaryAction().
118+
SelectedLines(
119+
Contains("+2f"),
120+
).
113121
NavigateToLine(Contains("+2g")).
114122
PressPrimaryAction().
123+
SelectedLines(
124+
Contains("+2h"),
125+
).
115126
Tap(func() {
116127
t.Views().Information().Content(Contains("Building patch"))
117128

0 commit comments

Comments
 (0)