mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-10-05 21:46:49 -04:00
A diff renderer may lay a diff out however it likes: line numbers in a gutter, the +/- column replaced by colour, the two sides in columns. Once it has, we can only tell which line of which file a row shows if the renderer says so. Under a renderer that doesn't say, the main view holds a diff that can be read but not staged, edited or copied from. That is no good now that the main view is where you stage. So focusing it brings git's own diff instead, and every re-render while it stays focused keeps to that, so staging a hunk doesn't flip back. Browsing is untouched: you see what the renderer produced until you focus the view to act on it. Whether the renderer says anything is settled by asking it rather than by watching it work: run it on empty input and see whether it announces the protocol. Announcing is a property of the renderer, so the answer is known before we render anything, and a diff with no lines to describe can't fool it. Watching would have to see a diff go by first, and a binary file's diff holds nothing that would tell the two cases apart. The answer is remembered until the renderer changes. git itself is asked the same question, with the renderer's own arguments, since it announces itself for exactly the formats whose output can't be read back as a diff. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
113 lines
4.8 KiB
Go
113 lines
4.8 KiB
Go
package helpers
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
|
"github.com/jesseduffield/lazygit/pkg/config"
|
|
"github.com/jesseduffield/lazygit/pkg/gocui"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
"github.com/jesseduffield/lazygit/pkg/tasks"
|
|
)
|
|
|
|
// Falling back to git's own diff when the configured one can't be acted on.
|
|
//
|
|
// A diff renderer is free to lay a diff out however it likes, and once it has, we can
|
|
// only tell which line of which file a row shows if the renderer says so. A renderer
|
|
// that doesn't produces a diff that can be read but not staged, edited or copied from,
|
|
// so when the user focuses the main view to act on it, we show git's own diff instead.
|
|
// Browsing keeps the renderer's version; only acting on it needs one we can follow.
|
|
|
|
// MainViewDiffMode says how a side panel should produce the diff it renders into the
|
|
// main view: as the user configured it, or as git's own — while the main view holds
|
|
// focus and what the renderer would produce couldn't be acted on.
|
|
//
|
|
// Every panel that renders a diff into the main view asks, so that a re-render while
|
|
// focused — after staging a hunk, say — stays with git's own diff rather than flipping
|
|
// back to the renderer's.
|
|
func (self *DiffLineHelper) MainViewDiffMode() git_commands.DiffMode {
|
|
if self.mainViewIsFocused() && self.diffNeedsMetadata() && !self.diffRendererEmitsMetadata() {
|
|
return git_commands.DiffModeRaw
|
|
}
|
|
return git_commands.DiffModeRendered
|
|
}
|
|
|
|
// RenderFocusedMainViewAgain has the panel beneath the focused main view render its
|
|
// diff again — which, the main view now holding focus, is git's own diff rather than
|
|
// the renderer's — and calls place once that is on screen.
|
|
//
|
|
// The whole diff is read before it is shown, rather than the first screenful: place
|
|
// looks at what is there to decide where to put the selection, and a change line
|
|
// further down would otherwise be missed.
|
|
//
|
|
// It is the same diff of the same files, so the view keeps the scroll position it has
|
|
// rather than starting from the top: git lays the changes out its own way and the line
|
|
// the user was on is somewhere else now, but the offset still puts them among the same
|
|
// part of the file — and the selection is then established from what that leaves on
|
|
// screen.
|
|
func (self *DiffLineHelper) RenderFocusedMainViewAgain(view *gocui.View, sidePanel types.Context, place func()) {
|
|
manager := self.c.GetOrCreateViewBufferManagerForView(view)
|
|
if manager == nil {
|
|
return
|
|
}
|
|
|
|
manager.SetKeepScrollPositionForNextTask()
|
|
manager.SetRestoreForNextTask(&tasks.RenderRestore{
|
|
FirstPaintReady: func() bool { return false },
|
|
Apply: func(swapIn func()) {
|
|
swapIn()
|
|
place()
|
|
},
|
|
})
|
|
|
|
sidePanel.HandleRenderToMain()
|
|
}
|
|
|
|
func (self *DiffLineHelper) mainViewIsFocused() bool {
|
|
current := self.c.Context().CurrentStatic().GetKey()
|
|
return current == self.c.Contexts().Normal.GetKey() ||
|
|
current == self.c.Contexts().NormalSecondary.GetKey()
|
|
}
|
|
|
|
// diffNeedsMetadata reports whether the diff we would show is one whose rows can only
|
|
// be placed in the file by the records the renderer states. Any custom renderer may
|
|
// restructure the diff; so may git itself, once the renderer's arguments ask for a word
|
|
// diff, whose markup is inline. Plain git output describes itself, and needs no records.
|
|
func (self *DiffLineHelper) diffNeedsMetadata() bool {
|
|
manager := self.c.State().GetDiffRendererConfigManager()
|
|
if manager.GetDiffRendererType() != config.DiffRendererType_RawGit {
|
|
return true
|
|
}
|
|
return len(manager.GetRawGitArgs()) > 0
|
|
}
|
|
|
|
// diffRendererEmitsMetadata is the probed verdict about the current diff renderer, asked
|
|
// once and remembered until the renderer changes — the user cycling to another one, or a
|
|
// changed config being reloaded.
|
|
func (self *DiffLineHelper) diffRendererEmitsMetadata() bool {
|
|
signature := self.diffRendererSignature()
|
|
if self.rendererEmitsMetadata == nil || signature != self.rendererSignature {
|
|
verdict := self.c.Git().Diff.ProbeDiffRendererEmitsMetadata()
|
|
self.rendererEmitsMetadata = &verdict
|
|
self.rendererSignature = signature
|
|
}
|
|
return *self.rendererEmitsMetadata
|
|
}
|
|
|
|
// diffRendererSignature identifies the current diff renderer, so that the remembered
|
|
// verdict is dropped when it stops describing the renderer we have. The values a command
|
|
// is resolved with are no part of its identity, so fixed ones are used.
|
|
func (self *DiffLineHelper) diffRendererSignature() string {
|
|
manager := self.c.State().GetDiffRendererConfigManager()
|
|
index, _ := manager.CurrentDiffRendererIndex()
|
|
values := config.DiffRendererValues{}
|
|
externalDiff, _ := manager.GetExternalDiffCommand(values)
|
|
stdinFilter, _ := manager.GetStdinFilterCommand(values)
|
|
return fmt.Sprintf("%d\x00%s\x00%s\x00%s",
|
|
index,
|
|
externalDiff,
|
|
stdinFilter,
|
|
strings.Join(manager.GetRawGitArgs(), "\x00"))
|
|
}
|