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:
Stefan Haller
2026-10-05 10:30:55 +02:00
co-authored by Claude Opus 5
parent 6e6894b639
commit 79e1ce256f
9 changed files with 552 additions and 63 deletions
+7
View File
@@ -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
+43 -40
View File
@@ -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))
}
+130 -23
View File
@@ -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
+15
View File
@@ -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"),
)
},
})
+4
View File
@@ -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,