1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-07-30 03:23:08 +03:00

Introduce filtered list view model

We're going to start supporting filtering of list views
This commit is contained in:
Jesse Duffield
2023-05-27 14:14:43 +10:00
parent fd861826bc
commit a9e2c8129f
43 changed files with 798 additions and 232 deletions

View 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)
}

View File

@ -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)

View File

@ -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{},
}
}

View 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)
}
}

View File

@ -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",

View File

@ -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},
}
}

View File

@ -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 {

View File

@ -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),

View 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)
}

View 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()
}