diff --git a/tools/tui/readline/actions.go b/tools/tui/readline/actions.go index 7c3d9508d..3d782af94 100644 --- a/tools/tui/readline/actions.go +++ b/tools/tui/readline/actions.go @@ -7,6 +7,7 @@ import ( "strings" "kitty/tools/utils" + "kitty/tools/wcswidth" ) var _ = fmt.Print @@ -90,3 +91,32 @@ func (self *Readline) add_text(text string) { } self.lines = new_lines } + +func (self *Readline) move_cursor_left(amt uint, traverse_line_breaks bool) uint { + var amt_moved uint + for ; amt > 0; amt -= 1 { + if self.cursor_pos_in_line == 0 { + if !traverse_line_breaks || self.cursor_line == 0 { + return amt_moved + } + self.cursor_line -= 1 + self.cursor_pos_in_line = len(self.lines[self.cursor_pos_in_line]) + amt_moved += 1 + continue + } + // This is an extremely inefficient algorithm but it does not matter since + // lines are not large. + line := self.lines[self.cursor_line] + runes := []rune(line[:self.cursor_pos_in_line]) + orig_width := wcswidth.Stringwidth(line[:self.cursor_pos_in_line]) + current_width := orig_width + for current_width == orig_width && len(runes) > 0 { + runes = runes[:len(runes)-1] + s := string(runes) + current_width = wcswidth.Stringwidth(s) + } + self.cursor_pos_in_line = len(string(runes)) + amt_moved += 1 + } + return amt_moved +} diff --git a/tools/tui/readline/actions_test.go b/tools/tui/readline/actions_test.go index 425aebdf2..a1b250f66 100644 --- a/tools/tui/readline/actions_test.go +++ b/tools/tui/readline/actions_test.go @@ -10,10 +10,9 @@ import ( var _ = fmt.Print -func TestAddText(t *testing.T) { - lp, _ := loop.New() - - dt := func(initial string, prepare func(rl *Readline), expected ...string) { +func test_func(t *testing.T) func(string, func(*Readline), ...string) *Readline { + return func(initial string, prepare func(rl *Readline), expected ...string) *Readline { + lp, _ := loop.New() rl := New(lp, RlInit{}) rl.add_text(initial) if prepare != nil { @@ -34,8 +33,13 @@ func TestAddText(t *testing.T) { t.Fatalf("Text not as expected for: %#v\n%#v != %#v", initial, expected[2], rl.all_text()) } } + return rl } +} + +func TestAddText(t *testing.T) { + dt := test_func(t) dt("test", nil, "test", "", "test") dt("1234\n", nil, "1234\n", "", "1234\n") dt("abcd", func(rl *Readline) { @@ -50,5 +54,36 @@ func TestAddText(t *testing.T) { rl.cursor_pos_in_line = 2 rl.add_text("12\n34") }, "abcd\nxy12\n34", "z", "abcd\nxy12\n34z") - +} + +func TestCursorMovement(t *testing.T) { + dt := test_func(t) + + left := func(rl *Readline, amt uint, moved_amt uint, traverse_line_breaks bool) { + actual := rl.move_cursor_left(amt, traverse_line_breaks) + if actual != moved_amt { + t.Fatalf("Failed to move cursor by %#v\nactual != expected: %#v != %#v", amt, actual, moved_amt) + } + } + dt("one\ntwo", func(rl *Readline) { + left(rl, 2, 2, false) + }, "one\nt", "wo") + dt("one\ntwo", func(rl *Readline) { + left(rl, 4, 3, false) + }, "one\n", "two") + dt("one\ntwo", func(rl *Readline) { + left(rl, 4, 4, true) + }, "one", "\ntwo") + dt("one\ntwo", func(rl *Readline) { + left(rl, 7, 7, true) + }, "", "one\ntwo") + dt("one\ntwo", func(rl *Readline) { + left(rl, 10, 7, true) + }, "", "one\ntwo") + dt("one😀", func(rl *Readline) { + left(rl, 1, 1, false) + }, "one", "😀") + dt("oneä", func(rl *Readline) { + left(rl, 1, 1, false) + }, "one", "ä") }