mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-02-09 13:47:11 +02:00
Introduce filtered list view model
We're going to start supporting filtering of list views
This commit is contained in:
parent
fd861826bc
commit
a9e2c8129f
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user