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:
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()
|
||||
}
|
Reference in New Issue
Block a user