From 7857f9995f8ef9887b978ed2c01e369cb17a954a Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Thu, 20 Aug 2026 21:04:41 +0200 Subject: [PATCH] Open file diffs in the focused main view Enter and double-click should take users to the diff where line actions now live, whether the file belongs to the working tree or a commit. Share the existing focus, raw-fallback, and selection setup while retaining directory, submodule, and conflict-specific behavior. Co-Authored-By: GitHub Copilot --- .../controllers/commits_files_controller.go | 33 ++------- pkg/gui/controllers/files_controller.go | 18 +++-- .../switch_to_focused_main_view_controller.go | 30 +++++--- .../tests/commit/discard_old_file_changes.go | 2 +- pkg/integration/tests/demo/custom_patch.go | 2 +- pkg/integration/tests/demo/stage_lines.go | 2 +- .../enter_and_double_click_focus_file_diff.go | 69 +++++++++++++++++++ pkg/integration/tests/test_list.go | 1 + 8 files changed, 104 insertions(+), 53 deletions(-) create mode 100644 pkg/integration/tests/main_view/enter_and_double_click_focus_file_diff.go diff --git a/pkg/gui/controllers/commits_files_controller.go b/pkg/gui/controllers/commits_files_controller.go index 8871aa425..f3332dddd 100644 --- a/pkg/gui/controllers/commits_files_controller.go +++ b/pkg/gui/controllers/commits_files_controller.go @@ -557,40 +557,15 @@ func (self *CommitFilesController) currentFromToReverseForPatchBuilding() (strin } func (self *CommitFilesController) enter(node *filetree.CommitFileNode) error { - return self.enterCommitFile(node, types.OnFocusOpts{ClickedWindowName: "", ClickedViewLineIdx: -1}) -} - -func (self *CommitFilesController) enterCommitFile(node *filetree.CommitFileNode, opts types.OnFocusOpts) error { if node.File == nil { return self.handleToggleCommitFileDirCollapsed(node) } - if self.c.UserConfig().Git.DiffContextSize == 0 { - return fmt.Errorf(self.c.Tr.Actions.NotEnoughContextForCustomPatch, - self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView) - } + return focusMainView(self.c, self.context(), -1) +} - from, to, reverse := self.currentFromToReverseForPatchBuilding() - mustDiscardPatch := self.c.Git().Patch.PatchBuilder.Active() && self.c.Git().Patch.PatchBuilder.NewPatchRequired(from, to, reverse) - return self.c.ConfirmIf(mustDiscardPatch, types.ConfirmOpts{ - Title: self.c.Tr.DiscardPatch, - Prompt: self.c.Tr.DiscardPatchConfirm, - HandleConfirm: func() error { - if mustDiscardPatch { - self.c.Git().Patch.PatchBuilder.Reset() - } - - if !self.c.Git().Patch.PatchBuilder.Active() { - if err := self.startPatchBuilder(); err != nil { - return err - } - } - - self.c.Context().Push(self.c.Contexts().CustomPatchBuilder, opts) - - return nil - }, - }) +func (self *CommitFilesController) GetOnDoubleClick() func() error { + return self.withItemGraceful(self.enter) } func (self *CommitFilesController) handleToggleCommitFileDirCollapsed(node *filetree.CommitFileNode) error { diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index 7b5685135..1f25e7e4e 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -415,9 +415,7 @@ func (self *FilesController) GetFocusedMainViewDiffSource() types.FocusedMainVie } func (self *FilesController) GetOnDoubleClick() func() error { - return self.withItemGraceful(func(node *filetree.FileNode) error { - return self.press([]*filetree.FileNode{node}) - }) + return self.enter } // if we are dealing with a status for which there is no key in this map, @@ -694,7 +692,7 @@ func (self *FilesController) getSelectedFile() *models.File { } func (self *FilesController) enter() error { - return self.EnterFile(types.OnFocusOpts{ClickedWindowName: "", ClickedViewLineIdx: -1}) + return self.enterFile(-1) } func (self *FilesController) collapseAll() error { @@ -713,7 +711,10 @@ func (self *FilesController) expandAll() error { return nil } -func (self *FilesController) EnterFile(opts types.OnFocusOpts) error { +// enterFile focuses the diff of the selected file, which is where the commands that +// act on its lines live. clickedViewLineIdx is the row of the diff a click landed on, +// or -1 when the diff wasn't clicked. +func (self *FilesController) enterFile(clickedViewLineIdx int) error { node := self.context().GetSelected() if node == nil { return nil @@ -739,10 +740,7 @@ func (self *FilesController) EnterFile(opts types.OnFocusOpts) error { return self.switchToMerge() } - context := lo.Ternary(opts.ClickedWindowName == "secondary", self.c.Contexts().StagingSecondary, self.c.Contexts().Staging) - self.c.Context().Push(context, opts) - - return nil + return focusMainView(self.c, self.context(), clickedViewLineIdx) } // conflictResolutionHint formats a conflict description for the main view, @@ -1533,7 +1531,7 @@ func (self *FilesController) handleStashSave(stashFunc func(message string) erro } func (self *FilesController) onClickMain(opts gocui.ViewMouseBindingOpts) error { - return self.EnterFile(types.OnFocusOpts{ClickedWindowName: "main", ClickedViewLineIdx: opts.Y}) + return self.enterFile(opts.Y) } func (self *FilesController) fetch() error { diff --git a/pkg/gui/controllers/switch_to_focused_main_view_controller.go b/pkg/gui/controllers/switch_to_focused_main_view_controller.go index b2e85914c..2bb6870a9 100644 --- a/pkg/gui/controllers/switch_to_focused_main_view_controller.go +++ b/pkg/gui/controllers/switch_to_focused_main_view_controller.go @@ -71,20 +71,28 @@ func (self *SwitchToFocusedMainViewController) onClickSecondary(opts gocui.ViewM } func (self *SwitchToFocusedMainViewController) handleFocusMainView() error { + return focusMainView(self.c, self.context, -1) +} + +func focusMainView(c *ControllerCommon, source types.Context, clickedLineIdx int) error { // Usually the main pane, but the content can be in the secondary one alone: a file // with nothing but staged changes shows them there. - mainViewContext := self.c.Contexts().Normal - if self.c.State().GetRepoState().GetMainPanes() == types.SecondaryPaneOnly { - mainViewContext = self.c.Contexts().NormalSecondary + mainViewContext := c.Contexts().Normal + if c.State().GetRepoState().GetMainPanes() == types.SecondaryPaneOnly { + mainViewContext = c.Contexts().NormalSecondary } - return self.focusMainView(mainViewContext, -1) + return focusMainViewPane(c, source, mainViewContext, clickedLineIdx) } func (self *SwitchToFocusedMainViewController) focusMainView(mainViewContext *context.MainContext, clickedLineIdx int) error { - mainViewContext.ClearSearchString() - self.c.Context().Push(mainViewContext, types.OnFocusOpts{}) + return focusMainViewPane(self.c, self.context, mainViewContext, clickedLineIdx) +} - if _, ok := self.context.(types.DiffMainViewContext); !ok { +func focusMainViewPane(c *ControllerCommon, source types.Context, mainViewContext *context.MainContext, clickedLineIdx int) error { + mainViewContext.ClearSearchString() + c.Context().Push(mainViewContext, types.OnFocusOpts{}) + + if _, ok := source.(types.DiffMainViewContext); !ok { return nil } @@ -93,13 +101,13 @@ func (self *SwitchToFocusedMainViewController) focusMainView(mainViewContext *co // 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) + if c.Helpers().DiffLine.MainViewDiffMode() == git_commands.DiffModeRaw { + c.Helpers().DiffLine.RenderFocusedMainViewAgain(mainViewContext.GetView(), source, func() { + c.Helpers().DiffLine.EstablishSelection(mainViewContext, clickedLineIdx) }) return nil } - self.c.Helpers().DiffLine.EstablishSelection(mainViewContext, clickedLineIdx) + c.Helpers().DiffLine.EstablishSelection(mainViewContext, clickedLineIdx) return nil } diff --git a/pkg/integration/tests/commit/discard_old_file_changes.go b/pkg/integration/tests/commit/discard_old_file_changes.go index d7f4acdc1..0d0dc7818 100644 --- a/pkg/integration/tests/commit/discard_old_file_changes.go +++ b/pkg/integration/tests/commit/discard_old_file_changes.go @@ -131,7 +131,7 @@ var DiscardOldFileChanges = NewIntegrationTest(NewIntegrationTestArgs{ Equals(" A file2ToRemove"), ). NavigateToLine(Contains("multiLineFile")). - Press(keys.Universal.FocusMainView) + PressEnter() t.Views().Main(). IsFocused(). diff --git a/pkg/integration/tests/demo/custom_patch.go b/pkg/integration/tests/demo/custom_patch.go index 34516f73b..3c082f27b 100644 --- a/pkg/integration/tests/demo/custom_patch.go +++ b/pkg/integration/tests/demo/custom_patch.go @@ -52,7 +52,7 @@ var CustomPatch = NewIntegrationTest(NewIntegrationTestArgs{ IsFocused(). NavigateToLine(Contains("users.go")). Wait(1000). - Press(keys.Universal.FocusMainView). + PressEnter(). Tap(func() { t.Views().Main(). IsFocused(). diff --git a/pkg/integration/tests/demo/stage_lines.go b/pkg/integration/tests/demo/stage_lines.go index 4881722a7..8d05749f6 100644 --- a/pkg/integration/tests/demo/stage_lines.go +++ b/pkg/integration/tests/demo/stage_lines.go @@ -56,7 +56,7 @@ var StageLines = NewIntegrationTest(NewIntegrationTestArgs{ t.Views().Files(). IsFocused(). - Press(keys.Universal.FocusMainView) + PressEnter() t.Views().Main(). IsFocused(). diff --git a/pkg/integration/tests/main_view/enter_and_double_click_focus_file_diff.go b/pkg/integration/tests/main_view/enter_and_double_click_focus_file_diff.go new file mode 100644 index 000000000..cecb8047e --- /dev/null +++ b/pkg/integration/tests/main_view/enter_and_double_click_focus_file_diff.go @@ -0,0 +1,69 @@ +package main_view + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var EnterAndDoubleClickFocusFileDiff = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Enter and double-click focus a file's diff in the main view", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + config.GetUserConfig().Gui.UseHunkModeInStagingView = false + }, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("file1", "base\n") + shell.Commit("add file") + shell.UpdateFile("file1", "changed\n") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Lines( + Contains("file1").IsSelected(), + ). + PressEnter() + + t.Views().Main(). + IsFocused(). + SelectedLines(Contains("-base")). + PressEscape() + + t.Views().Files(). + IsFocused(). + Click(3, 0). + Click(3, 0) + t.Views().Main(). + IsFocused(). + SelectedLines(Contains("-base")). + PressEscape() + + t.Views().Commits(). + Focus(). + Lines( + Contains("add file").IsSelected(), + ). + PressEnter() + + t.Views().CommitFiles(). + IsFocused(). + Lines( + Contains("file1").IsSelected(), + ). + PressEnter() + + t.Views().Main(). + IsFocused(). + SelectedLines(Contains("+base")). + PressEscape() + + t.Views().CommitFiles(). + IsFocused(). + Click(3, 0). + Click(3, 0) + t.Views().Main(). + IsFocused(). + SelectedLines(Contains("+base")) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index cd4a4c4a0..75da21ed8 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -392,6 +392,7 @@ var tests = []*components.IntegrationTest{ main_view.EditHistoricalDiffLine, main_view.EditHunkInFocusedDiff, main_view.EditSelectedDiffLine, + main_view.EnterAndDoubleClickFocusFileDiff, main_view.EscapeDismissesSelection, main_view.FileNavigationScrollsToTheTop, main_view.FocusFollowsAPaneEmptiedFromOutside,