From ffea66357aafaa06c1d32ff9b96130b77aeb9fe0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 6 Nov 2022 20:52:26 +0530 Subject: [PATCH] Start work on incremental history search --- tools/tui/readline/actions.go | 22 +++++- tools/tui/readline/api.go | 32 +++++---- tools/tui/readline/draw.go | 6 +- tools/tui/readline/history.go | 127 +++++++++++++++++++++++++++++++++- tools/tui/readline/keys.go | 8 +++ 5 files changed, 176 insertions(+), 19 deletions(-) diff --git a/tools/tui/readline/actions.go b/tools/tui/readline/actions.go index 3732c20a6..1fdde1bc4 100644 --- a/tools/tui/readline/actions.go +++ b/tools/tui/readline/actions.go @@ -647,10 +647,30 @@ func (self *Readline) perform_action(ac Action, repeat_count uint) error { self.loop.QueueWriteString("\r\n") self.ResetText() return nil + case ActionHistoryIncrementalSearchForwards: + if self.history_search == nil { + self.create_history_search(false, repeat_count) + return nil + } + if self.next_history_search(false, repeat_count) { + return nil + } + case ActionHistoryIncrementalSearchBackwards: + if self.history_search == nil { + self.create_history_search(true, repeat_count) + return nil + } + if self.next_history_search(true, repeat_count) { + return nil + } case ActionAddText: text := strings.Repeat(self.text_to_be_added, int(repeat_count)) self.text_to_be_added = "" - self.add_text(text) + if self.history_search != nil { + self.add_text_to_history_search(text) + } else { + self.add_text(text) + } return nil } return ErrCouldNotPerformAction diff --git a/tools/tui/readline/api.go b/tools/tui/readline/api.go index 0834a10d3..d026e90ae 100644 --- a/tools/tui/readline/api.go +++ b/tools/tui/readline/api.go @@ -59,6 +59,8 @@ const ( ActionHistoryPrevious ActionHistoryFirst ActionHistoryLast + ActionHistoryIncrementalSearchBackwards + ActionHistoryIncrementalSearchForwards ActionClearScreen ActionAddText ActionAbortCurrentLine @@ -132,7 +134,7 @@ type Prompt struct { } type Readline struct { - prompt, continuation_prompt, reverse_search_prompt, forward_search_prompt Prompt + prompt, continuation_prompt Prompt mark_prompts bool loop *loop.Loop @@ -152,11 +154,23 @@ type Readline struct { bracketed_paste_buffer strings.Builder last_action Action history_matches *HistoryMatches + history_search *HistorySearch keyboard_state KeyboardState fmt_ctx *markup.Context text_to_be_added string } +func (self *Readline) make_prompt(text string, is_secondary bool) Prompt { + if self.mark_prompts { + m := PROMPT_MARK + "A" + if is_secondary { + m += ";k=s" + } + text = m + ST + text + } + return Prompt{Text: text, Length: wcswidth.Stringwidth(text)} +} + func New(loop *loop.Loop, r RlInit) *Readline { hc := r.HistoryCount if hc == 0 { @@ -166,17 +180,7 @@ func New(loop *loop.Loop, r RlInit) *Readline { mark_prompts: !r.DontMarkPrompts, fmt_ctx: markup.New(true), loop: loop, lines: []string{""}, history: NewHistory(r.HistoryPath, hc), kill_ring: kill_ring{items: list.New().Init()}, } - make_prompt := func(text string, is_secondary bool) Prompt { - if ans.mark_prompts { - m := PROMPT_MARK + "A" - if is_secondary { - m += ";k=s" - } - text = m + ST + text - } - return Prompt{Text: text, Length: wcswidth.Stringwidth(text)} - } - ans.prompt = make_prompt(r.Prompt, false) + ans.prompt = ans.make_prompt(r.Prompt, false) t := "" if r.ContinuationPrompt != "" || !r.EmptyContinuationPrompt { t = r.ContinuationPrompt @@ -184,9 +188,7 @@ func New(loop *loop.Loop, r RlInit) *Readline { t = ans.fmt_ctx.Yellow(">") + " " } } - ans.continuation_prompt = make_prompt(t, true) - ans.reverse_search_prompt = make_prompt(ans.fmt_ctx.Blue("?")+": ", false) - ans.forward_search_prompt = make_prompt(ans.fmt_ctx.Cyan("/")+": ", false) + ans.continuation_prompt = ans.make_prompt(t, true) return ans } diff --git a/tools/tui/readline/draw.go b/tools/tui/readline/draw.go index 69651a627..237e27205 100644 --- a/tools/tui/readline/draw.go +++ b/tools/tui/readline/draw.go @@ -38,10 +38,12 @@ func (self *Readline) format_arg_prompt(cna string) string { func (self *Readline) prompt_for_line_number(i int) Prompt { is_line_with_cursor := i == self.cursor.Y if is_line_with_cursor && self.keyboard_state.current_numeric_argument != "" { - text := self.format_arg_prompt(self.keyboard_state.current_numeric_argument) - return Prompt{Text: text, Length: wcswidth.Stringwidth(text)} + return self.make_prompt(self.format_arg_prompt(self.keyboard_state.current_numeric_argument), i > 0) } if i == 0 { + if self.history_search != nil { + return self.make_prompt(self.history_search_prompt(), i > 0) + } return self.prompt } return self.continuation_prompt diff --git a/tools/tui/readline/history.go b/tools/tui/readline/history.go index c182c11ae..bab576545 100644 --- a/tools/tui/readline/history.go +++ b/tools/tui/readline/history.go @@ -11,6 +11,7 @@ import ( "time" "kitty/tools/utils" + "kitty/tools/wcswidth" ) var _ = fmt.Print @@ -29,6 +30,16 @@ type HistoryMatches struct { current_idx int } +type HistorySearch struct { + query string + tokens []string + items []*HistoryItem + current_idx int + backwards bool + original_lines []string + original_cursor Position +} + type History struct { file_path string file *os.File @@ -124,7 +135,7 @@ func (self *History) Read() { } var items []HistoryItem err = json.Unmarshal(data, &items) - if err != nil { + if err == nil { self.merge_items(items...) } } @@ -198,3 +209,117 @@ func (self *HistoryMatches) next(num uint) (ans *HistoryItem) { } return } + +func (self *Readline) create_history_search(backwards bool, num uint) { + self.history_search = &HistorySearch{backwards: backwards, original_lines: self.lines, original_cursor: self.cursor} + self.markup_history_search() +} + +func (self *Readline) end_history_search(accept bool) { + self.cursor = Position{} + if accept && self.history_search.current_idx < len(self.history_search.items) { + self.lines = utils.Splitlines(self.history_search.items[self.history_search.current_idx].Cmd) + self.cursor.Y = len(self.lines) - 1 + self.cursor.X = len(self.lines[self.cursor.Y]) + } else { + self.lines = self.history_search.original_lines + self.cursor = self.history_search.original_cursor + } + self.cursor = *self.ensure_position_in_bounds(&self.cursor) +} + +func (self *Readline) markup_history_search() { + if len(self.history_search.items) == 0 { + if len(self.history_search.tokens) == 0 { + self.lines = []string{""} + } else { + self.lines = []string{"No matches for: " + self.fmt_ctx.BrightRed(self.history_search.query)} + } + self.cursor = Position{X: wcswidth.Stringwidth(self.lines[0])} + return + } + lines := utils.Splitlines(self.history_search.items[self.history_search.current_idx].Cmd) + cursor := Position{Y: len(lines)} + for _, tok := range self.history_search.tokens { + for i, line := range lines { + if idx := strings.Index(line, tok); idx > -1 { + lines[i] = line[:idx] + self.fmt_ctx.Green(tok) + line[idx+len(tok):] + q := Position{Y: i, X: idx} + if q.Less(cursor) { + cursor = q + } + break + } + } + } + self.lines = lines + self.cursor = *self.ensure_position_in_bounds(&cursor) +} + +func (self *Readline) add_text_to_history_search(text string) { + self.history_search.query += text + self.history_search.tokens = strings.Split(self.history_search.query, " ") + var current_item *HistoryItem + if len(self.history_search.items) > 0 { + current_item = self.history_search.items[self.history_search.current_idx] + } + items := make([]*HistoryItem, len(self.history.items)) + for i, x := range self.history.items { + items[i] = &x + } + for _, token := range self.history_search.tokens { + matches := make([]*HistoryItem, 0, len(items)) + for _, item := range items { + if strings.Contains(item.Cmd, token) { + matches = append(matches, item) + } + } + items = matches + } + self.history_search.items = items + idx := -1 + for i, item := range self.history_search.items { + if item == current_item { + idx = i + break + } + } + if idx == -1 { + idx = len(self.history_search.items) - 1 + } + self.history_search.current_idx = utils.Max(0, idx) + self.markup_history_search() +} + +func (self *Readline) next_history_search(backwards bool, num uint) bool { + ni := self.history_search.current_idx + self.history_search.backwards = backwards + if len(self.history_search.items) == 0 { + return false + } + if backwards { + ni = utils.Max(0, ni-int(num)) + } else { + ni = utils.Min(ni+int(num), len(self.history_search.items)-1) + } + if ni == self.history_search.current_idx { + return false + } + self.history_search.current_idx = ni + self.markup_history_search() + return true +} + +func (self *Readline) history_search_prompt() string { + ans := "↑" + if !self.history_search.backwards { + ans = "↓" + } + failed := len(self.history_search.tokens) > 0 && len(self.history_search.items) == 0 + if failed { + ans = self.fmt_ctx.BrightRed(ans) + } else { + ans = self.fmt_ctx.Green(ans) + } + return fmt.Sprintf("history %s: ", ans) +} diff --git a/tools/tui/readline/keys.go b/tools/tui/readline/keys.go index f035845f4..f18d4f76d 100644 --- a/tools/tui/readline/keys.go +++ b/tools/tui/readline/keys.go @@ -79,6 +79,7 @@ func default_shortcuts() *ShortcutMap { sm.add(ActionClearScreen, "ctrl+l") sm.add(ActionAbortCurrentLine, "ctrl+c") + sm.add(ActionAbortCurrentLine, "ctrl+g") sm.add(ActionEndInput, "ctrl+d") sm.add(ActionAcceptInput, "enter") @@ -98,6 +99,10 @@ func default_shortcuts() *ShortcutMap { sm.add(ActionHistoryNext, "ctrl+n") sm.add(ActionHistoryFirst, "alt+<") sm.add(ActionHistoryLast, "alt+>") + sm.add(ActionHistoryIncrementalSearchBackwards, "ctrl+r") + sm.add(ActionHistoryIncrementalSearchBackwards, "ctrl+?") + sm.add(ActionHistoryIncrementalSearchForwards, "ctrl+s") + sm.add(ActionHistoryIncrementalSearchForwards, "ctrl+/") sm.add(ActionNumericArgumentDigit0, "alt+0") sm.add(ActionNumericArgumentDigit1, "alt+1") @@ -148,6 +153,9 @@ func (self *Readline) handle_numeric_arg(ac Action) { func (self *Readline) dispatch_key_action(ac Action) error { self.keyboard_state.current_pending_keys = nil if ActionNumericArgumentDigit0 <= ac && ac <= ActionNumericArgumentDigitMinus { + if self.history_search != nil { + return ErrCouldNotPerformAction + } self.handle_numeric_arg(ac) return nil }