mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
608 lines
24 KiB
Go
608 lines
24 KiB
Go
package helpers
|
|
|
|
import (
|
|
"github.com/jesseduffield/lazygit/pkg/gocui"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
"github.com/jesseduffield/lazygit/pkg/tasks"
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
// Keeping a diff view where it is when the same diff is rendered again differently.
|
|
// The line the user is on is remembered by identity (diff_line_helper.go), because a
|
|
// new rendering puts it on a different line of the view — and may not have it at all,
|
|
// which is what the fallbacks below are for.
|
|
|
|
// diffLineAnchor is a line for a restore to land on: the identity to find it by in
|
|
// the new rendering, and the screen row it was on, so that it can be put back there.
|
|
type diffLineAnchor struct {
|
|
identity types.DiffLineInfo
|
|
row int
|
|
}
|
|
|
|
// PreserveDiffPositionOnRerender remembers where a diff view is and puts it back
|
|
// there as it next re-renders, instead of leaving the user at the top of a new
|
|
// rendering of the diff they were already reading. Call it on the view about to be
|
|
// re-rendered, right before triggering the re-render — on both panes of the main
|
|
// 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 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
|
|
// restore installed on it would sit there and claim a later render instead.
|
|
if !view.Visible {
|
|
return
|
|
}
|
|
|
|
// The re-render is produced by a different command from the one behind what is on
|
|
// screen — another context size, another renderer — so without being told otherwise
|
|
// it would be taken for content the user has never seen and shown from the top.
|
|
// Whether or not a line of the old rendering can be found in the new one, the offset
|
|
// into it is nearer to where they were reading than the top is.
|
|
if manager := self.c.GetViewBufferManagerForView(view); manager != nil {
|
|
manager.SetKeepScrollPositionForNextTask()
|
|
}
|
|
|
|
showSelection := view.Highlight
|
|
anchorViewLine := view.MiddleVisibleLineIdx()
|
|
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 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),
|
|
func(anchor diffLineAnchor, viewLine int) {
|
|
// Put the line back on the screen row it was on, clamped into the view for
|
|
// the fallback lines, which can come from off screen.
|
|
row := lo.Clamp(anchor.row, 0, max(0, view.InnerHeight()-1))
|
|
view.SetOrigin(0, max(0, viewLine-row))
|
|
if showSelection {
|
|
// 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()
|
|
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 —
|
|
// found among its own candidates, since the anchor's are a search of the diff from
|
|
// somewhere else entirely.
|
|
func (self *DiffLineHelper) selectionLine(
|
|
view *gocui.View, candidates []diffLineAnchor, anchorViewLine int,
|
|
) int {
|
|
if len(candidates) == 0 {
|
|
return anchorViewLine
|
|
}
|
|
_, bufferLine := self.nearestSurvivingCandidate(view.DiffLineContents(), candidates)
|
|
if bufferLine == -1 {
|
|
return anchorViewLine
|
|
}
|
|
if viewLine, ok := view.ViewLineForBufferLine(bufferLine); ok {
|
|
return viewLine
|
|
}
|
|
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
|
|
// order (see nearbyDiffLines); if the rendering has none of them, place isn't called
|
|
// and the view re-renders as it otherwise would.
|
|
//
|
|
// The nearest candidate is looked for as the content loads, so that the re-render can
|
|
// be revealed at the right position as soon as that line and a screenful below it
|
|
// have arrived. Only the nearest one, because the candidates aren't in load order: a
|
|
// farther one can load first, and landing on it while a nearer one is still on its
|
|
// way would be settling for worse. The rest are considered together once the whole
|
|
// rendering is there.
|
|
func (self *DiffLineHelper) restoreDiffLinePositionOnRerender(
|
|
view *gocui.View, candidates []diffLineAnchor, place func(anchor diffLineAnchor, viewLine int),
|
|
) {
|
|
if len(candidates) == 0 {
|
|
return
|
|
}
|
|
|
|
// The search of the loading content runs on the task's own goroutine, where the
|
|
// repo we are in may not be read — a repo switch replaces it — so take it here, on
|
|
// the UI thread, for the search to work from.
|
|
worktreePath := self.c.Git().RepoPaths.WorktreePath()
|
|
|
|
// Which candidate the search settled on, for place to put back where it was.
|
|
found := diffLineAnchor{}
|
|
|
|
self.installDiffLineRestore(view,
|
|
func(rows []gocui.DiffLineContent, offset int) (int, bool) {
|
|
for i, row := range rows {
|
|
if rowShowsDiffLine(row, worktreePath, candidates[0].identity) {
|
|
found = candidates[0]
|
|
return offset + i, true
|
|
}
|
|
}
|
|
return 0, false
|
|
},
|
|
func(contents []gocui.DiffLineContent) (int, bool) {
|
|
anchor, bufferLine := self.nearestSurvivingCandidate(contents, candidates)
|
|
if bufferLine == -1 {
|
|
return 0, false
|
|
}
|
|
found = anchor
|
|
return bufferLine, true
|
|
},
|
|
func(viewLine int) { place(found, viewLine) },
|
|
nil,
|
|
)
|
|
}
|
|
|
|
// ChangeLineOrdinal returns how many change lines of view's rendered diff come before
|
|
// the one at the given view line — that line's place in the sequence of changes. ok is
|
|
// false when the view line belongs to no row of the content.
|
|
//
|
|
// It is how a place in a diff is remembered across acting on it: an action consumes
|
|
// the lines it acted on, so the identity of the line the user was on is gone, but the
|
|
// place it left behind is the same one that identity used to have.
|
|
func (self *DiffLineHelper) ChangeLineOrdinal(view *gocui.View, viewLine int) (int, bool) {
|
|
bufferLine, ok := view.BufferLineForViewLine(viewLine)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
|
|
ordinal := 0
|
|
for i, row := range self.resolveDiffLines(view.DiffLineContents()) {
|
|
if i >= bufferLine {
|
|
break
|
|
}
|
|
if row.ok && row.info.IsChange() {
|
|
ordinal++
|
|
}
|
|
}
|
|
return ordinal, true
|
|
}
|
|
|
|
// RevealChangeLineAtOrdinal arranges for view's next re-render to be shown with the
|
|
// change line at the given ordinal placed by place — the diff having changed under the
|
|
// user, this is where what they were doing carries on. When the new diff has fewer
|
|
// changes than that, because the ones acted on were its last, it lands on the last
|
|
// change left.
|
|
//
|
|
// done is called once the selection is where it belongs, or once it turns out that no
|
|
// render is coming to put it there, for a caller that must not let the user act again
|
|
// in between.
|
|
func (self *DiffLineHelper) RevealChangeLineAtOrdinal(
|
|
view *gocui.View, ordinal int, place func(viewLine int), done func(),
|
|
) {
|
|
// How many change lines the incremental search has passed, so that it can carry on
|
|
// counting where it left off.
|
|
seen := 0
|
|
|
|
self.installDiffLineRestore(view,
|
|
func(rows []gocui.DiffLineContent, offset int) (int, bool) {
|
|
for i, row := range rows {
|
|
if info, ok := self.diffLineInfoFromRecords(row.Metadata); ok && info.IsChange() {
|
|
if seen == ordinal {
|
|
return offset + i, true
|
|
}
|
|
seen++
|
|
}
|
|
}
|
|
return 0, false
|
|
},
|
|
func(contents []gocui.DiffLineContent) (int, bool) {
|
|
last, count := -1, 0
|
|
for i, row := range self.resolveDiffLines(contents) {
|
|
if !row.ok || !row.info.IsChange() {
|
|
continue
|
|
}
|
|
if count == ordinal {
|
|
return i, true
|
|
}
|
|
count++
|
|
last = i
|
|
}
|
|
return last, last != -1
|
|
},
|
|
place,
|
|
done,
|
|
)
|
|
}
|
|
|
|
// installDiffLineRestore is what the restores are built on: it arranges for view's
|
|
// next re-render to be revealed with the row a search finds in it placed by place,
|
|
// instead of from the top.
|
|
//
|
|
// The search comes in two halves, because the content arrives a line at a time.
|
|
// findEarly is given the rows that have loaded since it last looked, so that the
|
|
// re-render can be revealed as soon as the row is there rather than waiting for the
|
|
// rest of a long diff; it can only go by what the renderer states about a row, a
|
|
// partly-loaded diff being unparseable. findComplete is given the whole rendering at
|
|
// the swap, for a target the incremental search couldn't settle on. Either returns the
|
|
// buffer line it found, and place is not called at all when neither does.
|
|
func (self *DiffLineHelper) installDiffLineRestore(
|
|
view *gocui.View,
|
|
findEarly func(rows []gocui.DiffLineContent, offset int) (int, bool),
|
|
findComplete func(contents []gocui.DiffLineContent) (int, bool),
|
|
place func(viewLine int),
|
|
done func(),
|
|
) {
|
|
// Get-or-create, because the pane may not have rendered anything yet: a file whose
|
|
// diff has only just become split has a second pane whose first render is the one
|
|
// this restore is for.
|
|
manager := self.c.GetOrCreateViewBufferManagerForView(view)
|
|
if manager == nil {
|
|
if done != nil {
|
|
done()
|
|
}
|
|
return
|
|
}
|
|
|
|
// The readiness check below runs on the task's own goroutine, which may not read
|
|
// the view's dimensions, so take them here, on the UI thread.
|
|
viewHeight := view.InnerHeight()
|
|
|
|
// What the search of the loading content has found, and how far it has looked, so
|
|
// that each line is looked at once.
|
|
foundLine := -1
|
|
scanned := 0
|
|
|
|
manager.SetRestoreForNextTask(&tasks.RenderRestore{
|
|
FirstPaintReady: func() bool {
|
|
if foundLine == -1 {
|
|
rows := view.OffscreenDiffLineContentsFrom(scanned)
|
|
if bufferLine, ok := findEarly(rows, scanned); ok {
|
|
foundLine = bufferLine
|
|
}
|
|
scanned += len(rows)
|
|
if foundLine == -1 {
|
|
return false
|
|
}
|
|
}
|
|
// Wait for a screenful below the line as well, so that the re-render isn't
|
|
// revealed with it stranded at the bottom of a half-filled view.
|
|
return view.OffscreenLineCount() >= foundLine+viewHeight
|
|
},
|
|
Apply: func(swapIn func()) {
|
|
bufferLine := foundLine
|
|
if bufferLine == -1 {
|
|
if line, ok := findComplete(view.OffscreenDiffLineContents()); ok {
|
|
bufferLine = line
|
|
}
|
|
}
|
|
|
|
swapIn()
|
|
|
|
if bufferLine == -1 {
|
|
return
|
|
}
|
|
if viewLine, ok := view.ViewLineForBufferLine(bufferLine); ok {
|
|
place(viewLine)
|
|
}
|
|
},
|
|
Done: done,
|
|
})
|
|
}
|
|
|
|
// nearbyDiffLines collects the lines of view's rendered diff as candidates for a
|
|
// restore to land on, ordered by proximity to the anchor line — the anchor itself
|
|
// first, then outward, preferring at-or-below on ties — each tagged with the screen
|
|
// row it is on. A restore lands on the first of them its re-render still has, so this
|
|
// order makes it land as near as possible to where the user was.
|
|
//
|
|
// The walk covers the whole diff rather than stopping at the change lines on either
|
|
// side of the anchor, which a context-size change always keeps: ignoring whitespace
|
|
// keeps nothing in particular, and can take a hunk or a whole file out of the diff,
|
|
// leaving the nearest surviving line in a neighbouring file.
|
|
func (self *DiffLineHelper) nearbyDiffLines(view *gocui.View, anchorViewLine int) []diffLineAnchor {
|
|
anchor, ok := view.BufferLineForViewLine(anchorViewLine)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
resolved := self.resolveDiffLines(view.DiffLineContents())
|
|
if anchor >= len(resolved) {
|
|
return nil
|
|
}
|
|
rows := screenRows(view, len(resolved))
|
|
|
|
candidates := make([]diffLineAnchor, 0, len(resolved))
|
|
collect := func(bufferLine int) {
|
|
if line := resolved[bufferLine]; line.ok {
|
|
candidates = append(candidates, diffLineAnchor{identity: line.info, row: rows[bufferLine]})
|
|
}
|
|
}
|
|
collect(anchor)
|
|
for below, above := anchor+1, anchor-1; below < len(resolved) || above >= 0; below, above = below+1, above-1 {
|
|
if below < len(resolved) {
|
|
collect(below)
|
|
}
|
|
if above >= 0 {
|
|
collect(above)
|
|
}
|
|
}
|
|
return candidates
|
|
}
|
|
|
|
// screenRows maps each line of view's content to the screen row it is drawn on. The
|
|
// lines above the visible ones get -1 and those below them the view's height, so that
|
|
// putting one of them back where it was lands it at the top or bottom edge.
|
|
func screenRows(view *gocui.View, bufferLineCount int) []int {
|
|
height := view.InnerHeight()
|
|
originY := view.OriginY()
|
|
|
|
rows := make([]int, bufferLineCount)
|
|
for i := range rows {
|
|
rows[i] = -1
|
|
}
|
|
lastVisible := -1
|
|
for y := originY; y < min(originY+height, view.ViewLinesHeight()); y++ {
|
|
bufferLine, ok := view.BufferLineForViewLine(y)
|
|
if !ok || bufferLine >= bufferLineCount {
|
|
continue
|
|
}
|
|
if rows[bufferLine] == -1 {
|
|
rows[bufferLine] = y - originY
|
|
}
|
|
lastVisible = bufferLine
|
|
}
|
|
for i := lastVisible + 1; i < bufferLineCount; i++ {
|
|
rows[i] = height
|
|
}
|
|
return rows
|
|
}
|
|
|
|
// nearestSurvivingCandidate returns the first of the candidates that the given
|
|
// rendering still shows, and the line of it that does. The rendering is indexed
|
|
// first, rather than searched once per candidate: the candidate list is as long as
|
|
// the diff, and so is the rendering.
|
|
func (self *DiffLineHelper) nearestSurvivingCandidate(
|
|
contents []gocui.DiffLineContent, candidates []diffLineAnchor,
|
|
) (diffLineAnchor, int) {
|
|
rows := self.patchLineRows(contents)
|
|
for _, candidate := range candidates {
|
|
if line, ok := rows[patchLineOf(candidate.identity)]; ok {
|
|
return candidate, line
|
|
}
|
|
}
|
|
return diffLineAnchor{}, -1
|
|
}
|
|
|
|
// patchLineRows indexes a rendering by the diff lines it shows: for each of them, the
|
|
// first of its rows that does. A row can show more than one, and each is then a way
|
|
// of finding that row again.
|
|
func (self *DiffLineHelper) patchLineRows(contents []gocui.DiffLineContent) map[patchLine]int {
|
|
rows := map[patchLine]int{}
|
|
for i, identities := range self.resolveDiffLineIdentities(contents) {
|
|
for _, identity := range identities {
|
|
if _, seen := rows[patchLineOf(identity)]; !seen {
|
|
rows[patchLineOf(identity)] = i
|
|
}
|
|
}
|
|
}
|
|
return rows
|
|
}
|
|
|
|
// rowShowsDiffLine reports whether the given row of a rendering shows the given diff
|
|
// line — among any others it shows, since a side-by-side rendering puts a deletion
|
|
// beside the addition replacing it. It only knows what the renderer states about the
|
|
// row, since the alternative, parsing the rendering as a diff, needs whole hunks and
|
|
// this is asked of content that is still loading. It takes the repo's worktree path
|
|
// rather than reading it, being asked off the UI thread.
|
|
func rowShowsDiffLine(row gocui.DiffLineContent, worktreePath string, target types.DiffLineInfo) bool {
|
|
return lo.SomeBy(row.Metadata, func(record string) bool {
|
|
parsed, ok := parseDiffLineMetadata(record)
|
|
return ok && patchLineOf(diffLineInfoIn(worktreePath, parsed)) == patchLineOf(target)
|
|
})
|
|
}
|
|
|
|
// patchLine records what stays the same about a diff line when the same diff is
|
|
// rendered again differently: which file it belongs to, the line number that
|
|
// identifies it on the side it belongs to, and what kind of line it is.
|
|
type patchLine struct {
|
|
path string
|
|
// Every kind of content line collapses into DiffLineContext, since an addition
|
|
// and the context line it turns into when whitespace stops counting are the same
|
|
// line of the same file. The header rows keep their kind: a file's header and the
|
|
// first line of the file it heads are not the same place.
|
|
kind types.DiffLineType
|
|
// The old file's line number for a deletion, since two consecutive deletions
|
|
// share a new-file position and differ only here; the new file's otherwise.
|
|
line int
|
|
isDeletion bool
|
|
}
|
|
|
|
func patchLineOf(info types.DiffLineInfo) patchLine {
|
|
switch info.Type {
|
|
case types.DiffLineFileHeader, types.DiffLineHunkHeader:
|
|
return patchLine{path: info.Path, kind: info.Type, line: info.NewLine}
|
|
case types.DiffLineDeleted:
|
|
return patchLine{path: info.Path, kind: types.DiffLineContext, line: info.OldLine, isDeletion: true}
|
|
default:
|
|
return patchLine{path: info.Path, kind: types.DiffLineContext, line: info.NewLine}
|
|
}
|
|
}
|
|
|
|
// RevealSelectionAfterAction moves a diff pane's selection to the change that takes the
|
|
// place of the one just acted on, once the changed diff has re-rendered. Call it with
|
|
// the pane acted in, the pane the work carries on in, and the first line of the
|
|
// selection, before triggering the re-render.
|
|
//
|
|
// The line acted on is gone from the diff, so what is remembered is its place among the
|
|
// diff's changes: the next change moves up into it, which is where you want to be to
|
|
// carry on. A range collapses to a single line at its start, and hunk mode selects the
|
|
// whole block it lands in, so that pressing the key again acts on the next hunk. The
|
|
// target pane inherits that select mode, this being the same piece of work continuing
|
|
// in another pane — and shows no selection until the restore places one, so that what
|
|
// it was left showing the last time it was used doesn't appear for a frame.
|
|
//
|
|
// advanceBy moves on by that many changes past the place remembered, for an action that
|
|
// leaves the diff as it was: lines taken into a custom patch are still in the commit's
|
|
// diff, so the place remembered is still the line acted on, and carrying on means going
|
|
// past the lines just dealt with rather than staying on them.
|
|
//
|
|
// done, which may be nil, is called once the selection is where it belongs, or once it
|
|
// turns out that no render is coming to put it there — for a caller that must not let
|
|
// the user act again in between.
|
|
func (self *DiffLineHelper) RevealSelectionAfterAction(
|
|
source types.DiffPaneContext, target types.DiffPaneContext, firstLineIdx int, advanceBy int, done func(),
|
|
) {
|
|
ordinal, ok := self.ChangeLineOrdinal(source.GetView(), firstLineIdx)
|
|
if !ok {
|
|
if done != nil {
|
|
done()
|
|
}
|
|
return
|
|
}
|
|
|
|
sel := source.DiffSelectState()
|
|
if sel.Mode == types.DiffSelectModeRange {
|
|
sel.Mode = types.DiffSelectModeLine
|
|
sel.RangeIsSticky = false
|
|
}
|
|
*target.DiffSelectState() = *sel
|
|
selectHunk := sel.Mode == types.DiffSelectModeHunk
|
|
|
|
targetView := target.GetView()
|
|
if target != source {
|
|
target.SetHasSelectableContent(false)
|
|
self.c.Context().UpdateSelectionHighlights()
|
|
}
|
|
|
|
self.RevealChangeLineAtOrdinal(targetView, ordinal+advanceBy, func(viewLine int) {
|
|
if selectHunk {
|
|
self.SelectChangeBlock(target, viewLine, true)
|
|
return
|
|
}
|
|
targetView.CancelRangeSelect()
|
|
self.ShowSelectionAtLine(targetView, viewLine, true)
|
|
}, done)
|
|
}
|