summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--pkg/gui/context/filtered_list_view_model.go4
-rw-r--r--pkg/gui/context/history_trait.go20
-rw-r--r--pkg/gui/context/remotes_context.go1
-rw-r--r--pkg/gui/context/search_trait.go6
-rw-r--r--pkg/gui/controllers/helpers/search_helper.go35
-rw-r--r--pkg/gui/controllers/search_prompt_controller.go20
-rw-r--r--pkg/gui/types/context.go9
-rw-r--r--pkg/gui/types/search_state.go5
-rw-r--r--pkg/integration/tests/filter_and_search/filter_search_history.go77
-rw-r--r--pkg/integration/tests/test_list.go1
-rw-r--r--pkg/utils/history_buffer.go36
-rw-r--r--pkg/utils/history_buffer_test.go64
12 files changed, 270 insertions, 8 deletions
diff --git a/pkg/gui/context/filtered_list_view_model.go b/pkg/gui/context/filtered_list_view_model.go
index 77f6e1174..1e649550a 100644
--- a/pkg/gui/context/filtered_list_view_model.go
+++ b/pkg/gui/context/filtered_list_view_model.go
@@ -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)
diff --git a/pkg/gui/context/history_trait.go b/pkg/gui/context/history_trait.go
new file mode 100644
index 000000000..f850a3b77
--- /dev/null
+++ b/pkg/gui/context/history_trait.go
@@ -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
+}
diff --git a/pkg/gui/context/remotes_context.go b/pkg/gui/context/remotes_context.go
index f5e2a97ab..43da46576 100644
--- a/pkg/gui/context/remotes_context.go
+++ b/pkg/gui/context/remotes_context.go
@@ -9,6 +9,7 @@ import (
type RemotesContext struct {
*FilteredListViewModel[*models.Remote]
*ListContextTrait
+ *SearchHistory
}
var (
diff --git a/pkg/gui/context/search_trait.go b/pkg/gui/context/search_trait.go
index fad68d794..264c8217d 100644
--- a/pkg/gui/context/search_trait.go
+++ b/pkg/gui/context/search_trait.go
@@ -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 {
diff --git a/pkg/gui/controllers/helpers/search_helper.go b/pkg/gui/controllers/helpers/search_helper.go
index b244f20e4..8764337b1 100644
--- a/pkg/gui/controllers/helpers/search_helper.go
+++ b/pkg/gui/controllers/helpers/search_helper.go
@@ -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()
diff --git a/pkg/gui/controllers/search_prompt_controller.go b/pkg/gui/controllers/search_prompt_controller.go
index 2326ed1c1..014edd094 100644
--- a/pkg/gui/controllers/search_prompt_controller.go
+++ b/pkg/gui/controllers/search_prompt_controller.go
@@ -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
+}
diff --git a/pkg/gui/types/context.go b/pkg/gui/types/context.go
index dca5b042c..e06138a99 100644
--- a/pkg/gui/types/context.go
+++ b/pkg/gui/types/context.go
@@ -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
diff --git a/pkg/gui/types/search_state.go b/pkg/gui/types/search_state.go
index 9b24af095..af806f2c3 100644
--- a/pkg/gui/types/search_state.go
+++ b/pkg/gui/types/search_state.go
@@ -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 {
diff --git a/pkg/integration/tests/filter_and_search/filter_search_history.go b/pkg/integration/tests/filter_and_search/filter_search_history.go
new file mode 100644
index 000000000..1b906319f
--- /dev/null
+++ b/pkg/integration/tests/filter_and_search/filter_search_history.go
@@ -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"))
+ })
+ },
+})
diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go
index f71a3981e..41d58b418 100644
--- a/pkg/integration/tests/test_list.go
+++ b/pkg/integration/tests/test_list.go
@@ -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,
diff --git a/pkg/utils/history_buffer.go b/pkg/utils/history_buffer.go
new file mode 100644
index 000000000..73c33cb82
--- /dev/null
+++ b/pkg/utils/history_buffer.go
@@ -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
+}
diff --git a/pkg/utils/history_buffer_test.go b/pkg/utils/history_buffer_test.go
new file mode 100644
index 000000000..51644d42d
--- /dev/null
+++ b/pkg/utils/history_buffer_test.go
@@ -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())
+}