mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-04-23 12:18:51 +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",
|
"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),
|
formattedKey(config.Universal.Return),
|
||||||
),
|
),
|
||||||
fmt.Sprintf(
|
|
||||||
"To search for a string in your panel, press '%s'",
|
|
||||||
formattedKey(config.Universal.StartSearch),
|
|
||||||
),
|
|
||||||
fmt.Sprintf(
|
fmt.Sprintf(
|
||||||
"You can page through the items of a panel using '%s' and '%s'",
|
"You can page through the items of a panel using '%s' and '%s'",
|
||||||
formattedKey(config.Universal.PrevPage),
|
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 {
|
func (self *ContextMgr) deactivateContext(c types.Context, opts types.OnFocusLostOpts) error {
|
||||||
view, _ := self.gui.c.GocuiGui().View(c.GetViewName())
|
view, _ := self.gui.c.GocuiGui().View(c.GetViewName())
|
||||||
|
|
||||||
if view != nil && view.IsSearching() {
|
if opts.NewContextKey != context.SEARCH_CONTEXT_KEY {
|
||||||
if err := self.gui.onSearchEscape(); err != nil {
|
|
||||||
return err
|
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
|
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()
|
desiredTitle := c.Title()
|
||||||
if desiredTitle != "" {
|
if desiredTitle != "" {
|
||||||
v.Title = desiredTitle
|
v.Title = desiredTitle
|
||||||
@ -326,6 +349,30 @@ func (self *ContextMgr) IsCurrent(c types.Context) bool {
|
|||||||
return self.Current().GetKey() == c.GetKey()
|
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
|
// all list contexts
|
||||||
func (self *ContextMgr) AllList() []types.IListContext {
|
func (self *ContextMgr) AllList() []types.IListContext {
|
||||||
var listContexts []types.IListContext
|
var listContexts []types.IListContext
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type BranchesContext struct {
|
type BranchesContext struct {
|
||||||
*BasicViewModel[*models.Branch]
|
*FilteredListViewModel[*models.Branch]
|
||||||
*ListContextTrait
|
*ListContextTrait
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,11 +17,16 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func NewBranchesContext(c *ContextCommon) *BranchesContext {
|
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 {
|
getDisplayStrings := func(startIdx int, length int) [][]string {
|
||||||
return presentation.GetBranchListDisplayStrings(
|
return presentation.GetBranchListDisplayStrings(
|
||||||
c.Model().Branches,
|
viewModel.GetItems(),
|
||||||
c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL,
|
c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL,
|
||||||
c.Modes().Diffing.Ref,
|
c.Modes().Diffing.Ref,
|
||||||
c.Tr,
|
c.Tr,
|
||||||
@ -30,7 +35,7 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self := &BranchesContext{
|
self := &BranchesContext{
|
||||||
BasicViewModel: viewModel,
|
FilteredListViewModel: viewModel,
|
||||||
ListContextTrait: &ListContextTrait{
|
ListContextTrait: &ListContextTrait{
|
||||||
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
|
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
|
||||||
View: c.Views().Branches,
|
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"
|
import "github.com/jesseduffield/lazygit/pkg/gui/context/traits"
|
||||||
|
|
||||||
type BasicViewModel[T any] struct {
|
type ListViewModel[T any] struct {
|
||||||
*traits.ListCursor
|
*traits.ListCursor
|
||||||
getModel func() []T
|
getModel func() []T
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBasicViewModel[T any](getModel func() []T) *BasicViewModel[T] {
|
func NewListViewModel[T any](getModel func() []T) *ListViewModel[T] {
|
||||||
self := &BasicViewModel[T]{
|
self := &ListViewModel[T]{
|
||||||
getModel: getModel,
|
getModel: getModel,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,11 +17,11 @@ func NewBasicViewModel[T any](getModel func() []T) *BasicViewModel[T] {
|
|||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *BasicViewModel[T]) Len() int {
|
func (self *ListViewModel[T]) Len() int {
|
||||||
return len(self.getModel())
|
return len(self.getModel())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *BasicViewModel[T]) GetSelected() T {
|
func (self *ListViewModel[T]) GetSelected() T {
|
||||||
if self.Len() == 0 {
|
if self.Len() == 0 {
|
||||||
return Zero[T]()
|
return Zero[T]()
|
||||||
}
|
}
|
||||||
@ -29,6 +29,10 @@ func (self *BasicViewModel[T]) GetSelected() T {
|
|||||||
return self.getModel()[self.GetSelectedLineIdx()]
|
return self.getModel()[self.GetSelectedLineIdx()]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *ListViewModel[T]) GetItems() []T {
|
||||||
|
return self.getModel()
|
||||||
|
}
|
||||||
|
|
||||||
func Zero[T any]() T {
|
func Zero[T any]() T {
|
||||||
return *new(T)
|
return *new(T)
|
||||||
}
|
}
|
@ -13,6 +13,7 @@ import (
|
|||||||
type LocalCommitsContext struct {
|
type LocalCommitsContext struct {
|
||||||
*LocalCommitsViewModel
|
*LocalCommitsViewModel
|
||||||
*ListContextTrait
|
*ListContextTrait
|
||||||
|
*SearchTrait
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -57,8 +58,9 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &LocalCommitsContext{
|
ctx := &LocalCommitsContext{
|
||||||
LocalCommitsViewModel: viewModel,
|
LocalCommitsViewModel: viewModel,
|
||||||
|
SearchTrait: NewSearchTrait(c),
|
||||||
ListContextTrait: &ListContextTrait{
|
ListContextTrait: &ListContextTrait{
|
||||||
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
|
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
|
||||||
View: c.Views().Commits,
|
View: c.Views().Commits,
|
||||||
@ -73,6 +75,13 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
|
|||||||
refreshViewportOnChange: true,
|
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 {
|
func (self *LocalCommitsContext) GetSelectedItemId() string {
|
||||||
@ -85,7 +94,7 @@ func (self *LocalCommitsContext) GetSelectedItemId() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LocalCommitsViewModel struct {
|
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 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.
|
// 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 {
|
func NewLocalCommitsViewModel(getModel func() []*models.Commit, c *ContextCommon) *LocalCommitsViewModel {
|
||||||
self := &LocalCommitsViewModel{
|
self := &LocalCommitsViewModel{
|
||||||
BasicViewModel: NewBasicViewModel(getModel),
|
ListViewModel: NewListViewModel(getModel),
|
||||||
limitCommits: true,
|
limitCommits: true,
|
||||||
showWholeGitGraph: c.UserConfig.Git.Log.ShowWholeGraph,
|
showWholeGitGraph: c.UserConfig.Git.Log.ShowWholeGraph,
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,7 @@ func (self *MenuContext) GetSelectedItemId() string {
|
|||||||
type MenuViewModel struct {
|
type MenuViewModel struct {
|
||||||
c *ContextCommon
|
c *ContextCommon
|
||||||
menuItems []*types.MenuItem
|
menuItems []*types.MenuItem
|
||||||
*BasicViewModel[*types.MenuItem]
|
*FilteredListViewModel[*types.MenuItem]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMenuViewModel(c *ContextCommon) *MenuViewModel {
|
func NewMenuViewModel(c *ContextCommon) *MenuViewModel {
|
||||||
@ -65,7 +65,10 @@ func NewMenuViewModel(c *ContextCommon) *MenuViewModel {
|
|||||||
c: c,
|
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
|
return self
|
||||||
}
|
}
|
||||||
@ -76,11 +79,12 @@ func (self *MenuViewModel) SetMenuItems(items []*types.MenuItem) {
|
|||||||
|
|
||||||
// TODO: move into presentation package
|
// TODO: move into presentation package
|
||||||
func (self *MenuViewModel) GetDisplayStrings(_startIdx int, _length int) [][]string {
|
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 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
|
displayStrings := item.LabelColumns
|
||||||
|
|
||||||
if !showKeys {
|
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.Confirm,
|
||||||
self.c.UserConfig.Keybinding.Universal.Select,
|
self.c.UserConfig.Keybinding.Universal.Select,
|
||||||
self.c.UserConfig.Keybinding.Universal.Return,
|
self.c.UserConfig.Keybinding.Universal.Return,
|
||||||
|
self.c.UserConfig.Keybinding.Universal.StartSearch,
|
||||||
}
|
}
|
||||||
keyLabel := keybindings.LabelFromKey(item.Key)
|
keyLabel := keybindings.LabelFromKey(item.Key)
|
||||||
keyStyle := style.FgCyan
|
keyStyle := style.FgCyan
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
type PatchExplorerContext struct {
|
type PatchExplorerContext struct {
|
||||||
*SimpleContext
|
*SimpleContext
|
||||||
|
*SearchTrait
|
||||||
|
|
||||||
state *patch_exploring.State
|
state *patch_exploring.State
|
||||||
viewTrait *ViewTrait
|
viewTrait *ViewTrait
|
||||||
@ -28,7 +29,7 @@ func NewPatchExplorerContext(
|
|||||||
|
|
||||||
c *ContextCommon,
|
c *ContextCommon,
|
||||||
) *PatchExplorerContext {
|
) *PatchExplorerContext {
|
||||||
return &PatchExplorerContext{
|
ctx := &PatchExplorerContext{
|
||||||
state: nil,
|
state: nil,
|
||||||
viewTrait: NewViewTrait(view),
|
viewTrait: NewViewTrait(view),
|
||||||
c: c,
|
c: c,
|
||||||
@ -42,7 +43,18 @@ func NewPatchExplorerContext(
|
|||||||
Focusable: true,
|
Focusable: true,
|
||||||
HighlightOnFocus: 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() {}
|
func (self *PatchExplorerContext) IsPatchExplorerContext() {}
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ReflogCommitsContext struct {
|
type ReflogCommitsContext struct {
|
||||||
*BasicViewModel[*models.Commit]
|
*FilteredListViewModel[*models.Commit]
|
||||||
*ListContextTrait
|
*ListContextTrait
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,11 +19,16 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func NewReflogCommitsContext(c *ContextCommon) *ReflogCommitsContext {
|
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 {
|
getDisplayStrings := func(startIdx int, length int) [][]string {
|
||||||
return presentation.GetReflogCommitListDisplayStrings(
|
return presentation.GetReflogCommitListDisplayStrings(
|
||||||
c.Model().FilteredReflogCommits,
|
viewModel.GetItems(),
|
||||||
c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL,
|
c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL,
|
||||||
c.Modes().CherryPicking.SelectedShaSet(),
|
c.Modes().CherryPicking.SelectedShaSet(),
|
||||||
c.Modes().Diffing.Ref,
|
c.Modes().Diffing.Ref,
|
||||||
@ -35,7 +40,7 @@ func NewReflogCommitsContext(c *ContextCommon) *ReflogCommitsContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &ReflogCommitsContext{
|
return &ReflogCommitsContext{
|
||||||
BasicViewModel: viewModel,
|
FilteredListViewModel: viewModel,
|
||||||
ListContextTrait: &ListContextTrait{
|
ListContextTrait: &ListContextTrait{
|
||||||
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
|
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
|
||||||
View: c.Views().ReflogCommits,
|
View: c.Views().ReflogCommits,
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type RemoteBranchesContext struct {
|
type RemoteBranchesContext struct {
|
||||||
*BasicViewModel[*models.RemoteBranch]
|
*FilteredListViewModel[*models.RemoteBranch]
|
||||||
*ListContextTrait
|
*ListContextTrait
|
||||||
*DynamicTitleBuilder
|
*DynamicTitleBuilder
|
||||||
}
|
}
|
||||||
@ -20,15 +20,20 @@ var (
|
|||||||
func NewRemoteBranchesContext(
|
func NewRemoteBranchesContext(
|
||||||
c *ContextCommon,
|
c *ContextCommon,
|
||||||
) *RemoteBranchesContext {
|
) *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 {
|
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{
|
return &RemoteBranchesContext{
|
||||||
BasicViewModel: viewModel,
|
FilteredListViewModel: viewModel,
|
||||||
DynamicTitleBuilder: NewDynamicTitleBuilder(c.Tr.RemoteBranchesDynamicTitle),
|
DynamicTitleBuilder: NewDynamicTitleBuilder(c.Tr.RemoteBranchesDynamicTitle),
|
||||||
ListContextTrait: &ListContextTrait{
|
ListContextTrait: &ListContextTrait{
|
||||||
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
|
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
|
||||||
View: c.Views().RemoteBranches,
|
View: c.Views().RemoteBranches,
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type RemotesContext struct {
|
type RemotesContext struct {
|
||||||
*BasicViewModel[*models.Remote]
|
*FilteredListViewModel[*models.Remote]
|
||||||
*ListContextTrait
|
*ListContextTrait
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,14 +17,19 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func NewRemotesContext(c *ContextCommon) *RemotesContext {
|
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 {
|
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{
|
return &RemotesContext{
|
||||||
BasicViewModel: viewModel,
|
FilteredListViewModel: viewModel,
|
||||||
ListContextTrait: &ListContextTrait{
|
ListContextTrait: &ListContextTrait{
|
||||||
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
|
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
|
||||||
View: c.Views().Remotes,
|
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 {
|
type StashContext struct {
|
||||||
*BasicViewModel[*models.StashEntry]
|
*FilteredListViewModel[*models.StashEntry]
|
||||||
*ListContextTrait
|
*ListContextTrait
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,14 +19,19 @@ var (
|
|||||||
func NewStashContext(
|
func NewStashContext(
|
||||||
c *ContextCommon,
|
c *ContextCommon,
|
||||||
) *StashContext {
|
) *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 {
|
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{
|
return &StashContext{
|
||||||
BasicViewModel: viewModel,
|
FilteredListViewModel: viewModel,
|
||||||
ListContextTrait: &ListContextTrait{
|
ListContextTrait: &ListContextTrait{
|
||||||
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
|
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
|
||||||
View: c.Views().Stash,
|
View: c.Views().Stash,
|
||||||
|
@ -12,9 +12,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type SubCommitsContext struct {
|
type SubCommitsContext struct {
|
||||||
|
c *ContextCommon
|
||||||
|
|
||||||
*SubCommitsViewModel
|
*SubCommitsViewModel
|
||||||
*ListContextTrait
|
*ListContextTrait
|
||||||
*DynamicTitleBuilder
|
*DynamicTitleBuilder
|
||||||
|
*SearchTrait
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -26,7 +29,7 @@ func NewSubCommitsContext(
|
|||||||
c *ContextCommon,
|
c *ContextCommon,
|
||||||
) *SubCommitsContext {
|
) *SubCommitsContext {
|
||||||
viewModel := &SubCommitsViewModel{
|
viewModel := &SubCommitsViewModel{
|
||||||
BasicViewModel: NewBasicViewModel(
|
ListViewModel: NewListViewModel(
|
||||||
func() []*models.Commit { return c.Model().SubCommits },
|
func() []*models.Commit { return c.Model().SubCommits },
|
||||||
),
|
),
|
||||||
ref: nil,
|
ref: nil,
|
||||||
@ -60,8 +63,10 @@ func NewSubCommitsContext(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &SubCommitsContext{
|
ctx := &SubCommitsContext{
|
||||||
|
c: c,
|
||||||
SubCommitsViewModel: viewModel,
|
SubCommitsViewModel: viewModel,
|
||||||
|
SearchTrait: NewSearchTrait(c),
|
||||||
DynamicTitleBuilder: NewDynamicTitleBuilder(c.Tr.SubCommitsDynamicTitle),
|
DynamicTitleBuilder: NewDynamicTitleBuilder(c.Tr.SubCommitsDynamicTitle),
|
||||||
ListContextTrait: &ListContextTrait{
|
ListContextTrait: &ListContextTrait{
|
||||||
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
|
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
|
||||||
@ -78,12 +83,19 @@ func NewSubCommitsContext(
|
|||||||
refreshViewportOnChange: true,
|
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 {
|
type SubCommitsViewModel struct {
|
||||||
// name of the ref that the sub-commits are shown for
|
// name of the ref that the sub-commits are shown for
|
||||||
ref types.Ref
|
ref types.Ref
|
||||||
*BasicViewModel[*models.Commit]
|
*ListViewModel[*models.Commit]
|
||||||
|
|
||||||
limitCommits bool
|
limitCommits bool
|
||||||
}
|
}
|
||||||
|
@ -7,21 +7,26 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type SubmodulesContext struct {
|
type SubmodulesContext struct {
|
||||||
*BasicViewModel[*models.SubmoduleConfig]
|
*FilteredListViewModel[*models.SubmoduleConfig]
|
||||||
*ListContextTrait
|
*ListContextTrait
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ types.IListContext = (*SubmodulesContext)(nil)
|
var _ types.IListContext = (*SubmodulesContext)(nil)
|
||||||
|
|
||||||
func NewSubmodulesContext(c *ContextCommon) *SubmodulesContext {
|
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 {
|
getDisplayStrings := func(startIdx int, length int) [][]string {
|
||||||
return presentation.GetSubmoduleListDisplayStrings(c.Model().Submodules)
|
return presentation.GetSubmoduleListDisplayStrings(viewModel.GetItems())
|
||||||
}
|
}
|
||||||
|
|
||||||
return &SubmodulesContext{
|
return &SubmodulesContext{
|
||||||
BasicViewModel: viewModel,
|
FilteredListViewModel: viewModel,
|
||||||
ListContextTrait: &ListContextTrait{
|
ListContextTrait: &ListContextTrait{
|
||||||
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
|
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
|
||||||
View: c.Views().Submodules,
|
View: c.Views().Submodules,
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type SuggestionsContext struct {
|
type SuggestionsContext struct {
|
||||||
*BasicViewModel[*types.Suggestion]
|
*ListViewModel[*types.Suggestion]
|
||||||
*ListContextTrait
|
*ListContextTrait
|
||||||
|
|
||||||
State *SuggestionsContextState
|
State *SuggestionsContextState
|
||||||
@ -40,11 +40,11 @@ func NewSuggestionsContext(
|
|||||||
return presentation.GetSuggestionListDisplayStrings(state.Suggestions)
|
return presentation.GetSuggestionListDisplayStrings(state.Suggestions)
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel := NewBasicViewModel(getModel)
|
viewModel := NewListViewModel(getModel)
|
||||||
|
|
||||||
return &SuggestionsContext{
|
return &SuggestionsContext{
|
||||||
State: state,
|
State: state,
|
||||||
BasicViewModel: viewModel,
|
ListViewModel: viewModel,
|
||||||
ListContextTrait: &ListContextTrait{
|
ListContextTrait: &ListContextTrait{
|
||||||
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
|
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
|
||||||
View: c.Views().Suggestions,
|
View: c.Views().Suggestions,
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type TagsContext struct {
|
type TagsContext struct {
|
||||||
*BasicViewModel[*models.Tag]
|
*FilteredListViewModel[*models.Tag]
|
||||||
*ListContextTrait
|
*ListContextTrait
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,14 +19,19 @@ var (
|
|||||||
func NewTagsContext(
|
func NewTagsContext(
|
||||||
c *ContextCommon,
|
c *ContextCommon,
|
||||||
) *TagsContext {
|
) *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 {
|
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{
|
return &TagsContext{
|
||||||
BasicViewModel: viewModel,
|
FilteredListViewModel: viewModel,
|
||||||
ListContextTrait: &ListContextTrait{
|
ListContextTrait: &ListContextTrait{
|
||||||
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
|
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
|
||||||
View: c.Views().Tags,
|
View: c.Views().Tags,
|
||||||
|
@ -99,6 +99,7 @@ func (gui *Gui) resetHelpersAndControllers() {
|
|||||||
modeHelper,
|
modeHelper,
|
||||||
appStatusHelper,
|
appStatusHelper,
|
||||||
),
|
),
|
||||||
|
Search: helpers.NewSearchHelper(helperCommon),
|
||||||
}
|
}
|
||||||
|
|
||||||
gui.CustomCommandsClient = custom_commands.NewClient(
|
gui.CustomCommandsClient = custom_commands.NewClient(
|
||||||
@ -162,6 +163,16 @@ func (gui *Gui) resetHelpersAndControllers() {
|
|||||||
|
|
||||||
sideWindowControllerFactory := controllers.NewSideWindowControllerFactory(common)
|
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
|
// allow for navigating between side window contexts
|
||||||
for _, context := range []types.Context{
|
for _, context := range []types.Context{
|
||||||
gui.State.Contexts.Status,
|
gui.State.Contexts.Status,
|
||||||
@ -323,6 +334,10 @@ func (gui *Gui) resetHelpersAndControllers() {
|
|||||||
suggestionsController,
|
suggestionsController,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
controllers.AttachControllers(gui.State.Contexts.Search,
|
||||||
|
controllers.NewSearchPromptController(common),
|
||||||
|
)
|
||||||
|
|
||||||
controllers.AttachControllers(gui.State.Contexts.Global,
|
controllers.AttachControllers(gui.State.Contexts.Global,
|
||||||
syncController,
|
syncController,
|
||||||
undoController,
|
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() {
|
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
|
offset := 3
|
||||||
panelWidth := self.getPopupPanelWidth()
|
panelWidth := self.getPopupPanelWidth()
|
||||||
x0, y0, x1, y1 := self.getPopupPanelDimensionsForContentHeight(panelWidth, itemCount+offset)
|
x0, y0, x1, y1 := self.getPopupPanelDimensionsForContentHeight(panelWidth, itemCount+offset)
|
||||||
|
@ -46,6 +46,7 @@ type Helpers struct {
|
|||||||
Mode *ModeHelper
|
Mode *ModeHelper
|
||||||
AppStatus *AppStatusHelper
|
AppStatus *AppStatusHelper
|
||||||
WindowArrangement *WindowArrangementHelper
|
WindowArrangement *WindowArrangementHelper
|
||||||
|
Search *SearchHelper
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStubHelpers() *Helpers {
|
func NewStubHelpers() *Helpers {
|
||||||
@ -78,5 +79,6 @@ func NewStubHelpers() *Helpers {
|
|||||||
Mode: &ModeHelper{},
|
Mode: &ModeHelper{},
|
||||||
AppStatus: &AppStatusHelper{},
|
AppStatus: &AppStatusHelper{},
|
||||||
WindowArrangement: &WindowArrangementHelper{},
|
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()
|
self.c.Modes().Filtering.Active()
|
||||||
|
|
||||||
showInfoSection := self.c.UserConfig.Gui.ShowBottomLine ||
|
showInfoSection := self.c.UserConfig.Gui.ShowBottomLine ||
|
||||||
self.c.State().GetRepoState().IsSearching() ||
|
self.c.State().GetRepoState().InSearchPrompt() ||
|
||||||
self.modeHelper.IsAnyModeActive() ||
|
self.modeHelper.IsAnyModeActive() ||
|
||||||
self.appStatusHelper.HasStatus()
|
self.appStatusHelper.HasStatus()
|
||||||
infoSectionSize := 0
|
infoSectionSize := 0
|
||||||
@ -174,11 +174,17 @@ func (self *WindowArrangementHelper) getMidSectionWeights() (int, int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (self *WindowArrangementHelper) infoSectionChildren(informationStr string, appStatus string) []*boxlayout.Box {
|
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{
|
return []*boxlayout.Box{
|
||||||
{
|
{
|
||||||
Window: "searchPrefix",
|
Window: "searchPrefix",
|
||||||
Size: runewidth.StringWidth(self.c.Tr.SearchPrefix),
|
Size: runewidth.StringWidth(prefix),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Window: "search",
|
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.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.ScrollLeft), Handler: self.HandleScrollLeft},
|
||||||
{Tag: "navigation", Key: opts.GetKey(opts.Config.Universal.ScrollRight), Handler: self.HandleScrollRight},
|
{Tag: "navigation", Key: opts.GetKey(opts.Config.Universal.ScrollRight), Handler: self.HandleScrollRight},
|
||||||
{
|
{Tag: "navigation", Key: opts.GetKey(opts.Config.Universal.GotoBottom), Handler: self.HandleGotoBottom, Description: self.c.Tr.GotoBottom},
|
||||||
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",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -693,9 +693,7 @@ func (self *LocalCommitsController) openSearch() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.c.OpenSearch()
|
return self.c.Helpers().Search.OpenSearchPrompt(self.context())
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *LocalCommitsController) gotoBottom() error {
|
func (self *LocalCommitsController) gotoBottom() error {
|
||||||
|
@ -123,12 +123,6 @@ func (self *PatchExplorerController) GetKeybindings(opts types.KeybindingsOpts)
|
|||||||
Key: opts.GetKey(opts.Config.Universal.ScrollRight),
|
Key: opts.GetKey(opts.Config.Universal.ScrollRight),
|
||||||
Handler: self.withRenderAndFocus(self.HandleScrollRight),
|
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),
|
Key: opts.GetKey(opts.Config.Universal.CopyToClipboard),
|
||||||
Handler: self.withLock(self.CopySelectedToClipboard),
|
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
|
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{}
|
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 {
|
func NewFileTreeViewModel(getFiles func() []*models.File, log *logrus.Entry, showTree bool) *FileTreeViewModel {
|
||||||
fileTree := NewFileTree(getFiles, log, showTree)
|
fileTree := NewFileTree(getFiles, log, showTree)
|
||||||
listCursor := traits.NewListCursor(fileTree)
|
listCursor := traits.NewListCursor(fileTree)
|
||||||
|
@ -201,7 +201,7 @@ type GuiRepoState struct {
|
|||||||
SplitMainPanel bool
|
SplitMainPanel bool
|
||||||
LimitCommits bool
|
LimitCommits bool
|
||||||
|
|
||||||
Searching searchingState
|
SearchState *types.SearchState
|
||||||
StartupStage types.StartupStage // Allows us to not load everything at once
|
StartupStage types.StartupStage // Allows us to not load everything at once
|
||||||
|
|
||||||
ContextMgr *ContextMgr
|
ContextMgr *ContextMgr
|
||||||
@ -256,8 +256,12 @@ func (self *GuiRepoState) SetScreenMode(value types.WindowMaximisation) {
|
|||||||
self.ScreenMode = value
|
self.ScreenMode = value
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *GuiRepoState) IsSearching() bool {
|
func (self *GuiRepoState) InSearchPrompt() bool {
|
||||||
return self.Searching.isSearching
|
return self.SearchState.SearchType() != types.SearchTypeNone
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *GuiRepoState) GetSearchState() *types.SearchState {
|
||||||
|
return self.SearchState
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *GuiRepoState) SetSplitMainPanel(value bool) {
|
func (self *GuiRepoState) SetSplitMainPanel(value bool) {
|
||||||
@ -268,12 +272,6 @@ func (self *GuiRepoState) GetSplitMainPanel() bool {
|
|||||||
return self.SplitMainPanel
|
return self.SplitMainPanel
|
||||||
}
|
}
|
||||||
|
|
||||||
type searchingState struct {
|
|
||||||
view *gocui.View
|
|
||||||
isSearching bool
|
|
||||||
searchString string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, reuseState bool) error {
|
func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, reuseState bool) error {
|
||||||
var err error
|
var err error
|
||||||
gui.git, err = commands.NewGitCommand(
|
gui.git, err = commands.NewGitCommand(
|
||||||
@ -358,6 +356,7 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs, reuseState bool) types.
|
|||||||
ContextMgr: NewContextMgr(gui, contextTree),
|
ContextMgr: NewContextMgr(gui, contextTree),
|
||||||
Contexts: contextTree,
|
Contexts: contextTree,
|
||||||
WindowViewNameMap: initialWindowViewNameMap(contextTree),
|
WindowViewNameMap: initialWindowViewNameMap(contextTree),
|
||||||
|
SearchState: types.NewSearchState(),
|
||||||
}
|
}
|
||||||
|
|
||||||
gui.RepoStateMap[Repo(currentDir)] = gui.State
|
gui.RepoStateMap[Repo(currentDir)] = gui.State
|
||||||
@ -584,11 +583,12 @@ func (gui *Gui) Run(startArgs appTypes.StartArgs) error {
|
|||||||
})
|
})
|
||||||
deadlock.Opts.Disable = !gui.Debug
|
deadlock.Opts.Disable = !gui.Debug
|
||||||
|
|
||||||
gui.g.OnSearchEscape = gui.onSearchEscape
|
|
||||||
if err := gui.Config.ReloadUserConfig(); err != nil {
|
if err := gui.Config.ReloadUserConfig(); err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
userConfig := gui.UserConfig
|
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.SearchEscapeKey = keybindings.GetKey(userConfig.Keybinding.Universal.Return)
|
||||||
gui.g.NextSearchMatchKey = keybindings.GetKey(userConfig.Keybinding.Universal.NextMatch)
|
gui.g.NextSearchMatchKey = keybindings.GetKey(userConfig.Keybinding.Universal.NextMatch)
|
||||||
gui.g.PrevSearchMatchKey = keybindings.GetKey(userConfig.Keybinding.Universal.PrevMatch)
|
gui.g.PrevSearchMatchKey = keybindings.GetKey(userConfig.Keybinding.Universal.PrevMatch)
|
||||||
|
@ -128,10 +128,6 @@ func (self *guiCommon) Mutexes() types.Mutexes {
|
|||||||
return self.gui.Mutexes
|
return self.gui.Mutexes
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *guiCommon) OpenSearch() {
|
|
||||||
_ = self.gui.handleOpenSearch(self.gui.currentViewName())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *guiCommon) GocuiGui() *gocui.Gui {
|
func (self *guiCommon) GocuiGui() *gocui.Gui {
|
||||||
return self.gui.g
|
return self.gui.g
|
||||||
}
|
}
|
||||||
|
@ -215,18 +215,6 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi
|
|||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: self.scrollUpSecondary,
|
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",
|
ViewName: "confirmation",
|
||||||
Key: opts.GetKey(opts.Config.Universal.PrevItem),
|
Key: opts.GetKey(opts.Config.Universal.PrevItem),
|
||||||
|
@ -132,20 +132,6 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
view.SelBgColor = theme.GocuiSelectedLineBgColor
|
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()
|
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.Title = opts.Title
|
||||||
gui.Views.Menu.FgColor = theme.GocuiDefaultTextColor
|
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.Wrap = true
|
||||||
gui.Views.Tooltip.FgColor = theme.GocuiDefaultTextColor
|
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
|
Context() IContextMgr
|
||||||
|
|
||||||
ActivateContext(context Context) error
|
ActivateContext(context Context) error
|
||||||
// enters search mode for the current view
|
|
||||||
OpenSearch()
|
|
||||||
|
|
||||||
GetConfig() config.AppConfigurer
|
GetConfig() config.AppConfigurer
|
||||||
GetAppState() *config.AppState
|
GetAppState() *config.AppState
|
||||||
@ -251,7 +249,8 @@ type IRepoStateAccessor interface {
|
|||||||
SetCurrentPopupOpts(*CreatePopupPanelOpts)
|
SetCurrentPopupOpts(*CreatePopupPanelOpts)
|
||||||
GetScreenMode() WindowMaximisation
|
GetScreenMode() WindowMaximisation
|
||||||
SetScreenMode(WindowMaximisation)
|
SetScreenMode(WindowMaximisation)
|
||||||
IsSearching() bool
|
InSearchPrompt() bool
|
||||||
|
GetSearchState() *SearchState
|
||||||
SetSplitMainPanel(bool)
|
SetSplitMainPanel(bool)
|
||||||
GetSplitMainPanel() bool
|
GetSplitMainPanel() bool
|
||||||
}
|
}
|
||||||
|
@ -87,6 +87,24 @@ type Context interface {
|
|||||||
HandleRenderToMain() error
|
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 {
|
type DiffableContext interface {
|
||||||
Context
|
Context
|
||||||
|
|
||||||
@ -104,7 +122,6 @@ type IListContext interface {
|
|||||||
|
|
||||||
GetList() IList
|
GetList() IList
|
||||||
|
|
||||||
OnSearchSelect(selectedLineIdx int) error
|
|
||||||
FocusLine()
|
FocusLine()
|
||||||
IsListContext() // used for type switch
|
IsListContext() // used for type switch
|
||||||
}
|
}
|
||||||
@ -211,5 +228,7 @@ type IContextMgr interface {
|
|||||||
IsCurrent(c Context) bool
|
IsCurrent(c Context) bool
|
||||||
ForEach(func(Context))
|
ForEach(func(Context))
|
||||||
AllList() []IListContext
|
AllList() []IListContext
|
||||||
|
AllFilterable() []IFilterableContext
|
||||||
|
AllSearchable() []ISearchableContext
|
||||||
AllPatchExplorer() []IPatchExplorerContext
|
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.Views.SearchPrefix.Frame = false
|
||||||
gui.c.SetViewContent(gui.Views.SearchPrefix, gui.Tr.SearchPrefix)
|
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.Stash.Title = gui.c.Tr.StashTitle
|
||||||
|
|
||||||
gui.Views.Commits.Title = gui.c.Tr.CommitsTitle
|
gui.Views.Commits.Title = gui.c.Tr.CommitsTitle
|
||||||
|
@ -371,6 +371,7 @@ type TranslationSet struct {
|
|||||||
NextScreenMode string
|
NextScreenMode string
|
||||||
PrevScreenMode string
|
PrevScreenMode string
|
||||||
StartSearch string
|
StartSearch string
|
||||||
|
StartFilter string
|
||||||
Panel string
|
Panel string
|
||||||
Keybindings string
|
Keybindings string
|
||||||
KeybindingsLegend string
|
KeybindingsLegend string
|
||||||
@ -536,6 +537,7 @@ type TranslationSet struct {
|
|||||||
MatchesFor string
|
MatchesFor string
|
||||||
SearchKeybindings string
|
SearchKeybindings string
|
||||||
SearchPrefix string
|
SearchPrefix string
|
||||||
|
FilterPrefix string
|
||||||
ExitSearchMode string
|
ExitSearchMode string
|
||||||
Actions Actions
|
Actions Actions
|
||||||
Bisect Bisect
|
Bisect Bisect
|
||||||
@ -1061,7 +1063,8 @@ func EnglishTranslationSet() TranslationSet {
|
|||||||
ViewResetToUpstreamOptions: "View upstream reset options",
|
ViewResetToUpstreamOptions: "View upstream reset options",
|
||||||
NextScreenMode: "Next screen mode (normal/half/fullscreen)",
|
NextScreenMode: "Next screen mode (normal/half/fullscreen)",
|
||||||
PrevScreenMode: "Prev screen mode",
|
PrevScreenMode: "Prev screen mode",
|
||||||
StartSearch: "Start search",
|
StartSearch: "Search the current view",
|
||||||
|
StartFilter: "Filter the current view",
|
||||||
Panel: "Panel",
|
Panel: "Panel",
|
||||||
KeybindingsLegend: "Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b",
|
KeybindingsLegend: "Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b",
|
||||||
RenameBranch: "Rename branch",
|
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
|
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",
|
SearchKeybindings: "%s: Next match, %s: Previous match, %s: Exit search mode",
|
||||||
SearchPrefix: "Search: ",
|
SearchPrefix: "Search: ",
|
||||||
|
FilterPrefix: "Filter: ",
|
||||||
Actions: Actions{
|
Actions: Actions{
|
||||||
// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
|
// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
|
||||||
CheckoutCommit: "Checkout commit",
|
CheckoutCommit: "Checkout commit",
|
||||||
|
@ -113,3 +113,14 @@ func MoveElement[T any](slice []T, from int, to int) []T {
|
|||||||
|
|
||||||
return newSlice
|
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