From 649048c3369f03f5f5f1d5fd107c616f676d5caf Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Mon, 29 Jan 2024 17:50:45 +0100 Subject: Optionally keep sort order stable when filtering lists For some lists it is useful to keep the same sort order when filtering (rather than sorting by best match like we usually do). Add an optional function to FilteredList to make this possible, and use it whenever we show lists of things sorted by date (branches, stashes, reflog entries), as well as menu items because this allows us to keep the section headers in the keybindings menu, which is useful for understanding what you are looking at when filtering. --- pkg/gui/context/branches_context.go | 1 + pkg/gui/context/filtered_list.go | 19 +++++++++++++++---- pkg/gui/context/filtered_list_view_model.go | 4 ++-- pkg/gui/context/menu_context.go | 4 ++++ pkg/gui/context/reflog_commits_context.go | 1 + pkg/gui/context/remote_branches_context.go | 1 + pkg/gui/context/remotes_context.go | 1 + pkg/gui/context/stash_context.go | 1 + pkg/gui/context/submodules_context.go | 1 + pkg/gui/context/tags_context.go | 1 + pkg/gui/context/worktrees_context.go | 1 + .../filter_updates_when_model_changes.go | 5 +++-- 12 files changed, 32 insertions(+), 8 deletions(-) diff --git a/pkg/gui/context/branches_context.go b/pkg/gui/context/branches_context.go index d2647ef84..6317a60b2 100644 --- a/pkg/gui/context/branches_context.go +++ b/pkg/gui/context/branches_context.go @@ -22,6 +22,7 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext { func(branch *models.Branch) []string { return []string{branch.Name} }, + func() bool { return c.AppState.LocalBranchSortOrder != "alphabetical" }, ) getDisplayStrings := func(_ int, _ int) [][]string { diff --git a/pkg/gui/context/filtered_list.go b/pkg/gui/context/filtered_list.go index 82d9b81c8..13b9c166a 100644 --- a/pkg/gui/context/filtered_list.go +++ b/pkg/gui/context/filtered_list.go @@ -1,6 +1,7 @@ package context import ( + "slices" "strings" "github.com/jesseduffield/lazygit/pkg/utils" @@ -16,14 +17,21 @@ type FilteredList[T any] struct { getFilterFields func(T) []string filter string + // Normally, filtered items are presented sorted by best match. If this + // function returns true, they retain their original sort order instead; + // this is useful for lists that show items sorted by date, for example. + // Leaving this nil is equivalent to returning false. + shouldRetainSortOrder func() bool + mutex *deadlock.Mutex } -func NewFilteredList[T any](getList func() []T, getFilterFields func(T) []string) *FilteredList[T] { +func NewFilteredList[T any](getList func() []T, getFilterFields func(T) []string, shouldRetainSortOrder func() bool) *FilteredList[T] { return &FilteredList[T]{ - getList: getList, - getFilterFields: getFilterFields, - mutex: &deadlock.Mutex{}, + getList: getList, + getFilterFields: getFilterFields, + shouldRetainSortOrder: shouldRetainSortOrder, + mutex: &deadlock.Mutex{}, } } @@ -92,6 +100,9 @@ func (self *FilteredList[T]) applyFilter() { self.filteredIndices = lo.Map(matches, func(match fuzzy.Match, _ int) int { return match.Index }) + if self.shouldRetainSortOrder != nil && self.shouldRetainSortOrder() { + slices.Sort(self.filteredIndices) + } } } diff --git a/pkg/gui/context/filtered_list_view_model.go b/pkg/gui/context/filtered_list_view_model.go index 2c2841964..b52fcbc0a 100644 --- a/pkg/gui/context/filtered_list_view_model.go +++ b/pkg/gui/context/filtered_list_view_model.go @@ -6,8 +6,8 @@ type FilteredListViewModel[T HasID] struct { *SearchHistory } -func NewFilteredListViewModel[T HasID](getList func() []T, getFilterFields func(T) []string) *FilteredListViewModel[T] { - filteredList := NewFilteredList(getList, getFilterFields) +func NewFilteredListViewModel[T HasID](getList func() []T, getFilterFields func(T) []string, shouldRetainSortOrder func() bool) *FilteredListViewModel[T] { + filteredList := NewFilteredList(getList, getFilterFields, shouldRetainSortOrder) self := &FilteredListViewModel[T]{ FilteredList: filteredList, diff --git a/pkg/gui/context/menu_context.go b/pkg/gui/context/menu_context.go index bb1060de6..32d6d7610 100644 --- a/pkg/gui/context/menu_context.go +++ b/pkg/gui/context/menu_context.go @@ -61,6 +61,10 @@ func NewMenuViewModel(c *ContextCommon) *MenuViewModel { self.FilteredListViewModel = NewFilteredListViewModel( func() []*types.MenuItem { return self.menuItems }, func(item *types.MenuItem) []string { return item.LabelColumns }, + // The only menu that the user is likely to filter in is the keybindings + // menu; retain the sort order in that one because this allows us to + // keep the section headers while filtering: + func() bool { return true }, ) return self diff --git a/pkg/gui/context/reflog_commits_context.go b/pkg/gui/context/reflog_commits_context.go index 65137d633..6791932ba 100644 --- a/pkg/gui/context/reflog_commits_context.go +++ b/pkg/gui/context/reflog_commits_context.go @@ -24,6 +24,7 @@ func NewReflogCommitsContext(c *ContextCommon) *ReflogCommitsContext { func(commit *models.Commit) []string { return []string{commit.ShortSha(), commit.Name} }, + func() bool { return true }, ) getDisplayStrings := func(_ int, _ int) [][]string { diff --git a/pkg/gui/context/remote_branches_context.go b/pkg/gui/context/remote_branches_context.go index 884d3debb..9de792f27 100644 --- a/pkg/gui/context/remote_branches_context.go +++ b/pkg/gui/context/remote_branches_context.go @@ -26,6 +26,7 @@ func NewRemoteBranchesContext( func(remoteBranch *models.RemoteBranch) []string { return []string{remoteBranch.Name} }, + func() bool { return c.AppState.RemoteBranchSortOrder != "alphabetical" }, ) getDisplayStrings := func(_ int, _ int) [][]string { diff --git a/pkg/gui/context/remotes_context.go b/pkg/gui/context/remotes_context.go index 73ea428aa..51fc1c036 100644 --- a/pkg/gui/context/remotes_context.go +++ b/pkg/gui/context/remotes_context.go @@ -22,6 +22,7 @@ func NewRemotesContext(c *ContextCommon) *RemotesContext { func(remote *models.Remote) []string { return []string{remote.Name} }, + nil, ) getDisplayStrings := func(_ int, _ int) [][]string { diff --git a/pkg/gui/context/stash_context.go b/pkg/gui/context/stash_context.go index c8d487688..c832f85ff 100644 --- a/pkg/gui/context/stash_context.go +++ b/pkg/gui/context/stash_context.go @@ -24,6 +24,7 @@ func NewStashContext( func(stashEntry *models.StashEntry) []string { return []string{stashEntry.Name} }, + func() bool { return true }, ) getDisplayStrings := func(_ int, _ int) [][]string { diff --git a/pkg/gui/context/submodules_context.go b/pkg/gui/context/submodules_context.go index 82deb25af..aff8f64ab 100644 --- a/pkg/gui/context/submodules_context.go +++ b/pkg/gui/context/submodules_context.go @@ -19,6 +19,7 @@ func NewSubmodulesContext(c *ContextCommon) *SubmodulesContext { func(submodule *models.SubmoduleConfig) []string { return []string{submodule.Name} }, + nil, ) getDisplayStrings := func(_ int, _ int) [][]string { diff --git a/pkg/gui/context/tags_context.go b/pkg/gui/context/tags_context.go index d827564dd..c5ae2ccd5 100644 --- a/pkg/gui/context/tags_context.go +++ b/pkg/gui/context/tags_context.go @@ -24,6 +24,7 @@ func NewTagsContext( func(tag *models.Tag) []string { return []string{tag.Name, tag.Message} }, + nil, ) getDisplayStrings := func(_ int, _ int) [][]string { diff --git a/pkg/gui/context/worktrees_context.go b/pkg/gui/context/worktrees_context.go index 3e45f2d45..55618de85 100644 --- a/pkg/gui/context/worktrees_context.go +++ b/pkg/gui/context/worktrees_context.go @@ -19,6 +19,7 @@ func NewWorktreesContext(c *ContextCommon) *WorktreesContext { func(Worktree *models.Worktree) []string { return []string{Worktree.Name} }, + nil, ) getDisplayStrings := func(_ int, _ int) [][]string { 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 index ae3c862c0..60ce9b580 100644 --- 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 @@ -27,9 +27,10 @@ var FilterUpdatesWhenModelChanges = NewIntegrationTest(NewIntegrationTestArgs{ ). FilterOrSearch("branch"). Lines( - Contains("branch-to-delete").IsSelected(), - Contains("checked-out-branch"), + Contains("checked-out-branch").IsSelected(), + Contains("branch-to-delete"), ). + SelectNextItem(). Press(keys.Universal.Remove). Tap(func() { t.ExpectPopup(). -- cgit v1.2.3