diff --git a/common.go b/common.go index a85b025..d66ee21 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,8 +240,15 @@ func (s *State) SetMultiLineMode(mlmode bool) { s.multiLineMode = mlmode } +// 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 @@ -260,3 +273,22 @@ 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: + if line == "" { + ph = append(ph, s.history...) + } else { + 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..9517284 --- /dev/null +++ b/common_test.go @@ -0,0 +1,65 @@ +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"}, + }, + { + 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) { + 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 }