mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-10-05 21:46:49 -04:00
Keep both ends of a selected range across a re-render
A range or hunk selection covers a stretch of the diff, and restoring only the cursor left the other end pointing at whatever line the new rendering happened to put there — with more context lines above, a selected hunk would grow a tail of context it never covered. So the other end is remembered by identity too, and put back before the cursor. If it is a line the re-render dropped, the selection is left as the single line we landed on: there is no telling which line inherits a selection's edge, and a wrong guess acts on lines the user never chose. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b49aaef0db
commit
8f25e160e8
@@ -637,6 +637,13 @@ func (v *View) CancelRangeSelect() {
|
||||
v.rangeSelectStartY = -1
|
||||
}
|
||||
|
||||
// HasRangeSelect reports whether a range selection is anchored, as opposed to the
|
||||
// view showing a plain cursor. A range whose ends are on the same view line is still
|
||||
// one, which SelectedLineRange alone can't tell you.
|
||||
func (v *View) HasRangeSelect() bool {
|
||||
return v.rangeSelectStartY != -1
|
||||
}
|
||||
|
||||
func calculateNewOrigin(selectedLine int, oldOrigin int, lineCount int, viewHeight int) int {
|
||||
if viewHeight >= lineCount {
|
||||
return 0
|
||||
|
||||
@@ -32,53 +32,56 @@ func NewDiffLineHelper(c *HelperCommon) *DiffLineHelper {
|
||||
// ok is false when the row's identity can't be recovered, in which case the
|
||||
// caller must not act on the line at all.
|
||||
func (self *DiffLineHelper) GetDiffLineInfo(view *gocui.View, viewLineIdx int) (types.DiffLineInfo, bool) {
|
||||
// The cursor and clicks land on a view line, which counts wrapped segments;
|
||||
// the contents are indexed by unwrapped buffer line.
|
||||
bufferLineIdx, ok := view.BufferLineForViewLine(viewLineIdx)
|
||||
identities, ok := self.diffLineIdentitiesAt(view, viewLineIdx)
|
||||
if !ok {
|
||||
return types.DiffLineInfo{}, false
|
||||
}
|
||||
|
||||
contents := view.DiffLineContents()
|
||||
if bufferLineIdx >= len(contents) {
|
||||
return types.DiffLineInfo{}, false
|
||||
}
|
||||
|
||||
if renderingStatesDiffLines(contents) {
|
||||
if info, ok := self.diffLineInfoFromRecords(contents[bufferLineIdx].Metadata); ok {
|
||||
return info, true
|
||||
}
|
||||
return types.DiffLineInfo{}, false
|
||||
}
|
||||
|
||||
parsed, ok := parseDiffLineFromBuffer(diffLineTexts(contents), bufferLineIdx)
|
||||
if !ok {
|
||||
return types.DiffLineInfo{}, false
|
||||
}
|
||||
|
||||
return self.diffLineInfo(parsed), true
|
||||
}
|
||||
|
||||
// diffLineInfoFromRecords recovers a row's identity from the records the diff
|
||||
// renderer stated for it. ok is false when the row carries no record we understand.
|
||||
//
|
||||
// A row can carry more than one record, when the rendering puts two diff lines on it
|
||||
// (a side-by-side row shows a deletion and the addition replacing it); the leftmost
|
||||
// is the one a reader would call the row's own, so it is the row's identity.
|
||||
func (self *DiffLineHelper) diffLineInfoFromRecords(metadata []string) (types.DiffLineInfo, bool) {
|
||||
identities := self.diffLineIdentitiesFromRecords(metadata)
|
||||
if len(identities) == 0 {
|
||||
return types.DiffLineInfo{}, false
|
||||
}
|
||||
return identities[0], true
|
||||
}
|
||||
|
||||
// diffLineIdentitiesAt recovers every diff line the row at the given (wrapped) view
|
||||
// line shows, left to right. It is GetDiffLineInfo's form for a reader that can't
|
||||
// settle for the line the row leads with: an end of a selection covers its whole
|
||||
// row, so where a rendering puts a modification's two halves side by side it covers
|
||||
// both of them. ok is false when the row's identity can't be recovered at all.
|
||||
func (self *DiffLineHelper) diffLineIdentitiesAt(
|
||||
view *gocui.View, viewLineIdx int,
|
||||
) ([]types.DiffLineInfo, bool) {
|
||||
// The cursor and clicks land on a view line, which counts wrapped segments;
|
||||
// the contents are indexed by unwrapped buffer line.
|
||||
bufferLineIdx, ok := view.BufferLineForViewLine(viewLineIdx)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
contents := view.DiffLineContents()
|
||||
if bufferLineIdx >= len(contents) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if renderingStatesDiffLines(contents) {
|
||||
if identities := self.diffLineIdentitiesFromRecords(contents[bufferLineIdx].Metadata); len(identities) > 0 {
|
||||
return identities, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
parsed, ok := parseDiffLineFromBuffer(diffLineTexts(contents), bufferLineIdx)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return []types.DiffLineInfo{self.diffLineInfo(parsed)}, true
|
||||
}
|
||||
|
||||
// diffLineIdentitiesFromRecords recovers the identity of every diff line the row's
|
||||
// records state, left to right. Which of them a reader is after depends on the
|
||||
// reader: the one the row leads with is the row's own identity (see
|
||||
// diffLineInfoFromRecords), while a reader looking for a particular line has to
|
||||
// consider them all, since which of a modification's two halves leads a row is up to
|
||||
// the rendering.
|
||||
// records state, left to right. A row can carry more than one record, when the
|
||||
// rendering puts two diff lines on it (a side-by-side row shows a deletion and the
|
||||
// addition replacing it). Which of them a reader is after depends on the reader: the
|
||||
// one the row leads with is the row's own identity (see GetDiffLineInfo and
|
||||
// resolveDiffLines), while a reader looking for a particular line has to consider
|
||||
// them all, since which of a modification's two halves leads a row is up to the
|
||||
// rendering.
|
||||
func (self *DiffLineHelper) diffLineIdentitiesFromRecords(metadata []string) []types.DiffLineInfo {
|
||||
return self.diffLineInfos(parseDiffLineRecords(metadata))
|
||||
}
|
||||
|
||||
@@ -26,19 +26,22 @@ type diffLineAnchor struct {
|
||||
// window where both are being rendered again, since either of them may hold the diff
|
||||
// being read; a pane that isn't showing is left alone.
|
||||
//
|
||||
// The line to keep is the selected one, and the middle visible line when there is no
|
||||
// selection or it has been scrolled out of sight — what the user is looking at, rather
|
||||
// than the view's top edge or a selection they have long since left behind. It may not
|
||||
// survive the re-render: a context line goes when the context size shrinks, and a
|
||||
// whole hunk or file goes when whitespace stops counting. So the lines around it come
|
||||
// along as fallbacks and the view lands on the nearest one that is still there, put
|
||||
// back on the screen row it was on. With none of them left — and with a renderer that
|
||||
// says nothing about its rows there is nothing to look for in the first place — the
|
||||
// view keeps the scroll offset it had, which is still nearer to what was being read
|
||||
// than the top of the diff.
|
||||
// The line to keep is the end of the selection that is on screen, and the middle
|
||||
// visible line when there is no selection or the whole of it has been scrolled out of
|
||||
// sight — what the user is looking at, rather than the view's top edge or a selection
|
||||
// they have long since left behind. It may not survive the re-render: a context line
|
||||
// goes when the context size shrinks, and a whole hunk or file goes when whitespace
|
||||
// stops counting. So the lines around it come along as fallbacks and the view lands on
|
||||
// the nearest one that is still there, put back on the screen row it was on. With none
|
||||
// of them left — and with a renderer that says nothing about its rows there is nothing
|
||||
// to look for in the first place — the view keeps the scroll offset it had, which is
|
||||
// still nearer to what was being read than the top of the diff.
|
||||
//
|
||||
// An off-screen selection is still put back on the diff line it was on, wherever the
|
||||
// new rendering has that; it is only the view that stays where it is.
|
||||
//
|
||||
// A range or hunk selection has a second end, which is remembered the same way, so
|
||||
// that it still covers the same lines of the diff afterwards.
|
||||
func (self *DiffLineHelper) PreserveDiffPositionOnRerender(view *gocui.View) {
|
||||
// A view that isn't the one its window is currently showing — the merge-conflicts
|
||||
// view takes the main window over — isn't the one about to be re-rendered, so a
|
||||
@@ -57,17 +60,20 @@ func (self *DiffLineHelper) PreserveDiffPositionOnRerender(view *gocui.View) {
|
||||
}
|
||||
|
||||
showSelection := view.Highlight
|
||||
selectionOnScreen := showSelection && view.IsLineVisible(view.SelectedLineIdx())
|
||||
anchorViewLine := view.MiddleVisibleLineIdx()
|
||||
if selectionOnScreen {
|
||||
anchorViewLine = view.SelectedLineIdx()
|
||||
}
|
||||
// A selection that has been scrolled away from is put back by its own lines rather
|
||||
farEnd, hasFarEnd := types.DiffLineInfo{}, false
|
||||
// A cursor that has been scrolled away from is put back by its own lines rather
|
||||
// than by the anchor's, so that it comes out on the same line of the diff without
|
||||
// the view having to go there.
|
||||
var selectionCandidates []diffLineAnchor
|
||||
if showSelection && !selectionOnScreen {
|
||||
selectionCandidates = self.nearbyDiffLines(view, view.SelectedLineIdx())
|
||||
var cursorCandidates []diffLineAnchor
|
||||
if showSelection {
|
||||
farEnd, hasFarEnd = self.selectionFarEndIdentity(view)
|
||||
if end, ok := visibleSelectionEnd(view); ok {
|
||||
anchorViewLine = end
|
||||
}
|
||||
if anchorViewLine != view.SelectedLineIdx() {
|
||||
cursorCandidates = self.nearbyDiffLines(view, view.SelectedLineIdx())
|
||||
}
|
||||
}
|
||||
|
||||
self.restoreDiffLinePositionOnRerender(view, self.nearbyDiffLines(view, anchorViewLine),
|
||||
@@ -77,16 +83,63 @@ func (self *DiffLineHelper) PreserveDiffPositionOnRerender(view *gocui.View) {
|
||||
row := lo.Clamp(anchor.row, 0, max(0, view.InnerHeight()-1))
|
||||
view.SetOrigin(0, max(0, viewLine-row))
|
||||
if showSelection {
|
||||
// A range's other end is a view line, which the new rendering has made
|
||||
// mean something else, so the selection collapses to the line we landed
|
||||
// on. The origin is already where it should be, so moving the cursor
|
||||
// there mustn't scroll.
|
||||
// Put the far end back before the cursor, so that the selection covers
|
||||
// the same lines again; a selection whose far end didn't survive the
|
||||
// re-render is left as the single line we landed on. The origin is
|
||||
// already where it should be, so moving the cursor mustn't scroll.
|
||||
view.CancelRangeSelect()
|
||||
view.FocusPoint(0, self.selectionLine(view, selectionCandidates, viewLine), false)
|
||||
cursorViewLine := self.selectionLine(view, cursorCandidates, viewLine)
|
||||
if hasFarEnd {
|
||||
if farEndViewLine, ok := self.findDiffLine(view, farEnd); ok {
|
||||
cursorViewLine, farEndViewLine = coverWholeLines(view, cursorViewLine, farEndViewLine)
|
||||
view.SetRangeSelectStart(farEndViewLine)
|
||||
}
|
||||
}
|
||||
view.FocusPoint(0, cursorViewLine, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// coverWholeLines moves the two ends of a restored selection out to the edges of the
|
||||
// diff lines they are on, so that the selection covers those lines whole. Both ends
|
||||
// arrive on the first view line of their diff line, which is where looking one up by
|
||||
// identity lands, and the view draws a line it wraps as several — of which a
|
||||
// selection of that line means all.
|
||||
func coverWholeLines(view *gocui.View, cursorViewLine int, farEndViewLine int) (int, int) {
|
||||
if cursorViewLine <= farEndViewLine {
|
||||
return cursorViewLine, lastViewLineOfSameDiffLine(view, farEndViewLine)
|
||||
}
|
||||
return lastViewLineOfSameDiffLine(view, cursorViewLine), farEndViewLine
|
||||
}
|
||||
|
||||
// lastViewLineOfSameDiffLine returns the last view line showing the same line of the
|
||||
// diff as the given one, which is that line itself unless the view wrapped it.
|
||||
func lastViewLineOfSameDiffLine(view *gocui.View, viewLine int) int {
|
||||
bufferLine, ok := view.BufferLineForViewLine(viewLine)
|
||||
if !ok {
|
||||
return viewLine
|
||||
}
|
||||
if last, ok := view.LastViewLineForBufferLine(bufferLine); ok {
|
||||
return last
|
||||
}
|
||||
return viewLine
|
||||
}
|
||||
|
||||
// visibleSelectionEnd returns the end of the selection to keep in place across a
|
||||
// re-render: the selected line when it is on screen, and the range's other end when
|
||||
// that is and the selected line isn't — a range can be long enough for the user to be
|
||||
// looking at one end of it with the other far away. ok is false when the whole
|
||||
// selection is off screen, and there is nothing of it to keep in place.
|
||||
func visibleSelectionEnd(view *gocui.View) (int, bool) {
|
||||
if view.IsLineVisible(view.SelectedLineIdx()) {
|
||||
return view.SelectedLineIdx(), true
|
||||
}
|
||||
if farEnd, _, ok := selectionFarEndViewLine(view); ok && view.IsLineVisible(farEnd) {
|
||||
return farEnd, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// selectionLine returns the line to put the cursor on once a re-render is on screen:
|
||||
// the line the position anchor landed on, which is the selected one whenever it was
|
||||
// on screen, and otherwise the nearest surviving line to where the selection was —
|
||||
@@ -108,6 +161,60 @@ func (self *DiffLineHelper) selectionLine(
|
||||
return anchorViewLine
|
||||
}
|
||||
|
||||
// selectionFarEndIdentity returns the identity of the end of a range or hunk
|
||||
// selection the cursor isn't on, so that a re-render can put it back. ok is false for
|
||||
// a selection that is only a cursor, where restoring that is the whole job, and for
|
||||
// an end that resolves to no diff line.
|
||||
//
|
||||
// An end covers the whole of its row, so where the row shows more than one diff line
|
||||
// — a rendering that puts a modification's two halves side by side, or a word diff
|
||||
// that puts both on the one line it changed — the end takes the outermost of them:
|
||||
// the last for the range's lower end and the first for its upper one. Otherwise a
|
||||
// rendering that splits them apart again would get back only the half the row led
|
||||
// with, and half a change selected where a whole one was.
|
||||
func (self *DiffLineHelper) selectionFarEndIdentity(view *gocui.View) (types.DiffLineInfo, bool) {
|
||||
farEnd, isLowerEnd, ok := selectionFarEndViewLine(view)
|
||||
if !ok {
|
||||
return types.DiffLineInfo{}, false
|
||||
}
|
||||
identities, ok := self.diffLineIdentitiesAt(view, farEnd)
|
||||
if !ok {
|
||||
return types.DiffLineInfo{}, false
|
||||
}
|
||||
if isLowerEnd {
|
||||
return identities[len(identities)-1], true
|
||||
}
|
||||
return identities[0], true
|
||||
}
|
||||
|
||||
// selectionFarEndViewLine returns the view line of the end of a range or hunk
|
||||
// selection the cursor isn't on, and whether that is the lower of the two ends. ok
|
||||
// is false when there is no range at all, only a cursor.
|
||||
//
|
||||
// A range whose two ends are on the same view line still has one, and is not the
|
||||
// same thing as a cursor sitting there: it covers everything that row shows, which
|
||||
// may be two lines of the diff at once.
|
||||
func selectionFarEndViewLine(view *gocui.View) (int, bool, bool) {
|
||||
if !view.HasRangeSelect() {
|
||||
return 0, false, false
|
||||
}
|
||||
first, last := view.SelectedLineRange()
|
||||
if view.SelectedLineIdx() == first {
|
||||
return last, true, true
|
||||
}
|
||||
return first, false, true
|
||||
}
|
||||
|
||||
// findDiffLine returns the view line showing the given diff line in what view is
|
||||
// displaying now, for placing a remembered line once the re-render is on screen.
|
||||
func (self *DiffLineHelper) findDiffLine(view *gocui.View, identity types.DiffLineInfo) (int, bool) {
|
||||
bufferLine, ok := self.patchLineRows(view.DiffLineContents())[patchLineOf(identity)]
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
return view.ViewLineForBufferLine(bufferLine)
|
||||
}
|
||||
|
||||
// restoreDiffLinePositionOnRerender arranges for view's next re-render to land on the
|
||||
// first of the given candidate lines the new rendering still has, calling place with
|
||||
// that candidate and the view line it ended up on. The candidates are in priority
|
||||
|
||||
@@ -300,6 +300,21 @@ func (self *ViewDriver) SelectedLines(matchers ...*TextMatcher) *ViewDriver {
|
||||
return self
|
||||
}
|
||||
|
||||
// SelectedViewLineRange asserts which view lines the selection covers. View lines
|
||||
// count the wrapped segments a line is drawn as, so this can say whether a selection
|
||||
// covers a wrapped line to its end; SelectedLines, which reports the lines of the
|
||||
// content, cannot.
|
||||
func (self *ViewDriver) SelectedViewLineRange(first int, last int) *ViewDriver {
|
||||
self.t.assertWithRetries(func() (bool, string) {
|
||||
actualFirst, actualLast := self.getSelectedRange()
|
||||
return actualFirst == first && actualLast == last,
|
||||
fmt.Sprintf("%s: Expected view lines %d-%d to be selected, but %d-%d were.",
|
||||
self.context, first, last, actualFirst, actualLast)
|
||||
})
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *ViewDriver) validateMatchersPassed(matchers []*TextMatcher) {
|
||||
if len(matchers) < 1 {
|
||||
self.t.fail("'Lines' methods require at least one matcher to be passed as an argument. If you are trying to assert that there are no lines, use .IsEmpty()")
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package main_view
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var KeepAWrappedLineCoveredAcrossARerender = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "A selection over a line too long for the view still covers all of it after a re-render",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
Width: 80,
|
||||
Height: 20,
|
||||
SetupConfig: func(cfg *config.AppConfig) {
|
||||
cfg.GetUserConfig().Gui.UseHunkModeInStagingView = true
|
||||
},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
long := strings.Repeat("word ", 40)
|
||||
lines := make([]string, 20)
|
||||
for i := range lines {
|
||||
lines[i] = fmt.Sprintf("line%02d", i+1)
|
||||
}
|
||||
before := strings.Join(lines[:10], "\n") + "\n"
|
||||
after := strings.Join(lines[10:], "\n") + "\n"
|
||||
|
||||
shell.CreateFileAndAdd("file1", before+long+"\n"+after)
|
||||
shell.Commit("one")
|
||||
|
||||
shell.UpdateFile("file1", before+"CHANGED "+long+"\n"+after)
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Press(keys.Universal.FocusMainView)
|
||||
|
||||
// The changed line is far too long for the view, so each half of the change
|
||||
// is drawn as several view lines, and hunk mode selects all of them.
|
||||
t.Views().Main().
|
||||
IsFocused().
|
||||
SelectedLines(
|
||||
Contains("-word word"),
|
||||
Contains("+CHANGED word"),
|
||||
).
|
||||
SelectedViewLineRange(8, 16).
|
||||
// The same two lines of the diff, wrapped the same way, are still covered
|
||||
// to their ends once the diff has been rendered again.
|
||||
Press(keys.Universal.IncreaseContextInDiffView).
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Changed diff context size to 4"))
|
||||
}).
|
||||
SelectedLines(
|
||||
Contains("-word word"),
|
||||
Contains("+CHANGED word"),
|
||||
).
|
||||
SelectedViewLineRange(9, 17)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,68 @@
|
||||
package main_view
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var KeepBothHalvesOfAChangeSelected = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "A change selected on the one row a renderer draws it as is selected on both rows of a renderer that splits it",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(cfg *config.AppConfig) {
|
||||
cfg.GetUserConfig().Gui.UseHunkModeInStagingView = true
|
||||
cfg.GetUserConfig().Git.DiffRenderers = []config.DiffRendererConfig{
|
||||
// Git's own diff, which has a row for each half of a change. It announces
|
||||
// the metadata protocol, so lazygit acts on its output rather than
|
||||
// replacing it; it states no records of its own, so the rows are located
|
||||
// by parsing the text, which still looks like a diff.
|
||||
{Name: "unified", Command: `printf '\033]1717;1\007'; cat`},
|
||||
// A renderer that puts the two halves of a change beside each other on one
|
||||
// row. Only the records it states can say where those halves are; parsing
|
||||
// the text could not. It ignores its input and prints this one.
|
||||
{Name: "columns", Command: `printf '\033]1717;1\007'; ` +
|
||||
`printf '\033]1717;1;f;;;file1\007file1\n'; ` +
|
||||
`printf '\033]1717;1;h;1;;file1\007@@\n'; ` +
|
||||
`printf '\033]1717;1;c;1;;file1\007one one\n'; ` +
|
||||
`printf '\033]1717;1;d;2;2;file1\007two \033]1717;1;a;2;;file1\007TWO\n'; ` +
|
||||
`printf '\033]1717;1;c;3;;file1\007three three\n'; ` +
|
||||
`cat >/dev/null`},
|
||||
}
|
||||
},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.CreateFileAndAdd("file1", "one\ntwo\nthree\n")
|
||||
shell.Commit("one")
|
||||
|
||||
shell.UpdateFile("file1", "one\nTWO\nthree\n")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Press(keys.Universal.FocusMainView)
|
||||
|
||||
t.Views().Main().
|
||||
IsFocused().
|
||||
SelectedLines(
|
||||
Contains("-two"),
|
||||
Contains("+TWO"),
|
||||
).
|
||||
// The change is one row here, and selecting it selects that row: both
|
||||
// halves are on it.
|
||||
Press(keys.Universal.CycleDiffRenderers).
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Diff renderer: columns (2 of 2)"))
|
||||
}).
|
||||
SelectedLines(
|
||||
Contains("two TWO"),
|
||||
).
|
||||
// Split apart again, the same change is the same two lines it was.
|
||||
Press(keys.Universal.CycleDiffRenderers).
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Diff renderer: unified (1 of 2)"))
|
||||
}).
|
||||
SelectedLines(
|
||||
Contains("-two"),
|
||||
Contains("+TWO"),
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,102 @@
|
||||
package main_view
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var KeepPositionByTheVisibleEndOfASelection = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "A re-render keeps the place by the end of a selected hunk that is on screen when its other end isn't",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
Width: 120,
|
||||
Height: 30,
|
||||
SetupConfig: func(cfg *config.AppConfig) {
|
||||
cfg.GetUserConfig().Gui.UseHunkModeInStagingView = true
|
||||
// One line per scroll, so that the test can put the top of the view exactly
|
||||
// where it wants it.
|
||||
cfg.GetUserConfig().Gui.ScrollHeight = 1
|
||||
},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
lines := make([]string, 60)
|
||||
for i := range lines {
|
||||
lines[i] = fmt.Sprintf("line%02d", i+1)
|
||||
}
|
||||
shell.CreateFileAndAdd("file1", strings.Join(lines, "\n")+"\n")
|
||||
shell.Commit("one")
|
||||
|
||||
// A first change tall enough to be scrolled halfway out of the view, and more
|
||||
// of them below it, so that a context-size change moves the lines further down
|
||||
// the diff by more than it moves the first change.
|
||||
for _, i := range []int{10, 11, 12, 13, 14, 15, 30, 45} {
|
||||
lines[i-1] = strings.ToUpper(lines[i-1])
|
||||
}
|
||||
shell.UpdateFile("file1", strings.Join(lines, "\n")+"\n")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Press(keys.Universal.FocusMainView)
|
||||
|
||||
t.Views().Main().
|
||||
IsFocused().
|
||||
SelectedLines(
|
||||
Contains("-line10"),
|
||||
Contains("-line11"),
|
||||
Contains("-line12"),
|
||||
Contains("-line13"),
|
||||
Contains("-line14"),
|
||||
Contains("-line15"),
|
||||
Contains("+LINE10"),
|
||||
Contains("+LINE11"),
|
||||
Contains("+LINE12"),
|
||||
Contains("+LINE13"),
|
||||
Contains("+LINE14"),
|
||||
Contains("+LINE15"),
|
||||
).
|
||||
SelectedLineIdx(8).
|
||||
// Scroll past the start of the selected block, leaving its last lines on
|
||||
// screen and the cursor above the top of the view.
|
||||
Press(keys.Universal.ScrollDownMain).
|
||||
Press(keys.Universal.ScrollDownMain).
|
||||
Press(keys.Universal.ScrollDownMain).
|
||||
Press(keys.Universal.ScrollDownMain).
|
||||
Press(keys.Universal.ScrollDownMain).
|
||||
Press(keys.Universal.ScrollDownMain).
|
||||
Press(keys.Universal.ScrollDownMain).
|
||||
Press(keys.Universal.ScrollDownMain).
|
||||
Press(keys.Universal.ScrollDownMain).
|
||||
Press(keys.Universal.ScrollDownMain).
|
||||
Press(keys.Universal.ScrollDownMain).
|
||||
Press(keys.Universal.ScrollDownMain).
|
||||
Press(keys.Universal.ScrollDownMain).
|
||||
Press(keys.Universal.ScrollDownMain).
|
||||
OriginY(14).
|
||||
Press(keys.Universal.IncreaseContextInDiffView).
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Changed diff context size to 4"))
|
||||
}).
|
||||
// The block's last line was the fifth row of the screen, and one context
|
||||
// line more above the block puts it a line further down the diff: the view
|
||||
// follows it, rather than the middle visible line, which the hunks below
|
||||
// have pushed further still.
|
||||
OriginY(15).
|
||||
SelectedLines(
|
||||
Contains("-line10"),
|
||||
Contains("-line11"),
|
||||
Contains("-line12"),
|
||||
Contains("-line13"),
|
||||
Contains("-line14"),
|
||||
Contains("-line15"),
|
||||
Contains("+LINE10"),
|
||||
Contains("+LINE11"),
|
||||
Contains("+LINE12"),
|
||||
Contains("+LINE13"),
|
||||
Contains("+LINE14"),
|
||||
Contains("+LINE15"),
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,123 @@
|
||||
package main_view
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var KeepSelectedRangeWhenChangingContextSize = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "A range selection still covers the same lines of the diff after the context size changes",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
Width: 120,
|
||||
Height: 30,
|
||||
SetupConfig: func(cfg *config.AppConfig) {
|
||||
cfg.GetUserConfig().Gui.UseHunkModeInStagingView = false
|
||||
},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
lines := make([]string, 40)
|
||||
for i := range lines {
|
||||
lines[i] = fmt.Sprintf("line%02d", i+1)
|
||||
}
|
||||
shell.CreateFileAndAdd("file1", strings.Join(lines, "\n")+"\n")
|
||||
shell.Commit("one")
|
||||
|
||||
for _, i := range []int{5, 15, 25, 35} {
|
||||
lines[i-1] = strings.ToUpper(lines[i-1])
|
||||
}
|
||||
shell.UpdateFile("file1", strings.Join(lines, "\n")+"\n")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Press(keys.Universal.FocusMainView)
|
||||
|
||||
// A range from a change down into the context below it, so that the cursor is
|
||||
// on the last line of the selection and the other end is three lines above.
|
||||
t.Views().Main().
|
||||
IsFocused().
|
||||
Press(keys.Main.NextHunk).
|
||||
Press(keys.Main.NextHunk).
|
||||
Press(keys.Universal.ToggleRangeSelect).
|
||||
Press(keys.Universal.NextItem).
|
||||
Press(keys.Universal.NextItem).
|
||||
Press(keys.Universal.NextItem).
|
||||
SelectedLines(
|
||||
Contains("-line25"),
|
||||
Contains("+LINE25"),
|
||||
Contains(" line26"),
|
||||
Contains(" line27"),
|
||||
).
|
||||
// Both ends are still lines of the diff with more context around the
|
||||
// change, so the selection still covers the same four.
|
||||
Press(keys.Universal.IncreaseContextInDiffView).
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Changed diff context size to 4"))
|
||||
}).
|
||||
SelectedLines(
|
||||
Contains("-line25"),
|
||||
Contains("+LINE25"),
|
||||
Contains(" line26"),
|
||||
Contains(" line27"),
|
||||
).
|
||||
// With a single line of context, the line the cursor was on is no longer in
|
||||
// the diff. The end that survived stays put and the cursor lands on the
|
||||
// nearest line that is left, so the selection shrinks with the diff.
|
||||
Press(keys.Universal.DecreaseContextInDiffView).
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Changed diff context size to 3"))
|
||||
}).
|
||||
Press(keys.Universal.DecreaseContextInDiffView).
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Changed diff context size to 2"))
|
||||
}).
|
||||
Press(keys.Universal.DecreaseContextInDiffView).
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Changed diff context size to 1"))
|
||||
}).
|
||||
SelectedLines(
|
||||
Contains("-line25"),
|
||||
Contains("+LINE25"),
|
||||
Contains(" line26"),
|
||||
).
|
||||
// The other way round: a range extended upwards, so that it is the far end
|
||||
// that the shrinking context takes away. There is no guessing which line
|
||||
// inherits it, so what is left is the line the cursor is on.
|
||||
PressEscape().
|
||||
Press(keys.Universal.IncreaseContextInDiffView).
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Changed diff context size to 2"))
|
||||
}).
|
||||
Press(keys.Universal.IncreaseContextInDiffView).
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Changed diff context size to 3"))
|
||||
}).
|
||||
SelectedLines(
|
||||
Contains(" line26"),
|
||||
).
|
||||
Press(keys.Universal.NextItem).
|
||||
Press(keys.Universal.RangeSelectUp).
|
||||
Press(keys.Universal.RangeSelectUp).
|
||||
Press(keys.Universal.RangeSelectUp).
|
||||
SelectedLines(
|
||||
Contains("-line25"),
|
||||
Contains("+LINE25"),
|
||||
Contains(" line26"),
|
||||
Contains(" line27"),
|
||||
).
|
||||
Press(keys.Universal.DecreaseContextInDiffView).
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Changed diff context size to 2"))
|
||||
}).
|
||||
Press(keys.Universal.DecreaseContextInDiffView).
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Changed diff context size to 1"))
|
||||
}).
|
||||
SelectedLines(
|
||||
Contains("-line25"),
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -371,6 +371,9 @@ var tests = []*components.IntegrationTest{
|
||||
main_view.EscapeDismissesSelection,
|
||||
main_view.FileNavigationScrollsToTheTop,
|
||||
main_view.HideSelectionWhenChangesVanish,
|
||||
main_view.KeepAWrappedLineCoveredAcrossARerender,
|
||||
main_view.KeepBothHalvesOfAChangeSelected,
|
||||
main_view.KeepPositionByTheVisibleEndOfASelection,
|
||||
main_view.KeepPositionInBothPanesWhenChangingContextSize,
|
||||
main_view.KeepPositionInBothPanesWhenIgnoringWhitespace,
|
||||
main_view.KeepPositionInBothPanesWhenSwitchingDiffRenderers,
|
||||
@@ -380,6 +383,7 @@ var tests = []*components.IntegrationTest{
|
||||
main_view.KeepPositionWhenSwitchingDiffRenderers,
|
||||
main_view.KeepPositionWhenTheSelectionIsOffScreen,
|
||||
main_view.KeepScrollWhenTheDiffCantBeRead,
|
||||
main_view.KeepSelectedRangeWhenChangingContextSize,
|
||||
main_view.KeepSelectionVisibleWhenDiffShrinks,
|
||||
main_view.NavigateByHunkAndFile,
|
||||
main_view.NoSelectionOverABinaryDiff,
|
||||
|
||||
Reference in New Issue
Block a user