Keep the last conflict file selected after resolution (#5936)

When there is a single conflicting file left to be resolved, lazygit
dismisses the conflicted-files-only filter when the file no longer has
conflict markers. However, the selection moved to the top, which is
annoying because very often it is useful to look at that file's
resulting diff once more to confirm that conflicts were resolved
correctly, and finding it again can be cumbersome when there are many
changed files. So keep it selected.

Of course, this only helps for the last (or only) conflicted files; when
there are multiple, a resolved file disappears from the panel until all
are resolved, which makes it hard to double-check the resulting diffs.
Doing it afterwards is not easy because you'd have to remember which
files were conflicting. This needs a different solution, but for the
special case of only a single conflicting file this is already a big
improvement.
This commit is contained in:
Stefan Haller
2026-08-15 15:24:01 +02:00
committed by GitHub
7 changed files with 76 additions and 29 deletions
@@ -1413,7 +1413,7 @@ func (self *RefreshHelper) refreshStateFiles(captured capturedFilesState, env re
self.c.Contexts().Files.GetView().Subtitle = self.c.Tr.FilterLabelConflictingFiles
}
} else if conflictFileCount == 0 && fileTreeViewModel.GetStatusFilter() == filetree.DisplayConflicted {
fileTreeViewModel.SetStatusFilter(filetree.DisplayAll)
fileTreeViewModel.SetStatusFilterPreservingSelection(filetree.DisplayAll)
self.c.Contexts().Files.GetView().Subtitle = ""
}
+28 -16
View File
@@ -167,6 +167,31 @@ func (self *FileTreeViewModel) SetStatusFilter(filter FileTreeDisplayFilter) {
self.IListCursor.SetSelection(0)
}
func (self *FileTreeViewModel) SetStatusFilterPreservingSelection(filter FileTreeDisplayFilter) {
self.preserveSelection(func() {
self.SetStatusFilter(filter)
})
}
func (self *FileTreeViewModel) preserveSelection(f func()) {
selectedNode := self.GetSelected()
var selectedPath string
if selectedNode != nil {
selectedPath = selectedNode.GetInternalPath()
}
f()
if selectedPath != "" {
self.ExpandToPath(selectedPath)
if idx, found := self.GetIndexForPath(selectedPath); found {
self.SetSelection(idx)
return
}
}
self.ClampSelection()
}
// If we're going from flat to tree we want to select the same file.
// If we're going from tree to flat and we have a file selected we want to select that.
// If instead we've selected a directory we need to select the first file in that directory.
@@ -233,22 +258,9 @@ func (self *FileTreeViewModel) GetFilter() string {
}
func (self *FileTreeViewModel) ClearFilter() {
selectedNode := self.GetSelected()
var selectedPath string
if selectedNode != nil {
selectedPath = selectedNode.GetInternalPath()
}
self.IFileTree.SetTextFilter("", false)
if selectedPath != "" {
self.ExpandToPath(selectedPath)
if idx, found := self.GetIndexForPath(selectedPath); found {
self.SetSelection(idx)
return
}
}
self.ClampSelection()
self.preserveSelection(func() {
self.IFileTree.SetTextFilter("", false)
})
}
func (self *FileTreeViewModel) ReApplyFilter(useFuzzySearch bool) {
@@ -0,0 +1,32 @@
package filetree
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/stretchr/testify/assert"
)
func TestSetStatusFilterPreservingSelection(t *testing.T) {
files := []*models.File{
{Path: "file1"},
{Path: "file2", HasMergeConflicts: true},
{Path: "file3", HasMergeConflicts: true},
}
viewModel := NewFileTreeViewModel(
func() []*models.File { return files },
common.NewDummyCommon(),
false,
)
viewModel.SetTree()
viewModel.SetStatusFilter(DisplayConflicted)
viewModel.SetSelection(viewModel.Len() - 2)
viewModel.ToggleStickyRange()
viewModel.MoveSelectedLine(1)
viewModel.SetStatusFilterPreservingSelection(DisplayAll)
assert.Equal(t, "file3", viewModel.GetSelectedPath())
assert.False(t, viewModel.IsSelectingRange())
}
@@ -75,8 +75,8 @@ var RebaseConflictsFixBuildErrorsWithOutOfDateSubmodule = NewIntegrationTest(New
t.Views().Files().
Lines(
Equals("▼ /").IsSelected(),
Equals(" MM file"),
Equals("▼ /"),
Equals(" MM file").IsSelected(),
Equals(" M submodule (submodule)"),
Equals(" ?? untracked-file"),
)
@@ -90,8 +90,8 @@ var RebaseConflictsFixBuildErrorsWithOutOfDateSubmodule = NewIntegrationTest(New
t.Views().Files().
Lines(
Equals("▼ /").IsSelected(),
Equals(" M submodule (submodule)"),
Equals("▼ /"),
Equals(" M submodule (submodule)").IsSelected(),
Equals(" ?? untracked-file"),
)
@@ -34,6 +34,7 @@ var ResolveMultipleFiles = NewIntegrationTest(NewIntegrationTestArgs{
Contains("First Change"),
Contains("======="),
).
SelectNextItem().
PressPrimaryAction()
t.Views().Files().
@@ -47,12 +48,14 @@ var ResolveMultipleFiles = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().MergeConflicts().
IsFocused().
SelectedLines(
Contains("<<<<<<< HEAD"),
Contains("First Change"),
Contains("======="),
Contains("Second Change"),
Contains(">>>>>>>"),
).
PressPrimaryAction()
t.Views().Files().SelectedLines(Contains("file2"))
t.Common().ContinueOnConflictsResolved("merge")
},
})
@@ -46,12 +46,12 @@ var DiscardVariousChangesRangeSelect = NewIntegrationTest(NewIntegrationTestArgs
Cancel()
}).
Lines(
Equals("▼ /").IsSelected(),
Equals("▼ /"),
Equals(" AM added-changed.txt"),
Equals(" MD change-delete.txt"),
Equals(" D delete-change.txt"),
Equals(" D deleted-staged.txt"),
Equals(" D deleted.txt"),
Equals(" D deleted.txt").IsSelected(),
Equals(" MM double-modded.txt"),
Equals(" M modded-staged.txt"),
Equals(" M modded.txt"),
@@ -59,6 +59,7 @@ var DiscardVariousChangesRangeSelect = NewIntegrationTest(NewIntegrationTestArgs
Equals(" ?? new.txt"),
Equals(" R renamed.txt → renamed2.txt"),
).
NavigateToLine(Equals("▼ /")).
Press(keys.Universal.ToggleRangeSelect).
NavigateToLine(Contains("renamed.txt")).
Press(keys.Universal.Remove).
@@ -83,11 +83,10 @@ var ApplyInReverseWithConflict = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Files().
Focus().
Lines(
Equals("▼ /").IsSelected(),
Equals(" M file1"),
Equals("▼ /"),
Equals(" M file1").IsSelected(),
Equals(" M file2"),
).
SelectNextItem()
)
t.Views().Main().
ContainsLines(