mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-07-17 01:42:45 +02:00
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.
This commit is contained in:
@ -72,30 +72,22 @@ func (self *patchPresenter) format() string {
|
|||||||
|
|
||||||
lineIdx++
|
lineIdx++
|
||||||
}
|
}
|
||||||
appendFormattedLine := func(line string, style style.TextStyle) {
|
|
||||||
formattedLine := self.formatLine(
|
|
||||||
line,
|
|
||||||
style,
|
|
||||||
lineIdx,
|
|
||||||
)
|
|
||||||
|
|
||||||
appendLine(formattedLine)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, line := range self.patch.header {
|
for _, line := range self.patch.header {
|
||||||
appendFormattedLine(line, theme.DefaultTextColor.SetBold())
|
// always passing false for 'included' here because header lines are not part of the patch
|
||||||
|
appendLine(self.formatLineAux(line, theme.DefaultTextColor.SetBold(), false))
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, hunk := range self.patch.hunks {
|
for _, hunk := range self.patch.hunks {
|
||||||
appendLine(
|
appendLine(
|
||||||
self.formatLine(
|
self.formatLineAux(
|
||||||
hunk.formatHeaderStart(),
|
hunk.formatHeaderStart(),
|
||||||
style.FgCyan,
|
style.FgCyan,
|
||||||
lineIdx,
|
false,
|
||||||
) +
|
) +
|
||||||
// we're splitting the line into two parts: the diff header and the context
|
// we're splitting the line into two parts: the diff header and the context
|
||||||
// We explicitly pass 'included' as false here so that we're only tagging the
|
// We explicitly pass 'included' as false for both because these are not part
|
||||||
// first half of the line as included if the line is indeed included.
|
// of the actual patch
|
||||||
self.formatLineAux(
|
self.formatLineAux(
|
||||||
hunk.headerContext,
|
hunk.headerContext,
|
||||||
theme.DefaultTextColor,
|
theme.DefaultTextColor,
|
||||||
@ -104,7 +96,12 @@ func (self *patchPresenter) format() string {
|
|||||||
)
|
)
|
||||||
|
|
||||||
for _, line := range hunk.bodyLines {
|
for _, line := range hunk.bodyLines {
|
||||||
appendFormattedLine(line.Content, self.patchLineStyle(line))
|
style := self.patchLineStyle(line)
|
||||||
|
if line.IsChange() {
|
||||||
|
appendLine(self.formatLine(line.Content, style, lineIdx))
|
||||||
|
} else {
|
||||||
|
appendLine(self.formatLineAux(line.Content, style, false))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,15 +115,22 @@ func (self *Patch) HunkContainingLine(idx int) int {
|
|||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the patch line index of the next change (i.e. addition or deletion).
|
// Returns the patch line index of the next change (i.e. addition or deletion)
|
||||||
func (self *Patch) GetNextChangeIdx(idx int) int {
|
// that matches the same "included" state, given the includedLines. If you don't
|
||||||
|
// care about included states, pass nil for includedLines and false for included.
|
||||||
|
func (self *Patch) GetNextChangeIdxOfSameIncludedState(idx int, includedLines []int, included bool) (int, bool) {
|
||||||
idx = lo.Clamp(idx, 0, self.LineCount()-1)
|
idx = lo.Clamp(idx, 0, self.LineCount()-1)
|
||||||
|
|
||||||
lines := self.Lines()
|
lines := self.Lines()
|
||||||
|
|
||||||
|
isMatch := func(i int, line *PatchLine) bool {
|
||||||
|
sameIncludedState := lo.Contains(includedLines, i) == included
|
||||||
|
return line.IsChange() && sameIncludedState
|
||||||
|
}
|
||||||
|
|
||||||
for i, line := range lines[idx:] {
|
for i, line := range lines[idx:] {
|
||||||
if line.isChange() {
|
if isMatch(i+idx, line) {
|
||||||
return i + idx
|
return i + idx, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,13 +138,18 @@ func (self *Patch) GetNextChangeIdx(idx int) int {
|
|||||||
// return the index of the last change
|
// return the index of the last change
|
||||||
for i := len(lines) - 1; i >= 0; i-- {
|
for i := len(lines) - 1; i >= 0; i-- {
|
||||||
line := lines[i]
|
line := lines[i]
|
||||||
if line.isChange() {
|
if isMatch(i, line) {
|
||||||
return i
|
return i, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// should not be possible
|
return 0, false
|
||||||
return 0
|
}
|
||||||
|
|
||||||
|
// Returns the patch line index of the next change (i.e. addition or deletion).
|
||||||
|
func (self *Patch) GetNextChangeIdx(idx int) int {
|
||||||
|
result, _ := self.GetNextChangeIdxOfSameIncludedState(idx, nil, false)
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the length of the patch in lines
|
// Returns the length of the patch in lines
|
||||||
|
@ -124,14 +124,6 @@ func (p *PatchBuilder) RemoveFile(filename string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getIndicesForRange(first, last int) []int {
|
|
||||||
indices := []int{}
|
|
||||||
for i := first; i <= last; i++ {
|
|
||||||
indices = append(indices, i)
|
|
||||||
}
|
|
||||||
return indices
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PatchBuilder) getFileInfo(filename string) (*fileInfo, error) {
|
func (p *PatchBuilder) getFileInfo(filename string) (*fileInfo, error) {
|
||||||
info, ok := p.fileInfoMap[filename]
|
info, ok := p.fileInfoMap[filename]
|
||||||
if ok {
|
if ok {
|
||||||
@ -152,24 +144,24 @@ func (p *PatchBuilder) getFileInfo(filename string) (*fileInfo, error) {
|
|||||||
return info, nil
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PatchBuilder) AddFileLineRange(filename string, firstLineIdx, lastLineIdx int) error {
|
func (p *PatchBuilder) AddFileLineRange(filename string, lineIndices []int) error {
|
||||||
info, err := p.getFileInfo(filename)
|
info, err := p.getFileInfo(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
info.mode = PART
|
info.mode = PART
|
||||||
info.includedLineIndices = lo.Union(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
|
info.includedLineIndices = lo.Union(info.includedLineIndices, lineIndices)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PatchBuilder) RemoveFileLineRange(filename string, firstLineIdx, lastLineIdx int) error {
|
func (p *PatchBuilder) RemoveFileLineRange(filename string, lineIndices []int) error {
|
||||||
info, err := p.getFileInfo(filename)
|
info, err := p.getFileInfo(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
info.mode = PART
|
info.mode = PART
|
||||||
info.includedLineIndices, _ = lo.Difference(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
|
info.includedLineIndices, _ = lo.Difference(info.includedLineIndices, lineIndices)
|
||||||
if len(info.includedLineIndices) == 0 {
|
if len(info.includedLineIndices) == 0 {
|
||||||
p.removeFile(info)
|
p.removeFile(info)
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ type PatchLine struct {
|
|||||||
Content string // something like '+ hello' (note the first character is not removed)
|
Content string // something like '+ hello' (note the first character is not removed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *PatchLine) isChange() bool {
|
func (self *PatchLine) IsChange() bool {
|
||||||
return self.Kind == ADDITION || self.Kind == DELETION
|
return self.Kind == ADDITION || self.Kind == DELETION
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,7 +126,6 @@ func (self *PatchBuildingController) toggleSelection() error {
|
|||||||
self.context().GetMutex().Lock()
|
self.context().GetMutex().Lock()
|
||||||
defer self.context().GetMutex().Unlock()
|
defer self.context().GetMutex().Unlock()
|
||||||
|
|
||||||
toggleFunc := self.c.Git().Patch.PatchBuilder.AddFileLineRange
|
|
||||||
filename := self.c.Contexts().CommitFiles.GetSelectedPath()
|
filename := self.c.Contexts().CommitFiles.GetSelectedPath()
|
||||||
if filename == "" {
|
if filename == "" {
|
||||||
return nil
|
return nil
|
||||||
@ -134,19 +133,26 @@ func (self *PatchBuildingController) toggleSelection() error {
|
|||||||
|
|
||||||
state := self.context().GetState()
|
state := self.context().GetState()
|
||||||
|
|
||||||
|
// Get added/deleted lines in the selected patch range
|
||||||
|
lineIndicesToToggle := state.ChangeLinesInSelectedPatchRange()
|
||||||
|
if len(lineIndicesToToggle) == 0 {
|
||||||
|
// Only context lines or header lines selected, so nothing to do
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
includedLineIndices, err := self.c.Git().Patch.PatchBuilder.GetFileIncLineIndices(filename)
|
includedLineIndices, err := self.c.Git().Patch.PatchBuilder.GetFileIncLineIndices(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
currentLineIsStaged := lo.Contains(includedLineIndices, state.GetSelectedPatchLineIdx())
|
|
||||||
if currentLineIsStaged {
|
toggleFunc := self.c.Git().Patch.PatchBuilder.AddFileLineRange
|
||||||
|
firstSelectedChangeLineIsStaged := lo.Contains(includedLineIndices, lineIndicesToToggle[0])
|
||||||
|
if firstSelectedChangeLineIsStaged {
|
||||||
toggleFunc = self.c.Git().Patch.PatchBuilder.RemoveFileLineRange
|
toggleFunc = self.c.Git().Patch.PatchBuilder.RemoveFileLineRange
|
||||||
}
|
}
|
||||||
|
|
||||||
// add range of lines to those set for the file
|
// add range of lines to those set for the file
|
||||||
firstLineIdx, lastLineIdx := state.SelectedPatchRange()
|
if err := toggleFunc(filename, lineIndicesToToggle); err != nil {
|
||||||
|
|
||||||
if err := toggleFunc(filename, firstLineIdx, lastLineIdx); err != nil {
|
|
||||||
// might actually want to return an error here
|
// might actually want to return an error here
|
||||||
self.c.Log.Error(err)
|
self.c.Log.Error(err)
|
||||||
}
|
}
|
||||||
@ -155,6 +161,8 @@ func (self *PatchBuildingController) toggleSelection() error {
|
|||||||
state.SetLineSelectMode()
|
state.SetLineSelectMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state.SelectNextStageableLineOfSameIncludedState(self.context().GetIncludedLineIndices(), firstSelectedChangeLineIsStaged)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,6 +282,20 @@ func (s *State) SelectedPatchRange() (int, int) {
|
|||||||
return s.patchLineIndices[start], s.patchLineIndices[end]
|
return s.patchLineIndices[start], s.patchLineIndices[end]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns the line indices of the selected patch range that are changes (i.e. additions or deletions)
|
||||||
|
func (s *State) ChangeLinesInSelectedPatchRange() []int {
|
||||||
|
viewStart, viewEnd := s.SelectedViewRange()
|
||||||
|
patchStart, patchEnd := s.patchLineIndices[viewStart], s.patchLineIndices[viewEnd]
|
||||||
|
lines := s.patch.Lines()
|
||||||
|
indices := []int{}
|
||||||
|
for i := patchStart; i <= patchEnd; i++ {
|
||||||
|
if lines[i].IsChange() {
|
||||||
|
indices = append(indices, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return indices
|
||||||
|
}
|
||||||
|
|
||||||
func (s *State) CurrentLineNumber() int {
|
func (s *State) CurrentLineNumber() int {
|
||||||
return s.patch.LineNumberOfLine(s.patchLineIndices[s.selectedLineIdx])
|
return s.patch.LineNumberOfLine(s.patchLineIndices[s.selectedLineIdx])
|
||||||
}
|
}
|
||||||
@ -324,3 +338,11 @@ func wrapPatchLines(diff string, view *gocui.View) ([]int, []int) {
|
|||||||
view.Wrap, view.Editable, strings.TrimSuffix(diff, "\n"), view.InnerWidth(), view.TabWidth)
|
view.Wrap, view.Editable, strings.TrimSuffix(diff, "\n"), view.InnerWidth(), view.TabWidth)
|
||||||
return viewLineIndices, patchLineIndices
|
return viewLineIndices, patchLineIndices
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *State) SelectNextStageableLineOfSameIncludedState(includedLines []int, included bool) {
|
||||||
|
_, lastLineIdx := s.SelectedPatchRange()
|
||||||
|
patchLineIdx, found := s.patch.GetNextChangeIdxOfSameIncludedState(lastLineIdx+1, includedLines, included)
|
||||||
|
if found {
|
||||||
|
s.SelectLine(s.viewLineIndices[patchLineIdx])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -48,7 +48,6 @@ var MoveToIndexPartial = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
Contains(`+third line2`),
|
Contains(`+third line2`),
|
||||||
).
|
).
|
||||||
PressPrimaryAction().
|
PressPrimaryAction().
|
||||||
SelectNextItem().
|
|
||||||
PressPrimaryAction().
|
PressPrimaryAction().
|
||||||
Tap(func() {
|
Tap(func() {
|
||||||
t.Views().Information().Content(Contains("Building patch"))
|
t.Views().Information().Content(Contains("Building patch"))
|
||||||
|
@ -66,18 +66,20 @@ var SpecificSelection = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
Contains(` 1f`),
|
Contains(` 1f`),
|
||||||
).
|
).
|
||||||
PressPrimaryAction().
|
PressPrimaryAction().
|
||||||
// unlike in the staging panel, we don't remove lines from the patch building panel
|
|
||||||
// upon 'adding' them. So the same lines will be selected
|
|
||||||
SelectedLines(
|
SelectedLines(
|
||||||
Contains(`@@ -1,6 +1,6 @@`),
|
Contains(`@@ -17,9 +17,9 @@`),
|
||||||
Contains(`-1a`),
|
Contains(` 1q`),
|
||||||
Contains(`+aa`),
|
Contains(` 1r`),
|
||||||
Contains(` 1b`),
|
Contains(` 1s`),
|
||||||
Contains(`-1c`),
|
Contains(`-1t`),
|
||||||
Contains(`+cc`),
|
Contains(`-1u`),
|
||||||
Contains(` 1d`),
|
Contains(`-1v`),
|
||||||
Contains(` 1e`),
|
Contains(`+tt`),
|
||||||
Contains(` 1f`),
|
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"))
|
||||||
@ -106,12 +108,21 @@ var SpecificSelection = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
Contains("+2a"),
|
Contains("+2a"),
|
||||||
).
|
).
|
||||||
PressPrimaryAction().
|
PressPrimaryAction().
|
||||||
|
SelectedLines(
|
||||||
|
Contains("+2b"),
|
||||||
|
).
|
||||||
NavigateToLine(Contains("+2c")).
|
NavigateToLine(Contains("+2c")).
|
||||||
Press(keys.Universal.ToggleRangeSelect).
|
Press(keys.Universal.ToggleRangeSelect).
|
||||||
NavigateToLine(Contains("+2e")).
|
NavigateToLine(Contains("+2e")).
|
||||||
PressPrimaryAction().
|
PressPrimaryAction().
|
||||||
|
SelectedLines(
|
||||||
|
Contains("+2f"),
|
||||||
|
).
|
||||||
NavigateToLine(Contains("+2g")).
|
NavigateToLine(Contains("+2g")).
|
||||||
PressPrimaryAction().
|
PressPrimaryAction().
|
||||||
|
SelectedLines(
|
||||||
|
Contains("+2h"),
|
||||||
|
).
|
||||||
Tap(func() {
|
Tap(func() {
|
||||||
t.Views().Information().Content(Contains("Building patch"))
|
t.Views().Information().Content(Contains("Building patch"))
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user