diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go index cf097b67e..29784b5ff 100644 --- a/pkg/gui/controllers/helpers/refresh_helper.go +++ b/pkg/gui/controllers/helpers/refresh_helper.go @@ -322,7 +322,7 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr var capturedReflog capturedReflogState var capturedBranches capturedBranchState self.captureOnUIThread(calledFromWorker, env.background, func() { - capturedCommits = self.captureCommitsState(options.CommitSelection) + capturedCommits = self.captureCommitsState() capturedReflog = self.captureReflogState() capturedBranches = self.captureBranchState() }) @@ -704,7 +704,6 @@ func (self *RefreshHelper) refreshReflogAndBranches(capturedReflog capturedReflo // worker computes from an immutable snapshot rather than reading state the UI // thread concurrently mutates. type capturedCommitState struct { - selectionRange *localCommitSelectionRange limitCommits bool showWholeGitGraph bool filterPath string @@ -716,17 +715,12 @@ type capturedCommitState struct { // captureCommitsState reads the commits refresh's model/context/mode inputs // into an immutable snapshot. It must run on the UI thread. -func (self *RefreshHelper) captureCommitsState(commitSelection types.CommitSelectionBehavior) capturedCommitState { - var selectionRange *localCommitSelectionRange - if commitSelection == types.KeepCommitSelectionByHash { - selectedIdx, rangeStartIdx, rangeSelectMode := self.c.Contexts().LocalCommits.GetSelectionRangeAndMode() - selectionRange = captureLocalCommitSelectionRange(self.c.Model().Commits, selectedIdx, rangeStartIdx, rangeSelectMode) - } - +// The selection is captured later, when applying the refresh, so user input +// received while the git work is in flight is not overwritten. +func (self *RefreshHelper) captureCommitsState() capturedCommitState { parentCtx := self.c.Contexts().CommitFiles.GetParentContext() return capturedCommitState{ - selectionRange: selectionRange, limitCommits: self.c.Contexts().LocalCommits.GetLimitCommits(), showWholeGitGraph: self.c.Contexts().LocalCommits.GetShowWholeGitGraph(), filterPath: self.c.Modes().Filtering.GetPath(), @@ -815,6 +809,12 @@ func (self *RefreshHelper) refreshCommitsWithLimit(captured capturedCommitState, workingTreeState := env.git.Status.WorkingTreeState() self.onUIThreadUnlessRepoChanged(env, func() { + var selectionRange *localCommitSelectionRange + if commitSelection == types.KeepCommitSelectionByHash { + selectedIdx, rangeStartIdx, rangeSelectMode := self.c.Contexts().LocalCommits.GetSelectionRangeAndMode() + selectionRange = captureLocalCommitSelectionRange(self.c.Model().Commits, selectedIdx, rangeStartIdx, rangeSelectMode) + } + self.c.Model().BisectInfo = bisectInfo self.c.Model().Commits = commits self.RefreshAuthors(commits) @@ -833,10 +833,10 @@ func (self *RefreshHelper) refreshCommitsWithLimit(captured capturedCommitState, scrollSelectionIntoView = true } case types.KeepCommitSelectionByHash: - if captured.selectionRange != nil { - selectedIdx, rangeStartIdx, didMove, found := findLocalCommitSelectionRange(commits, captured.selectionRange) + if selectionRange != nil { + selectedIdx, rangeStartIdx, didMove, found := findLocalCommitSelectionRange(commits, selectionRange) if found { - self.c.Contexts().LocalCommits.SetSelectionRangeAndMode(selectedIdx, rangeStartIdx, captured.selectionRange.mode) + self.c.Contexts().LocalCommits.SetSelectionRangeAndMode(selectedIdx, rangeStartIdx, selectionRange.mode) scrollSelectionIntoView = didMove } } diff --git a/pkg/gui/gui_driver.go b/pkg/gui/gui_driver.go index 74a8109a7..9e06f483b 100644 --- a/pkg/gui/gui_driver.go +++ b/pkg/gui/gui_driver.go @@ -77,6 +77,25 @@ func (self *GuiDriver) FocusIn() { self.waitTillIdle() } +func (self *GuiDriver) FocusInAndClick(x, y int) { + self.CheckAllToastsAcknowledged() + + self.gui.g.ReplayFocusEvent(gocui.NewTcellFocusEventWrapper( + tcell.NewEventFocus(true), + 0, + )) + self.gui.g.ReplayMouseEvent(gocui.NewTcellMouseEventWrapper( + tcell.NewEventMouse(x, y, tcell.ButtonPrimary, 0), + 0, + )) + self.waitTillIdle() + self.gui.g.ReplayMouseEvent(gocui.NewTcellMouseEventWrapper( + tcell.NewEventMouse(x, y, tcell.ButtonNone, 0), + 0, + )) + self.waitTillIdle() +} + func (self *GuiDriver) PretendMergeOrRebaseStartedInLazygit() { self.gui.onUIThread(func() error { self.gui.State.SetMergeOrRebaseStartedInLazygit(true) diff --git a/pkg/integration/components/test_driver.go b/pkg/integration/components/test_driver.go index 42ce8ac35..19219707a 100644 --- a/pkg/integration/components/test_driver.go +++ b/pkg/integration/components/test_driver.go @@ -73,6 +73,12 @@ func (self *TestDriver) FocusIn() { self.Wait(self.inputDelay) } +func (self *TestDriver) focusInAndClick(x, y int) { + self.SetCaption(fmt.Sprintf("Focusing window and clicking %d, %d", x, y)) + self.gui.FocusInAndClick(x, y) + self.Wait(self.inputDelay) +} + func (self *TestDriver) typeContent(content string) { for _, char := range content { self.pressFast(string(char)) diff --git a/pkg/integration/components/test_test.go b/pkg/integration/components/test_test.go index 8fd4417ea..7196779eb 100644 --- a/pkg/integration/components/test_test.go +++ b/pkg/integration/components/test_test.go @@ -41,6 +41,10 @@ func (self *fakeGuiDriver) Click(x, y int) { func (self *fakeGuiDriver) FocusIn() { } +func (self *fakeGuiDriver) FocusInAndClick(x, y int) { + self.clickedCoordinates = append(self.clickedCoordinates, coordinate{x: x, y: y}) +} + func (self *fakeGuiDriver) Keys() config.KeybindingConfig { return config.KeybindingConfig{} } diff --git a/pkg/integration/components/view_driver.go b/pkg/integration/components/view_driver.go index 920c610be..2cfaba338 100644 --- a/pkg/integration/components/view_driver.go +++ b/pkg/integration/components/view_driver.go @@ -475,6 +475,14 @@ func (self *ViewDriver) Click(x, y int) *ViewDriver { return self } +func (self *ViewDriver) FocusInAndClick(x, y int) *ViewDriver { + offsetX, offsetY, _, _ := self.getView().Dimensions() + + self.t.focusInAndClick(offsetX+1+x, offsetY+1+y) + + return self +} + // i.e. pressing down arrow func (self *ViewDriver) SelectNextItem() *ViewDriver { return self.PressFast(self.t.keys.Universal.NextItem) diff --git a/pkg/integration/tests/commit/keep_clicked_commit_selected_after_focus_in.go b/pkg/integration/tests/commit/keep_clicked_commit_selected_after_focus_in.go new file mode 100644 index 000000000..cfd4e23c5 --- /dev/null +++ b/pkg/integration/tests/commit/keep_clicked_commit_selected_after_focus_in.go @@ -0,0 +1,26 @@ +package commit + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var KeepClickedCommitSelectedAfterFocusIn = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Keep a clicked commit selected when focus-in immediately precedes the click", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.CreateNCommits(2) + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + Lines( + Contains("commit-02").IsSelected(), + Contains("commit-01"), + ). + FocusInAndClick(1, 1). + SelectedLine(Contains("commit-01")) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 1bb06741f..07a12e2be 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -140,6 +140,7 @@ var tests = []*components.IntegrationTest{ commit.Highlight, commit.History, commit.HistoryComplex, + commit.KeepClickedCommitSelectedAfterFocusIn, commit.KeepSelectedCommitAfterExternalCommit, commit.NewBranch, commit.PasteCommitMessage, diff --git a/pkg/integration/types/types.go b/pkg/integration/types/types.go index 12009315a..ea76c45be 100644 --- a/pkg/integration/types/types.go +++ b/pkg/integration/types/types.go @@ -31,6 +31,9 @@ type GuiDriver interface { // Simulate the terminal window regaining focus (which triggers a reload of // changed config files) FocusIn() + // Simulate a terminal dispatching focus-in immediately followed by a click, + // without waiting for the focus refresh to finish in between. + FocusInAndClick(int, int) Keys() config.KeybindingConfig CurrentContext() types.Context ContextForView(viewName string) types.Context