mirror of
				https://github.com/jesseduffield/lazygit.git
				synced 2025-10-30 23:57:43 +02:00 
			
		
		
		
	Introduce filtered list view model
We're going to start supporting filtering of list views
This commit is contained in:
		| @@ -135,10 +135,6 @@ func (gui *Gui) getRandomTip() string { | ||||
| 			"To escape a mode, for example cherry-picking, patch-building, diffing, or filtering mode, you can just spam the '%s' button. Unless of course you have `quitOnTopLevelReturn` enabled in your config", | ||||
| 			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), | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
							
								
								
									
										56
									
								
								pkg/gui/context/filtered_list.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								pkg/gui/context/filtered_list.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										26
									
								
								pkg/gui/context/filtered_list_view_model.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								pkg/gui/context/filtered_list_view_model.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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() {} | ||||
| @@ -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) | ||||
| } | ||||
| @@ -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, | ||||
| 	} | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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() {} | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
							
								
								
									
										70
									
								
								pkg/gui/context/search_trait.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								pkg/gui/context/search_trait.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| 	} | ||||
| } | ||||
| @@ -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, | ||||
|   | ||||
| @@ -12,9 +12,12 @@ import ( | ||||
| ) | ||||
|  | ||||
| type SubCommitsContext struct { | ||||
| 	c *ContextCommon | ||||
|  | ||||
| 	*SubCommitsViewModel | ||||
| 	*ListContextTrait | ||||
| 	*DynamicTitleBuilder | ||||
| 	*SearchTrait | ||||
| } | ||||
|  | ||||
| var ( | ||||
| @@ -26,7 +29,7 @@ func NewSubCommitsContext( | ||||
| 	c *ContextCommon, | ||||
| ) *SubCommitsContext { | ||||
| 	viewModel := &SubCommitsViewModel{ | ||||
| 		BasicViewModel: NewBasicViewModel( | ||||
| 		ListViewModel: NewListViewModel( | ||||
| 			func() []*models.Commit { return c.Model().SubCommits }, | ||||
| 		), | ||||
| 		ref:          nil, | ||||
| @@ -60,8 +63,10 @@ func NewSubCommitsContext( | ||||
| 		) | ||||
| 	} | ||||
|  | ||||
| 	return &SubCommitsContext{ | ||||
| 	ctx := &SubCommitsContext{ | ||||
| 		c:                   c, | ||||
| 		SubCommitsViewModel: viewModel, | ||||
| 		SearchTrait:         NewSearchTrait(c), | ||||
| 		DynamicTitleBuilder: NewDynamicTitleBuilder(c.Tr.SubCommitsDynamicTitle), | ||||
| 		ListContextTrait: &ListContextTrait{ | ||||
| 			Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ | ||||
| @@ -78,12 +83,19 @@ func NewSubCommitsContext( | ||||
| 			refreshViewportOnChange: true, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(func(selectedLineIdx int) error { | ||||
| 		ctx.GetList().SetSelectedLineIdx(selectedLineIdx) | ||||
| 		return ctx.HandleFocus(types.OnFocusOpts{}) | ||||
| 	})) | ||||
|  | ||||
| 	return ctx | ||||
| } | ||||
|  | ||||
| type SubCommitsViewModel struct { | ||||
| 	// name of the ref that the sub-commits are shown for | ||||
| 	ref types.Ref | ||||
| 	*BasicViewModel[*models.Commit] | ||||
| 	*ListViewModel[*models.Commit] | ||||
|  | ||||
| 	limitCommits bool | ||||
| } | ||||
|   | ||||
| @@ -7,21 +7,26 @@ import ( | ||||
| ) | ||||
|  | ||||
| type SubmodulesContext struct { | ||||
| 	*BasicViewModel[*models.SubmoduleConfig] | ||||
| 	*FilteredListViewModel[*models.SubmoduleConfig] | ||||
| 	*ListContextTrait | ||||
| } | ||||
|  | ||||
| var _ types.IListContext = (*SubmodulesContext)(nil) | ||||
|  | ||||
| func NewSubmodulesContext(c *ContextCommon) *SubmodulesContext { | ||||
| 	viewModel := NewBasicViewModel(func() []*models.SubmoduleConfig { return c.Model().Submodules }) | ||||
| 	viewModel := NewFilteredListViewModel( | ||||
| 		func() []*models.SubmoduleConfig { return c.Model().Submodules }, | ||||
| 		func(submodule *models.SubmoduleConfig) []string { | ||||
| 			return []string{submodule.Name} | ||||
| 		}, | ||||
| 	) | ||||
|  | ||||
| 	getDisplayStrings := func(startIdx int, length int) [][]string { | ||||
| 		return presentation.GetSubmoduleListDisplayStrings(c.Model().Submodules) | ||||
| 		return presentation.GetSubmoduleListDisplayStrings(viewModel.GetItems()) | ||||
| 	} | ||||
|  | ||||
| 	return &SubmodulesContext{ | ||||
| 		BasicViewModel: viewModel, | ||||
| 		FilteredListViewModel: viewModel, | ||||
| 		ListContextTrait: &ListContextTrait{ | ||||
| 			Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ | ||||
| 				View:       c.Views().Submodules, | ||||
|   | ||||
| @@ -7,7 +7,7 @@ import ( | ||||
| ) | ||||
|  | ||||
| type SuggestionsContext struct { | ||||
| 	*BasicViewModel[*types.Suggestion] | ||||
| 	*ListViewModel[*types.Suggestion] | ||||
| 	*ListContextTrait | ||||
|  | ||||
| 	State *SuggestionsContextState | ||||
| @@ -40,11 +40,11 @@ func NewSuggestionsContext( | ||||
| 		return presentation.GetSuggestionListDisplayStrings(state.Suggestions) | ||||
| 	} | ||||
|  | ||||
| 	viewModel := NewBasicViewModel(getModel) | ||||
| 	viewModel := NewListViewModel(getModel) | ||||
|  | ||||
| 	return &SuggestionsContext{ | ||||
| 		State:          state, | ||||
| 		BasicViewModel: viewModel, | ||||
| 		State:         state, | ||||
| 		ListViewModel: viewModel, | ||||
| 		ListContextTrait: &ListContextTrait{ | ||||
| 			Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ | ||||
| 				View:                  c.Views().Suggestions, | ||||
|   | ||||
| @@ -7,7 +7,7 @@ import ( | ||||
| ) | ||||
|  | ||||
| type TagsContext struct { | ||||
| 	*BasicViewModel[*models.Tag] | ||||
| 	*FilteredListViewModel[*models.Tag] | ||||
| 	*ListContextTrait | ||||
| } | ||||
|  | ||||
| @@ -19,14 +19,19 @@ var ( | ||||
| func NewTagsContext( | ||||
| 	c *ContextCommon, | ||||
| ) *TagsContext { | ||||
| 	viewModel := NewBasicViewModel(func() []*models.Tag { return c.Model().Tags }) | ||||
| 	viewModel := NewFilteredListViewModel( | ||||
| 		func() []*models.Tag { return c.Model().Tags }, | ||||
| 		func(tag *models.Tag) []string { | ||||
| 			return []string{tag.Name, tag.Message} | ||||
| 		}, | ||||
| 	) | ||||
|  | ||||
| 	getDisplayStrings := func(startIdx int, length int) [][]string { | ||||
| 		return presentation.GetTagListDisplayStrings(c.Model().Tags, c.Modes().Diffing.Ref) | ||||
| 		return presentation.GetTagListDisplayStrings(viewModel.GetItems(), c.Modes().Diffing.Ref) | ||||
| 	} | ||||
|  | ||||
| 	return &TagsContext{ | ||||
| 		BasicViewModel: viewModel, | ||||
| 		FilteredListViewModel: viewModel, | ||||
| 		ListContextTrait: &ListContextTrait{ | ||||
| 			Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ | ||||
| 				View:       c.Views().Tags, | ||||
|   | ||||
| @@ -99,6 +99,7 @@ func (gui *Gui) resetHelpersAndControllers() { | ||||
| 			modeHelper, | ||||
| 			appStatusHelper, | ||||
| 		), | ||||
| 		Search: helpers.NewSearchHelper(helperCommon), | ||||
| 	} | ||||
|  | ||||
| 	gui.CustomCommandsClient = custom_commands.NewClient( | ||||
| @@ -162,6 +163,16 @@ func (gui *Gui) resetHelpersAndControllers() { | ||||
|  | ||||
| 	sideWindowControllerFactory := controllers.NewSideWindowControllerFactory(common) | ||||
|  | ||||
| 	filterControllerFactory := controllers.NewFilterControllerFactory(common) | ||||
| 	for _, context := range gui.c.Context().AllFilterable() { | ||||
| 		controllers.AttachControllers(context, filterControllerFactory.Create(context)) | ||||
| 	} | ||||
|  | ||||
| 	searchControllerFactory := controllers.NewSearchControllerFactory(common) | ||||
| 	for _, context := range gui.c.Context().AllSearchable() { | ||||
| 		controllers.AttachControllers(context, searchControllerFactory.Create(context)) | ||||
| 	} | ||||
|  | ||||
| 	// allow for navigating between side window contexts | ||||
| 	for _, context := range []types.Context{ | ||||
| 		gui.State.Contexts.Status, | ||||
| @@ -323,6 +334,10 @@ func (gui *Gui) resetHelpersAndControllers() { | ||||
| 		suggestionsController, | ||||
| 	) | ||||
|  | ||||
| 	controllers.AttachControllers(gui.State.Contexts.Search, | ||||
| 		controllers.NewSearchPromptController(common), | ||||
| 	) | ||||
|  | ||||
| 	controllers.AttachControllers(gui.State.Contexts.Global, | ||||
| 		syncController, | ||||
| 		undoController, | ||||
|   | ||||
							
								
								
									
										48
									
								
								pkg/gui/controllers/filter_controller.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								pkg/gui/controllers/filter_controller.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| package controllers | ||||
|  | ||||
| import ( | ||||
| 	"github.com/jesseduffield/lazygit/pkg/gui/types" | ||||
| ) | ||||
|  | ||||
| type FilterControllerFactory struct { | ||||
| 	c *ControllerCommon | ||||
| } | ||||
|  | ||||
| func NewFilterControllerFactory(c *ControllerCommon) *FilterControllerFactory { | ||||
| 	return &FilterControllerFactory{ | ||||
| 		c: c, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (self *FilterControllerFactory) Create(context types.IFilterableContext) *FilterController { | ||||
| 	return &FilterController{ | ||||
| 		baseController: baseController{}, | ||||
| 		c:              self.c, | ||||
| 		context:        context, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type FilterController struct { | ||||
| 	baseController | ||||
| 	c *ControllerCommon | ||||
|  | ||||
| 	context types.IFilterableContext | ||||
| } | ||||
|  | ||||
| func (self *FilterController) Context() types.Context { | ||||
| 	return self.context | ||||
| } | ||||
|  | ||||
| func (self *FilterController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { | ||||
| 	return []*types.Binding{ | ||||
| 		{ | ||||
| 			Key:         opts.GetKey(opts.Config.Universal.StartSearch), | ||||
| 			Handler:     self.OpenFilterPrompt, | ||||
| 			Description: self.c.Tr.StartFilter, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (self *FilterController) OpenFilterPrompt() error { | ||||
| 	return self.c.Helpers().Search.OpenFilterPrompt(self.context) | ||||
| } | ||||
| @@ -292,7 +292,9 @@ func (self *ConfirmationHelper) ResizePopupPanel(v *gocui.View, content string) | ||||
| } | ||||
|  | ||||
| func (self *ConfirmationHelper) resizeMenu() { | ||||
| 	itemCount := self.c.Contexts().Menu.GetList().Len() | ||||
| 	// we want the unfiltered length here so that if we're filtering we don't | ||||
| 	// resize the window | ||||
| 	itemCount := self.c.Contexts().Menu.UnfilteredLen() | ||||
| 	offset := 3 | ||||
| 	panelWidth := self.getPopupPanelWidth() | ||||
| 	x0, y0, x1, y1 := self.getPopupPanelDimensionsForContentHeight(panelWidth, itemCount+offset) | ||||
|   | ||||
| @@ -46,6 +46,7 @@ type Helpers struct { | ||||
| 	Mode              *ModeHelper | ||||
| 	AppStatus         *AppStatusHelper | ||||
| 	WindowArrangement *WindowArrangementHelper | ||||
| 	Search            *SearchHelper | ||||
| } | ||||
|  | ||||
| func NewStubHelpers() *Helpers { | ||||
| @@ -78,5 +79,6 @@ func NewStubHelpers() *Helpers { | ||||
| 		Mode:              &ModeHelper{}, | ||||
| 		AppStatus:         &AppStatusHelper{}, | ||||
| 		WindowArrangement: &WindowArrangementHelper{}, | ||||
| 		Search:            &SearchHelper{}, | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										196
									
								
								pkg/gui/controllers/helpers/search_helper.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								pkg/gui/controllers/helpers/search_helper.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,196 @@ | ||||
| package helpers | ||||
|  | ||||
| import ( | ||||
| 	"github.com/jesseduffield/gocui" | ||||
| 	"github.com/jesseduffield/lazygit/pkg/gui/types" | ||||
| ) | ||||
|  | ||||
| // NOTE: this helper supports both filtering and searching. Filtering is when | ||||
| // the contents of the list are filtered, whereas searching does not actually | ||||
| // change the contents of the list but instead just highlights the search. | ||||
| // The general term we use to capture both searching and filtering is... | ||||
| // 'searching', which is unfortunate but I can't think of a better name. | ||||
|  | ||||
| type SearchHelper struct { | ||||
| 	c *HelperCommon | ||||
| } | ||||
|  | ||||
| func NewSearchHelper( | ||||
| 	c *HelperCommon, | ||||
| ) *SearchHelper { | ||||
| 	return &SearchHelper{ | ||||
| 		c: c, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (self *SearchHelper) OpenFilterPrompt(context types.IFilterableContext) error { | ||||
| 	state := self.searchState() | ||||
|  | ||||
| 	state.Context = context | ||||
|  | ||||
| 	self.searchPrefixView().SetContent(self.c.Tr.FilterPrefix) | ||||
| 	promptView := self.promptView() | ||||
| 	promptView.ClearTextArea() | ||||
| 	promptView.TextArea.TypeString(context.GetFilter()) | ||||
| 	promptView.RenderTextArea() | ||||
|  | ||||
| 	if err := self.c.PushContext(self.c.Contexts().Search); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (self *SearchHelper) OpenSearchPrompt(context types.Context) error { | ||||
| 	state := self.searchState() | ||||
|  | ||||
| 	state.Context = context | ||||
|  | ||||
| 	self.searchPrefixView().SetContent(self.c.Tr.SearchPrefix) | ||||
| 	promptView := self.promptView() | ||||
| 	// TODO: should we show the currently searched thing here? Perhaps we can store that on the context | ||||
| 	promptView.ClearTextArea() | ||||
| 	promptView.RenderTextArea() | ||||
|  | ||||
| 	if err := self.c.PushContext(self.c.Contexts().Search); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (self *SearchHelper) DisplayFilterPrompt(context types.IFilterableContext) { | ||||
| 	state := self.searchState() | ||||
|  | ||||
| 	state.Context = context | ||||
| 	searchString := context.GetFilter() | ||||
|  | ||||
| 	self.searchPrefixView().SetContent(self.c.Tr.FilterPrefix) | ||||
| 	promptView := self.promptView() | ||||
| 	promptView.ClearTextArea() | ||||
| 	promptView.TextArea.TypeString(searchString) | ||||
| 	promptView.RenderTextArea() | ||||
| } | ||||
|  | ||||
| func (self *SearchHelper) DisplaySearchPrompt(context types.ISearchableContext) { | ||||
| 	state := self.searchState() | ||||
|  | ||||
| 	state.Context = context | ||||
| 	searchString := context.GetSearchString() | ||||
|  | ||||
| 	self.searchPrefixView().SetContent(self.c.Tr.SearchPrefix) | ||||
| 	promptView := self.promptView() | ||||
| 	promptView.ClearTextArea() | ||||
| 	promptView.TextArea.TypeString(searchString) | ||||
| 	promptView.RenderTextArea() | ||||
| } | ||||
|  | ||||
| func (self *SearchHelper) searchState() *types.SearchState { | ||||
| 	return self.c.State().GetRepoState().GetSearchState() | ||||
| } | ||||
|  | ||||
| func (self *SearchHelper) searchPrefixView() *gocui.View { | ||||
| 	return self.c.Views().SearchPrefix | ||||
| } | ||||
|  | ||||
| func (self *SearchHelper) promptView() *gocui.View { | ||||
| 	return self.c.Contexts().Search.GetView() | ||||
| } | ||||
|  | ||||
| func (self *SearchHelper) promptContent() string { | ||||
| 	return self.c.Contexts().Search.GetView().TextArea.GetContent() | ||||
| } | ||||
|  | ||||
| func (self *SearchHelper) Confirm() error { | ||||
| 	state := self.searchState() | ||||
| 	if self.promptContent() == "" { | ||||
| 		return self.CancelPrompt() | ||||
| 	} | ||||
|  | ||||
| 	switch state.SearchType() { | ||||
| 	case types.SearchTypeFilter: | ||||
| 		return self.ConfirmFilter() | ||||
| 	case types.SearchTypeSearch: | ||||
| 		return self.ConfirmSearch() | ||||
| 	case types.SearchTypeNone: | ||||
| 		return self.c.PopContext() | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (self *SearchHelper) ConfirmFilter() error { | ||||
| 	// We also do this on each keypress but we do it here again just in case | ||||
| 	state := self.searchState() | ||||
|  | ||||
| 	context, ok := state.Context.(types.IFilterableContext) | ||||
| 	if !ok { | ||||
| 		self.c.Log.Warnf("Context %s is not filterable", state.Context.GetKey()) | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	context.SetFilter(self.promptContent()) | ||||
| 	_ = self.c.PostRefreshUpdate(state.Context) | ||||
|  | ||||
| 	return self.c.PopContext() | ||||
| } | ||||
|  | ||||
| func (self *SearchHelper) ConfirmSearch() error { | ||||
| 	state := self.searchState() | ||||
|  | ||||
| 	if err := self.c.PopContext(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	context, ok := state.Context.(types.ISearchableContext) | ||||
| 	if !ok { | ||||
| 		self.c.Log.Warnf("Context %s is searchable", state.Context.GetKey()) | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	searchString := self.promptContent() | ||||
| 	context.SetSearchString(searchString) | ||||
|  | ||||
| 	view := context.GetView() | ||||
|  | ||||
| 	if err := view.Search(searchString); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (self *SearchHelper) CancelPrompt() error { | ||||
| 	self.Cancel() | ||||
|  | ||||
| 	return self.c.PopContext() | ||||
| } | ||||
|  | ||||
| func (self *SearchHelper) Cancel() { | ||||
| 	state := self.searchState() | ||||
|  | ||||
| 	switch context := state.Context.(type) { | ||||
| 	case types.IFilterableContext: | ||||
| 		context.SetFilter("") | ||||
| 		_ = self.c.PostRefreshUpdate(context) | ||||
| 	case types.ISearchableContext: | ||||
| 		context.GetView().ClearSearch() | ||||
| 	default: | ||||
| 		// do nothing | ||||
| 	} | ||||
|  | ||||
| 	state.Context = nil | ||||
| } | ||||
|  | ||||
| func (self *SearchHelper) OnPromptContentChanged(searchString string) { | ||||
| 	state := self.searchState() | ||||
| 	switch context := state.Context.(type) { | ||||
| 	case types.IFilterableContext: | ||||
| 		context.SetFilter(searchString) | ||||
| 		_ = self.c.PostRefreshUpdate(context) | ||||
| 	case types.ISearchableContext: | ||||
| 		// do nothing | ||||
| 	default: | ||||
| 		// do nothing (shouldn't land here) | ||||
| 	} | ||||
| } | ||||
| @@ -55,7 +55,7 @@ func (self *WindowArrangementHelper) GetWindowDimensions(informationStr string, | ||||
| 	self.c.Modes().Filtering.Active() | ||||
|  | ||||
| 	showInfoSection := self.c.UserConfig.Gui.ShowBottomLine || | ||||
| 		self.c.State().GetRepoState().IsSearching() || | ||||
| 		self.c.State().GetRepoState().InSearchPrompt() || | ||||
| 		self.modeHelper.IsAnyModeActive() || | ||||
| 		self.appStatusHelper.HasStatus() | ||||
| 	infoSectionSize := 0 | ||||
| @@ -174,11 +174,17 @@ func (self *WindowArrangementHelper) getMidSectionWeights() (int, int) { | ||||
| } | ||||
|  | ||||
| func (self *WindowArrangementHelper) infoSectionChildren(informationStr string, appStatus string) []*boxlayout.Box { | ||||
| 	if self.c.State().GetRepoState().IsSearching() { | ||||
| 	if self.c.State().GetRepoState().InSearchPrompt() { | ||||
| 		var prefix string | ||||
| 		if self.c.State().GetRepoState().GetSearchState().SearchType() == types.SearchTypeSearch { | ||||
| 			prefix = self.c.Tr.SearchPrefix | ||||
| 		} else { | ||||
| 			prefix = self.c.Tr.FilterPrefix | ||||
| 		} | ||||
| 		return []*boxlayout.Box{ | ||||
| 			{ | ||||
| 				Window: "searchPrefix", | ||||
| 				Size:   runewidth.StringWidth(self.c.Tr.SearchPrefix), | ||||
| 				Size:   runewidth.StringWidth(prefix), | ||||
| 			}, | ||||
| 			{ | ||||
| 				Window: "search", | ||||
|   | ||||
| @@ -150,18 +150,7 @@ func (self *ListController) GetKeybindings(opts types.KeybindingsOpts) []*types. | ||||
| 		{Tag: "navigation", Key: opts.GetKey(opts.Config.Universal.GotoTop), Handler: self.HandleGotoTop, Description: self.c.Tr.GotoTop}, | ||||
| 		{Tag: "navigation", Key: opts.GetKey(opts.Config.Universal.ScrollLeft), Handler: self.HandleScrollLeft}, | ||||
| 		{Tag: "navigation", Key: opts.GetKey(opts.Config.Universal.ScrollRight), Handler: self.HandleScrollRight}, | ||||
| 		{ | ||||
| 			Key:         opts.GetKey(opts.Config.Universal.StartSearch), | ||||
| 			Handler:     func() error { self.c.OpenSearch(); return nil }, | ||||
| 			Description: self.c.Tr.StartSearch, | ||||
| 			Tag:         "navigation", | ||||
| 		}, | ||||
| 		{ | ||||
| 			Key:         opts.GetKey(opts.Config.Universal.GotoBottom), | ||||
| 			Description: self.c.Tr.GotoBottom, | ||||
| 			Handler:     self.HandleGotoBottom, | ||||
| 			Tag:         "navigation", | ||||
| 		}, | ||||
| 		{Tag: "navigation", Key: opts.GetKey(opts.Config.Universal.GotoBottom), Handler: self.HandleGotoBottom, Description: self.c.Tr.GotoBottom}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -693,9 +693,7 @@ func (self *LocalCommitsController) openSearch() error { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	self.c.OpenSearch() | ||||
|  | ||||
| 	return nil | ||||
| 	return self.c.Helpers().Search.OpenSearchPrompt(self.context()) | ||||
| } | ||||
|  | ||||
| func (self *LocalCommitsController) gotoBottom() error { | ||||
|   | ||||
| @@ -123,12 +123,6 @@ func (self *PatchExplorerController) GetKeybindings(opts types.KeybindingsOpts) | ||||
| 			Key:     opts.GetKey(opts.Config.Universal.ScrollRight), | ||||
| 			Handler: self.withRenderAndFocus(self.HandleScrollRight), | ||||
| 		}, | ||||
| 		{ | ||||
| 			Tag:         "navigation", | ||||
| 			Key:         opts.GetKey(opts.Config.Universal.StartSearch), | ||||
| 			Handler:     func() error { self.c.OpenSearch(); return nil }, | ||||
| 			Description: self.c.Tr.StartSearch, | ||||
| 		}, | ||||
| 		{ | ||||
| 			Key:         opts.GetKey(opts.Config.Universal.CopyToClipboard), | ||||
| 			Handler:     self.withLock(self.CopySelectedToClipboard), | ||||
|   | ||||
							
								
								
									
										48
									
								
								pkg/gui/controllers/search_controller.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								pkg/gui/controllers/search_controller.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| package controllers | ||||
|  | ||||
| import ( | ||||
| 	"github.com/jesseduffield/lazygit/pkg/gui/types" | ||||
| ) | ||||
|  | ||||
| type SearchControllerFactory struct { | ||||
| 	c *ControllerCommon | ||||
| } | ||||
|  | ||||
| func NewSearchControllerFactory(c *ControllerCommon) *SearchControllerFactory { | ||||
| 	return &SearchControllerFactory{ | ||||
| 		c: c, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (self *SearchControllerFactory) Create(context types.ISearchableContext) *SearchController { | ||||
| 	return &SearchController{ | ||||
| 		baseController: baseController{}, | ||||
| 		c:              self.c, | ||||
| 		context:        context, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type SearchController struct { | ||||
| 	baseController | ||||
| 	c *ControllerCommon | ||||
|  | ||||
| 	context types.ISearchableContext | ||||
| } | ||||
|  | ||||
| func (self *SearchController) Context() types.Context { | ||||
| 	return self.context | ||||
| } | ||||
|  | ||||
| func (self *SearchController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { | ||||
| 	return []*types.Binding{ | ||||
| 		{ | ||||
| 			Key:         opts.GetKey(opts.Config.Universal.StartSearch), | ||||
| 			Handler:     self.OpenSearchPrompt, | ||||
| 			Description: self.c.Tr.StartSearch, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (self *SearchController) OpenSearchPrompt() error { | ||||
| 	return self.c.Helpers().Search.OpenSearchPrompt(self.context) | ||||
| } | ||||
							
								
								
									
										53
									
								
								pkg/gui/controllers/search_prompt_controller.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								pkg/gui/controllers/search_prompt_controller.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| package controllers | ||||
|  | ||||
| import ( | ||||
| 	"github.com/jesseduffield/gocui" | ||||
| 	"github.com/jesseduffield/lazygit/pkg/gui/types" | ||||
| ) | ||||
|  | ||||
| type SearchPromptController struct { | ||||
| 	baseController | ||||
| 	c *ControllerCommon | ||||
| } | ||||
|  | ||||
| var _ types.IController = &SearchPromptController{} | ||||
|  | ||||
| func NewSearchPromptController( | ||||
| 	common *ControllerCommon, | ||||
| ) *SearchPromptController { | ||||
| 	return &SearchPromptController{ | ||||
| 		baseController: baseController{}, | ||||
| 		c:              common, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (self *SearchPromptController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { | ||||
| 	return []*types.Binding{ | ||||
| 		{ | ||||
| 			Key:      opts.GetKey(opts.Config.Universal.Confirm), | ||||
| 			Modifier: gocui.ModNone, | ||||
| 			Handler:  self.confirm, | ||||
| 		}, | ||||
| 		{ | ||||
| 			Key:      opts.GetKey(opts.Config.Universal.Return), | ||||
| 			Modifier: gocui.ModNone, | ||||
| 			Handler:  self.cancel, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (self *SearchPromptController) Context() types.Context { | ||||
| 	return self.context() | ||||
| } | ||||
|  | ||||
| func (self *SearchPromptController) context() types.Context { | ||||
| 	return self.c.Contexts().Search | ||||
| } | ||||
|  | ||||
| func (self *SearchPromptController) confirm() error { | ||||
| 	return self.c.Helpers().Search.Confirm() | ||||
| } | ||||
|  | ||||
| func (self *SearchPromptController) cancel() error { | ||||
| 	return self.c.Helpers().Search.CancelPrompt() | ||||
| } | ||||
| @@ -89,3 +89,14 @@ func (gui *Gui) promptEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Mo | ||||
|  | ||||
| 	return matched | ||||
| } | ||||
|  | ||||
| func (gui *Gui) searchEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool { | ||||
| 	matched := gui.handleEditorKeypress(v.TextArea, key, ch, mod, false) | ||||
| 	v.RenderTextArea() | ||||
|  | ||||
| 	searchString := v.TextArea.GetContent() | ||||
|  | ||||
| 	gui.helpers.Search.OnPromptContentChanged(searchString) | ||||
|  | ||||
| 	return matched | ||||
| } | ||||
|   | ||||
| @@ -26,6 +26,8 @@ type FileTreeViewModel struct { | ||||
|  | ||||
| var _ IFileTreeViewModel = &FileTreeViewModel{} | ||||
|  | ||||
| // how to tackle this? We could just filter down the list of files at a high point and then the rest will take care of itself. | ||||
|  | ||||
| func NewFileTreeViewModel(getFiles func() []*models.File, log *logrus.Entry, showTree bool) *FileTreeViewModel { | ||||
| 	fileTree := NewFileTree(getFiles, log, showTree) | ||||
| 	listCursor := traits.NewListCursor(fileTree) | ||||
|   | ||||
| @@ -201,7 +201,7 @@ type GuiRepoState struct { | ||||
| 	SplitMainPanel bool | ||||
| 	LimitCommits   bool | ||||
|  | ||||
| 	Searching    searchingState | ||||
| 	SearchState  *types.SearchState | ||||
| 	StartupStage types.StartupStage // Allows us to not load everything at once | ||||
|  | ||||
| 	ContextMgr *ContextMgr | ||||
| @@ -256,8 +256,12 @@ func (self *GuiRepoState) SetScreenMode(value types.WindowMaximisation) { | ||||
| 	self.ScreenMode = value | ||||
| } | ||||
|  | ||||
| func (self *GuiRepoState) IsSearching() bool { | ||||
| 	return self.Searching.isSearching | ||||
| func (self *GuiRepoState) InSearchPrompt() bool { | ||||
| 	return self.SearchState.SearchType() != types.SearchTypeNone | ||||
| } | ||||
|  | ||||
| func (self *GuiRepoState) GetSearchState() *types.SearchState { | ||||
| 	return self.SearchState | ||||
| } | ||||
|  | ||||
| func (self *GuiRepoState) SetSplitMainPanel(value bool) { | ||||
| @@ -268,12 +272,6 @@ func (self *GuiRepoState) GetSplitMainPanel() bool { | ||||
| 	return self.SplitMainPanel | ||||
| } | ||||
|  | ||||
| type searchingState struct { | ||||
| 	view         *gocui.View | ||||
| 	isSearching  bool | ||||
| 	searchString string | ||||
| } | ||||
|  | ||||
| func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, reuseState bool) error { | ||||
| 	var err error | ||||
| 	gui.git, err = commands.NewGitCommand( | ||||
| @@ -358,6 +356,7 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs, reuseState bool) types. | ||||
| 		ContextMgr:        NewContextMgr(gui, contextTree), | ||||
| 		Contexts:          contextTree, | ||||
| 		WindowViewNameMap: initialWindowViewNameMap(contextTree), | ||||
| 		SearchState:       types.NewSearchState(), | ||||
| 	} | ||||
|  | ||||
| 	gui.RepoStateMap[Repo(currentDir)] = gui.State | ||||
| @@ -584,11 +583,12 @@ func (gui *Gui) Run(startArgs appTypes.StartArgs) error { | ||||
| 	}) | ||||
| 	deadlock.Opts.Disable = !gui.Debug | ||||
|  | ||||
| 	gui.g.OnSearchEscape = gui.onSearchEscape | ||||
| 	if err := gui.Config.ReloadUserConfig(); err != nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	userConfig := gui.UserConfig | ||||
|  | ||||
| 	gui.g.OnSearchEscape = func() error { gui.helpers.Search.Cancel(); return nil } | ||||
| 	gui.g.SearchEscapeKey = keybindings.GetKey(userConfig.Keybinding.Universal.Return) | ||||
| 	gui.g.NextSearchMatchKey = keybindings.GetKey(userConfig.Keybinding.Universal.NextMatch) | ||||
| 	gui.g.PrevSearchMatchKey = keybindings.GetKey(userConfig.Keybinding.Universal.PrevMatch) | ||||
|   | ||||
| @@ -128,10 +128,6 @@ func (self *guiCommon) Mutexes() types.Mutexes { | ||||
| 	return self.gui.Mutexes | ||||
| } | ||||
|  | ||||
| func (self *guiCommon) OpenSearch() { | ||||
| 	_ = self.gui.handleOpenSearch(self.gui.currentViewName()) | ||||
| } | ||||
|  | ||||
| func (self *guiCommon) GocuiGui() *gocui.Gui { | ||||
| 	return self.gui.g | ||||
| } | ||||
|   | ||||
| @@ -215,18 +215,6 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi | ||||
| 			Modifier: gocui.ModNone, | ||||
| 			Handler:  self.scrollUpSecondary, | ||||
| 		}, | ||||
| 		{ | ||||
| 			ViewName: "search", | ||||
| 			Key:      opts.GetKey(opts.Config.Universal.Confirm), | ||||
| 			Modifier: gocui.ModNone, | ||||
| 			Handler:  self.handleSearch, | ||||
| 		}, | ||||
| 		{ | ||||
| 			ViewName: "search", | ||||
| 			Key:      opts.GetKey(opts.Config.Universal.Return), | ||||
| 			Modifier: gocui.ModNone, | ||||
| 			Handler:  self.handleSearchEscape, | ||||
| 		}, | ||||
| 		{ | ||||
| 			ViewName: "confirmation", | ||||
| 			Key:      opts.GetKey(opts.Config.Universal.PrevItem), | ||||
|   | ||||
| @@ -132,20 +132,6 @@ func (gui *Gui) layout(g *gocui.Gui) error { | ||||
| 		} | ||||
|  | ||||
| 		view.SelBgColor = theme.GocuiSelectedLineBgColor | ||||
|  | ||||
| 		// I doubt this is expensive though it's admittedly redundant after the first render | ||||
| 		view.SetOnSelectItem(gui.onSelectItemWrapper(listContext.OnSearchSelect)) | ||||
| 	} | ||||
|  | ||||
| 	for _, context := range gui.c.Context().AllPatchExplorer() { | ||||
| 		context := context | ||||
| 		context.GetView().SetOnSelectItem(gui.onSelectItemWrapper( | ||||
| 			func(selectedLineIdx int) error { | ||||
| 				context.GetMutex().Lock() | ||||
| 				defer context.GetMutex().Unlock() | ||||
| 				return context.NavigateTo(gui.c.IsCurrentContext(context), selectedLineIdx) | ||||
| 			}), | ||||
| 		) | ||||
| 	} | ||||
|  | ||||
| 	mainViewWidth, mainViewHeight := gui.Views.Main.Size() | ||||
|   | ||||
| @@ -46,9 +46,6 @@ func (gui *Gui) createMenu(opts types.CreateMenuOptions) error { | ||||
|  | ||||
| 	gui.Views.Menu.Title = opts.Title | ||||
| 	gui.Views.Menu.FgColor = theme.GocuiDefaultTextColor | ||||
| 	gui.Views.Menu.SetOnSelectItem(gui.onSelectItemWrapper(func(selectedLine int) error { | ||||
| 		return nil | ||||
| 	})) | ||||
|  | ||||
| 	gui.Views.Tooltip.Wrap = true | ||||
| 	gui.Views.Tooltip.FgColor = theme.GocuiDefaultTextColor | ||||
|   | ||||
| @@ -1,103 +0,0 @@ | ||||
| package gui | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
|  | ||||
| 	"github.com/jesseduffield/lazygit/pkg/gui/keybindings" | ||||
| 	"github.com/jesseduffield/lazygit/pkg/theme" | ||||
| ) | ||||
|  | ||||
| func (gui *Gui) handleOpenSearch(viewName string) error { | ||||
| 	view, err := gui.g.View(viewName) | ||||
| 	if err != nil { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	gui.State.Searching.isSearching = true | ||||
| 	gui.State.Searching.view = view | ||||
|  | ||||
| 	gui.Views.Search.ClearTextArea() | ||||
|  | ||||
| 	if err := gui.c.PushContext(gui.State.Contexts.Search); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (gui *Gui) handleSearch() error { | ||||
| 	gui.State.Searching.searchString = gui.Views.Search.TextArea.GetContent() | ||||
| 	if err := gui.c.PopContext(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	view := gui.State.Searching.view | ||||
| 	if view == nil { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	if err := view.Search(gui.State.Searching.searchString); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (gui *Gui) onSelectItemWrapper(innerFunc func(int) error) func(int, int, int) error { | ||||
| 	keybindingConfig := gui.c.UserConfig.Keybinding | ||||
|  | ||||
| 	return func(y int, index int, total int) error { | ||||
| 		if total == 0 { | ||||
| 			gui.c.SetViewContent( | ||||
| 				gui.Views.Search, | ||||
| 				fmt.Sprintf( | ||||
| 					gui.Tr.NoMatchesFor, | ||||
| 					gui.State.Searching.searchString, | ||||
| 					theme.OptionsFgColor.Sprintf(gui.Tr.ExitSearchMode, keybindings.Label(keybindingConfig.Universal.Return)), | ||||
| 				), | ||||
| 			) | ||||
| 			return nil | ||||
| 		} | ||||
| 		gui.c.SetViewContent( | ||||
| 			gui.Views.Search, | ||||
| 			fmt.Sprintf( | ||||
| 				gui.Tr.MatchesFor, | ||||
| 				gui.State.Searching.searchString, | ||||
| 				index+1, | ||||
| 				total, | ||||
| 				theme.OptionsFgColor.Sprintf( | ||||
| 					gui.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 | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (gui *Gui) onSearchEscape() error { | ||||
| 	gui.State.Searching.isSearching = false | ||||
| 	if gui.State.Searching.view != nil { | ||||
| 		gui.State.Searching.view.ClearSearch() | ||||
| 		gui.State.Searching.view = nil | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (gui *Gui) handleSearchEscape() error { | ||||
| 	if err := gui.onSearchEscape(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if err := gui.c.PopContext(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
| @@ -68,8 +68,6 @@ type IGuiCommon interface { | ||||
| 	Context() IContextMgr | ||||
|  | ||||
| 	ActivateContext(context Context) error | ||||
| 	// enters search mode for the current view | ||||
| 	OpenSearch() | ||||
|  | ||||
| 	GetConfig() config.AppConfigurer | ||||
| 	GetAppState() *config.AppState | ||||
| @@ -251,7 +249,8 @@ type IRepoStateAccessor interface { | ||||
| 	SetCurrentPopupOpts(*CreatePopupPanelOpts) | ||||
| 	GetScreenMode() WindowMaximisation | ||||
| 	SetScreenMode(WindowMaximisation) | ||||
| 	IsSearching() bool | ||||
| 	InSearchPrompt() bool | ||||
| 	GetSearchState() *SearchState | ||||
| 	SetSplitMainPanel(bool) | ||||
| 	GetSplitMainPanel() bool | ||||
| } | ||||
|   | ||||
| @@ -87,6 +87,24 @@ type Context interface { | ||||
| 	HandleRenderToMain() error | ||||
| } | ||||
|  | ||||
| type IFilterableContext interface { | ||||
| 	Context | ||||
|  | ||||
| 	SetFilter(string) | ||||
| 	GetFilter() string | ||||
| 	ClearFilter() | ||||
| 	IsFilterableContext() | ||||
| } | ||||
|  | ||||
| type ISearchableContext interface { | ||||
| 	Context | ||||
|  | ||||
| 	SetSearchString(string) | ||||
| 	GetSearchString() string | ||||
| 	ClearSearchString() | ||||
| 	IsSearchableContext() | ||||
| } | ||||
|  | ||||
| type DiffableContext interface { | ||||
| 	Context | ||||
|  | ||||
| @@ -104,7 +122,6 @@ type IListContext interface { | ||||
|  | ||||
| 	GetList() IList | ||||
|  | ||||
| 	OnSearchSelect(selectedLineIdx int) error | ||||
| 	FocusLine() | ||||
| 	IsListContext() // used for type switch | ||||
| } | ||||
| @@ -211,5 +228,7 @@ type IContextMgr interface { | ||||
| 	IsCurrent(c Context) bool | ||||
| 	ForEach(func(Context)) | ||||
| 	AllList() []IListContext | ||||
| 	AllFilterable() []IFilterableContext | ||||
| 	AllSearchable() []ISearchableContext | ||||
| 	AllPatchExplorer() []IPatchExplorerContext | ||||
| } | ||||
|   | ||||
							
								
								
									
										31
									
								
								pkg/gui/types/search_state.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								pkg/gui/types/search_state.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| package types | ||||
|  | ||||
| type SearchType int | ||||
|  | ||||
| const ( | ||||
| 	SearchTypeNone SearchType = iota | ||||
| 	// searching is where matches are highlighted but the content is not filtered down | ||||
| 	SearchTypeSearch | ||||
| 	// filter is where the list is filtered down to only matches | ||||
| 	SearchTypeFilter | ||||
| ) | ||||
|  | ||||
| // TODO: could we remove this entirely? | ||||
| type SearchState struct { | ||||
| 	Context Context | ||||
| } | ||||
|  | ||||
| func NewSearchState() *SearchState { | ||||
| 	return &SearchState{} | ||||
| } | ||||
|  | ||||
| func (self *SearchState) SearchType() SearchType { | ||||
| 	switch self.Context.(type) { | ||||
| 	case IFilterableContext: | ||||
| 		return SearchTypeFilter | ||||
| 	case ISearchableContext: | ||||
| 		return SearchTypeSearch | ||||
| 	default: | ||||
| 		return SearchTypeNone | ||||
| 	} | ||||
| } | ||||
| @@ -95,6 +95,8 @@ func (gui *Gui) createAllViews() error { | ||||
| 	gui.Views.SearchPrefix.Frame = false | ||||
| 	gui.c.SetViewContent(gui.Views.SearchPrefix, gui.Tr.SearchPrefix) | ||||
|  | ||||
| 	gui.Views.Search.Editor = gocui.EditorFunc(gui.searchEditor) | ||||
|  | ||||
| 	gui.Views.Stash.Title = gui.c.Tr.StashTitle | ||||
|  | ||||
| 	gui.Views.Commits.Title = gui.c.Tr.CommitsTitle | ||||
|   | ||||
| @@ -371,6 +371,7 @@ type TranslationSet struct { | ||||
| 	NextScreenMode                      string | ||||
| 	PrevScreenMode                      string | ||||
| 	StartSearch                         string | ||||
| 	StartFilter                         string | ||||
| 	Panel                               string | ||||
| 	Keybindings                         string | ||||
| 	KeybindingsLegend                   string | ||||
| @@ -536,6 +537,7 @@ type TranslationSet struct { | ||||
| 	MatchesFor                          string | ||||
| 	SearchKeybindings                   string | ||||
| 	SearchPrefix                        string | ||||
| 	FilterPrefix                        string | ||||
| 	ExitSearchMode                      string | ||||
| 	Actions                             Actions | ||||
| 	Bisect                              Bisect | ||||
| @@ -1061,7 +1063,8 @@ func EnglishTranslationSet() TranslationSet { | ||||
| 		ViewResetToUpstreamOptions:          "View upstream reset options", | ||||
| 		NextScreenMode:                      "Next screen mode (normal/half/fullscreen)", | ||||
| 		PrevScreenMode:                      "Prev screen mode", | ||||
| 		StartSearch:                         "Start search", | ||||
| 		StartSearch:                         "Search the current view", | ||||
| 		StartFilter:                         "Filter the current view", | ||||
| 		Panel:                               "Panel", | ||||
| 		KeybindingsLegend:                   "Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b", | ||||
| 		RenameBranch:                        "Rename branch", | ||||
| @@ -1226,6 +1229,7 @@ func EnglishTranslationSet() TranslationSet { | ||||
| 		MatchesFor:                          "matches for '%s' (%d of %d) %s", // lowercase because it's after other text | ||||
| 		SearchKeybindings:                   "%s: Next match, %s: Previous match, %s: Exit search mode", | ||||
| 		SearchPrefix:                        "Search: ", | ||||
| 		FilterPrefix:                        "Filter: ", | ||||
| 		Actions: Actions{ | ||||
| 			// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm) | ||||
| 			CheckoutCommit:                    "Checkout commit", | ||||
|   | ||||
| @@ -113,3 +113,14 @@ func MoveElement[T any](slice []T, from int, to int) []T { | ||||
|  | ||||
| 	return newSlice | ||||
| } | ||||
|  | ||||
| func ValuesAtIndices[T any](slice []T, indices []int) []T { | ||||
| 	result := make([]T, len(indices)) | ||||
| 	for i, index := range indices { | ||||
| 		// gracefully handling the situation where the index is out of bounds | ||||
| 		if index < len(slice) { | ||||
| 			result[i] = slice[index] | ||||
| 		} | ||||
| 	} | ||||
| 	return result | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user