From ebd4b67100e7842fc6a13a9da8d108e9a2fbaa07 Mon Sep 17 00:00:00 2001 From: Anthony Alves Date: Tue, 20 Jun 2023 19:21:38 -0400 Subject: [PATCH 1/4] support pattern mode for history searching --- common.go | 47 ++++++++++++++++++++++++++++++++++++---------- common_test.go | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++ line.go | 8 ++++---- 3 files changed, 92 insertions(+), 14 deletions(-) create mode 100644 common_test.go diff --git a/common.go b/common.go index a85b025..82ec3d2 100644 --- a/common.go +++ b/common.go @@ -22,6 +22,7 @@ type commonState struct { inputRedirected bool history []string historyMutex sync.RWMutex + historyMode HistoryMode completer WordCompleter columns int killRing *ring.Ring @@ -52,6 +53,21 @@ const ( TabPrints ) +// HistoryMode defines the type of matching is used when searching +// through previously entered items. +type HistoryMode int + +const ( + // HistoryModePrefix will match a given item against items in the history + // by history items' prefix using the strings.HasPrefix function. + HistoryModePrefix HistoryMode = iota + + // HistoryModePattern will match a given item against items in the history + // by substring matching. History items matching the given patter will + // be returned. + HistoryModePattern +) + // ErrPromptAborted is returned from Prompt or PasswordPrompt when the user presses Ctrl-C // if SetCtrlCAborts(true) has been called on the State var ErrPromptAborted = errors.New("prompt aborted") @@ -152,16 +168,6 @@ func (s *State) ClearHistory() { s.history = nil } -// Returns the history lines starting with prefix -func (s *State) getHistoryByPrefix(prefix string) (ph []string) { - for _, h := range s.history { - if strings.HasPrefix(h, prefix) { - ph = append(ph, h) - } - } - return -} - // Returns the history lines matching the intelligent search func (s *State) getHistoryByPattern(pattern string) (ph []string, pos []int) { if pattern == "" { @@ -234,6 +240,12 @@ func (s *State) SetMultiLineMode(mlmode bool) { s.multiLineMode = mlmode } +// SetHistoryMode sets the pattern behavior when searching through the history. The default HistoryModePrefix where +// returned history items are matched by the string prefix. +func (s *State) SetHistoryMode(mode HistoryMode) { + s.historyMode = mode +} + // ShouldRestart is passed the error generated by readNext and returns true if // the the read should be restarted or false if the error should be returned. type ShouldRestart func(err error) bool @@ -260,3 +272,18 @@ func (s *State) promptUnsupported(p string) (string, error) { } return string(linebuf), nil } + +func (s *State) getHistory(line string) (ph []string) { + switch s.historyMode { + case HistoryModePattern: + ph, _ = s.getHistoryByPattern(line) + return ph + default: + for _, h := range s.history { + if strings.HasPrefix(h, line) { + ph = append(ph, h) + } + } + return ph + } +} diff --git a/common_test.go b/common_test.go new file mode 100644 index 0000000..7a21e5c --- /dev/null +++ b/common_test.go @@ -0,0 +1,51 @@ +package liner + +import ( + "reflect" + "testing" +) + +func TestState_getHistory(t *testing.T) { + tests := []struct { + name string + historyMode HistoryMode + line string + history []string + want []string + }{ + { + name: "no specified mode uses default prefix matching mode", + line: "foo", + history: []string{"food", "foot", "tool"}, + want: []string{"food", "foot"}, + }, + { + name: "explicit prefix mode matches", + line: "foo", + historyMode: HistoryModePrefix, + history: []string{"food", "foot", "tool"}, + want: []string{"food", "foot"}, + }, + { + name: "pattern mode matches history substrings", + line: "oo", + historyMode: HistoryModePattern, + history: []string{"food", "foot", "tool"}, + want: []string{"food", "foot", "tool"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := NewLiner() + s.SetHistoryMode(tt.historyMode) + for _, line := range tt.history { + s.AppendHistory(line) + } + + got := s.getHistory(tt.line) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getHistory() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/line.go b/line.go index 417bb90..06d1ff0 100644 --- a/line.go +++ b/line.go @@ -728,7 +728,7 @@ mainLoop: case ctrlP: // up historyAction = true if historyStale { - historyPrefix = s.getHistoryByPrefix(string(line)) + historyPrefix = s.getHistory(string(line)) historyPos = len(historyPrefix) historyStale = false } @@ -746,7 +746,7 @@ mainLoop: case ctrlN: // down historyAction = true if historyStale { - historyPrefix = s.getHistoryByPrefix(string(line)) + historyPrefix = s.getHistory(string(line)) historyPos = len(historyPrefix) historyStale = false } @@ -913,7 +913,7 @@ mainLoop: case up: historyAction = true if historyStale { - historyPrefix = s.getHistoryByPrefix(string(line)) + historyPrefix = s.getHistory(string(line)) historyPos = len(historyPrefix) historyStale = false } @@ -930,7 +930,7 @@ mainLoop: case down: historyAction = true if historyStale { - historyPrefix = s.getHistoryByPrefix(string(line)) + historyPrefix = s.getHistory(string(line)) historyPos = len(historyPrefix) historyStale = false } From 0128c3d1ff06f2993e2ca2eaebc3569959c55074 Mon Sep 17 00:00:00 2001 From: Anthony Alves Date: Tue, 20 Jun 2023 19:35:50 -0400 Subject: [PATCH 2/4] update comments --- common.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/common.go b/common.go index 82ec3d2..287a266 100644 --- a/common.go +++ b/common.go @@ -240,14 +240,15 @@ func (s *State) SetMultiLineMode(mlmode bool) { s.multiLineMode = mlmode } -// SetHistoryMode sets the pattern behavior when searching through the history. The default HistoryModePrefix where -// returned history items are matched by the string prefix. +// SetHistoryMode sets the pattern behavior when searching through the history. +// The default is HistoryModePrefix; where returned history items are matched +// by the string prefix of a history item. func (s *State) SetHistoryMode(mode HistoryMode) { s.historyMode = mode } // ShouldRestart is passed the error generated by readNext and returns true if -// the the read should be restarted or false if the error should be returned. +// the read should be restarted or false if the error should be returned. type ShouldRestart func(err error) bool // SetShouldRestart sets the restart function that Liner will call to determine From fc3bff8c49d33af8be94f843c84c5724be6acc75 Mon Sep 17 00:00:00 2001 From: Anthony Alves Date: Fri, 23 Jun 2023 10:03:12 -0400 Subject: [PATCH 3/4] return full history when substring searching on an empty string --- common.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common.go b/common.go index 287a266..d66ee21 100644 --- a/common.go +++ b/common.go @@ -277,7 +277,11 @@ func (s *State) promptUnsupported(p string) (string, error) { func (s *State) getHistory(line string) (ph []string) { switch s.historyMode { case HistoryModePattern: - ph, _ = s.getHistoryByPattern(line) + if line == "" { + ph = append(ph, s.history...) + } else { + ph, _ = s.getHistoryByPattern(line) + } return ph default: for _, h := range s.history { From 0164432369ae6db9da4ec628e470568ebe7e68c7 Mon Sep 17 00:00:00 2001 From: Anthony Alves Date: Fri, 23 Jun 2023 10:09:14 -0400 Subject: [PATCH 4/4] add more tests --- common_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/common_test.go b/common_test.go index 7a21e5c..9517284 100644 --- a/common_test.go +++ b/common_test.go @@ -33,6 +33,20 @@ func TestState_getHistory(t *testing.T) { history: []string{"food", "foot", "tool"}, want: []string{"food", "foot", "tool"}, }, + { + name: "empty string with pattern mode matches whole history", + line: "", + historyMode: HistoryModePattern, + history: []string{"food", "foot", "tool"}, + want: []string{"food", "foot", "tool"}, + }, + { + name: "empty string with prefix mode matches whole history", + line: "", + historyMode: HistoryModePrefix, + history: []string{"food", "foot", "tool"}, + want: []string{"food", "foot", "tool"}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {