Repository: jesseduffield/lazygit (language: go)

Issue:
Filtered view doesn't update when underlying model changes
It seems that the filtered view remembers indices into the model of the items that it should display. If the model changes, these are not updated, so the wrong items are shown.

There are probably many variations of situations in which this happens, but the two most common ones that I have run into are:

### Checking out a filtered branch
1. Press `/` and filter the list of branches down so that only a single branch is shown (which is not the current one)
2. Press space to check out this branch
3. Expected: the same branch should still be displayed in the filtered list; actual: an arbitrary different one is displayed instead

### Deleting a filtered branch
1. Filter the list of branches down so that three branches are shown
2. Select one of these and press `d` to delete it
3. Expected: only two branches should be displayed now; actual: it still shows three branches, and (depending on the order of the branches) totally arbitrary ones

**Version info:**
commit=a30537a4da5e6633135f9a7f423a5b23b14d9797, build date=2023-07-21T15:58:51Z, build source=unknown, version=a30537a4, os=darwin, arch=arm64, git version=2.41.0.49.g2c35e38aee



Discussion / hints:
I've seen this too. Annoying indeed.
> Expected: the same branch should still be displayed in the filtered list; actual: an arbitrary different one is displayed instead

It's actually not the "arbitrary" branch displayed, but the branches with previous index positions.

Eg, if you have 10 branches, and filtering displays only 3rd, 5th and 7th, if you checkout 5th, it becomes number one in the list and all other branches (with indexes lower than checked out branch) will be shifted.
 
| available branches | displayed after filtering | index list after 5th branch checked out | filtered list after checkout |
| ---- | ----- | ----- | ---- |
| branch#1<br>branch#2<br>branch#3<br>branch#4<br>branch#5<br>branch#6<br>branch#7<br>branch#8<br>branch#9<br>branch#10 | -<br>-<br>branch#3<br>-<br>branch#5<br>-<br>branch#7<br>-<br>-<br>- | branch#5<br>branch#1<br>branch#2<br>branch#3<br>branch#4<br>branch#6<br>branch#7<br>branch#8<br>branch#9<br>branch#10 | -<br>-<br>branch#2<br>-<br>branch#4<br>-<br>branch#7<br>-<br>-<br>- |

Would be nice to see this fixed

I guess just re-applying current filter automatically after all list-reordering-actions should do the trick.

Fix patch:
diff --git a/pkg/gui/context/filtered_list.go b/pkg/gui/context/filtered_list.go
index d2361ad53f9..82d9b81c802 100644
--- a/pkg/gui/context/filtered_list.go
+++ b/pkg/gui/context/filtered_list.go
@@ -41,6 +41,10 @@ func (self *FilteredList[T]) ClearFilter() {
 	self.SetFilter("")
 }
 
+func (self *FilteredList[T]) ReApplyFilter() {
+	self.applyFilter()
+}
+
 func (self *FilteredList[T]) IsFiltering() bool {
 	return self.filter != ""
 }
