From eff239a19515c4e6e61138e4c18faaddc3e57121 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 6 Oct 2022 21:14:28 +0530 Subject: [PATCH] Code to erase character ranges --- tools/tui/readline/actions.go | 162 +++++++++++++++++++++++------ tools/tui/readline/actions_test.go | 76 +++++++++++++- tools/tui/readline/api.go | 17 ++- tools/tui/readline/draw.go | 6 +- 4 files changed, 216 insertions(+), 45 deletions(-) diff --git a/tools/tui/readline/actions.go b/tools/tui/readline/actions.go index 9f155291c..16181e2c0 100644 --- a/tools/tui/readline/actions.go +++ b/tools/tui/readline/actions.go @@ -16,8 +16,8 @@ func (self *Readline) text_upto_cursor_pos() string { buf := strings.Builder{} buf.Grow(1024) for i, line := range self.lines { - if i == self.cursor_line { - buf.WriteString(line[:self.cursor_pos_in_line]) + if i == self.cursor.Y { + buf.WriteString(line[:self.cursor.X]) break } else { buf.WriteString(line) @@ -31,10 +31,10 @@ func (self *Readline) text_after_cursor_pos() string { buf := strings.Builder{} buf.Grow(1024) for i, line := range self.lines { - if i == self.cursor_line { - buf.WriteString(line[self.cursor_pos_in_line:]) + if i == self.cursor.Y { + buf.WriteString(line[self.cursor.X:]) buf.WriteString("\n") - } else if i > self.cursor_line { + } else if i > self.cursor.Y { buf.WriteString(line) buf.WriteString("\n") } @@ -50,30 +50,30 @@ func (self *Readline) all_text() string { 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_line]...) + new_lines = append(new_lines, self.lines[:self.cursor.Y]...) var lines_after []string - if len(self.lines) > self.cursor_line+1 { - lines_after = self.lines[self.cursor_line+1:] + if len(self.lines) > self.cursor.Y+1 { + lines_after = self.lines[self.cursor.Y+1:] } has_trailing_newline := strings.HasSuffix(text, "\n") add_line_break := func(line string) { new_lines = append(new_lines, line) - self.cursor_pos_in_line = len(line) - self.cursor_line += 1 + self.cursor.X = len(line) + self.cursor.Y += 1 } - cline := self.lines[self.cursor_line] - before_first_line := cline[:self.cursor_pos_in_line] + cline := self.lines[self.cursor.Y] + before_first_line := cline[:self.cursor.X] after_first_line := "" - if self.cursor_pos_in_line < len(cline) { - after_first_line = cline[self.cursor_pos_in_line:] + if self.cursor.X < len(cline) { + after_first_line = cline[self.cursor.X:] } for i, line := range utils.Splitlines(text) { if i > 0 { add_line_break(line) } else { line := before_first_line + line - self.cursor_pos_in_line = len(line) + self.cursor.X = len(line) new_lines = append(new_lines, line) } } @@ -95,27 +95,27 @@ func (self *Readline) add_text(text string) { 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 { + if self.cursor.X == 0 { + if !traverse_line_breaks || self.cursor.Y == 0 { return amt_moved } - self.cursor_line -= 1 - self.cursor_pos_in_line = len(self.lines[self.cursor_pos_in_line]) + self.cursor.Y -= 1 + self.cursor.X = len(self.lines[self.cursor.Y]) 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]) + line := self.lines[self.cursor.Y] + runes := []rune(line[:self.cursor.X]) + orig_width := wcswidth.Stringwidth(line[:self.cursor.X]) 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)) + self.cursor.X = len(string(runes)) amt_moved += 1 } return amt_moved @@ -124,21 +124,21 @@ func (self *Readline) move_cursor_left(amt uint, traverse_line_breaks bool) uint func (self *Readline) move_cursor_right(amt uint, traverse_line_breaks bool) uint { var amt_moved uint for ; amt > 0; amt -= 1 { - line := self.lines[self.cursor_line] - if self.cursor_pos_in_line >= len(line) { - if !traverse_line_breaks || self.cursor_line == len(self.lines)-1 { + line := self.lines[self.cursor.Y] + if self.cursor.X >= len(line) { + if !traverse_line_breaks || self.cursor.Y == len(self.lines)-1 { return amt_moved } - self.cursor_line += 1 - self.cursor_pos_in_line = 0 + self.cursor.Y += 1 + self.cursor.X = 0 amt_moved += 1 continue } // This is an extremely inefficient algorithm but it does not matter since // lines are not large. - before_runes := []rune(line[:self.cursor_pos_in_line]) - after_runes := []rune(line[self.cursor_pos_in_line:]) - orig_width := wcswidth.Stringwidth(line[:self.cursor_pos_in_line]) + before_runes := []rune(line[:self.cursor.X]) + after_runes := []rune(line[self.cursor.X:]) + orig_width := wcswidth.Stringwidth(line[:self.cursor.X]) current_width := orig_width for current_width == orig_width && len(after_runes) > 0 { before_runes = append(before_runes, after_runes[0]) @@ -155,8 +155,106 @@ func (self *Readline) move_cursor_right(amt uint, traverse_line_breaks bool) uin after_runes = after_runes[1:] before_runes = q } - self.cursor_pos_in_line = len(string(before_runes)) + self.cursor.X = len(string(before_runes)) amt_moved += 1 } return amt_moved } + +func (self *Readline) move_to_start_of_line() bool { + if self.cursor.X > 0 { + self.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) { + return false + } + self.cursor.X = len(line) + return true +} + +func (self *Readline) move_to_start() bool { + if self.cursor.Y == 0 && self.cursor.X == 0 { + return false + } + self.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) { + return false + } + self.cursor.Y = len(self.lines) - 1 + self.move_to_end_of_line() + return true +} + +func (self *Readline) erase_between(start, end Position) { + if end.Less(start) { + start, end = end, start + } + if start.Y == end.Y { + line := self.lines[start.Y] + 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 + } else { + self.cursor.X -= end.X - start.X + } + } + return + } + lines := make([]string, 0, len(self.lines)) + for i, line := range self.lines { + if i < start.Y || i > end.Y { + lines = append(lines, line) + } else if i == start.Y { + lines = append(lines, line[:start.X]) + if self.cursor.Y == i && self.cursor.X > start.X { + self.cursor.X = start.X + } + } else if i == end.Y { + lines[len(lines)-1] += line[end.X:] + if i == self.cursor.Y { + self.cursor.Y = start.Y + if self.cursor.X < end.X { + self.cursor.X = start.X + } else { + self.cursor.X -= end.X - start.X + } + } + } else if i == self.cursor.Y { + self.cursor = start + } + } + self.lines = lines +} + +func (self *Readline) erase_chars_before_cursor(amt uint, traverse_line_breaks bool) uint { + pos := self.cursor + num := self.move_cursor_left(amt, traverse_line_breaks) + if num == 0 { + return num + } + self.erase_between(self.cursor, pos) + return num +} + +func (self *Readline) erase_chars_after_cursor(amt uint, traverse_line_breaks bool) uint { + pos := self.cursor + num := self.move_cursor_right(amt, traverse_line_breaks) + if num == 0 { + return num + } + self.erase_between(pos, self.cursor) + return num +} diff --git a/tools/tui/readline/actions_test.go b/tools/tui/readline/actions_test.go index 2bbeed1cc..474d59131 100644 --- a/tools/tui/readline/actions_test.go +++ b/tools/tui/readline/actions_test.go @@ -43,15 +43,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_pos_in_line = 2 + rl.cursor.X = 2 rl.add_text("12") }, "ab12", "cd", "ab12cd") dt("abcd", func(rl *Readline) { - rl.cursor_pos_in_line = 2 + rl.cursor.X = 2 rl.add_text("12\n34") }, "ab12\n34", "cd", "ab12\n34cd") dt("abcd\nxyz", func(rl *Readline) { - rl.cursor_pos_in_line = 2 + rl.cursor.X = 2 rl.add_text("12\n34") }, "abcd\nxy12\n34", "z", "abcd\nxy12\n34z") } @@ -88,8 +88,8 @@ func TestCursorMovement(t *testing.T) { }, "one", "à") right := func(rl *Readline, amt uint, moved_amt uint, traverse_line_breaks bool) { - rl.cursor_line = 0 - rl.cursor_pos_in_line = 0 + rl.cursor.Y = 0 + rl.cursor.X = 0 actual := rl.move_cursor_right(amt, traverse_line_breaks) if actual != moved_amt { t.Fatalf("Failed to move cursor by %#v\nactual != expected: %#v != %#v", amt, actual, moved_amt) @@ -111,3 +111,69 @@ func TestCursorMovement(t *testing.T) { right(rl, 1, 1, false) }, "à", "b") } + +func TestEraseChars(t *testing.T) { + dt := test_func(t) + + backspace := func(rl *Readline, amt uint, erased_amt uint, traverse_line_breaks bool) { + actual := rl.erase_chars_before_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) + } + } + dt("one\ntwo", func(rl *Readline) { + backspace(rl, 2, 2, false) + }, "one\nt", "") + dt("one\ntwo", func(rl *Readline) { + rl.cursor.X = 1 + backspace(rl, 2, 1, false) + }, "one\n", "wo") + dt("one\ntwo", func(rl *Readline) { + rl.cursor.X = 1 + backspace(rl, 2, 2, true) + }, "one", "wo") + dt("a😀", func(rl *Readline) { + backspace(rl, 1, 1, false) + }, "a", "") + dt("bà", func(rl *Readline) { + backspace(rl, 1, 1, false) + }, "b", "") + + del := func(rl *Readline, amt uint, erased_amt uint, traverse_line_breaks bool) { + rl.cursor.Y = 0 + rl.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) + } + } + dt("one\ntwo", func(rl *Readline) { + del(rl, 2, 2, false) + }, "", "e\ntwo") + dt("😀a", func(rl *Readline) { + del(rl, 1, 1, false) + }, "", "a") + dt("àb", func(rl *Readline) { + del(rl, 1, 1, false) + }, "", "b") + + dt("one\ntwo\nthree", func(rl *Readline) { + rl.erase_between(Position{X: 1}, Position{X: 2, Y: 2}) + }, "oree", "") + dt("one\ntwo\nthree", func(rl *Readline) { + rl.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.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.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.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 97f29cc82..72eb13ed2 100644 --- a/tools/tui/readline/api.go +++ b/tools/tui/readline/api.go @@ -21,6 +21,15 @@ type RlInit struct { DontMarkPrompts bool } +type Position struct { + X int + Y int +} + +func (self Position) Less(other Position) bool { + return self.Y < other.Y || (self.Y == other.Y && self.X < other.X) +} + type Readline struct { prompt string prompt_len int @@ -29,14 +38,12 @@ type Readline struct { mark_prompts bool loop *loop.Loop - // The number of lines after the initial line + // The number of lines after the initial line on the screen cursor_y int // Input lines lines []string - // The line the cursor is at currently - cursor_line int - // The offset into the text of the cursor line - cursor_pos_in_line int + // The cursor position in the text + cursor Position } func New(loop *loop.Loop, r RlInit) *Readline { diff --git a/tools/tui/readline/draw.go b/tools/tui/readline/draw.go index efd5bb785..c31aee378 100644 --- a/tools/tui/readline/draw.go +++ b/tools/tui/readline/draw.go @@ -46,13 +46,13 @@ func (self *Readline) redraw() { p = self.continuation_prompt } num_lines := self.write_line_with_prompt(line, p, int(screen_size.WidthCells)) - if i == self.cursor_line { + if i == self.cursor.Y { line_with_cursor = y } y += num_lines } self.loop.MoveCursorVertically(-y + line_with_cursor) - line := self.lines[self.cursor_line] - line_with_cursor += self.move_cursor_to_text_position(wcswidth.Stringwidth(line[:self.cursor_pos_in_line]), int(screen_size.WidthCells)) + line := self.lines[self.cursor.Y] + line_with_cursor += self.move_cursor_to_text_position(wcswidth.Stringwidth(line[:self.cursor.X]), int(screen_size.WidthCells)) self.cursor_y = line_with_cursor }