mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 23:56:24 -04:00
The only menu that ever asked for it was the keybindings menu, which now filters as you type and doesn't use the prompt at all. That leaves every filterable context with the same prompt, so the whole hook can go, and with it the two implementations that only existed to satisfy it. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
335 lines
8.9 KiB
Go
335 lines
8.9 KiB
Go
package helpers
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/gocui"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
"github.com/jesseduffield/lazygit/pkg/theme"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
)
|
|
|
|
// 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) {
|
|
state := self.searchState()
|
|
|
|
state.PrevSearchIndex = -1
|
|
|
|
state.Context = context
|
|
|
|
self.searchPrefixView().SetContent(self.c.Tr.FilterPrefix)
|
|
promptView := self.promptView()
|
|
promptView.ClearTextArea()
|
|
self.OnPromptContentChanged("")
|
|
promptView.RenderTextArea()
|
|
|
|
self.c.Context().Push(self.c.Contexts().Search, types.OnFocusOpts{})
|
|
|
|
self.c.ResetKeybindings()
|
|
}
|
|
|
|
func (self *SearchHelper) OpenSearchPrompt(context types.ISearchableContext) {
|
|
state := self.searchState()
|
|
|
|
state.PrevSearchIndex = -1
|
|
|
|
state.Context = context
|
|
|
|
self.searchPrefixView().SetContent(self.c.Tr.SearchPrefix)
|
|
promptView := self.promptView()
|
|
promptView.ClearTextArea()
|
|
promptView.RenderTextArea()
|
|
|
|
self.c.Context().Push(self.c.Contexts().Search, types.OnFocusOpts{})
|
|
|
|
self.c.ResetKeybindings()
|
|
}
|
|
|
|
func (self *SearchHelper) DisplayFilterStatus(context types.IFilterableContext) {
|
|
state := self.searchState()
|
|
|
|
state.Context = context
|
|
searchString := context.GetFilter()
|
|
|
|
self.searchPrefixView().SetContent(self.c.Tr.FilterPrefix)
|
|
|
|
promptView := self.promptView()
|
|
keybindingConfig := self.c.UserConfig().Keybinding
|
|
promptView.SetContent(fmt.Sprintf("matches for '%s' ", searchString) + theme.OptionsFgColor.Sprintf(self.c.Tr.ExitTextFilterMode, keybindingConfig.Universal.Return))
|
|
}
|
|
|
|
func (self *SearchHelper) DisplaySearchStatus(context types.ISearchableContext) {
|
|
state := self.searchState()
|
|
|
|
state.Context = context
|
|
|
|
self.searchPrefixView().SetContent(self.c.Tr.SearchPrefix)
|
|
index, totalCount := context.GetView().GetSearchStatus()
|
|
context.RenderSearchStatus(index, totalCount)
|
|
}
|
|
|
|
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() {
|
|
state := self.searchState()
|
|
if self.promptContent() == "" {
|
|
self.CancelPrompt()
|
|
return
|
|
}
|
|
|
|
switch state.SearchType() {
|
|
case types.SearchTypeFilter:
|
|
self.ConfirmFilter()
|
|
case types.SearchTypeSearch:
|
|
self.ConfirmSearch()
|
|
case types.SearchTypeNone:
|
|
self.c.Context().Pop()
|
|
}
|
|
|
|
self.c.ResetKeybindings()
|
|
}
|
|
|
|
func (self *SearchHelper) ConfirmFilter() {
|
|
// 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
|
|
}
|
|
|
|
self.OnPromptContentChanged(self.promptContent())
|
|
filterString := self.promptContent()
|
|
if filterString != "" {
|
|
context.GetSearchHistory().Push(filterString)
|
|
}
|
|
|
|
self.c.Context().Pop()
|
|
}
|
|
|
|
func (self *SearchHelper) ConfirmSearch() {
|
|
state := self.searchState()
|
|
|
|
context, ok := state.Context.(types.ISearchableContext)
|
|
if !ok {
|
|
self.c.Log.Warnf("Context %s is searchable", state.Context.GetKey())
|
|
return
|
|
}
|
|
|
|
searchString := self.promptContent()
|
|
context.SetSearchString(searchString)
|
|
if searchString != "" {
|
|
context.GetSearchHistory().Push(searchString)
|
|
}
|
|
|
|
self.c.Context().Pop()
|
|
|
|
context.GetView().Search(searchString, modelSearchResults(context))
|
|
}
|
|
|
|
func modelSearchResults(context types.ISearchableContext) []gocui.SearchPosition {
|
|
searchString := context.GetSearchString()
|
|
|
|
var normalizedSearchStr string
|
|
// if we have any uppercase characters we'll do a case-sensitive search
|
|
caseSensitive := utils.ContainsUppercase(searchString)
|
|
if caseSensitive {
|
|
normalizedSearchStr = searchString
|
|
} else {
|
|
normalizedSearchStr = strings.ToLower(searchString)
|
|
}
|
|
|
|
return context.ModelSearchResults(normalizedSearchStr, caseSensitive)
|
|
}
|
|
|
|
func (self *SearchHelper) CancelPrompt() {
|
|
self.Cancel()
|
|
|
|
self.c.Context().Pop()
|
|
|
|
self.c.ResetKeybindings()
|
|
}
|
|
|
|
func (self *SearchHelper) ScrollHistory(scrollIncrement int) {
|
|
state := self.searchState()
|
|
|
|
context, ok := state.Context.(types.ISearchHistoryContext)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
states := context.GetSearchHistory()
|
|
|
|
if val, err := states.PeekAt(state.PrevSearchIndex + scrollIncrement); err == nil {
|
|
state.PrevSearchIndex += scrollIncrement
|
|
promptView := self.promptView()
|
|
promptView.ClearTextArea()
|
|
promptView.TextArea.TypeString(val)
|
|
promptView.RenderTextArea()
|
|
self.OnPromptContentChanged(val)
|
|
}
|
|
}
|
|
|
|
func (self *SearchHelper) Cancel() {
|
|
state := self.searchState()
|
|
|
|
switch context := state.Context.(type) {
|
|
case types.IFilterableContext:
|
|
context.ClearFilter()
|
|
self.c.PostRefreshUpdate(context)
|
|
case types.ISearchableContext:
|
|
context.ClearSearchString()
|
|
context.GetView().ClearSearch()
|
|
default:
|
|
// do nothing
|
|
}
|
|
|
|
self.HidePrompt()
|
|
}
|
|
|
|
func (self *SearchHelper) OnPromptContentChanged(searchString string) {
|
|
state := self.searchState()
|
|
switch context := state.Context.(type) {
|
|
case types.IFilterableContext:
|
|
self.ApplyFilter(context, searchString)
|
|
case types.ISearchableContext:
|
|
// do nothing
|
|
default:
|
|
// do nothing (shouldn't land here)
|
|
}
|
|
}
|
|
|
|
func (self *SearchHelper) ApplyFilter(context types.IFilterableContext, filter string) {
|
|
context.SetSelection(0)
|
|
context.SetFilter(filter, self.c.UserConfig().Gui.UseFuzzySearch())
|
|
self.c.PostRefreshUpdate(context)
|
|
}
|
|
|
|
func (self *SearchHelper) ReApplyFilter(context types.Context) {
|
|
filterableContext, ok := context.(types.IFilterableContext)
|
|
if ok {
|
|
state := self.searchState()
|
|
if context == state.Context && self.c.Context().Current().GetKey() == self.c.Contexts().Search.GetKey() {
|
|
filterableContext.SetSelection(0)
|
|
// This runs as part of a refresh, and a refresh that no user action
|
|
// is behind keeps the scroll position, which would leave the view
|
|
// scrolled somewhere the filtered list no longer has anything at.
|
|
filterableContext.GetView().SetOriginY(0)
|
|
}
|
|
filterableContext.ReApplyFilter(self.c.UserConfig().Gui.UseFuzzySearch())
|
|
}
|
|
}
|
|
|
|
func (self *SearchHelper) ReApplySearch(ctx types.Context) {
|
|
// Reapply the search if the model has changed. This is needed for contexts
|
|
// that use the model for searching, to pass the new model search positions
|
|
// to the view.
|
|
searchableContext, ok := ctx.(types.ISearchableContext)
|
|
if ok {
|
|
ctx.GetView().UpdateSearchResults(searchableContext.GetSearchString(), modelSearchResults(searchableContext))
|
|
|
|
state := self.searchState()
|
|
if ctx == state.Context {
|
|
// Re-render the "x of y" search status, unless the search prompt is
|
|
// open for typing.
|
|
if self.c.Context().Current().GetKey() != context.SEARCH_CONTEXT_KEY {
|
|
self.RenderSearchStatus(searchableContext)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (self *SearchHelper) RenderSearchStatus(c types.Context) {
|
|
if c.GetKey() == context.SEARCH_CONTEXT_KEY {
|
|
return
|
|
}
|
|
|
|
if searchableContext, ok := c.(types.ISearchableContext); ok {
|
|
if searchableContext.IsSearching() {
|
|
self.setSearchingFrameColor()
|
|
self.DisplaySearchStatus(searchableContext)
|
|
return
|
|
}
|
|
}
|
|
if filterableContext, ok := c.(types.IFilterableContext); ok {
|
|
if filterableContext.IsFiltering() {
|
|
self.setSearchingFrameColor()
|
|
self.DisplayFilterStatus(filterableContext)
|
|
return
|
|
}
|
|
}
|
|
|
|
self.HidePrompt()
|
|
}
|
|
|
|
func (self *SearchHelper) CancelSearchIfSearching(c types.Context) {
|
|
if searchableContext, ok := c.(types.ISearchableContext); ok {
|
|
view := searchableContext.GetView()
|
|
if view != nil && view.IsSearching() {
|
|
view.ClearSearch()
|
|
searchableContext.ClearSearchString()
|
|
self.Cancel()
|
|
}
|
|
return
|
|
}
|
|
|
|
if filterableContext, ok := c.(types.IFilterableContext); ok {
|
|
if filterableContext.IsFiltering() {
|
|
filterableContext.ClearFilter()
|
|
self.Cancel()
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
func (self *SearchHelper) HidePrompt() {
|
|
self.setNonSearchingFrameColor()
|
|
|
|
state := self.searchState()
|
|
state.Context = nil
|
|
}
|
|
|
|
func (self *SearchHelper) setSearchingFrameColor() {
|
|
self.c.GocuiGui().SelFgColor = theme.SearchingActiveBorderColor
|
|
self.c.GocuiGui().SelFrameColor = theme.SearchingActiveBorderColor
|
|
}
|
|
|
|
func (self *SearchHelper) setNonSearchingFrameColor() {
|
|
self.c.GocuiGui().SelFgColor = theme.ActiveBorderColor
|
|
self.c.GocuiGui().SelFrameColor = theme.ActiveBorderColor
|
|
}
|