diff options
43 files changed, 798 insertions, 232 deletions
diff --git a/pkg/gui/command_log_panel.go b/pkg/gui/command_log_panel.go index 0a5ccfae3..2faee3572 100644 --- a/pkg/gui/command_log_panel.go +++ b/pkg/gui/command_log_panel.go @@ -136,10 +136,6 @@ func (gui *Gui) getRandomTip() string { formattedKey(config.Universal.Return), ), fmt.Sprintf( - "To search for a string in your panel, press '%s'", - formattedKey(config.Universal.StartSearch), - ), - fmt.Sprintf( "You can page through the items of a panel using '%s' and '%s'", formattedKey(config.Universal.PrevPage), formattedKey(config.Universal.NextPage), diff --git a/pkg/gui/context.go b/pkg/gui/context.go index b55713f27..26cec4c23 100644 --- a/pkg/gui/context.go +++ b/pkg/gui/context.go @@ -200,9 +200,21 @@ func (self *ContextMgr) RemoveContexts(contextsToRemove []types.Context) error { func (self *ContextMgr) deactivateContext(c types.Context, opts types.OnFocusLostOpts) error { view, _ := self.gui.c.GocuiGui().View(c.GetViewName()) - if view != nil && view.IsSearching() { - if err := self.gui.onSearchEscape(); err != nil { - return err + if opts.NewContextKey != context.SEARCH_CONTEXT_KEY { + + if searchableContext, ok := c.(types.ISearchableContext); ok { + if view != nil && view.IsSearching() { + view.ClearSearch() + searchableContext.ClearSearchString() + self.gui.helpers.Search.Cancel() + } + } + + if filterableContext, ok := c.(types.IFilterableContext); ok { + if filterableContext.GetFilter() != "" { + filterableContext.ClearFilter() + self.gui.helpers.Search.Cancel() + } } } @@ -234,6 +246,17 @@ func (self *ContextMgr) ActivateContext(c types.Context, opts types.OnFocusOpts) return err } + if searchableContext, ok := c.(types.ISearchableContext); ok { + if searchableContext.GetSearchString() != "" { + self.gui.helpers.Search.DisplaySearchPrompt(searchableContext) + } + } + if filterableContext, ok := c.(types.IFilterableContext); ok { + if filterableContext.GetFilter() != "" { + self.gui.helpers.Search.DisplayFilterPrompt(filterableContext) + } + } + desiredTitle := c.Title() if desiredTitle != "" { v.Title = desiredTitle @@ -326,6 +349,30 @@ func (self *ContextMgr) IsCurrent(c types.Context) bool { return self.Current().GetKey() == c.GetKey() } +func (self *ContextMgr) AllFilterable() []types.IFilterableContext { + var result []types.IFilterableContext + + for _, context := range self.allContexts.Flatten() { + if ctx, ok := context.(types.IFilterableContext); ok { + result = append(result, ctx) + } + } + + return result +} + +func (self *ContextMgr) AllSearchable() []types.ISearchableContext { + var result []types.ISearchableContext + + for _, context := range self.allContexts.Flatten() { + if ctx, ok := context.(types.ISearchableContext); ok { + result = append(result, ctx) + } + } + + return result +} + // all list contexts func (self *ContextMgr) AllList() []types.IListContext { var listContexts []types.IListContext diff --git a/pkg/gui/context/branches_context.go b/pkg/gui/context/branches_context.go index c2463ad20..497b3a2c4 100644 --- a/pkg/gui/context/branches_context.go +++ b/pkg/gui/context/branches_context.go @@ -7,7 +7,7 @@ import ( ) type BranchesContext struct { - *BasicViewModel[*models.Branch] + *FilteredListViewModel[*models.Branch] *ListContextTrait } @@ -17,11 +17,16 @@ var ( ) func NewBranchesContext(c *ContextCommon) *BranchesContext { - viewModel := NewBasicViewModel(func() []*models.Branch { return c.Model().Branches }) + viewModel := NewFilteredListViewModel( + func() []*models.Branch { return c.Model().Branches }, + func(branch *models.Branch) []string { + return []string{branch.Name} + }, + ) getDisplayStrings := func(startIdx int, length int) [][]string { return presentation.GetBranchListDisplayStrings( - c.Model().Branches, + viewModel.GetItems(), c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL, c.Modes().Diffing.Ref, c.Tr, @@ -30,7 +35,7 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext { } self := &BranchesContext{ - BasicViewModel: viewModel, + FilteredListViewModel: viewModel, ListContextTrait: &ListContextTrait{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ View: c.Views().Branches, diff --git a/pkg/gui/context/filtered_list.go b/pkg/gui/context/filtered_list.go new file mode 100644 index 000000000..1317924ad --- /dev/null +++ b/pkg/gui/context/filtered_list.go @@ -0,0 +1,56 @@ +package context + +import ( + "strings" + + "github.com/jesseduffield/lazygit/pkg/utils" +) + +type FilteredList[T any] struct { + filteredIndices []int // if nil, we are not filtering + + getList func() []T + getFilterFields func(T) []string + filter string +} + +func (self *FilteredList[T]) GetFilter() string { + return self.filter +} + +func (self *FilteredList[T]) SetFilter(filter string) { + self.filter = filter + + self.applyFilter() +} + +func (self *FilteredList[T]) ClearFilter() { + self.SetFilter("") +} + +func (self *FilteredList[T]) GetList() []T { + if self.filteredIndices == nil { + return self.getList() + } + return utils.ValuesAtIndices(self.getList(), self.filteredIndices) +} + +func (self *FilteredList[T]) UnfilteredLen() int { + return len(self.getList()) +} + +func (self *FilteredList[T]) applyFilter() { + if self.filter == "" { + self.filteredIndices = nil + } else { + self.filteredIndices = []int{} + for i, item := range self.getList() { + for _, field := range self.getFilterFields(item) { + if strings.Contains(field, self.filter) { + self.filteredIndices = append(self.filteredIndices, i) + break + } + } + } + } +} diff --git a/pkg/gui/context/filtered_list_view_model.go b/pkg/gui/context/filtered_list_view_model.go new file mode 100644 index 000000000..01b020841 --- /dev/null +++ b/pkg/gui/context/filtered_list_view_model.go @@ -0,0 +1,26 @@ +package context + +type FilteredListViewModel[T any] struct { + *FilteredList[T] + *ListViewModel[T] +} + +func NewFilteredListViewModel[T any](getList func() []T, getFilterFields func(T) []string) *FilteredListViewModel[T] { + filteredList := &FilteredList[T]{ + getList: getList, + getFilterFields: getFilterFields, + } + + self := &FilteredListViewModel[T]{ + FilteredList: filteredList, + } + + listViewModel := NewListViewModel(filteredList.GetList) + + self.ListViewModel = listViewModel + + return self +} + +// used for type switch +func (self *FilteredListViewModel[T]) IsFilterableContext() {} diff --git a/pkg/gui/context/basic_view_model.go b/pkg/gui/context/list_view_model.go index a53be4d91..b70330d7d 100644 --- a/pkg/gui/context/basic_view_model.go +++ b/pkg/gui/context/list_view_model.go @@ -2,13 +2,13 @@ package context import "github.com/jesseduffield/lazygit/pkg/gui/context/traits" -type BasicViewModel[T any] struct { +type ListViewModel[T any] struct { *traits.ListCursor getModel func() []T } -func NewBasicViewModel[T any](getModel func() []T) *BasicViewModel[T] { - self := &BasicViewModel[T]{ +func NewListViewModel[T any](getModel func() []T) *ListViewModel[T] { + self := &ListViewModel[T]{ getModel: getModel, } @@ -17,11 +17,11 @@ func NewBasicViewModel[T any](getModel func() []T) *BasicViewModel[T] { return self } -func (self *BasicViewModel[T]) Len() int { +func (self *ListViewModel[T]) Len() int { return len(self.getModel()) } -func (self *BasicViewModel[T]) GetSelected() T { +func (self *ListViewModel[T]) GetSelected() T { if self.Len() == 0 { return Zero[T]() } @@ -29,6 +29,10 @@ func (self *BasicViewModel[T]) GetSelected() T { return self.getModel()[self.GetSelectedLineIdx()] } +func (self *ListViewModel[T]) GetItems() []T { + return self.getModel() +} + func Zero[T any]() T { return *new(T) } diff --git a/pkg/gui/context/local_commits_context.go b/pkg/gui/context/local_commits_context.go index 74363c52f..84204591c 100644 --- a/pkg/gui/context/local_commits_context.go +++ b/pkg/gui/context/local_commits_context.go @@ -13,6 +13,7 @@ import ( type LocalCommitsContext struct { *LocalCommitsViewModel *ListContextTrait + *SearchTrait } var ( @@ -57,8 +58,9 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext { ) } - return &LocalCommitsContext{ + ctx := &LocalCommitsContext{ LocalCommitsViewModel: viewModel, + SearchTrait: NewSearchTrait(c), ListContextTrait: &ListContextTrait{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ View: c.Views().Commits, @@ -73,6 +75,13 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext { refreshViewportOnChange: true, }, } + + ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(func(selectedLineIdx int) error { + ctx.GetList().SetSelectedLineIdx(selectedLineIdx) + return ctx.HandleFocus(types.OnFocusOpts{}) + })) + + return ctx } func (self *LocalCommitsContext) GetSelectedItemId() string { @@ -85,7 +94,7 @@ func (self *LocalCommitsContext) GetSelectedItemId() string { } type LocalCommitsViewModel struct { - *BasicViewModel[*models.Commit] + *ListViewModel[*models.Commit] // If this is true we limit the amount of commits we load, for the sake of keeping things fast. // If the user attempts to scroll past the end of the list, we will load more commits. @@ -97,7 +106,7 @@ type LocalCommitsViewModel struct { func NewLocalCommitsViewModel(getModel func() []*models.Commit, c *ContextCommon) *LocalCommitsViewModel { self := &LocalCommitsViewModel{ - BasicViewModel: NewBasicViewModel(getModel), + ListViewModel: NewListViewModel(getModel), limitCommits: true, showWholeGitGraph: c.UserConfig.Git.Log.ShowWholeGraph, } diff --git a/pkg/gui/context/menu_context.go b/pkg/gui/context/menu_context.go index 6f84a8274..088640ea0 100644 --- a/pkg/gui/context/menu_context.go +++ b/pkg/gui/context/menu_context.go @@ -56,7 +56,7 @@ func (self *MenuContext) GetSelectedItemId() string { type MenuViewModel struct { c *ContextCommon menuItems []*types.MenuItem - *BasicViewModel[*types.MenuItem] + *FilteredListViewModel[*types.MenuItem] } func NewMenuViewModel(c *ContextCommon) *MenuViewModel { @@ -65,7 +65,10 @@ func NewMenuViewModel(c *ContextCommon) *MenuViewModel { c: c, } - self.BasicViewModel = NewBasicViewModel(func() []*types.MenuItem { return self.menuItems }) + self.FilteredListViewModel = NewFilteredListViewModel( + func() []*types.MenuItem { return self.menuItems }, + func(item *types.MenuItem) []string { return item.LabelColumns }, + ) return self } @@ -76,11 +79,12 @@ func (self *MenuViewModel) SetMenuItems(items []*types.MenuItem) { // TODO: move into presentation package func (self *MenuViewModel) GetDisplayStrings(_startIdx int, _length int) [][]string { - showKeys := slices.Some(self.menuItems, func(item *types.MenuItem) bool { + menuItems := self.FilteredListViewModel.GetItems() + showKeys := slices.Some(menuItems, func(item *types.MenuItem) bool { return item.Key != nil }) - return slices.Map(self.menuItems, func(item *types.MenuItem) []string { + return slices.Map(menuItems, func(item *types.MenuItem) []string { displayStrings := item.LabelColumns if !showKeys { @@ -93,6 +97,7 @@ func (self *MenuViewModel) GetDisplayStrings(_startIdx int, _length int) [][]str self.c.UserConfig.Keybinding.Universal.Confirm, self.c.UserConfig.Keybinding.Universal.Select, self.c.UserConfig.Keybinding.Universal.Return, + self.c.UserConfig.Keybinding.Universal.StartSearch, } keyLabel := keybindings.LabelFromKey(item.Key) keyStyle := style.FgCyan diff --git a/pkg/gui/context/patch_explorer_context.go b/pkg/gui/context/patch_explorer_context.go index 1c986ee1d..17ecae4ae 100644 --- a/pkg/gui/context/patch_explorer_context.go +++ b/pkg/gui/context/patch_explorer_context.go @@ -9,6 +9,7 @@ import ( type PatchExplorerContext struct { *SimpleContext + *SearchTrait state *patch_exploring.State viewTrait *ViewTrait @@ -28,7 +29,7 @@ func NewPatchExplorerContext( c *ContextCommon, ) *PatchExplorerContext { - return &PatchExplorerContext{ + ctx := &PatchExplorerContext{ state: nil, viewTrait: NewViewTrait(view), c: c, @@ -42,7 +43,18 @@ func NewPatchExplorerContext( Focusable: true, HighlightOnFocus: true, })), + SearchTrait: NewSearchTrait(c), } + + ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper( + func(selectedLineIdx int) error { + ctx.GetMutex().Lock() + defer ctx.GetMutex().Unlock() + return ctx.NavigateTo(ctx.c.IsCurrentContext(ctx), selectedLineIdx) + }), + ) + + return ctx } func (self *PatchExplorerContext) IsPatchExplorerContext() {} diff --git a/pkg/gui/context/reflog_commits_context.go b/pkg/gui/context/reflog_commits_context.go index a92d605c8..421a7c8d5 100644 --- a/pkg/gui/context/reflog_commits_context.go +++ b/pkg/gui/context/reflog_commits_context.go @@ -9,7 +9,7 @@ import ( ) type ReflogCommitsContext struct { - *BasicViewModel[*models.Commit] + *FilteredListViewModel[*models.Commit] *ListContextTrait } @@ -19,11 +19,16 @@ var ( ) func NewReflogCommitsContext(c *ContextCommon) *ReflogCommitsContext { - viewModel := NewBasicViewModel(func() []*models.Commit { return c.Model().FilteredReflogCommits }) + viewModel := NewFilteredListViewModel( + func() []*models.Commit { return c.Model().FilteredReflogCommits }, + func(commit *models.Commit) []string { + return []string{commit.ShortSha(), commit.Name} + }, + ) getDisplayStrings := func(startIdx int, length int) [][]string { return presentation.GetReflogCommitListDisplayStrings( - c.Model().FilteredReflogCommits, + viewModel.GetItems(), c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL, c.Modes().CherryPicking.SelectedShaSet(), c.Modes().Diffing.Ref, @@ -35,7 +40,7 @@ func NewReflogCommitsContext(c *ContextCommon) *ReflogCommitsContext { } return &ReflogCommitsContext{ - BasicViewModel: viewModel, + FilteredListViewModel: viewModel, ListContextTrait: &ListContextTrait{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ View: c.Views().ReflogCommits, diff --git a/pkg/gui/context/remote_branches_context.go b/pkg/gui/context/remote_branches_context.go index a085c18cc..602a19a65 100644 --- a/pkg/gui/context/remote_branches_context.go +++ b/pkg/gui/context/remote_branches_context.go @@ -7,7 +7,7 @@ import ( ) type RemoteBranchesContext struct { - *BasicViewModel[*models.RemoteBranch] + *FilteredListViewModel[*models.RemoteBranch] *ListContextTrait *DynamicTitleBuilder } @@ -20,15 +20,20 @@ var ( func NewRemoteBranchesContext( c *ContextCommon, ) *RemoteBranchesContext { - viewModel := NewBasicViewModel(func() []*models.RemoteBranch { return c.Model().RemoteBranches }) + viewModel := NewFilteredListViewModel( + func() []*models.RemoteBranch { return c.Model().RemoteBranches }, + func(remoteBranch *models.RemoteBranch) []string { + return []string{remoteBranch.Name} + }, + ) getDisplayStrings := func(startIdx int, length int) [][]string { - return presentation.GetRemoteBranchListDisplayStrings(c.Model().RemoteBranches, c.Modes().Diffing.Ref) + return presentation.GetRemoteBranchListDisplayStrings(viewModel.GetItems(), c.Modes().Diffing.Ref) } return &RemoteBranchesContext{ - BasicViewModel: viewModel, - DynamicTitleBuilder: NewDynamicTitleBuilder(c.Tr.RemoteBranchesDynamicTitle), + FilteredListViewModel: viewModel, + DynamicTitleBuilder: NewDynamicTitleBuilder(c.Tr.RemoteBranchesDynamicTitle), ListContextTrait: &ListContextTrait{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ View: c.Views().RemoteBranches, diff --git a/pkg/gui/context/remotes_context.go b/pkg/gui/context/remotes_context.go index d1082ab52..f5e2a97ab 100644 --- a/pkg/gui/context/remotes_context.go +++ b/pkg/gui/context/remotes_context.go @@ -7,7 +7,7 @@ import ( ) type RemotesContext struct { - *BasicViewModel[*models.Remote] + *FilteredListViewModel[*models.Remote] *ListContextTrait } @@ -17,14 +17,19 @@ var ( ) func NewRemotesContext(c *ContextCommon) *RemotesContext { - viewModel := NewBasicViewModel(func() []*models.Remote { return c.Model().Remotes }) + viewModel := NewFilteredListViewModel( + func() []*models.Remote { return c.Model().Remotes }, + func(remote *models.Remote) []string { + return []string{remote.Name} + }, + ) getDisplayStrings := func(startIdx int, length int) [][]string { - return presentation.GetRemoteListDisplayStrings(c.Model().Remotes, c.Modes().Diffing.Ref) + return presentation.GetRemoteListDisplayStrings(viewModel.GetItems(), c.Modes().Diffing.Ref) } return &RemotesContext{ - BasicViewModel: viewModel, + FilteredListViewModel: viewModel, ListContextTrait: &ListContextTrait{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ View: c.Views().Remotes, diff --git a/pkg/gui/context/search_trait.go b/pkg/gui/context/search_trait.go new file mode 100644 index 000000000..5e745f995 --- /dev/null +++ b/pkg/gui/context/search_trait.go @@ -0,0 +1,70 @@ +package context + +import ( + "fmt" + + "github.com/jesseduffield/lazygit/pkg/gui/keybindings" + "github.com/jesseduffield/lazygit/pkg/theme" +) + +type SearchTrait struct { + c *ContextCommon + + searchString string +} + +func NewSearchTrait(c *ContextCommon) *SearchTrait { + return &SearchTrait{c: c} +} + +func (self *SearchTrait) GetSearchString() string { + return self.searchString +} + +func (self *SearchTrait) SetSearchString(searchString string) { + self.searchString = searchString +} + +func (self *SearchTrait) ClearSearchString() { + self.SetSearchString("") +} + +// used for type switch +func (self *SearchTrait) IsSearchableContext() {} + +func (self *SearchTrait) onSelectItemWrapper(innerFunc func(int) error) func(int, int, int) error { + keybindingConfig := self.c.UserConfig.Keybinding + + return func(y int, index int, total int) error { + if total == 0 { + self.c.SetViewContent( + self.c.Views().Search, + fmt.Sprintf( + self.c.Tr.NoMatchesFor, + self.searchString, + theme.OptionsFgColor.Sprintf(self.c.Tr.ExitSearchMode, keybindings.Label(keybindingConfig.Universal.Return)), + ), + ) + return nil + } + self.c.SetViewContent( + self.c.Views().Search, + fmt.Sprintf( + self.c.Tr.MatchesFor, + self.searchString, + index+1, + total, + theme.OptionsFgColor.Sprintf( + self.c.Tr.SearchKeybindings, + keybindings.Label(keybindingConfig.Universal.NextMatch), + keybindings.Label(keybindingConfig.Universal.PrevMatch), + keybindings.Label(keybindingConfig.Universal.Return), + ), + ), + ) + if err := innerFunc(y); err != nil { + return err + } + return nil + } +} diff --git a/pkg/gui/context/stash_context.go b/pkg/gui/context/stash_context.go index 386292c00..7bd4740f8 100644 --- a/pkg/gui/context/stash_context.go +++ b/pkg/gui/context/stash_context.go @@ -7,7 +7,7 @@ import ( ) type StashContext struct { - *BasicViewModel[*models.StashEntry] + *FilteredListViewModel[*models.StashEntry] *ListContextTrait } @@ -19,14 +19,19 @@ var ( func NewStashContext( c *ContextCommon, ) *StashContext { - viewModel := NewBasicViewModel(func() []*models.StashEntry { return c.Model().StashEntries }) + viewModel := NewFilteredListViewModel( + func() []*models.StashEntry { return c.Model().StashEntries }, + func(stashEntry *models.StashEntry) []string { + return []string{stashEntry.Name} + }, + ) getDisplayStrings := func(startIdx int, length int) [][]string { - return presentation.GetStashEntryListDisplayStrings(c.Model().StashEntries, c.Modes().Diffing.Ref) + return presentation.GetStashEntryListDisplayStrings(viewModel.GetItems(), c.Modes().Diffing.Ref) } return &StashContext{ - BasicViewModel: viewModel, + FilteredListViewModel: viewModel, ListContextTrait: &ListContextTrait{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ View: c.Views().Stash, diff --git a/pkg/gui/context/sub_commits_context.go b/pkg/gui/context/sub_commits_context.go index e2c89aa14..0cf884589 100644 --- a/pkg/gui/context/sub_commits_context.go +++ b/pkg/gui/context/sub_commits_context.go @@ -12,9 +12,12 @@ import ( ) |