From a008c627e3e98b3924fc0cff037a3d18b346dd72 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 23 Oct 2022 14:45:25 +0530 Subject: [PATCH] Implement bindings for moving by simple word --- tools/tui/readline/actions.go | 98 +++++++++++++++++++++++++++++- tools/tui/readline/actions_test.go | 50 +++++++++++++++ tools/tui/readline/api.go | 2 + tools/tui/readline/keys.go | 5 ++ 4 files changed, 152 insertions(+), 3 deletions(-) diff --git a/tools/tui/readline/actions.go b/tools/tui/readline/actions.go index 2a662b9a7..0695d1099 100644 --- a/tools/tui/readline/actions.go +++ b/tools/tui/readline/actions.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "strings" + "unicode" "kitty/tools/utils" "kitty/tools/wcswidth" @@ -18,7 +19,7 @@ func (self *Readline) text_upto_cursor_pos() string { buf.Grow(1024) for i, line := range self.lines { if i == self.cursor.Y { - buf.WriteString(line[:self.cursor.X]) + buf.WriteString(line[:utils.Min(len(line), self.cursor.X)]) break } else { buf.WriteString(line) @@ -297,8 +298,91 @@ func (self *Readline) erase_chars_after_cursor(amt uint, traverse_line_breaks bo return num } -func (self *Readline) next_word_char_pos(traverse_line_breaks bool) int { - return 0 +func has_word_chars(text string) bool { + for _, ch := range text { + if unicode.IsLetter(ch) || unicode.IsDigit(ch) { + return true + } + } + return false +} + +func (self *Readline) move_to_end_of_word(amt uint, traverse_line_breaks bool) (num_of_words_moved uint) { + if amt == 0 { + return 0 + } + line := self.lines[self.cursor.Y] + in_word := false + ci := wcswidth.NewCellIterator(line[self.cursor.X:]) + sz := 0 + + for ci.Forward() { + current_is_word_char := has_word_chars(ci.Current()) + plen := sz + sz += len(ci.Current()) + if current_is_word_char { + in_word = true + } else if in_word { + self.cursor.X += plen + amt-- + num_of_words_moved++ + if amt == 0 { + return + } + in_word = false + } + } + if self.move_to_end_of_line() { + amt-- + 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 + num_of_words_moved += self.move_to_end_of_word(amt, traverse_line_breaks) + } + } + return +} + +func (self *Readline) move_to_start_of_word(amt uint, traverse_line_breaks bool) (num_of_words_moved uint) { + if amt == 0 { + return 0 + } + line := self.lines[self.cursor.Y] + in_word := false + ci := wcswidth.NewCellIterator(line[:self.cursor.X]).GotoEnd() + sz := 0 + + for ci.Backward() { + current_is_word_char := has_word_chars(ci.Current()) + plen := sz + sz += len(ci.Current()) + if current_is_word_char { + in_word = true + } else if in_word { + self.cursor.X -= plen + amt-- + num_of_words_moved++ + if amt == 0 { + return + } + in_word = false + } + } + if self.move_to_start_of_line() { + amt-- + 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]) + num_of_words_moved += self.move_to_start_of_word(amt, traverse_line_breaks) + } + } + return } func (self *Readline) perform_action(ac Action, repeat_count uint) error { @@ -319,6 +403,14 @@ func (self *Readline) perform_action(ac Action, repeat_count uint) error { if self.move_to_end_of_line() { return nil } + case ActionMoveToEndOfWord: + if self.move_to_end_of_word(repeat_count, true) > 0 { + return nil + } + case ActionMoveToStartOfWord: + if self.move_to_start_of_word(repeat_count, true) > 0 { + return nil + } case ActionMoveToStartOfDocument: if self.move_to_start() { return nil diff --git a/tools/tui/readline/actions_test.go b/tools/tui/readline/actions_test.go index 02c4a3d61..5ffb4549d 100644 --- a/tools/tui/readline/actions_test.go +++ b/tools/tui/readline/actions_test.go @@ -201,6 +201,56 @@ func TestCursorMovement(t *testing.T) { vert(-1, -1, "1234567xy\nabc", Position{X: 3, Y: 2}) vert(-2, -2, "1234567xy", Position{X: 3, Y: 2}) vert(-30, -3, "123", Position{X: 3, Y: 2}) + + rl.ResetText() + rl.add_text("o\u0300ne two three\nfour five") + + wf := func(amt uint, expected_amt uint, text_before_cursor string) { + pos := rl.cursor + actual_amt := rl.move_to_end_of_word(amt, true) + if actual_amt != expected_amt { + t.Fatalf("Failed to move to word end, expected amt (%d) != actual amt (%d)", expected_amt, actual_amt) + } + if diff := cmp.Diff(text_before_cursor, rl.TextBeforeCursor()); diff != "" { + t.Fatalf("Did not get expected text before cursor for: %#v and cursor: %+v\n%s", rl.AllText(), pos, diff) + } + } + rl.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{} + wf(5, 5, "òne two three\nfour five") + rl.cursor = Position{X: 5} + wf(1, 1, "òne two") + + wb := func(amt uint, expected_amt uint, text_before_cursor string) { + pos := rl.cursor + actual_amt := rl.move_to_start_of_word(amt, true) + if actual_amt != expected_amt { + t.Fatalf("Failed to move to word end, expected amt (%d) != actual amt (%d)", expected_amt, actual_amt) + } + if diff := cmp.Diff(text_before_cursor, rl.TextBeforeCursor()); diff != "" { + 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} + wb(1, 1, "") + rl.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} + wb(5, 5, "") + rl.cursor = Position{X: 5} + wb(1, 1, "") + } func TestEraseChars(t *testing.T) { diff --git a/tools/tui/readline/api.go b/tools/tui/readline/api.go index 4d32e5404..1e59bc0f2 100644 --- a/tools/tui/readline/api.go +++ b/tools/tui/readline/api.go @@ -43,6 +43,8 @@ const ( ActionMoveToEndOfLine ActionMoveToStartOfDocument ActionMoveToEndOfDocument + ActionMoveToEndOfWord + ActionMoveToStartOfWord ActionCursorLeft ActionCursorRight ActionEndInput diff --git a/tools/tui/readline/keys.go b/tools/tui/readline/keys.go index b8e16a4d5..33c9f5b91 100644 --- a/tools/tui/readline/keys.go +++ b/tools/tui/readline/keys.go @@ -25,6 +25,11 @@ var default_shortcuts = map[string]Action{ "ctrl+home": ActionMoveToStartOfDocument, "ctrl+end": ActionMoveToEndOfDocument, + "alt+f": ActionMoveToEndOfWord, + "ctrl+right": ActionMoveToEndOfWord, + "ctrl+left": ActionMoveToStartOfWord, + "alt+b": ActionMoveToStartOfWord, + "left": ActionCursorLeft, "ctrl+b": ActionCursorLeft, "right": ActionCursorRight,