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:
parent
ab5875c78f
commit
edec116ceb
@ -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)
|
||||
|
20
pkg/gui/context/history_trait.go
Normal file
20
pkg/gui/context/history_trait.go
Normal 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
|
||||
}
|
@ -9,6 +9,7 @@ import (
|
||||
type RemotesContext struct {
|
||||
*FilteredListViewModel[*models.Remote]
|
||||
*ListContextTrait
|
||||
*SearchHistory
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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"))
|
||||
})
|
||||
},
|
||||
})
|
@ -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,
|
||||
|
36
pkg/utils/history_buffer.go
Normal file
36
pkg/utils/history_buffer.go
Normal 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
|
||||
}
|
64
pkg/utils/history_buffer_test.go
Normal file
64
pkg/utils/history_buffer_test.go
Normal 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())
|
||||
}
|
Loading…
Reference in New Issue
Block a user