Fix clicking in commits panel of unfocused VS Code window (#5859)

When lazygit is running in a VS Code window that doesn't have the focus
(e.g. a split tab) and you click in the commits panel to focus it and
select a commit at the same time, it would briefly select the commit you
clicked but then flash back to the commit that was previously selected.
This PR fixes that so that the clicked commit stays selected.

This was only a problem with VS Code; in Zed's builtin terminal it
worked, apparently because it first dispatches the click and then the
focus-in event. Ghostty and iTerm2 were not affected because they don't
dispatch clicks in inactive windows or tabs at all.

Labelling as ignore-for-release because it fixes a regression that was
introduced since the last release.
This commit is contained in:
Stefan Haller
2026-07-25 18:09:39 +02:00
committed by GitHub
8 changed files with 80 additions and 13 deletions
+13 -13
View File
@@ -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
}
}
+19
View File
@@ -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)
@@ -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))
+4
View File
@@ -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{}
}
@@ -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)
@@ -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"))
},
})
+1
View File
@@ -140,6 +140,7 @@ var tests = []*components.IntegrationTest{
commit.Highlight,
commit.History,
commit.HistoryComplex,
commit.KeepClickedCommitSelectedAfterFocusIn,
commit.KeepSelectedCommitAfterExternalCommit,
commit.NewBranch,
commit.PasteCommitMessage,
+3
View File
@@ -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