mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-10-06 14:06:32 -04:00
Show git's own diff when the renderer's can't be acted on
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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
104ebaa612
commit
74aa1a05ac
@@ -2,10 +2,135 @@ package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/mgutz/str"
|
||||
)
|
||||
|
||||
// metadataHandshake is the record a diff renderer that speaks the OSC 1717 protocol
|
||||
// emits before anything else, to announce that it does: a version-only record, with
|
||||
// none of the fields a line's record has. See ProbeDiffRendererEmitsMetadata, and
|
||||
// gocui's escape interpreter for how it is kept off the screen on a real render.
|
||||
const metadataHandshake = "\x1b]1717"
|
||||
|
||||
// probeWidth is the width a probe asks the renderer to lay out to. A probe has
|
||||
// nothing to render, so any width does, as long as it is one a renderer will
|
||||
// accept: a renderer that lays its rendering out in columns may well refuse a
|
||||
// width of zero, and would then never get as far as announcing itself.
|
||||
const probeWidth = 80
|
||||
|
||||
// ProbeDiffRendererEmitsMetadata reports whether the configured diff renderer states
|
||||
// which line of which file it is rendering, by running it on empty input and looking
|
||||
// for the handshake. The answer decides whether a diff the renderer produced can be
|
||||
// acted on at all, or has to be replaced by git's own when the user wants to act on it
|
||||
// (see DiffLineHelper.MainViewDiffMode).
|
||||
//
|
||||
// Asking rather than watching a real render: the handshake is the renderer's first
|
||||
// output whatever the diff, so the answer is a property of the renderer, known before
|
||||
// we render anything — where watching would have to see a diff go by first, and would
|
||||
// be fooled by a diff with no lines to describe.
|
||||
//
|
||||
// No terminal is needed. git only invokes a stdin filter when it thinks it is talking
|
||||
// to one, but the renderer itself doesn't care: it announces itself whenever OSC1717 is
|
||||
// set, so it can be run directly with empty input.
|
||||
func (self *DiffCommands) ProbeDiffRendererEmitsMetadata() bool {
|
||||
manager := self.diffRendererConfigManager
|
||||
values := config.DiffRendererValues{Width: probeWidth, DiffContext: 3}
|
||||
|
||||
switch manager.GetDiffRendererType() {
|
||||
case config.DiffRendererType_StdinFilter:
|
||||
if command, err := manager.GetStdinFilterCommand(values); err == nil && command != "" {
|
||||
return self.probeEmitsMetadata(self.cmd.NewShell(command, ""))
|
||||
}
|
||||
case config.DiffRendererType_ExtDiff:
|
||||
// An empty command means git's own diff.external config, which picks a driver
|
||||
// per file through .gitattributes: there is no one renderer to ask, and a single
|
||||
// diff can be produced by several, so we take it that it says nothing.
|
||||
if command, err := manager.GetExternalDiffCommand(values); err == nil && command != "" {
|
||||
return self.externalDiffEmitsMetadata(command)
|
||||
}
|
||||
case config.DiffRendererType_RawGit:
|
||||
// git describes only the formats whose output can't be read back as a diff, and
|
||||
// asked with the renderer's own arguments it answers for exactly the format
|
||||
// those select: a handshake for a word diff, silence for a unified one. With no
|
||||
// arguments there is nothing to fall back to anyway, since this already is git's
|
||||
// own diff.
|
||||
if args := manager.GetRawGitArgs(); len(args) > 0 {
|
||||
return self.rawGitEmitsMetadata(args)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// rawGitEmitsMetadata asks git itself, run with the diff renderer's own arguments.
|
||||
func (self *DiffCommands) rawGitEmitsMetadata(rawGitArgs []string) bool {
|
||||
oldPath, newPath, cleanup, ok := self.probeFiles()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
return self.probeEmitsMetadata(self.cmd.New(
|
||||
NewGitCmd("diff").
|
||||
Arg("--no-index").
|
||||
Arg(rawGitArgs...).
|
||||
Arg(oldPath, newPath).
|
||||
ToArgv(),
|
||||
))
|
||||
}
|
||||
|
||||
// externalDiffEmitsMetadata asks an external diff command, invoking it the way git
|
||||
// invokes one — with the seven positional arguments of git's diff.external convention —
|
||||
// over two empty files, so that it announces itself without having a diff to render.
|
||||
func (self *DiffCommands) externalDiffEmitsMetadata(externalDiffCommand string) bool {
|
||||
oldPath, newPath, cleanup, ok := self.probeFiles()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
args := append(str.ToArgv(externalDiffCommand),
|
||||
"probe", oldPath, "0000000", "100644", newPath, "0000000", "100644")
|
||||
return self.probeEmitsMetadata(self.cmd.New(args))
|
||||
}
|
||||
|
||||
// probeFiles makes the two empty files a probe stands a diff up from, and the cleanup
|
||||
// that removes them. Empty, because what the probe wants is for the renderer to announce
|
||||
// itself, not for it to have anything to say.
|
||||
func (self *DiffCommands) probeFiles() (string, string, func(), bool) {
|
||||
tempDir := self.os.GetTempDir()
|
||||
|
||||
oldFile, err := os.CreateTemp(tempDir, "lazygit-probe-old-*")
|
||||
if err != nil {
|
||||
return "", "", nil, false
|
||||
}
|
||||
oldFile.Close()
|
||||
|
||||
newFile, err := os.CreateTemp(tempDir, "lazygit-probe-new-*")
|
||||
if err != nil {
|
||||
os.Remove(oldFile.Name())
|
||||
return "", "", nil, false
|
||||
}
|
||||
newFile.Close()
|
||||
|
||||
return oldFile.Name(), newFile.Name(), func() {
|
||||
os.Remove(oldFile.Name())
|
||||
os.Remove(newFile.Name())
|
||||
}, true
|
||||
}
|
||||
|
||||
func (self *DiffCommands) probeEmitsMetadata(cmdObj *oscommands.CmdObj) bool {
|
||||
cmdObj.AddEnvVars("OSC1717=V1")
|
||||
// A renderer may well object to being handed nothing to render. We want to know
|
||||
// whatever it said before objecting, and that is captured either way.
|
||||
output, _ := cmdObj.RunWithOutput()
|
||||
return strings.Contains(output, metadataHandshake)
|
||||
}
|
||||
|
||||
type DiffCommands struct {
|
||||
*GitCommon
|
||||
}
|
||||
|
||||
@@ -175,9 +175,10 @@ func (self *CommitFilesController) GetOnRenderToMain() func() {
|
||||
from, to := self.context().GetFromAndToForDiff()
|
||||
from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(from)
|
||||
|
||||
mode := self.c.Helpers().DiffLine.MainViewDiffMode()
|
||||
paths := self.pathsForDiff(node)
|
||||
cmdObj := self.c.Git().WorkingTree.ShowFileDiffCmdObj(from, to, reverse, paths, git_commands.DiffModeRendered)
|
||||
task := types.NewRunDiffRendererTask(cmdObj.GetCmd())
|
||||
cmdObj := self.c.Git().WorkingTree.ShowFileDiffCmdObj(from, to, reverse, paths, mode)
|
||||
task := types.NewMainViewDiffTask(cmdObj.GetCmd(), mode)
|
||||
|
||||
self.c.RenderToMainViews(types.RefreshMainOpts{
|
||||
Pair: self.c.MainViewPairs().Normal,
|
||||
|
||||
@@ -380,13 +380,17 @@ func (self *FilesController) renderWorkingTreeDiff(node *filetree.FileNode) {
|
||||
showStaged := node.GetHasStagedChanges() || alwaysSplit
|
||||
showUnstaged := node.GetHasUnstagedChanges() || alwaysSplit || !showStaged
|
||||
|
||||
// While the main view is focused to act on this diff, it may have to be git's own
|
||||
// rather than the diff renderer's; both panes have to agree about that.
|
||||
mode := self.c.Helpers().DiffLine.MainViewDiffMode()
|
||||
|
||||
paths := self.pathsForDiff(node)
|
||||
refreshOpts := types.RefreshMainOpts{Pair: self.c.MainViewPairs().Normal}
|
||||
|
||||
if showUnstaged {
|
||||
cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, git_commands.DiffModeRendered, false, paths)
|
||||
cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, mode, false, paths)
|
||||
refreshOpts.Main = &types.ViewUpdateOpts{
|
||||
Task: types.NewRunDiffRendererTask(cmdObj.GetCmd()),
|
||||
Task: types.NewMainViewDiffTask(cmdObj.GetCmd(), mode),
|
||||
SubTitle: self.c.Helpers().Diff.IgnoringWhitespaceSubTitle(),
|
||||
Title: self.c.Tr.UnstagedChanges,
|
||||
NothingToActOn: !node.GetHasUnstagedChanges(),
|
||||
@@ -394,9 +398,9 @@ func (self *FilesController) renderWorkingTreeDiff(node *filetree.FileNode) {
|
||||
}
|
||||
|
||||
if showStaged {
|
||||
cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, git_commands.DiffModeRendered, true, paths)
|
||||
cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, mode, true, paths)
|
||||
refreshOpts.Secondary = &types.ViewUpdateOpts{
|
||||
Task: types.NewRunDiffRendererTask(cmdObj.GetCmd()),
|
||||
Task: types.NewMainViewDiffTask(cmdObj.GetCmd(), mode),
|
||||
SubTitle: self.c.Helpers().Diff.IgnoringWhitespaceSubTitle(),
|
||||
Title: self.c.Tr.StagedChanges,
|
||||
NothingToActOn: !node.GetHasStagedChanges(),
|
||||
|
||||
@@ -55,6 +55,8 @@ func (self *DiffHelper) DiffArgs() []string {
|
||||
// either there's no range, or it can't be diffed for some reason), then we want
|
||||
// to fall back to rendering the diff for the single commit.
|
||||
func (self *DiffHelper) GetUpdateTaskForRenderingCommitsDiff(commit *models.Commit, refRange *types.RefRange) types.UpdateTask {
|
||||
mode := self.diffLineHelper.MainViewDiffMode()
|
||||
|
||||
if refRange != nil {
|
||||
from, to := refRange.From, refRange.To
|
||||
args := []string{from.ParentRefName(), to.RefName(), "--stat", "-p"}
|
||||
@@ -74,13 +76,13 @@ func (self *DiffHelper) GetUpdateTaskForRenderingCommitsDiff(commit *models.Comm
|
||||
args = append(args, filterPath)
|
||||
}
|
||||
}
|
||||
cmdObj := self.c.Git().Diff.DiffCmdObj(args, git_commands.DiffModeRendered)
|
||||
cmdObj := self.c.Git().Diff.DiffCmdObj(args, mode)
|
||||
prefix := style.FgYellow.Sprintf("%s %s-%s\n\n", self.c.Tr.ShowingDiffForRange, from.ShortRefName(), to.ShortRefName())
|
||||
return types.NewRunDiffRendererTaskWithPrefix(cmdObj.GetCmd(), prefix)
|
||||
return types.NewMainViewDiffTaskWithPrefix(cmdObj.GetCmd(), prefix, mode)
|
||||
}
|
||||
|
||||
cmdObj := self.c.Git().Commit.ShowCmdObj(commit.Hash(), self.FilterPathsForCommit(commit), git_commands.DiffModeRendered)
|
||||
return types.NewRunDiffRendererTask(cmdObj.GetCmd())
|
||||
cmdObj := self.c.Git().Commit.ShowCmdObj(commit.Hash(), self.FilterPathsForCommit(commit), mode)
|
||||
return types.NewMainViewDiffTask(cmdObj.GetCmd(), mode)
|
||||
}
|
||||
|
||||
// PlainDiffBetweenRefs returns the diff of the given files between two refs as git
|
||||
|
||||
@@ -9,6 +9,11 @@ import (
|
||||
|
||||
type DiffLineHelper struct {
|
||||
c *HelperCommon
|
||||
|
||||
// What the probe said about the diff renderer that rendererSignature names, or nil
|
||||
// before it has been asked about any (see diffRendererEmitsMetadata).
|
||||
rendererEmitsMetadata *bool
|
||||
rendererSignature string
|
||||
}
|
||||
|
||||
func NewDiffLineHelper(c *HelperCommon) *DiffLineHelper {
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
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"))
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
@@ -60,9 +59,10 @@ func (self *ReflogCommitsController) GetOnRenderToMain() func() {
|
||||
if commit == nil {
|
||||
task = types.NewRenderStringTask("No reflog history")
|
||||
} else {
|
||||
cmdObj := self.c.Git().Commit.ShowCmdObj(commit.Hash(), self.c.Helpers().Diff.FilterPathsForCommit(commit), git_commands.DiffModeRendered)
|
||||
mode := self.c.Helpers().DiffLine.MainViewDiffMode()
|
||||
cmdObj := self.c.Git().Commit.ShowCmdObj(commit.Hash(), self.c.Helpers().Diff.FilterPathsForCommit(commit), mode)
|
||||
|
||||
task = types.NewRunDiffRendererTask(cmdObj.GetCmd())
|
||||
task = types.NewMainViewDiffTask(cmdObj.GetCmd(), mode)
|
||||
}
|
||||
|
||||
self.c.RenderToMainViews(types.RefreshMainOpts{
|
||||
|
||||
@@ -3,8 +3,6 @@ package controllers
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
@@ -94,10 +92,12 @@ func (self *StashController) GetOnRenderToMain() func() {
|
||||
if stashEntry == nil {
|
||||
task = types.NewRenderStringTask(self.c.Tr.NoStashEntries)
|
||||
} else {
|
||||
mode := self.c.Helpers().DiffLine.MainViewDiffMode()
|
||||
prefix := style.FgYellow.Sprintf("%s\n\n", stashEntry.Description())
|
||||
task = types.NewRunDiffRendererTaskWithPrefix(
|
||||
self.c.Git().Stash.ShowStashEntryCmdObj(stashEntry.Index, git_commands.DiffModeRendered).GetCmd(),
|
||||
task = types.NewMainViewDiffTaskWithPrefix(
|
||||
self.c.Git().Stash.ShowStashEntryCmdObj(stashEntry.Index, mode).GetCmd(),
|
||||
prefix,
|
||||
mode,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
@@ -83,9 +84,22 @@ func (self *SwitchToFocusedMainViewController) focusMainView(mainViewContext *co
|
||||
mainViewContext.ClearSearchString()
|
||||
self.c.Context().Push(mainViewContext, types.OnFocusOpts{})
|
||||
|
||||
if _, ok := self.context.(types.DiffMainViewContext); ok {
|
||||
self.c.Helpers().DiffLine.EstablishSelection(mainViewContext, clickedLineIdx)
|
||||
if _, ok := self.context.(types.DiffMainViewContext); !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// The diff on screen was produced for reading, and the renderer that produced it may
|
||||
// have laid it out in a way that says nothing about which line of which file each row
|
||||
// is. Now that the user wants to act on it, it is re-rendered as git's own diff — the
|
||||
// panel below decides that for itself, from the same question — and the selection
|
||||
// goes on that instead of on rows we can't place.
|
||||
if self.c.Helpers().DiffLine.MainViewDiffMode() == git_commands.DiffModeRaw {
|
||||
self.c.Helpers().DiffLine.RenderFocusedMainViewAgain(mainViewContext.GetView(), self.context, func() {
|
||||
self.c.Helpers().DiffLine.EstablishSelection(mainViewContext, clickedLineIdx)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
self.c.Helpers().DiffLine.EstablishSelection(mainViewContext, clickedLineIdx)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package types
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
)
|
||||
|
||||
type MainContextPair struct {
|
||||
@@ -116,3 +118,18 @@ func NewRunDiffRendererTask(cmd *exec.Cmd) *RunDiffRendererTask {
|
||||
func NewRunDiffRendererTaskWithPrefix(cmd *exec.Cmd, prefix string) *RunDiffRendererTask {
|
||||
return &RunDiffRendererTask{Cmd: cmd, Prefix: prefix}
|
||||
}
|
||||
|
||||
// NewMainViewDiffTask returns the task for rendering a diff into a main view. A diff
|
||||
// normally goes through the diff renderer, however the render arranges to feed it. A
|
||||
// diff we are producing with git itself, because the renderer's version of it couldn't
|
||||
// be acted on, has to keep the renderer out, so it runs as a plain command instead.
|
||||
func NewMainViewDiffTask(cmd *exec.Cmd, mode git_commands.DiffMode) UpdateTask {
|
||||
return NewMainViewDiffTaskWithPrefix(cmd, "", mode)
|
||||
}
|
||||
|
||||
func NewMainViewDiffTaskWithPrefix(cmd *exec.Cmd, prefix string, mode git_commands.DiffMode) UpdateTask {
|
||||
if mode == git_commands.DiffModeRaw {
|
||||
return NewRunCommandTaskWithPrefix(cmd, prefix)
|
||||
}
|
||||
return NewRunDiffRendererTaskWithPrefix(cmd, prefix)
|
||||
}
|
||||
|
||||
@@ -23,11 +23,13 @@ var CopySelectedDiffLines = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
// Emulate the clipboard by writing to a file.
|
||||
cfg.GetUserConfig().OS.CopyToClipboardCmd = "printf '%s' {{text}} > clipboard"
|
||||
// A renderer that decorates every line of a diff's body, so that what is on
|
||||
// screen is not what the diff says. It reads the +/- column, so it wants its
|
||||
// input uncoloured.
|
||||
// screen is not what the diff says. It announces the metadata protocol, so that
|
||||
// its output is taken at its word rather than replaced by git's own; and it
|
||||
// reads the +/- column, so it wants its input uncoloured.
|
||||
cfg.GetUserConfig().Git.DiffRenderers = []config.DiffRendererConfig{
|
||||
{
|
||||
Command: `awk '/^@@/ { body = 1 } body && /^[-+ ]/ { print $0 " <<<"; next } { print }'`,
|
||||
Command: `printf '\033]1717;1\007'; ` +
|
||||
`awk '/^@@/ { body = 1 } body && /^[-+ ]/ { print $0 " <<<"; next } { print }'`,
|
||||
ColorArg: "never",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,11 +16,14 @@ var KeepPositionWhenSwitchingDiffRenderers = NewIntegrationTest(NewIntegrationTe
|
||||
Height: 30,
|
||||
SetupConfig: func(cfg *config.AppConfig) {
|
||||
cfg.GetUserConfig().Gui.UseHunkModeInStagingView = false
|
||||
// Both announce the metadata protocol, so that their output is taken at its
|
||||
// word and shown as it is; a renderer that says nothing about what it renders
|
||||
// is replaced by git's own diff as soon as the main view is focused.
|
||||
cfg.GetUserConfig().Git.DiffRenderers = []config.DiffRendererConfig{
|
||||
{Name: "plain", Command: "cat"},
|
||||
{Name: "plain", Command: `printf '\033]1717;1\007'; cat`},
|
||||
// The same diff, three lines further down the view. (Lines before the
|
||||
// diff's own header aren't part of it, so it still reads the same.)
|
||||
{Name: "banner", Command: `printf 'rendered for you\n\n\n'; cat`},
|
||||
{Name: "banner", Command: `printf '\033]1717;1\007rendered for you\n\n\n'; cat`},
|
||||
}
|
||||
},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
|
||||
@@ -12,14 +12,15 @@ var KeepSelectionVisibleWhenDiffShrinks = NewIntegrationTest(NewIntegrationTestA
|
||||
SetupConfig: func(cfg *config.AppConfig) {
|
||||
cfg.GetUserConfig().Gui.UseHunkModeInStagingView = false
|
||||
cfg.GetUserConfig().Git.DiffRenderers = []config.DiffRendererConfig{
|
||||
{Command: "cat"},
|
||||
{Name: "plain", Command: `printf '\033]1717;1\007'; cat`},
|
||||
// The same diff in fewer lines, as a renderer that collapses or elides
|
||||
// parts of it would give us: the addition at the end goes, and the hunk
|
||||
// header says so, since a diff that contradicts its own header can't be
|
||||
// read as one. (It has to read all of its input: one that exits early
|
||||
// leaves the render looking like it is still loading, which holds off the
|
||||
// clamping this test is about.)
|
||||
{Name: "shrinking", Command: `sed -e 's/@@ -1,5 +1,5 @@/@@ -1,5 +1,4 @@/' -e '$d'`},
|
||||
{Name: "shrinking", Command: `printf '\033]1717;1\007'; ` +
|
||||
`sed -e 's/@@ -1,5 +1,5 @@/@@ -1,5 +1,4 @@/' -e '$d'`},
|
||||
}
|
||||
},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package main_view
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var RawFallbackUnderAnExternalDiff = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Focusing the main view under an external diff that says nothing about its rows brings git's own diff, keeping the scroll position",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
Width: 120,
|
||||
Height: 30,
|
||||
SetupConfig: func(cfg *config.AppConfig) {
|
||||
cfg.GetUserConfig().Gui.UseHunkModeInStagingView = false
|
||||
// An external diff whose output has nothing to do with the diff it was given,
|
||||
// let alone anything to say about which line of which file each row shows. It
|
||||
// is long enough to be scrolled about in.
|
||||
cfg.GetUserConfig().Git.DiffRenderers = []config.DiffRendererConfig{
|
||||
{Name: "opaque", Type: "extDiff", Command: `sh -c 'seq -f "EXT-%g" 40'`},
|
||||
}
|
||||
},
|
||||
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) {
|
||||
// Browsing shows what the renderer produced, whatever that is.
|
||||
t.Views().Main().
|
||||
Content(Contains("EXT-1")).
|
||||
Tap(func() {
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Press(keys.Universal.ScrollDownMain).
|
||||
Press(keys.Universal.ScrollDownMain).
|
||||
Press(keys.Universal.ScrollDownMain)
|
||||
}).
|
||||
OriginY(6).
|
||||
Tap(func() {
|
||||
t.Views().Files().Press(keys.Universal.FocusMainView)
|
||||
}).
|
||||
IsFocused().
|
||||
// Focusing the view to act on it brings git's own diff instead — the
|
||||
// renderer's rows can't be placed in the file — and leaves the view at the
|
||||
// offset it was at, rather than at the top.
|
||||
Content(Contains("+LINE05")).
|
||||
Content(DoesNotContain("EXT-1")).
|
||||
OriginY(6).
|
||||
SelectionIsActive()
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,55 @@
|
||||
package main_view
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var StageUnderConformingDiffRenderer = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "A diff renderer that announces the metadata protocol is taken at its word, so its diff is what stays on screen when the main view is focused",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(cfg *config.AppConfig) {
|
||||
cfg.GetUserConfig().Gui.UseHunkModeInStagingView = false
|
||||
// A renderer that announces the protocol with a version-only record before
|
||||
// anything else, then says who it is and passes the diff through. The diff it
|
||||
// passes through keeps its structure, so the rows can be placed by reading it.
|
||||
cfg.GetUserConfig().Git.DiffRenderers = []config.DiffRendererConfig{
|
||||
{Command: `printf '\033]1717;1\007RENDERED BY ME\n'; cat`},
|
||||
}
|
||||
},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.CreateFileAndAdd("file1", "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\n")
|
||||
shell.Commit("one")
|
||||
|
||||
shell.UpdateFile("file1", "one\ntwo\nTHREE\nfour\nfive\nsix\nseven\neight\nNINE\nten\n")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().IsFocused()
|
||||
|
||||
// The announcement itself leaves nothing on the screen; the line after it shows
|
||||
// whose output this is.
|
||||
t.Views().Main().Content(Contains("RENDERED BY ME"))
|
||||
|
||||
t.Views().Files().Press(keys.Universal.FocusMainView)
|
||||
|
||||
// Focusing left the renderer's output alone, and the selection went on it.
|
||||
t.Views().Main().
|
||||
IsFocused().
|
||||
Content(Contains("RENDERED BY ME")).
|
||||
SelectedLines(
|
||||
Contains("-three"),
|
||||
).
|
||||
Press(keys.Main.ToggleSelectHunk).
|
||||
SelectedLines(
|
||||
Contains("-three"),
|
||||
Contains("+THREE"),
|
||||
).
|
||||
PressPrimaryAction()
|
||||
|
||||
t.Views().Secondary().ContainsLines(
|
||||
Contains("-three"),
|
||||
Contains("+THREE"),
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,71 @@
|
||||
package main_view
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var StageUnderUnsupportedDiffRenderer = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "A diff renderer that says nothing about what it renders is replaced by git's own diff when the main view is focused, so the diff can still be staged from",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(cfg *config.AppConfig) {
|
||||
cfg.GetUserConfig().Gui.UseHunkModeInStagingView = false
|
||||
// `cat -n` numbers every line, which pushes the +/- column off the start of it:
|
||||
// the diff can't be read back from the text, and cat says nothing about what it
|
||||
// is rendering, so there is no way to act on what it produces.
|
||||
cfg.GetUserConfig().Git.DiffRenderers = []config.DiffRendererConfig{
|
||||
{Command: "cat -n"},
|
||||
}
|
||||
},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.CreateFileAndAdd("file1", "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\n")
|
||||
shell.Commit("one")
|
||||
|
||||
shell.UpdateFile("file1", "one\ntwo\nTHREE\nfour\nfive\nsix\nseven\neight\nNINE\nten\n")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
// While browsing, the view shows the renderer's output.
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Lines(
|
||||
Contains("file1").IsSelected(),
|
||||
)
|
||||
t.Views().Main().Content(Contains("1 diff --git a/file1 b/file1"))
|
||||
|
||||
// Focusing it to act on it brings git's own diff instead, and the selection goes
|
||||
// on that.
|
||||
t.Views().Files().Press(keys.Universal.FocusMainView)
|
||||
|
||||
t.Views().Main().
|
||||
IsFocused().
|
||||
Content(DoesNotContain("1 diff --git")).
|
||||
SelectedLines(
|
||||
Contains("-three"),
|
||||
).
|
||||
Press(keys.Main.ToggleSelectHunk).
|
||||
SelectedLines(
|
||||
Contains("-three"),
|
||||
Contains("+THREE"),
|
||||
).
|
||||
PressPrimaryAction()
|
||||
|
||||
t.Views().Secondary().ContainsLines(
|
||||
Contains("-three"),
|
||||
Contains("+THREE"),
|
||||
)
|
||||
// The re-render after staging stays with git's diff, so the next hunk can be
|
||||
// staged as well.
|
||||
t.Views().Main().
|
||||
IsFocused().
|
||||
SelectedLines(
|
||||
Contains("-nine"),
|
||||
Contains("+NINE"),
|
||||
).
|
||||
PressPrimaryAction()
|
||||
|
||||
t.Views().Files().Lines(
|
||||
Contains("M file1"),
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -403,6 +403,7 @@ var tests = []*components.IntegrationTest{
|
||||
main_view.NoSelectionOverACommitLog,
|
||||
main_view.NoSelectionWhenNoChanges,
|
||||
main_view.RangeSelectDiffLines,
|
||||
main_view.RawFallbackUnderAnExternalDiff,
|
||||
main_view.SearchCollapsesTheSelection,
|
||||
main_view.SearchFollowsTheSelection,
|
||||
main_view.SelectBelowALongCommitMessage,
|
||||
@@ -422,6 +423,8 @@ var tests = []*components.IntegrationTest{
|
||||
main_view.StageDiffLines,
|
||||
main_view.StageDiffLinesOfAPathWithASpace,
|
||||
main_view.StageRangeSpanningFiles,
|
||||
main_view.StageUnderConformingDiffRenderer,
|
||||
main_view.StageUnderUnsupportedDiffRenderer,
|
||||
main_view.UnstageDiffLines,
|
||||
misc.ConfirmOnQuit,
|
||||
misc.CopyConfirmationMessageToClipboard,
|
||||
|
||||
Reference in New Issue
Block a user