mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
Every one of these did by hand what focusing the list now does on its own: five hand-added scroll requests, and four origin resets that paired a "select the first item" with a "and show the top of the list". The scroll that the commits refresh performed when it found the selected commit at a new index goes too. It is now unconditional for a foreground refresh, and deliberately absent for a background one: when an agent commits in another window, we would rather see the new commits arrive than have the view yank itself back to the commit we had selected. The one origin reset that stays is the one in ReApplyFilter, which runs as part of a refresh and so can't rely on the refresh scrolling. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
330 lines
8.8 KiB
Go
330 lines
8.8 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) error {
|
|
state := self.searchState()
|
|
|
|
state.PrevSearchIndex = -1
|
|
|
|
state.Context = context
|
|
|
|
self.searchPrefixView().SetContent(context.FilterPrefix(self.c.Tr))
|
|
promptView := self.promptView()
|
|
promptView.ClearTextArea()
|
|
self.OnPromptContentChanged("")
|
|
promptView.RenderTextArea()
|
|
|
|
self.c.Context().Push(self.c.Contexts().Search, types.OnFocusOpts{})
|
|
|
|
return self.c.ResetKeybindings()
|
|
}
|
|
|
|
func (self *SearchHelper) OpenSearchPrompt(context types.ISearchableContext) error {
|
|
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{})
|
|
|
|
return self.c.ResetKeybindings()
|
|
}
|
|
|
|
func (self *SearchHelper) DisplayFilterStatus(context types.IFilterableContext) {
|
|
state := self.searchState()
|
|
|
|
state.Context = context
|
|
searchString := context.GetFilter()
|
|
|
|
self.searchPrefixView().SetContent(context.FilterPrefix(self.c.Tr))
|
|
|
|
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() error {
|
|
state := self.searchState()
|
|
if self.promptContent() == "" {
|
|
return self.CancelPrompt()
|
|
}
|
|
|
|
switch state.SearchType() {
|
|
case types.SearchTypeFilter:
|
|
self.ConfirmFilter()
|
|
case types.SearchTypeSearch:
|
|
self.ConfirmSearch()
|
|
case types.SearchTypeNone:
|
|
self.c.Context().Pop()
|
|
}
|
|
|
|
return 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() error {
|
|
self.Cancel()
|
|
|
|
self.c.Context().Pop()
|
|
|
|
return 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:
|
|
context.SetSelection(0)
|
|
context.SetFilter(searchString, self.c.UserConfig().Gui.UseFuzzySearch())
|
|
self.c.PostRefreshUpdate(context)
|
|
case types.ISearchableContext:
|
|
// do nothing
|
|
default:
|
|
// do nothing (shouldn't land here)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|