diff --git a/pkg/gui/controllers.go b/pkg/gui/controllers.go
index 43b226b587a..6a751e1efa1 100644
--- a/pkg/gui/controllers.go
+++ b/pkg/gui/controllers.go
@@ -47,6 +47,7 @@ func (gui *Gui) resetHelpersAndControllers() {
 	patchBuildingHelper := helpers.NewPatchBuildingHelper(helperCommon)
 	stagingHelper := helpers.NewStagingHelper(helperCommon)
 	mergeConflictsHelper := helpers.NewMergeConflictsHelper(helperCommon)
+	searchHelper := helpers.NewSearchHelper(helperCommon)
 
 	refreshHelper := helpers.NewRefreshHelper(
 		helperCommon,
@@ -56,6 +57,7 @@ func (gui *Gui) resetHelpersAndControllers() {
 		stagingHelper,
 		mergeConflictsHelper,
 		worktreeHelper,
+		searchHelper,
 	)
 	diffHelper := helpers.NewDiffHelper(helperCommon)
 	cherryPickHelper := helpers.NewCherryPickHelper(
@@ -119,7 +121,7 @@ func (gui *Gui) resetHelpersAndControllers() {
 			modeHelper,
 			appStatusHelper,
 		),
-		Search:     helpers.NewSearchHelper(helperCommon),
+		Search:     searchHelper,
 		Worktree:   worktreeHelper,
 		SubCommits: helpers.NewSubCommitsHelper(helperCommon, refreshHelper, setSubCommits),
 	}
diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go
index a299ea431d1..dc43844e293 100644
--- a/pkg/gui/controllers/helpers/refresh_helper.go
+++ b/pkg/gui/controllers/helpers/refresh_helper.go
@@ -28,6 +28,7 @@ type RefreshHelper struct {
 	stagingHelper        *StagingHelper
 	mergeConflictsHelper *MergeConflictsHelper
 	worktreeHelper       *WorktreeHelper
+	searchHelper         *SearchHelper
 }
 
 func NewRefreshHelper(
@@ -38,6 +39,7 @@ func NewRefreshHelper(
 	stagingHelper *StagingHelper,
 	mergeConflictsHelper *MergeConflictsHelper,
 	worktreeHelper *WorktreeHelper,
+	searchHelper *SearchHelper,
 ) *RefreshHelper {
 	return &RefreshHelper{
 		c:                    c,
@@ -47,6 +49,7 @@ func NewRefreshHelper(
 		stagingHelper:        stagingHelper,
 		mergeConflictsHelper: mergeConflictsHelper,
 		worktreeHelper:       worktreeHelper,
+		searchHelper:         searchHelper,
 	}
 }
 
@@ -328,7 +331,7 @@ func (self *RefreshHelper) refreshCommitsWithLimit() error {
 	self.c.Model().WorkingTreeStateAtLastCommitRefresh = self.c.Git().Status.WorkingTreeState()
 	self.c.Model().CheckedOutBranch = checkedOutBranchName
 
-	return self.c.PostRefreshUpdate(self.c.Contexts().LocalCommits)
+	return self.refreshView(self.c.Contexts().LocalCommits)
 }
 
 func (self *RefreshHelper) refreshSubCommitsWithLimit() error {
@@ -351,7 +354,7 @@ func (self *RefreshHelper) refreshSubCommitsWithLimit() error {
 	self.c.Model().SubCommits = commits
 	self.RefreshAuthors(commits)
 
-	return self.c.PostRefreshUpdate(self.c.Contexts().SubCommits)
+	return self.refreshView(self.c.Contexts().SubCommits)
 }
 
 func (self *RefreshHelper) RefreshAuthors(commits []*models.Commit) {
@@ -381,7 +384,7 @@ func (self *RefreshHelper) refreshCommitFilesContext() error {
 	self.c.Model().CommitFiles = files
 	self.c.Contexts().CommitFiles.CommitFileTreeViewModel.SetTree()
 
-	return self.c.PostRefreshUpdate(self.c.Contexts().CommitFiles)
+	return self.refreshView(self.c.Contexts().CommitFiles)
 }
 
 func (self *RefreshHelper) refreshRebaseCommits() error {
@@ -395,7 +398,7 @@ func (self *RefreshHelper) refreshRebaseCommits() error {
 	self.c.Model().Commits = updatedCommits
 	self.c.Model().WorkingTreeStateAtLastCommitRefresh = self.c.Git().Status.WorkingTreeState()
 
-	return self.c.PostRefreshUpdate(self.c.Contexts().LocalCommits)
+	return self.refreshView(self.c.Contexts().LocalCommits)
 }
 
 func (self *RefreshHelper) refreshTags() error {
@@ -406,7 +409,7 @@ func (self *RefreshHelper) refreshTags() error {
 
 	self.c.Model().Tags = tags
 
-	return self.c.PostRefreshUpdate(self.c.Contexts().Tags)
+	return self.refreshView(self.c.Contexts().Tags)
 }
 
 func (self *RefreshHelper) refreshStateSubmoduleConfigs() error {
@@ -448,12 +451,12 @@ func (self *RefreshHelper) refreshBranches(refreshWorktrees bool) {
 
 	if refreshWorktrees {
 		self.loadWorktrees()
-		if err := self.c.PostRefreshUpdate(self.c.Contexts().Worktrees); err != nil {
+		if err := self.refreshView(self.c.Contexts().Worktrees); err != nil {
 			self.c.Log.Error(err)
 		}
 	}
 
-	if err := self.c.PostRefreshUpdate(self.c.Contexts().Branches); err != nil {
+	if err := self.refreshView(self.c.Contexts().Branches); err != nil {
 		self.c.Log.Error(err)
 	}
 
@@ -485,11 +488,11 @@ func (self *RefreshHelper) refreshFilesAndSubmodules() error {
 	}
 
 	self.c.OnUIThread(func() error {
-		if err := self.c.PostRefreshUpdate(self.c.Contexts().Submodules); err != nil {
+		if err := self.refreshView(self.c.Contexts().Submodules); err != nil {
 			self.c.Log.Error(err)
 		}
 
-		if err := self.c.PostRefreshUpdate(self.c.Contexts().Files); err != nil {
+		if err := self.refreshView(self.c.Contexts().Files); err != nil {
 			self.c.Log.Error(err)
 		}
 
@@ -611,7 +614,7 @@ func (self *RefreshHelper) refreshReflogCommits() error {
 		model.FilteredReflogCommits = model.ReflogCommits
 	}
 
-	return self.c.PostRefreshUpdate(self.c.Contexts().ReflogCommits)
+	return self.refreshView(self.c.Contexts().ReflogCommits)
 }
 
 func (self *RefreshHelper) refreshRemotes() error {
@@ -635,11 +638,11 @@ func (self *RefreshHelper) refreshRemotes() error {
 		}
 	}
 
-	if err := self.c.PostRefreshUpdate(self.c.Contexts().Remotes); err != nil {
+	if err := self.refreshView(self.c.Contexts().Remotes); err != nil {
 		return err
 	}
 
-	if err := self.c.PostRefreshUpdate(self.c.Contexts().RemoteBranches); err != nil {
+	if err := self.refreshView(self.c.Contexts().RemoteBranches); err != nil {
 		return err
 	}
 
@@ -661,18 +664,18 @@ func (self *RefreshHelper) refreshWorktrees() error {
 
 	// need to refresh branches because the branches view shows worktrees against
 	// branches
-	if err := self.c.PostRefreshUpdate(self.c.Contexts().Branches); err != nil {
+	if err := self.refreshView(self.c.Contexts().Branches); err != nil {
 		return err
 	}
 
-	return self.c.PostRefreshUpdate(self.c.Contexts().Worktrees)
+	return self.refreshView(self.c.Contexts().Worktrees)
 }
 
 func (self *RefreshHelper) refreshStashEntries() error {
 	self.c.Model().StashEntries = self.c.Git().Loaders.StashLoader.
 		GetStashEntries(self.c.Modes().Filtering.GetPath())
 
-	return self.c.PostRefreshUpdate(self.c.Contexts().Stash)
+	return self.refreshView(self.c.Contexts().Stash)
 }
 
 // never call this on its own, it should only be called from within refreshCommits()
@@ -711,3 +714,8 @@ func (self *RefreshHelper) refForLog() string {
 
 	return bisectInfo.GetStartSha()
 }
+
+func (self *RefreshHelper) refreshView(context types.Context) error {
+	self.searchHelper.ReApplyFilter(context)
+	return self.c.PostRefreshUpdate(context)
+}
diff --git a/pkg/gui/controllers/helpers/search_helper.go b/pkg/gui/controllers/helpers/search_helper.go
index c036bda3d4d..4c4b6918c53 100644
--- a/pkg/gui/controllers/helpers/search_helper.go
+++ b/pkg/gui/controllers/helpers/search_helper.go
@@ -227,6 +227,18 @@ func (self *SearchHelper) OnPromptContentChanged(searchString string) {
 	}
 }
 
+func (self *SearchHelper) ReApplyFilter(context types.Context) {
+	state := self.searchState()
+	if context == state.Context {
+		filterableContext, ok := context.(types.IFilterableContext)
+		if ok {
+			filterableContext.SetSelectedLineIdx(0)
+			_ = filterableContext.GetView().SetOriginY(0)
+			filterableContext.ReApplyFilter()
+		}
+	}
+}
+
 func (self *SearchHelper) RenderSearchStatus(c types.Context) {
 	if c.GetKey() == context.SEARCH_CONTEXT_KEY {
 		return
diff --git a/pkg/gui/types/context.go b/pkg/gui/types/context.go
index df16cf2bd3b..c47945aee79 100644
--- a/pkg/gui/types/context.go
+++ b/pkg/gui/types/context.go
@@ -102,6 +102,7 @@ type IFilterableContext interface {
 	SetFilter(string)
 	GetFilter() string
 	ClearFilter()
+	ReApplyFilter()
 	IsFiltering() bool
 	IsFilterableContext()
 }


Test patch:
diff --git a/pkg/integration/tests/filter_and_search/filter_updates_when_model_changes.go b/pkg/integration/tests/filter_and_search/filter_updates_when_model_changes.go
new file mode 100644
index 00000000000..ae3c862c073
--- /dev/null
+++ b/pkg/integration/tests/filter_and_search/filter_updates_when_model_changes.go
@@ -0,0 +1,52 @@
+package filter_and_search
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var FilterUpdatesWhenModelChanges = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Verify that after deleting a branch the filter is reapplied to show only the remaining branches",
+	ExtraCmdArgs: []string{},
+	Skip:         false,
+	SetupConfig:  func(config *config.AppConfig) {},
+	SetupRepo: func(shell *Shell) {
+		shell.EmptyCommit("first commit")
+		shell.NewBranch("branch-to-delete")
+		shell.NewBranch("other")
+		shell.NewBranch("checked-out-branch")
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Branches().
+			Focus().
+			Lines(
+				Contains("checked-out-branch").IsSelected(),
+				Contains("other"),
+				Contains("branch-to-delete"),
+				Contains("master"),
+			).
+			FilterOrSearch("branch").
+			Lines(
+				Contains("branch-to-delete").IsSelected(),
+				Contains("checked-out-branch"),
+			).
+			Press(keys.Universal.Remove).
+			Tap(func() {
+				t.ExpectPopup().
+					Menu().
+					Title(Equals("Delete branch 'branch-to-delete'?")).
+					Select(Contains("Delete local branch")).
+					Confirm()
+			}).
+			Lines(
+				Contains("checked-out-branch").IsSelected(),
+			).
+			// cancel the filter
+			PressEscape().
+			Lines(
+				Contains("checked-out-branch").IsSelected(),
+				Contains("other"),
+				Contains("master"),
+			)
+	},
+})
diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go
index 521656d1095..cea838380f0 100644
--- a/pkg/integration/tests/test_list.go
+++ b/pkg/integration/tests/test_list.go
@@ -133,6 +133,7 @@ var tests = []*components.IntegrationTest{
 	filter_and_search.FilterRemoteBranches,
 	filter_and_search.FilterRemotes,
 	filter_and_search.FilterSearchHistory,
+	filter_and_search.FilterUpdatesWhenModelChanges,
 	filter_and_search.NestedFilter,
 	filter_and_search.NestedFilterTransient,
 	filter_and_search.NewSearch,