diff --git a/pkg/gui/context/main_context.go b/pkg/gui/context/main_context.go index 92ce65d37..f726b4ea6 100644 --- a/pkg/gui/context/main_context.go +++ b/pkg/gui/context/main_context.go @@ -19,6 +19,11 @@ type MainContext struct { // was worked out from. What there is to select is a property of the content, so an // answer about the content of another render says nothing about this one. selectableContentRenderKey string + // contentIsDiff says the render this pane was last given is the diff the panel + // beneath offers here, rather than a message or an explanation (see + // types.ContentIsDiff). It is known as the render is asked for, well before there + // is any content to look at. + contentIsDiff bool } var ( @@ -65,6 +70,17 @@ func (self *MainContext) SetSelectableContentRenderKey(key string) { self.selectableContentRenderKey = key } +// ContentIsDiff reports whether this pane is showing the panel's diff (see +// contentIsDiff). +func (self *MainContext) ContentIsDiff() bool { + return self.contentIsDiff +} + +// SetContentIsDiff records what kind of render the pane is being given. +func (self *MainContext) SetContentIsDiff(value bool) { + self.contentIsDiff = value +} + func NewMainContext( view *gocui.View, windowName string, diff --git a/pkg/gui/controllers/helpers/diff_helper.go b/pkg/gui/controllers/helpers/diff_helper.go index fed353d82..0c829639c 100644 --- a/pkg/gui/controllers/helpers/diff_helper.go +++ b/pkg/gui/controllers/helpers/diff_helper.go @@ -141,7 +141,7 @@ func (self *DiffHelper) RenderDiff() { self.c.Tr.ShowingGitDiff, "git diff "+strings.Join(args, " "), ) - task := types.NewRunDiffRendererTaskWithPrefix(cmdObj.GetCmd(), prefix) + task := types.NewMainViewDiffTaskWithPrefix(cmdObj.GetCmd(), prefix, git_commands.DiffModeRendered) self.c.RenderToMainViews(types.RefreshMainOpts{ Pair: self.c.MainViewPairs().Normal, diff --git a/pkg/gui/controllers/helpers/diff_line_selection.go b/pkg/gui/controllers/helpers/diff_line_selection.go index 1b42cc181..a8b465140 100644 --- a/pkg/gui/controllers/helpers/diff_line_selection.go +++ b/pkg/gui/controllers/helpers/diff_line_selection.go @@ -30,10 +30,10 @@ func (self *DiffLineHelper) EstablishSelection(mainContext *context.MainContext, mainContext.ResetDiffSelectMode() view := mainContext.GetView() - // The panel beneath renders a diff, but that diff may hold nothing to act on: a - // binary file, or an empty commit. Rendering it worked that out, so the pane is - // already showing no selection and there is nowhere to put one. - if !self.ViewHasChangeLines(view) { + // The pane may hold nothing to act on: a diff of a binary file or an empty commit, + // or content that is no diff of the panel's at all. Rendering it worked that out, + // so the pane is already showing no selection and there is nowhere to put one. + if !mainContext.HasSelectableContent() { return } diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index 1e1a01427..72c99d2ad 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -727,7 +727,7 @@ func secondaryPatchPanelUpdateOpts(c *ControllerCommon) *types.ViewUpdateOpts { patch := c.Git().Patch.PatchBuilder.RenderAggregatedPatch(false) return &types.ViewUpdateOpts{ - Task: types.NewRenderStringWithoutScrollTask(patch), + Task: types.NewMainViewDiffStringTask(patch), Title: c.Tr.CustomPatch, } } diff --git a/pkg/gui/main_panels.go b/pkg/gui/main_panels.go index ba8e7c200..5db9d1925 100644 --- a/pkg/gui/main_panels.go +++ b/pkg/gui/main_panels.go @@ -66,6 +66,10 @@ func (gui *Gui) RefreshMainView(opts *types.ViewUpdateOpts, context types.Contex view.Subtitle = opts.SubTitle + if mainContext := gui.mainContextForView(view); mainContext != nil { + mainContext.SetContentIsDiff(types.ContentIsDiff(opts.Task)) + } + if err := gui.runTaskForView(view, opts.Task); err != nil { gui.c.Log.Error(err) } @@ -290,6 +294,9 @@ func (gui *Gui) clearMainView(mainContext types.Context) { view.Clear() view.SetOrigin(0, 0) mainContext.SetHasSelectableContent(false) + if pane := gui.mainContextForView(view); pane != nil { + pane.SetContentIsDiff(false) + } gui.State.ContextMgr.UpdateSelectionHighlights() if manager := gui.getViewBufferManagerForView(view); manager != nil { manager.ForgetRenderedContent() @@ -298,10 +305,10 @@ func (gui *Gui) clearMainView(mainContext types.Context) { // updateDiffSelectionVisibility works out whether a main pane holds anything for a // selection to sit on, from what it is now showing: only beneath a panel whose main -// view is a diff, and only while that diff holds something to select — never over a -// message like "No changed files", and never over a diff with nothing in it, such as a -// binary file's or an empty commit's. Whether the selection is then drawn, and drawn as -// the active one, follows from the context stack. +// view is a diff, only while the pane is showing that diff rather than a message like +// "No changed files", and only while the diff holds something to select — never over +// one with nothing in it, such as a binary file's or an empty commit's. Whether the +// selection is then drawn, and drawn as the active one, follows from the context stack. // // It is asked wherever the pane's content changes: as a string is rendered, at the // paint that reveals a command's output, with every further batch of that output, and @@ -379,6 +386,14 @@ func (gui *Gui) readOnUntilTheDiffPaneCanTell(view *gocui.View) { func (gui *Gui) diffPaneHasSomethingToSelect( mainContext *context.MainContext, view *gocui.View, contentIsComplete bool, ) (bool, bool) { + if !mainContext.ContentIsDiff() { + // The pane is holding something other than the panel's diff: a message, or the + // hint shown for a merge conflict that has to be resolved by picking a side, + // which explains itself with a diff of what the sides did. Whatever is in there + // is not ours to act on, so this needs no content to answer either. + return false, true + } + if _, showsDiff := gui.State.ContextMgr.CurrentSide().(types.DiffMainViewContext); !showsDiff { // Under a panel that shows no diff there is nothing to select whatever the pane // ends up holding, so this needs no content to answer. Answering it now matters, diff --git a/pkg/gui/types/rendering.go b/pkg/gui/types/rendering.go index 6f791d1bf..ac8932a9b 100644 --- a/pkg/gui/types/rendering.go +++ b/pkg/gui/types/rendering.go @@ -69,6 +69,9 @@ func NewRenderStringTask(str string) *RenderStringTask { type RenderStringWithoutScrollTask struct { Str string + + // contentIsDiff marks a string that is a panel's own diff; see ContentIsDiff. + contentIsDiff bool } func (t *RenderStringWithoutScrollTask) IsUpdateTask() {} @@ -77,6 +80,16 @@ func NewRenderStringWithoutScrollTask(str string) *RenderStringWithoutScrollTask return &RenderStringWithoutScrollTask{Str: str} } +// NewMainViewDiffStringTask returns the task for rendering a diff we hold as text +// rather than as a command to run — the custom patch being built, which we assemble +// ourselves. The view stays where it is, the patch being rendered again on every +// change to it. +func NewMainViewDiffStringTask(str string) UpdateTask { + task := NewRenderStringWithoutScrollTask(str) + task.contentIsDiff = true + return task +} + type RenderStringWithScrollTask struct { Str string OriginX int @@ -92,6 +105,9 @@ func NewRenderStringWithScrollTask(str string, originX int, originY int) *Render type RunCommandTask struct { Cmd *exec.Cmd Prefix string + + // contentIsDiff marks output that is a panel's own diff; see ContentIsDiff. + contentIsDiff bool } func (t *RunCommandTask) IsUpdateTask() {} @@ -107,6 +123,9 @@ func NewRunCommandTaskWithPrefix(cmd *exec.Cmd, prefix string) *RunCommandTask { type RunDiffRendererTask struct { Cmd *exec.Cmd Prefix string + + // contentIsDiff marks output that is a panel's own diff; see ContentIsDiff. + contentIsDiff bool } func (t *RunDiffRendererTask) IsUpdateTask() {} @@ -123,13 +142,36 @@ func NewRunDiffRendererTaskWithPrefix(cmd *exec.Cmd, prefix string) *RunDiffRend // 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. +// +// The task it returns is the one that says its output is a diff, so a pane rendering +// it can be pointed at (see ContentIsDiff). 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) + task := NewRunCommandTaskWithPrefix(cmd, prefix) + task.contentIsDiff = true + return task } - return NewRunDiffRendererTaskWithPrefix(cmd, prefix) + task := NewRunDiffRendererTaskWithPrefix(cmd, prefix) + task.contentIsDiff = true + return task +} + +// ContentIsDiff reports whether the given render fills a main pane with the diff a +// panel offers there, as opposed to a message, a commit log, or a diff that is part of +// an explanation. A selection means the lines of the panel's diff, so it is only over +// such a render that there is anything to point at. +func ContentIsDiff(task UpdateTask) bool { + switch task := task.(type) { + case *RenderStringWithoutScrollTask: + return task.contentIsDiff + case *RunCommandTask: + return task.contentIsDiff + case *RunDiffRendererTask: + return task.contentIsDiff + } + return false } diff --git a/pkg/integration/tests/main_view/no_selection_over_a_conflict_hint.go b/pkg/integration/tests/main_view/no_selection_over_a_conflict_hint.go new file mode 100644 index 000000000..45cd19ce2 --- /dev/null +++ b/pkg/integration/tests/main_view/no_selection_over_a_conflict_hint.go @@ -0,0 +1,52 @@ +package main_view + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var NoSelectionOverAConflictHint = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "The hint for a conflict that has to be resolved by picking a side shows no selection, diff and all", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + config.GetUserConfig().Gui.ShowFileTree = false + }, + SetupRepo: func(shell *Shell) { + shell.RunShellCommand(`echo 1 > foo && echo 1 > bar`) + shell.RunShellCommand(`git checkout -b base && git add . && git commit -m base`) + + // theirs: delete foo, modify bar + shell.RunShellCommand(`git checkout -b theirs`) + shell.RunShellCommand(`git rm foo && echo 2 > bar && git add bar && git commit -m theirs`) + + // ours: modify foo, delete bar + shell.RunShellCommand(`git checkout base && git checkout -b ours`) + shell.RunShellCommand(`echo 2 > foo && git add foo && git rm bar && git commit -m ours`) + + shell.RunCommandExpectError([]string{"git", "merge", "theirs"}) + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + // The hint for a file deleted on one side and modified on the other explains + // itself with a diff of what the other side did. That diff is no more ours to + // stage than the words above it, so the pane holds nothing to point at. + t.Views().Files(). + IsFocused(). + NavigateToLine(Contains("DU bar")). + Tap(func() { + t.Views().Main(). + Content(Contains("Conflict: this file was deleted in the current changes")). + Content(Contains("Incoming changes:")). + Content(Contains("+2")) + }). + Press(keys.Universal.FocusMainView) + + t.Views().Main(). + IsFocused(). + SelectionIsHidden(). + PressPrimaryAction(). + Tap(func() { + t.ExpectToast(Contains("There is nothing to select here")) + }) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 4abf98c7c..176ad019f 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -400,6 +400,7 @@ var tests = []*components.IntegrationTest{ main_view.NavigateByHunkAndFile, main_view.NoSelectionOverABinaryDiff, main_view.NoSelectionOverACommitLog, + main_view.NoSelectionOverAConflictHint, main_view.NoSelectionWhenNoChanges, main_view.RangeSelectDiffLines, main_view.RawFallbackUnderAnExternalDiff,