From 0c828323563b5a20fa66d5e02f4a511d3a8d1587 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 8 Nov 2022 06:21:02 +0530 Subject: [PATCH] more work on history search --- tools/tui/readline/actions.go | 230 +++++++++++++++-------------- tools/tui/readline/actions_test.go | 56 +++---- tools/tui/readline/api.go | 51 +++++-- tools/tui/readline/draw.go | 47 +++++- tools/tui/readline/history.go | 102 ++++++++----- tools/tui/readline/keys.go | 63 +++++++- 6 files changed, 356 insertions(+), 193 deletions(-) diff --git a/tools/tui/readline/actions.go b/tools/tui/readline/actions.go index 1fdde1bc4..68761e05f 100644 --- a/tools/tui/readline/actions.go +++ b/tools/tui/readline/actions.go @@ -17,9 +17,9 @@ var _ = fmt.Print func (self *Readline) text_upto_cursor_pos() string { buf := strings.Builder{} buf.Grow(1024) - for i, line := range self.lines { - if i == self.cursor.Y { - buf.WriteString(line[:utils.Min(len(line), self.cursor.X)]) + for i, line := range self.input_state.lines { + if i == self.input_state.cursor.Y { + buf.WriteString(line[:utils.Min(len(line), self.input_state.cursor.X)]) break } else { buf.WriteString(line) @@ -32,11 +32,11 @@ func (self *Readline) text_upto_cursor_pos() string { func (self *Readline) text_after_cursor_pos() string { buf := strings.Builder{} buf.Grow(1024) - for i, line := range self.lines { - if i == self.cursor.Y { - buf.WriteString(line[utils.Min(len(line), self.cursor.X):]) + for i, line := range self.input_state.lines { + if i == self.input_state.cursor.Y { + buf.WriteString(line[utils.Min(len(line), self.input_state.cursor.X):]) buf.WriteString("\n") - } else if i > self.cursor.Y { + } else if i > self.input_state.cursor.Y { buf.WriteString(line) buf.WriteString("\n") } @@ -49,35 +49,35 @@ func (self *Readline) text_after_cursor_pos() string { } func (self *Readline) all_text() string { - return strings.Join(self.lines, "\n") + return strings.Join(self.input_state.lines, "\n") } func (self *Readline) add_text(text string) { - new_lines := make([]string, 0, len(self.lines)+4) - new_lines = append(new_lines, self.lines[:self.cursor.Y]...) + new_lines := make([]string, 0, len(self.input_state.lines)+4) + new_lines = append(new_lines, self.input_state.lines[:self.input_state.cursor.Y]...) var lines_after []string - if len(self.lines) > self.cursor.Y+1 { - lines_after = self.lines[self.cursor.Y+1:] + if len(self.input_state.lines) > self.input_state.cursor.Y+1 { + lines_after = self.input_state.lines[self.input_state.cursor.Y+1:] } has_trailing_newline := strings.HasSuffix(text, "\n") add_line_break := func(line string) { new_lines = append(new_lines, line) - self.cursor.X = len(line) - self.cursor.Y += 1 + self.input_state.cursor.X = len(line) + self.input_state.cursor.Y += 1 } - cline := self.lines[self.cursor.Y] - before_first_line := cline[:self.cursor.X] + cline := self.input_state.lines[self.input_state.cursor.Y] + before_first_line := cline[:self.input_state.cursor.X] after_first_line := "" - if self.cursor.X < len(cline) { - after_first_line = cline[self.cursor.X:] + if self.input_state.cursor.X < len(cline) { + after_first_line = cline[self.input_state.cursor.X:] } for i, line := range utils.Splitlines(text) { if i > 0 { add_line_break(line) } else { line := before_first_line + line - self.cursor.X = len(line) + self.input_state.cursor.X = len(line) new_lines = append(new_lines, line) } } @@ -93,23 +93,23 @@ func (self *Readline) add_text(text string) { if len(lines_after) > 0 { new_lines = append(new_lines, lines_after...) } - self.lines = new_lines + self.input_state.lines = new_lines } func (self *Readline) move_cursor_left(amt uint, traverse_line_breaks bool) (amt_moved uint) { for amt_moved < amt { - if self.cursor.X == 0 { - if !traverse_line_breaks || self.cursor.Y == 0 { + if self.input_state.cursor.X == 0 { + if !traverse_line_breaks || self.input_state.cursor.Y == 0 { return amt_moved } - self.cursor.Y -= 1 - self.cursor.X = len(self.lines[self.cursor.Y]) + self.input_state.cursor.Y -= 1 + self.input_state.cursor.X = len(self.input_state.lines[self.input_state.cursor.Y]) amt_moved++ continue } - line := self.lines[self.cursor.Y] - for ci := wcswidth.NewCellIterator(line[:self.cursor.X]).GotoEnd(); amt_moved < amt && ci.Backward(); amt_moved++ { - self.cursor.X -= len(ci.Current()) + line := self.input_state.lines[self.input_state.cursor.Y] + for ci := wcswidth.NewCellIterator(line[:self.input_state.cursor.X]).GotoEnd(); amt_moved < amt && ci.Backward(); amt_moved++ { + self.input_state.cursor.X -= len(ci.Current()) } } return amt_moved @@ -117,19 +117,19 @@ func (self *Readline) move_cursor_left(amt uint, traverse_line_breaks bool) (amt func (self *Readline) move_cursor_right(amt uint, traverse_line_breaks bool) (amt_moved uint) { for amt_moved < amt { - line := self.lines[self.cursor.Y] - if self.cursor.X >= len(line) { - if !traverse_line_breaks || self.cursor.Y == len(self.lines)-1 { + line := self.input_state.lines[self.input_state.cursor.Y] + if self.input_state.cursor.X >= len(line) { + if !traverse_line_breaks || self.input_state.cursor.Y == len(self.input_state.lines)-1 { return amt_moved } - self.cursor.Y += 1 - self.cursor.X = 0 + self.input_state.cursor.Y += 1 + self.input_state.cursor.X = 0 amt_moved++ continue } - for ci := wcswidth.NewCellIterator(line[self.cursor.X:]); amt_moved < amt && ci.Forward(); amt_moved++ { - self.cursor.X += len(ci.Current()) + for ci := wcswidth.NewCellIterator(line[self.input_state.cursor.X:]); amt_moved < amt && ci.Forward(); amt_moved++ { + self.input_state.cursor.X += len(ci.Current()) } } return amt_moved @@ -138,9 +138,9 @@ func (self *Readline) move_cursor_right(amt uint, traverse_line_breaks bool) (am func (self *Readline) move_cursor_to_target_line(source_line, target_line *ScreenLine) { if source_line != target_line { visual_distance_into_text := source_line.CursorCell - source_line.Prompt.Length - self.cursor.Y = target_line.ParentLineNumber + self.input_state.cursor.Y = target_line.ParentLineNumber tp := wcswidth.TruncateToVisualLength(target_line.Text, visual_distance_into_text) - self.cursor.X = target_line.OffsetInParentLine + len(tp) + self.input_state.cursor.X = target_line.OffsetInParentLine + len(tp) } } @@ -173,37 +173,37 @@ func (self *Readline) move_cursor_down(amt uint) uint { } func (self *Readline) move_to_start_of_line() bool { - if self.cursor.X > 0 { - self.cursor.X = 0 + if self.input_state.cursor.X > 0 { + self.input_state.cursor.X = 0 return true } return false } func (self *Readline) move_to_end_of_line() bool { - line := self.lines[self.cursor.Y] - if self.cursor.X >= len(line) { + line := self.input_state.lines[self.input_state.cursor.Y] + if self.input_state.cursor.X >= len(line) { return false } - self.cursor.X = len(line) + self.input_state.cursor.X = len(line) return true } func (self *Readline) move_to_start() bool { - if self.cursor.Y == 0 && self.cursor.X == 0 { + if self.input_state.cursor.Y == 0 && self.input_state.cursor.X == 0 { return false } - self.cursor.Y = 0 + self.input_state.cursor.Y = 0 self.move_to_start_of_line() return true } func (self *Readline) move_to_end() bool { - line := self.lines[self.cursor.Y] - if self.cursor.Y == len(self.lines)-1 && self.cursor.X >= len(line) { + line := self.input_state.lines[self.input_state.cursor.Y] + if self.input_state.cursor.Y == len(self.input_state.lines)-1 && self.input_state.cursor.X >= len(line) { return false } - self.cursor.Y = len(self.lines) - 1 + self.input_state.cursor.Y = len(self.input_state.lines) - 1 self.move_to_end_of_line() return true } @@ -214,68 +214,68 @@ func (self *Readline) erase_between(start, end Position) string { } buf := strings.Builder{} if start.Y == end.Y { - line := self.lines[start.Y] + line := self.input_state.lines[start.Y] buf.WriteString(line[start.X:end.X]) - self.lines[start.Y] = line[:start.X] + line[end.X:] - if self.cursor.Y == start.Y && self.cursor.X >= start.X { - if self.cursor.X < end.X { - self.cursor.X = start.X + self.input_state.lines[start.Y] = line[:start.X] + line[end.X:] + if self.input_state.cursor.Y == start.Y && self.input_state.cursor.X >= start.X { + if self.input_state.cursor.X < end.X { + self.input_state.cursor.X = start.X } else { - self.cursor.X -= end.X - start.X + self.input_state.cursor.X -= end.X - start.X } } return buf.String() } - lines := make([]string, 0, len(self.lines)) - for i, line := range self.lines { + lines := make([]string, 0, len(self.input_state.lines)) + for i, line := range self.input_state.lines { if i < start.Y || i > end.Y { lines = append(lines, line) } else if i == start.Y { lines = append(lines, line[:start.X]) buf.WriteString(line[start.X:]) - if self.cursor.Y == i && self.cursor.X > start.X { - self.cursor.X = start.X + if self.input_state.cursor.Y == i && self.input_state.cursor.X > start.X { + self.input_state.cursor.X = start.X } } else if i == end.Y { lines[len(lines)-1] += line[end.X:] buf.WriteString(line[:end.X]) - if i == self.cursor.Y { - self.cursor.Y = start.Y - if self.cursor.X < end.X { - self.cursor.X = start.X + if i == self.input_state.cursor.Y { + self.input_state.cursor.Y = start.Y + if self.input_state.cursor.X < end.X { + self.input_state.cursor.X = start.X } else { - self.cursor.X -= end.X - start.X + self.input_state.cursor.X -= end.X - start.X } } } else { - if i == self.cursor.Y { - self.cursor = start + if i == self.input_state.cursor.Y { + self.input_state.cursor = start } buf.WriteString(line) buf.WriteString("\n") } } - self.lines = lines + self.input_state.lines = lines return buf.String() } func (self *Readline) erase_chars_before_cursor(amt uint, traverse_line_breaks bool) uint { - pos := self.cursor + pos := self.input_state.cursor num := self.move_cursor_left(amt, traverse_line_breaks) if num == 0 { return num } - self.erase_between(self.cursor, pos) + self.erase_between(self.input_state.cursor, pos) return num } func (self *Readline) erase_chars_after_cursor(amt uint, traverse_line_breaks bool) uint { - pos := self.cursor + pos := self.input_state.cursor num := self.move_cursor_right(amt, traverse_line_breaks) if num == 0 { return num } - self.erase_between(pos, self.cursor) + self.erase_between(pos, self.input_state.cursor) return num } @@ -292,9 +292,9 @@ func (self *Readline) move_to_end_of_word(amt uint, traverse_line_breaks bool, i if amt == 0 { return 0 } - line := self.lines[self.cursor.Y] + line := self.input_state.lines[self.input_state.cursor.Y] in_word := false - ci := wcswidth.NewCellIterator(line[self.cursor.X:]) + ci := wcswidth.NewCellIterator(line[self.input_state.cursor.X:]) sz := 0 for ci.Forward() { @@ -304,7 +304,7 @@ func (self *Readline) move_to_end_of_word(amt uint, traverse_line_breaks bool, i if current_is_word_char { in_word = true } else if in_word { - self.cursor.X += plen + self.input_state.cursor.X += plen amt-- num_of_words_moved++ if amt == 0 { @@ -318,9 +318,9 @@ func (self *Readline) move_to_end_of_word(amt uint, traverse_line_breaks bool, i num_of_words_moved++ } if amt > 0 { - if traverse_line_breaks && self.cursor.Y < len(self.lines)-1 { - self.cursor.Y++ - self.cursor.X = 0 + if traverse_line_breaks && self.input_state.cursor.Y < len(self.input_state.lines)-1 { + self.input_state.cursor.Y++ + self.input_state.cursor.X = 0 num_of_words_moved += self.move_to_end_of_word(amt, traverse_line_breaks, is_part_of_word) } } @@ -331,9 +331,9 @@ func (self *Readline) move_to_start_of_word(amt uint, traverse_line_breaks bool, if amt == 0 { return 0 } - line := self.lines[self.cursor.Y] + line := self.input_state.lines[self.input_state.cursor.Y] in_word := false - ci := wcswidth.NewCellIterator(line[:self.cursor.X]).GotoEnd() + ci := wcswidth.NewCellIterator(line[:self.input_state.cursor.X]).GotoEnd() sz := 0 for ci.Backward() { @@ -343,7 +343,7 @@ func (self *Readline) move_to_start_of_word(amt uint, traverse_line_breaks bool, if current_is_word_char { in_word = true } else if in_word { - self.cursor.X -= plen + self.input_state.cursor.X -= plen amt-- num_of_words_moved++ if amt == 0 { @@ -357,9 +357,9 @@ func (self *Readline) move_to_start_of_word(amt uint, traverse_line_breaks bool, num_of_words_moved++ } if amt > 0 { - if traverse_line_breaks && self.cursor.Y > 0 { - self.cursor.Y-- - self.cursor.X = len(self.lines[self.cursor.Y]) + if traverse_line_breaks && self.input_state.cursor.Y > 0 { + self.input_state.cursor.Y-- + self.input_state.cursor.X = len(self.input_state.lines[self.input_state.cursor.Y]) num_of_words_moved += self.move_to_start_of_word(amt, traverse_line_breaks, has_word_chars) } } @@ -375,40 +375,40 @@ func (self *Readline) kill_text(text string) { } func (self *Readline) kill_to_end_of_line() bool { - line := self.lines[self.cursor.Y] - if self.cursor.X >= len(line) { + line := self.input_state.lines[self.input_state.cursor.Y] + if self.input_state.cursor.X >= len(line) { return false } - self.lines[self.cursor.Y] = line[:self.cursor.X] - self.kill_text(line[self.cursor.X:]) + self.input_state.lines[self.input_state.cursor.Y] = line[:self.input_state.cursor.X] + self.kill_text(line[self.input_state.cursor.X:]) return true } func (self *Readline) kill_to_start_of_line() bool { - line := self.lines[self.cursor.Y] - if self.cursor.X <= 0 { + line := self.input_state.lines[self.input_state.cursor.Y] + if self.input_state.cursor.X <= 0 { return false } - self.lines[self.cursor.Y] = line[self.cursor.X:] - self.kill_text(line[:self.cursor.X]) - self.cursor.X = 0 + self.input_state.lines[self.input_state.cursor.Y] = line[self.input_state.cursor.X:] + self.kill_text(line[:self.input_state.cursor.X]) + self.input_state.cursor.X = 0 return true } func (self *Readline) kill_next_word(amt uint, traverse_line_breaks bool) (num_killed uint) { - before := self.cursor + before := self.input_state.cursor num_killed = self.move_to_end_of_word(amt, traverse_line_breaks, has_word_chars) if num_killed > 0 { - self.kill_text(self.erase_between(before, self.cursor)) + self.kill_text(self.erase_between(before, self.input_state.cursor)) } return num_killed } func (self *Readline) kill_previous_word(amt uint, traverse_line_breaks bool) (num_killed uint) { - before := self.cursor + before := self.input_state.cursor num_killed = self.move_to_start_of_word(amt, traverse_line_breaks, has_word_chars) if num_killed > 0 { - self.kill_text(self.erase_between(self.cursor, before)) + self.kill_text(self.erase_between(self.input_state.cursor, before)) } return num_killed } @@ -423,17 +423,17 @@ func has_no_space_chars(text string) bool { } func (self *Readline) kill_previous_space_delimited_word(amt uint, traverse_line_breaks bool) (num_killed uint) { - before := self.cursor + before := self.input_state.cursor num_killed = self.move_to_start_of_word(amt, traverse_line_breaks, has_no_space_chars) if num_killed > 0 { - self.kill_text(self.erase_between(self.cursor, before)) + self.kill_text(self.erase_between(self.input_state.cursor, before)) } return num_killed } func (self *Readline) ensure_position_in_bounds(pos *Position) *Position { - pos.Y = utils.Max(0, utils.Min(pos.Y, len(self.lines)-1)) - line := self.lines[pos.Y] + pos.Y = utils.Max(0, utils.Min(pos.Y, len(self.input_state.lines)-1)) + line := self.input_state.lines[pos.Y] pos.X = utils.Max(0, utils.Min(pos.X, len(line))) return pos } @@ -451,24 +451,24 @@ func (self *Readline) yank(repeat_count uint, pop bool) bool { if text == "" { return false } - before := self.cursor + before := self.input_state.cursor if pop { self.ensure_position_in_bounds(&self.last_yank_extent.start) self.ensure_position_in_bounds(&self.last_yank_extent.end) self.erase_between(self.last_yank_extent.start, self.last_yank_extent.end) - self.cursor = self.last_yank_extent.start - before = self.cursor + self.input_state.cursor = self.last_yank_extent.start + before = self.input_state.cursor } self.add_text(text) self.last_yank_extent.start = before - self.last_yank_extent.end = self.cursor + self.last_yank_extent.end = self.input_state.cursor return true } func (self *Readline) apply_history_text(text string) { - self.lines = utils.Splitlines(text) - if len(self.lines) == 0 { - self.lines = []string{""} + self.input_state.lines = utils.Splitlines(text) + if len(self.input_state.lines) == 0 { + self.input_state.lines = []string{""} } } @@ -528,8 +528,14 @@ func (self *Readline) perform_action(ac Action, repeat_count uint) error { defer func() { self.last_action = ac }() switch ac { case ActionBackspace: - if self.erase_chars_before_cursor(repeat_count, true) > 0 { - return nil + if self.history_search != nil { + if self.remove_text_from_history_search(repeat_count) > 0 { + return nil + } + } else { + if self.erase_chars_before_cursor(repeat_count, true) > 0 { + return nil + } } case ActionDelete: if self.erase_chars_after_cursor(repeat_count, true) > 0 { @@ -568,7 +574,7 @@ func (self *Readline) perform_action(ac Action, repeat_count uint) error { return nil } case ActionEndInput: - line := self.lines[self.cursor.Y] + line := self.input_state.lines[self.input_state.cursor.Y] if line == "" { return io.EOF } @@ -672,6 +678,16 @@ func (self *Readline) perform_action(ac Action, repeat_count uint) error { self.add_text(text) } return nil + case ActionTerminateHistorySearchAndRestore: + if self.history_search != nil { + self.end_history_search(false) + return nil + } + case ActionTerminateHistorySearchAndApply: + if self.history_search != nil { + self.end_history_search(true) + return nil + } } return ErrCouldNotPerformAction } diff --git a/tools/tui/readline/actions_test.go b/tools/tui/readline/actions_test.go index d26b6d107..037a1e0e8 100644 --- a/tools/tui/readline/actions_test.go +++ b/tools/tui/readline/actions_test.go @@ -48,15 +48,15 @@ func TestAddText(t *testing.T) { dt("test", nil, "test", "", "test") dt("1234\n", nil, "1234\n", "", "1234\n") dt("abcd", func(rl *Readline) { - rl.cursor.X = 2 + rl.input_state.cursor.X = 2 rl.add_text("12") }, "ab12", "cd", "ab12cd") dt("abcd", func(rl *Readline) { - rl.cursor.X = 2 + rl.input_state.cursor.X = 2 rl.add_text("12\n34") }, "ab12\n34", "cd", "ab12\n34cd") dt("abcd\nxyz", func(rl *Readline) { - rl.cursor.X = 2 + rl.input_state.cursor.X = 2 rl.add_text("12\n34") }, "abcd\nxy12\n34", "z", "abcd\nxy12\n34z") } @@ -80,7 +80,7 @@ func TestGetScreenLines(t *testing.T) { actual[i] = *x } if diff := cmp.Diff(expected, actual); diff != "" { - t.Fatalf("Did not get expected screen lines for: %#v and cursor: %+v\n%s", rl.AllText(), rl.cursor, diff) + t.Fatalf("Did not get expected screen lines for: %#v and cursor: %+v\n%s", rl.AllText(), rl.input_state.cursor, diff) } } tsl(ScreenLine{Prompt: p(true), CursorCell: 3}) @@ -105,19 +105,19 @@ func TestGetScreenLines(t *testing.T) { ScreenLine{ParentLineNumber: 1, Prompt: p(false), Text: "456abcde", TextLengthInCells: 8, CursorCell: -1, CursorTextPos: -1}, ScreenLine{OffsetInParentLine: 8, ParentLineNumber: 1, TextLengthInCells: 3, CursorCell: 3, CursorTextPos: 3, Text: "XYZ"}, ) - rl.cursor = Position{X: 2} + rl.input_state.cursor = Position{X: 2} tsl( ScreenLine{Prompt: p(true), CursorCell: 5, Text: "123", CursorTextPos: 2, TextLengthInCells: 3}, ScreenLine{ParentLineNumber: 1, Prompt: p(false), Text: "456abcde", TextLengthInCells: 8, CursorCell: -1, CursorTextPos: -1}, ScreenLine{OffsetInParentLine: 8, ParentLineNumber: 1, TextLengthInCells: 3, CursorCell: -1, CursorTextPos: -1, Text: "XYZ"}, ) - rl.cursor = Position{X: 2, Y: 1} + rl.input_state.cursor = Position{X: 2, Y: 1} tsl( ScreenLine{Prompt: p(true), CursorCell: -1, Text: "123", CursorTextPos: -1, TextLengthInCells: 3}, ScreenLine{ParentLineNumber: 1, Prompt: p(false), Text: "456abcde", TextLengthInCells: 8, CursorCell: 4, CursorTextPos: 2}, ScreenLine{OffsetInParentLine: 8, ParentLineNumber: 1, TextLengthInCells: 3, CursorCell: -1, CursorTextPos: -1, Text: "XYZ"}, ) - rl.cursor = Position{X: 8, Y: 1} + rl.input_state.cursor = Position{X: 8, Y: 1} tsl( ScreenLine{Prompt: p(true), CursorCell: -1, Text: "123", CursorTextPos: -1, TextLengthInCells: 3}, ScreenLine{ParentLineNumber: 1, Prompt: p(false), Text: "456abcde", TextLengthInCells: 8, CursorCell: -1, CursorTextPos: -1}, @@ -125,7 +125,7 @@ func TestGetScreenLines(t *testing.T) { ) rl.ResetText() rl.add_text("1234567\nabc") - rl.cursor = Position{X: 7} + rl.input_state.cursor = Position{X: 7} tsl( ScreenLine{Prompt: p(true), CursorCell: -1, Text: "1234567", CursorTextPos: -1, TextLengthInCells: 7}, ScreenLine{ParentLineNumber: 1, Prompt: p(false), Text: "abc", CursorCell: 2, TextLengthInCells: 3, CursorTextPos: 0}, @@ -164,8 +164,8 @@ func TestCursorMovement(t *testing.T) { }, "one", "à") right := func(rl *Readline, amt uint, moved_amt uint, traverse_line_breaks bool) { - rl.cursor.Y = 0 - rl.cursor.X = 0 + rl.input_state.cursor.Y = 0 + rl.input_state.cursor.X = 0 actual := rl.move_cursor_right(amt, traverse_line_breaks) if actual != moved_amt { t.Fatalf("Failed to move cursor by %d\nactual != expected: %d != %d", amt, actual, moved_amt) @@ -196,7 +196,7 @@ func TestCursorMovement(t *testing.T) { if len(initials) > 0 { initial = initials[0] } - rl.cursor = initial + rl.input_state.cursor = initial actual := rl.move_cursor_vertically(amt) if actual != moved_amt { t.Fatalf("Failed to move cursor by %#v for: %#v \nactual != expected: %#v != %#v", amt, rl.AllText(), actual, moved_amt) @@ -216,7 +216,7 @@ func TestCursorMovement(t *testing.T) { rl.add_text("o\u0300ne two three\nfour five") wf := func(amt uint, expected_amt uint, text_before_cursor string) { - pos := rl.cursor + pos := rl.input_state.cursor actual_amt := rl.move_to_end_of_word(amt, true, has_word_chars) if actual_amt != expected_amt { t.Fatalf("Failed to move to word end, expected amt (%d) != actual amt (%d)", expected_amt, actual_amt) @@ -225,20 +225,20 @@ func TestCursorMovement(t *testing.T) { t.Fatalf("Did not get expected text before cursor for: %#v and cursor: %+v\n%s", rl.AllText(), pos, diff) } } - rl.cursor = Position{} + rl.input_state.cursor = Position{} wf(1, 1, "òne") wf(1, 1, "òne two") wf(1, 1, "òne two three") wf(1, 1, "òne two three\nfour") wf(1, 1, "òne two three\nfour five") wf(1, 0, "òne two three\nfour five") - rl.cursor = Position{} + rl.input_state.cursor = Position{} wf(5, 5, "òne two three\nfour five") - rl.cursor = Position{X: 5} + rl.input_state.cursor = Position{X: 5} wf(1, 1, "òne two") wb := func(amt uint, expected_amt uint, text_before_cursor string) { - pos := rl.cursor + pos := rl.input_state.cursor actual_amt := rl.move_to_start_of_word(amt, true, has_word_chars) if actual_amt != expected_amt { t.Fatalf("Failed to move to word end, expected amt (%d) != actual amt (%d)", expected_amt, actual_amt) @@ -247,18 +247,18 @@ func TestCursorMovement(t *testing.T) { t.Fatalf("Did not get expected text before cursor for: %#v and cursor: %+v\n%s", rl.AllText(), pos, diff) } } - rl.cursor = Position{X: 2} + rl.input_state.cursor = Position{X: 2} wb(1, 1, "") - rl.cursor = Position{X: 8, Y: 1} + rl.input_state.cursor = Position{X: 8, Y: 1} wb(1, 1, "òne two three\nfour ") wb(1, 1, "òne two three\n") wb(1, 1, "òne two ") wb(1, 1, "òne ") wb(1, 1, "") wb(1, 0, "") - rl.cursor = Position{X: 8, Y: 1} + rl.input_state.cursor = Position{X: 8, Y: 1} wb(5, 5, "") - rl.cursor = Position{X: 5} + rl.input_state.cursor = Position{X: 5} wb(1, 1, "") } @@ -330,11 +330,11 @@ func TestEraseChars(t *testing.T) { backspace(rl, 2, 2, false) }, "one\nt", "") dt("one\ntwo", func(rl *Readline) { - rl.cursor.X = 1 + rl.input_state.cursor.X = 1 backspace(rl, 2, 1, false) }, "one\n", "wo") dt("one\ntwo", func(rl *Readline) { - rl.cursor.X = 1 + rl.input_state.cursor.X = 1 backspace(rl, 2, 2, true) }, "one", "wo") dt("a😀", func(rl *Readline) { @@ -345,8 +345,8 @@ func TestEraseChars(t *testing.T) { }, "b", "") del := func(rl *Readline, amt uint, erased_amt uint, traverse_line_breaks bool) { - rl.cursor.Y = 0 - rl.cursor.X = 0 + rl.input_state.cursor.Y = 0 + rl.input_state.cursor.X = 0 actual := rl.erase_chars_after_cursor(amt, traverse_line_breaks) if actual != erased_amt { t.Fatalf("Failed to move cursor by %#v\nactual != expected: %d != %d", amt, actual, erased_amt) @@ -366,19 +366,19 @@ func TestEraseChars(t *testing.T) { rl.erase_between(Position{X: 1}, Position{X: 2, Y: 2}) }, "oree", "") dt("one\ntwo\nthree", func(rl *Readline) { - rl.cursor.X = 1 + rl.input_state.cursor.X = 1 rl.erase_between(Position{X: 1}, Position{X: 2, Y: 2}) }, "o", "ree") dt("one\ntwo\nthree", func(rl *Readline) { - rl.cursor = Position{X: 1, Y: 1} + rl.input_state.cursor = Position{X: 1, Y: 1} rl.erase_between(Position{X: 1}, Position{X: 2, Y: 2}) }, "o", "ree") dt("one\ntwo\nthree", func(rl *Readline) { - rl.cursor = Position{X: 1, Y: 0} + rl.input_state.cursor = Position{X: 1, Y: 0} rl.erase_between(Position{X: 1}, Position{X: 2, Y: 2}) }, "o", "ree") dt("one\ntwo\nthree", func(rl *Readline) { - rl.cursor = Position{X: 0, Y: 0} + rl.input_state.cursor = Position{X: 0, Y: 0} rl.erase_between(Position{X: 1}, Position{X: 2, Y: 2}) }, "", "oree") } diff --git a/tools/tui/readline/api.go b/tools/tui/readline/api.go index d026e90ae..bbff4af93 100644 --- a/tools/tui/readline/api.go +++ b/tools/tui/readline/api.go @@ -17,6 +17,8 @@ var _ = fmt.Print const ST = "\x1b\\" const PROMPT_MARK = "\x1b]133;" +type SyntaxHighlightFunction func(text string, x, y int) string + type RlInit struct { Prompt string HistoryPath string @@ -24,6 +26,7 @@ type RlInit struct { ContinuationPrompt string EmptyContinuationPrompt bool DontMarkPrompts bool + SyntaxHighlighter SyntaxHighlightFunction } type Position struct { @@ -35,6 +38,7 @@ func (self Position) Less(other Position) bool { return self.Y < other.Y || (self.Y == other.Y && self.X < other.X) } +// Actions {{{ type Action uint const ( @@ -61,6 +65,8 @@ const ( ActionHistoryLast ActionHistoryIncrementalSearchBackwards ActionHistoryIncrementalSearchForwards + ActionTerminateHistorySearchAndApply + ActionTerminateHistorySearchAndRestore ActionClearScreen ActionAddText ActionAbortCurrentLine @@ -89,6 +95,8 @@ const ( ActionNumericArgumentDigitMinus ) +// }}} + type kill_ring struct { items *list.List } @@ -133,6 +141,28 @@ type Prompt struct { Length int } +type InputState struct { + // Input lines + lines []string + // The cursor position in the text + cursor Position +} + +func (self InputState) copy() InputState { + ans := self + l := make([]string, len(self.lines)) + copy(l, self.lines) + ans.lines = l + return ans +} + +type syntax_highlighted struct { + lines []string + src_for_last_highlight string + highlighter SyntaxHighlightFunction + last_highlighter_name string +} + type Readline struct { prompt, continuation_prompt Prompt @@ -141,13 +171,10 @@ type Readline struct { history *History kill_ring kill_ring + input_state InputState // The number of lines after the initial line on the screen - cursor_y int - screen_width int - // Input lines - lines []string - // The cursor position in the text - cursor Position + cursor_y int + screen_width int last_yank_extent struct { start, end Position } @@ -158,6 +185,7 @@ type Readline struct { keyboard_state KeyboardState fmt_ctx *markup.Context text_to_be_added string + syntax_highlighted syntax_highlighted } func (self *Readline) make_prompt(text string, is_secondary bool) Prompt { @@ -178,7 +206,9 @@ func New(loop *loop.Loop, r RlInit) *Readline { } ans := &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()}, + loop: loop, input_state: InputState{lines: []string{""}}, history: NewHistory(r.HistoryPath, hc), + syntax_highlighted: syntax_highlighted{highlighter: r.SyntaxHighlighter}, + kill_ring: kill_ring{items: list.New().Init()}, } ans.prompt = ans.make_prompt(r.Prompt, false) t := "" @@ -201,11 +231,10 @@ func (self *Readline) AddHistoryItem(hi HistoryItem) { } func (self *Readline) ResetText() { - self.lines = []string{""} - self.cursor = Position{} - self.cursor_y = 0 + self.input_state = InputState{lines: []string{""}} self.last_action = ActionNil self.keyboard_state = KeyboardState{} + self.history_search = nil } func (self *Readline) ChangeLoopAndResetText(lp *loop.Loop) { @@ -278,7 +307,7 @@ func (self *Readline) AllText() string { } func (self *Readline) CursorAtEndOfLine() bool { - return self.cursor.X >= len(self.lines[self.cursor.Y]) + return self.input_state.cursor.X >= len(self.input_state.lines[self.input_state.cursor.Y]) } func (self *Readline) OnResize(old_size loop.ScreenSize, new_size loop.ScreenSize) error { diff --git a/tools/tui/readline/draw.go b/tools/tui/readline/draw.go index 237e27205..e1850a7dc 100644 --- a/tools/tui/readline/draw.go +++ b/tools/tui/readline/draw.go @@ -4,7 +4,9 @@ package readline import ( "fmt" + "kitty/tools/utils" "kitty/tools/wcswidth" + "strings" ) var _ = fmt.Print @@ -36,7 +38,7 @@ 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 + is_line_with_cursor := i == self.input_state.cursor.Y if is_line_with_cursor && self.keyboard_state.current_numeric_argument != "" { return self.make_prompt(self.format_arg_prompt(self.keyboard_state.current_numeric_argument), i > 0) } @@ -49,17 +51,48 @@ func (self *Readline) prompt_for_line_number(i int) Prompt { return self.continuation_prompt } +func (self *Readline) apply_syntax_highlighting() (lines []string, cursor Position) { + highlighter := self.syntax_highlighted.highlighter + highlighter_name := "default" + if self.history_search != nil { + highlighter = self.history_search_highlighter + highlighter_name = "## history ##" + } + if highlighter == nil { + return self.input_state.lines, self.input_state.cursor + } + src := strings.Join(self.input_state.lines, "\n") + if len(self.syntax_highlighted.lines) > 0 && self.syntax_highlighted.last_highlighter_name == highlighter_name && self.syntax_highlighted.src_for_last_highlight == src { + lines = self.syntax_highlighted.lines + } else { + if src == "" { + lines = []string{""} + } else { + text := highlighter(src, self.input_state.cursor.X, self.input_state.cursor.Y) + lines = utils.Splitlines(text) + for len(lines) < len(self.input_state.lines) { + lines = append(lines, "syntax highlighter malfunctioned") + } + } + } + line := lines[self.input_state.cursor.Y] + w := wcswidth.Stringwidth(self.input_state.lines[self.input_state.cursor.Y][:self.input_state.cursor.X]) + x := len(wcswidth.TruncateToVisualLength(line, w)) + return lines, Position{X: x, Y: self.input_state.cursor.Y} +} + func (self *Readline) get_screen_lines() []*ScreenLine { if self.screen_width == 0 { self.update_current_screen_size() } - ans := make([]*ScreenLine, 0, len(self.lines)) + lines, cursor := self.apply_syntax_highlighting() + ans := make([]*ScreenLine, 0, len(lines)) found_cursor := false cursor_at_start_of_next_line := false - for i, line := range self.lines { + for i, line := range lines { prompt := self.prompt_for_line_number(i) offset := 0 - has_cursor := i == self.cursor.Y + has_cursor := i == cursor.Y for is_first := true; is_first || offset < len(line); is_first = false { l, width := wcswidth.TruncateToVisualLengthWithWidth(line[offset:], self.screen_width-prompt.Length) sl := ScreenLine{ @@ -73,12 +106,12 @@ func (self *Readline) get_screen_lines() []*ScreenLine { sl.CursorTextPos = 0 } ans = append(ans, &sl) - if has_cursor && !found_cursor && offset <= self.cursor.X && self.cursor.X <= offset+len(l) { + if has_cursor && !found_cursor && offset <= cursor.X && cursor.X <= offset+len(l) { found_cursor = true - ctpos := self.cursor.X - offset + ctpos := cursor.X - offset ccell := prompt.Length + wcswidth.Stringwidth(l[:ctpos]) if ccell >= self.screen_width { - if offset+len(l) < len(line) || i < len(self.lines)-1 { + if offset+len(l) < len(line) || i < len(lines)-1 { cursor_at_start_of_next_line = true } else { ans = append(ans, &ScreenLine{ParentLineNumber: i, OffsetInParentLine: len(line)}) diff --git a/tools/tui/readline/history.go b/tools/tui/readline/history.go index bab576545..feade6daf 100644 --- a/tools/tui/readline/history.go +++ b/tools/tui/readline/history.go @@ -12,6 +12,8 @@ import ( "kitty/tools/utils" "kitty/tools/wcswidth" + + "github.com/google/shlex" ) var _ = fmt.Print @@ -31,13 +33,12 @@ type HistoryMatches struct { } type HistorySearch struct { - query string - tokens []string - items []*HistoryItem - current_idx int - backwards bool - original_lines []string - original_cursor Position + query string + tokens []string + items []*HistoryItem + current_idx int + backwards bool + original_input_state InputState } type History struct { @@ -211,31 +212,32 @@ func (self *HistoryMatches) next(num uint) (ans *HistoryItem) { } 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.history_search = &HistorySearch{backwards: backwards, original_input_state: self.input_state.copy()} + self.push_keyboard_map(history_search_shortcuts()) 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]) + self.input_state.lines = utils.Splitlines(self.history_search.items[self.history_search.current_idx].Cmd) + self.input_state.cursor.Y = len(self.input_state.lines) - 1 + self.input_state.cursor.X = len(self.input_state.lines[self.input_state.cursor.Y]) } else { - self.lines = self.history_search.original_lines - self.cursor = self.history_search.original_cursor + self.input_state = self.history_search.original_input_state } - self.cursor = *self.ensure_position_in_bounds(&self.cursor) + self.input_state.cursor = *self.ensure_position_in_bounds(&self.input_state.cursor) + self.pop_keyboard_map() + self.history_search = nil } func (self *Readline) markup_history_search() { if len(self.history_search.items) == 0 { if len(self.history_search.tokens) == 0 { - self.lines = []string{""} + self.input_state.lines = []string{""} } else { - self.lines = []string{"No matches for: " + self.fmt_ctx.BrightRed(self.history_search.query)} + self.input_state.lines = []string{"No matches for: " + self.history_search.query} } - self.cursor = Position{X: wcswidth.Stringwidth(self.lines[0])} + self.input_state.cursor = Position{X: wcswidth.Stringwidth(self.input_state.lines[0])} return } lines := utils.Splitlines(self.history_search.items[self.history_search.current_idx].Cmd) @@ -243,7 +245,6 @@ func (self *Readline) markup_history_search() { 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 @@ -252,31 +253,64 @@ func (self *Readline) markup_history_search() { } } } - self.lines = lines - self.cursor = *self.ensure_position_in_bounds(&cursor) + self.input_state.lines = lines + self.input_state.cursor = *self.ensure_position_in_bounds(&cursor) +} + +func (self *Readline) remove_text_from_history_search(num uint) uint { + l := len(self.history_search.query) + nl := utils.Max(0, l-int(num)) + self.history_search.query = self.history_search.query[:nl] + num_removed := uint(l - nl) + self.add_text_to_history_search("") // update the search results + return num_removed +} + +func (self *Readline) history_search_highlighter(text string, x, y int) string { + if len(self.history_search.items) == 0 { + return text + } + lines := utils.Splitlines(text) + 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):] + break + } + } + } + return strings.Join(lines, "\n") } 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, " ") + tokens, err := shlex.Split(self.history_search.query) + if err != nil { + tokens = strings.Split(self.history_search.query, " ") + } + self.history_search.tokens = tokens 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) - } + if len(self.history_search.tokens) == 0 { + self.history_search.items = []*HistoryItem{} + } else { + items := make([]*HistoryItem, len(self.history.items)) + for i, x := range self.history.items { + items[i] = &x } - items = matches + 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 } - self.history_search.items = items idx := -1 for i, item := range self.history_search.items { if item == current_item { diff --git a/tools/tui/readline/keys.go b/tools/tui/readline/keys.go index f18d4f76d..e11d9a2ab 100644 --- a/tools/tui/readline/keys.go +++ b/tools/tui/readline/keys.go @@ -121,18 +121,69 @@ func default_shortcuts() *ShortcutMap { return _default_shortcuts } -func (self *Readline) action_for_key_event(event *loop.KeyEvent, shortcuts map[string]Action) Action { - for sc, ac := range shortcuts { - if event.MatchesPressOrRepeat(sc) { - return ac - } +var _history_search_shortcuts *ShortcutMap + +func history_search_shortcuts() *ShortcutMap { + if _history_search_shortcuts == nil { + sm := ShortcutMap{leaves: make(map[string]Action, 32), children: map[string]*ShortcutMap{}} + sm.add(ActionBackspace, "backspace") + sm.add(ActionBackspace, "ctrl+h") + + sm.add(ActionTerminateHistorySearchAndRestore, "home") + sm.add(ActionTerminateHistorySearchAndRestore, "ctrl+a") + + sm.add(ActionTerminateHistorySearchAndRestore, "end") + sm.add(ActionTerminateHistorySearchAndRestore, "ctrl+e") + + sm.add(ActionTerminateHistorySearchAndRestore, "ctrl+home") + sm.add(ActionTerminateHistorySearchAndRestore, "ctrl+end") + + sm.add(ActionTerminateHistorySearchAndRestore, "alt+f") + sm.add(ActionTerminateHistorySearchAndRestore, "ctrl+right") + sm.add(ActionTerminateHistorySearchAndRestore, "ctrl+left") + sm.add(ActionTerminateHistorySearchAndRestore, "alt+b") + + sm.add(ActionTerminateHistorySearchAndRestore, "left") + sm.add(ActionTerminateHistorySearchAndRestore, "ctrl+b") + sm.add(ActionTerminateHistorySearchAndRestore, "right") + sm.add(ActionTerminateHistorySearchAndRestore, "ctrl+f") + sm.add(ActionTerminateHistorySearchAndRestore, "up") + sm.add(ActionTerminateHistorySearchAndRestore, "down") + + sm.add(ActionTerminateHistorySearchAndRestore, "ctrl+c") + sm.add(ActionTerminateHistorySearchAndRestore, "ctrl+g") + sm.add(ActionTerminateHistorySearchAndRestore, "escape") + + sm.add(ActionTerminateHistorySearchAndApply, "ctrl+d") + sm.add(ActionTerminateHistorySearchAndApply, "enter") + sm.add(ActionTerminateHistorySearchAndApply, "ctrl+j") + + _history_search_shortcuts = &sm } - return ActionNil + return _history_search_shortcuts } var ErrCouldNotPerformAction = errors.New("Could not perform the specified action") var ErrAcceptInput = errors.New("Accept input") +func (self *Readline) push_keyboard_map(m *ShortcutMap) { + maps := self.keyboard_state.active_shortcut_maps + self.keyboard_state = KeyboardState{} + if maps == nil { + maps = make([]*ShortcutMap, 0, 2) + } + self.keyboard_state.active_shortcut_maps = append(maps, m) +} + +func (self *Readline) pop_keyboard_map() { + maps := self.keyboard_state.active_shortcut_maps + self.keyboard_state = KeyboardState{} + if len(maps) > 0 { + maps = maps[:len(maps)-1] + self.keyboard_state.active_shortcut_maps = maps + } +} + func (self *Readline) handle_numeric_arg(ac Action) { t := "-" num := int(ac - ActionNumericArgumentDigit0)