1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2024-11-30 09:16:47 +02:00

Add search history

Add search history for filterable and searchable views.
This commit is contained in:
Karim Khaleel 2023-08-04 14:01:30 +03:00
parent ab5875c78f
commit edec116ceb
12 changed files with 270 additions and 8 deletions

View File

@ -3,13 +3,15 @@ package context
type FilteredListViewModel[T any] struct {
*FilteredList[T]
*ListViewModel[T]
*SearchHistory
}
func NewFilteredListViewModel[T any](getList func() []T, getFilterFields func(T) []string) *FilteredListViewModel[T] {
filteredList := NewFilteredList(getList, getFilterFields)
self := &FilteredListViewModel[T]{
FilteredList: filteredList,
FilteredList: filteredList,
SearchHistory: NewSearchHistory(),
}
listViewModel := NewListViewModel(filteredList.GetFilteredList)

View File

@ -0,0 +1,20 @@
package context
import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
// Maintains a list of strings that have previously been searched/filtered for
type SearchHistory struct {
history *utils.HistoryBuffer[string]
}
func NewSearchHistory() *SearchHistory {
return &SearchHistory{
history: utils.NewHistoryBuffer[string](1000),
}
}
func (self *SearchHistory) GetSearchHistory() *utils.HistoryBuffer[string] {
return self.history
}

View File

@ -9,6 +9,7 @@ import (
type RemotesContext struct {
*FilteredListViewModel[*models.Remote]
*ListContextTrait
*SearchHistory
}
var (

View File

@ -9,12 +9,16 @@ import (
type SearchTrait struct {
c *ContextCommon
*SearchHistory
searchString string
}
func NewSearchTrait(c *ContextCommon) *SearchTrait {
return &SearchTrait{c: c}
return &SearchTrait{
c: c,
SearchHistory: NewSearchHistory(),
}
}
func (self *SearchTrait) GetSearchString() string {

View File

@ -36,7 +36,7 @@ func (self *SearchHelper) OpenFilterPrompt(context types.IFilterableContext) err
self.searchPrefixView().SetContent(self.c.Tr.FilterPrefix)
promptView := self.promptView()
promptView.ClearTextArea()
promptView.TextArea.TypeString(context.GetFilter())
self.OnPromptContentChanged("")
promptView.RenderTextArea()
if err := self.c.PushContext(self.c.Contexts().Search); err != nil {
@ -49,13 +49,13 @@ func (self *SearchHelper) OpenFilterPrompt(context types.IFilterableContext) err
func (self *SearchHelper) OpenSearchPrompt(context types.ISearchableContext) error {
state := self.searchState()
state.PrevSearchIndex = -1
state.Context = context
searchString := context.GetSearchString()
self.searchPrefixView().SetContent(self.c.Tr.SearchPrefix)
promptView := self.promptView()
promptView.ClearTextArea()
promptView.TextArea.TypeString(searchString)
promptView.RenderTextArea()
if err := self.c.PushContext(self.c.Contexts().Search); err != nil {
@ -125,13 +125,17 @@ func (self *SearchHelper) ConfirmFilter() error {
// We also do this on each keypress but we do it here again just in case
state := self.searchState()
_, ok := state.Context.(types.IFilterableContext)
context, ok := state.Context.(types.IFilterableContext)
if !ok {
self.c.Log.Warnf("Context %s is not filterable", state.Context.GetKey())
return nil
}
self.OnPromptContentChanged(self.promptContent())
filterString := self.promptContent()
if filterString != "" {
context.GetSearchHistory().Push(filterString)
}
return self.c.PopContext()
}
@ -147,6 +151,9 @@ func (self *SearchHelper) ConfirmSearch() error {
searchString := self.promptContent()
context.SetSearchString(searchString)
if searchString != "" {
context.GetSearchHistory().Push(searchString)
}
view := context.GetView()
@ -167,6 +174,26 @@ func (self *SearchHelper) CancelPrompt() error {
return self.c.PopContext()
}
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()

View File

@ -33,6 +33,16 @@ func (self *SearchPromptController) GetKeybindings(opts types.KeybindingsOpts) [
Modifier: gocui.ModNone,
Handler: self.cancel,
},
{
Key: opts.GetKey(opts.Config.Universal.PrevItem),
Modifier: gocui.ModNone,
Handler: self.prevHistory,
},
{
Key: opts.GetKey(opts.Config.Universal.NextItem),
Modifier: gocui.ModNone,
Handler: self.nextHistory,
},
}
}
@ -51,3 +61,13 @@ func (self *SearchPromptController) confirm() error {
func (self *SearchPromptController) cancel() error {
return self.c.Helpers().Search.CancelPrompt()
}
func (self *SearchPromptController) prevHistory() error {
self.c.Helpers().Search.ScrollHistory(1)
return nil
}
func (self *SearchPromptController) nextHistory() error {
self.c.Helpers().Search.ScrollHistory(-1)
return nil
}

View File

@ -4,6 +4,7 @@ import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/patch_exploring"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sasha-s/go-deadlock"
)
@ -87,9 +88,16 @@ type Context interface {
HandleRenderToMain() error
}
type ISearchHistoryContext interface {
Context
GetSearchHistory() *utils.HistoryBuffer[string]
}
type IFilterableContext interface {
Context
IListPanelState
ISearchHistoryContext
SetFilter(string)
GetFilter() string
@ -100,6 +108,7 @@ type IFilterableContext interface {
type ISearchableContext interface {
Context
ISearchHistoryContext
SetSearchString(string)
GetSearchString() string

View File

@ -12,11 +12,12 @@ const (
// TODO: could we remove this entirely?
type SearchState struct {
Context Context
Context Context
PrevSearchIndex int
}
func NewSearchState() *SearchState {
return &SearchState{}
return &SearchState{PrevSearchIndex: -1}
}
func (self *SearchState) SearchType() SearchType {

View File

@ -0,0 +1,77 @@
package filter_and_search
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var FilterSearchHistory = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Navigating search history",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
// populate search history with some values
FilterOrSearch("1").
FilterOrSearch("2").
FilterOrSearch("3").
Press(keys.Universal.StartSearch).
// clear initial search value
Tap(func() {
t.ExpectSearch().Clear()
}).
// test main search history functionality
Tap(func() {
t.Views().Search().
Press(keys.Universal.PrevItem).
Content(Contains("3")).
Press(keys.Universal.PrevItem).
Content(Contains("2")).
Press(keys.Universal.PrevItem).
Content(Contains("1")).
Press(keys.Universal.PrevItem).
Content(Contains("1")).
Press(keys.Universal.NextItem).
Content(Contains("2")).
Press(keys.Universal.NextItem).
Content(Contains("3")).
Press(keys.Universal.NextItem).
Content(Contains("")).
Press(keys.Universal.NextItem).
Content(Contains("")).
Press(keys.Universal.PrevItem).
Content(Contains("3")).
PressEscape()
}).
// test that it resets after you enter and exit a search
Press(keys.Universal.StartSearch).
Tap(func() {
t.Views().Search().
Press(keys.Universal.PrevItem).
Content(Contains("3")).
PressEscape()
})
// test that the histories are separate for each view
t.Views().Commits().
Focus().
FilterOrSearch("a").
FilterOrSearch("b").
FilterOrSearch("c").
Press(keys.Universal.StartSearch).
Tap(func() {
t.ExpectSearch().Clear()
}).
Tap(func() {
t.Views().Search().
Press(keys.Universal.PrevItem).
Content(Contains("c")).
Press(keys.Universal.PrevItem).
Content(Contains("b")).
Press(keys.Universal.PrevItem).
Content(Contains("a"))
})
},
})

View File

@ -118,6 +118,7 @@ var tests = []*components.IntegrationTest{
filter_and_search.FilterFuzzy,
filter_and_search.FilterMenu,
filter_and_search.FilterRemoteBranches,
filter_and_search.FilterSearchHistory,
filter_and_search.NestedFilter,
filter_and_search.NestedFilterTransient,
filter_by_path.CliArg,

View File

@ -0,0 +1,36 @@
package utils
import "fmt"
type HistoryBuffer[T any] struct {
maxSize int
items []T
}
func NewHistoryBuffer[T any](maxSize int) *HistoryBuffer[T] {
return &HistoryBuffer[T]{
maxSize: maxSize,
items: make([]T, 0, maxSize),
}
}
func (self *HistoryBuffer[T]) Push(item T) {
if len(self.items) == self.maxSize {
self.items = self.items[:len(self.items)-1]
}
self.items = append([]T{item}, self.items...)
}
func (self *HistoryBuffer[T]) PeekAt(index int) (T, error) {
var item T
if len(self.items) == 0 {
return item, fmt.Errorf("Buffer is empty")
}
if len(self.items) <= index || index < -1 {
return item, fmt.Errorf("Index out of range")
}
if index == -1 {
return item, nil
}
return self.items[index], nil
}

View File

@ -0,0 +1,64 @@
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewHistoryBuffer(t *testing.T) {
hb := NewHistoryBuffer[int](5)
assert.NotNil(t, hb)
assert.Equal(t, 5, hb.maxSize)
assert.Equal(t, 0, len(hb.items))
}
func TestPush(t *testing.T) {
hb := NewHistoryBuffer[int](3)
hb.Push(1)
hb.Push(2)
hb.Push(3)
hb.Push(4)
assert.Equal(t, 3, len(hb.items))
assert.Equal(t, []int{4, 3, 2}, hb.items)
}
func TestPeekAt(t *testing.T) {
hb := NewHistoryBuffer[int](3)
hb.Push(1)
hb.Push(2)
hb.Push(3)
item, err := hb.PeekAt(0)
assert.Nil(t, err)
assert.Equal(t, 3, item)
item, err = hb.PeekAt(1)
assert.Nil(t, err)
assert.Equal(t, 2, item)
item, err = hb.PeekAt(2)
assert.Nil(t, err)
assert.Equal(t, 1, item)
item, err = hb.PeekAt(-1)
assert.Nil(t, err)
assert.Equal(t, 0, item)
_, err = hb.PeekAt(3)
assert.NotNil(t, err)
assert.Equal(t, "Index out of range", err.Error())
_, err = hb.PeekAt(-2)
assert.NotNil(t, err)
assert.Equal(t, "Index out of range", err.Error())
}
func TestPeekAtEmptyBuffer(t *testing.T) {
hb := NewHistoryBuffer[int](3)
_, err := hb.PeekAt(0)
assert.NotNil(t, err)
assert.Equal(t, "Buffer is empty", err.Error())
}