From 3c4a411cadbf63a326efe97f153f1650eb8a7d3c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 16 Oct 2022 20:52:53 +0530 Subject: [PATCH] Rewrite the readline redraw code to make the screen lines explicit --- go.mod | 1 + go.sum | 2 + tools/tui/loop/api.go | 8 +++ tools/tui/readline/actions.go | 52 ++++++++++++++ tools/tui/readline/actions_test.go | 29 ++++++++ tools/tui/readline/api.go | 6 ++ tools/tui/readline/draw.go | 110 +++++++++++++++++++---------- 7 files changed, 171 insertions(+), 37 deletions(-) diff --git a/go.mod b/go.mod index ba6ebd207..20f9bf3e3 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.19 require ( github.com/ALTree/bigfloat v0.0.0-20220102081255-38c8b72a9924 + github.com/google/go-cmp v0.5.8 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid v1.3.0 github.com/jamesruan/go-rfc1924 v0.0.0-20170108144916-2767ca7c638f diff --git a/go.sum b/go.sum index 983372e83..746e8eb81 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/ALTree/bigfloat v0.0.0-20220102081255-38c8b72a9924 h1:DG4UyTVIujioxwJc8Zj8Nabz1L1wTgQ/xNBSQDfdP3I= github.com/ALTree/bigfloat v0.0.0-20220102081255-38c8b72a9924/go.mod h1:+NaH2gLeY6RPBPPQf4aRotPPStg+eXc8f9ZaE4vRfD4= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= diff --git a/tools/tui/loop/api.go b/tools/tui/loop/api.go index 7af13b0c1..13a733c76 100644 --- a/tools/tui/loop/api.go +++ b/tools/tui/loop/api.go @@ -244,6 +244,14 @@ func (self *Loop) EndBracketedPaste() { self.QueueWriteString(BRACKETED_PASTE.EscapeCodeToReset()) } +func (self *Loop) AllowLineWrapping(allow bool) { + if allow { + self.QueueWriteString(DECAWM.EscapeCodeToSet()) + } else { + self.QueueWriteString(DECAWM.EscapeCodeToReset()) + } +} + func (self *Loop) Quit(exit_code int) { self.exit_code = exit_code self.keep_going = false diff --git a/tools/tui/readline/actions.go b/tools/tui/readline/actions.go index 3c29d4f90..16170363a 100644 --- a/tools/tui/readline/actions.go +++ b/tools/tui/readline/actions.go @@ -162,6 +162,33 @@ func (self *Readline) move_cursor_right(amt uint, traverse_line_breaks bool) uin return amt_moved } +func move_up_one_line(self *Readline) bool { + return false +} + +func (self *Readline) move_cursor_up(amt uint) uint { + ans := uint(0) + if self.screen_width == 0 { + self.update_current_screen_size() + } + for ans < amt { + if move_up_one_line(self) { + ans++ + } else { + break + } + } + return ans +} + +func (self *Readline) move_cursor_down(amt uint) uint { + ans := uint(0) + if self.screen_width == 0 { + self.update_current_screen_size() + } + return ans +} + func (self *Readline) move_to_start_of_line() bool { if self.cursor.X > 0 { self.cursor.X = 0 @@ -306,6 +333,31 @@ func (self *Readline) perform_action(ac Action, repeat_count uint) error { return self.perform_action(ActionAcceptInput, 1) case ActionAcceptInput: return ErrAcceptInput + case ActionCursorUp: + if self.move_cursor_up(repeat_count) > 0 { + return nil + } + case ActionCursorDown: + if self.move_cursor_down(repeat_count) > 0 { + return nil + } + case ActionHistoryPreviousOrCursorUp: + if self.cursor.Y == 0 { + r := self.perform_action(ActionHistoryPrevious, repeat_count) + if r == nil { + return nil + } + } + return self.perform_action(ActionCursorUp, repeat_count) + case ActionHistoryNextOrCursorDown: + if self.cursor.Y == 0 { + r := self.perform_action(ActionHistoryNext, repeat_count) + if r == nil { + return nil + } + } + return self.perform_action(ActionCursorDown, repeat_count) + } return ErrCouldNotPerformAction } diff --git a/tools/tui/readline/actions_test.go b/tools/tui/readline/actions_test.go index 474d59131..1c91b8645 100644 --- a/tools/tui/readline/actions_test.go +++ b/tools/tui/readline/actions_test.go @@ -6,6 +6,8 @@ import ( "fmt" "kitty/tools/tui/loop" "testing" + + "github.com/google/go-cmp/cmp" ) var _ = fmt.Print @@ -56,6 +58,33 @@ func TestAddText(t *testing.T) { }, "abcd\nxy12\n34", "z", "abcd\nxy12\n34z") } +func TestGetScreenLines(t *testing.T) { + lp, _ := loop.New() + rl := New(lp, RlInit{Prompt: "$$ "}) + rl.screen_width = 10 + + tsl := func(expected ...ScreenLine) { + q := rl.get_screen_lines() + actual := make([]ScreenLine, len(q)) + for i, x := range q { + actual[i] = *x + } + if diff := cmp.Diff(expected, actual); diff != "" { + t.Fatalf("Did not get expected screen lines for: %#v\n%s", rl.AllText(), diff) + } + } + tsl(ScreenLine{PromptLen: 3, CursorCell: 3}) + rl.add_text("123") + tsl(ScreenLine{PromptLen: 3, CursorCell: 6, Text: "123", CursorTextPos: 3, TextLengthInCells: 3}) + rl.add_text("456") + tsl(ScreenLine{PromptLen: 3, CursorCell: 9, Text: "123456", CursorTextPos: 6, TextLengthInCells: 6}) + rl.add_text("7") + tsl( + ScreenLine{PromptLen: 3, CursorCell: -1, Text: "1234567", CursorTextPos: -1, TextLengthInCells: 7}, + ScreenLine{OffsetInParentLine: 7}, + ) +} + func TestCursorMovement(t *testing.T) { dt := test_func(t) diff --git a/tools/tui/readline/api.go b/tools/tui/readline/api.go index a8ab8d725..61cf8430f 100644 --- a/tools/tui/readline/api.go +++ b/tools/tui/readline/api.go @@ -47,6 +47,12 @@ const ( ActionCursorRight ActionEndInput ActionAcceptInput + ActionCursorUp + ActionHistoryPreviousOrCursorUp + ActionCursorDown + ActionHistoryNextOrCursorDown + ActionHistoryNext + ActionHistoryPrevious ) type Readline struct { diff --git a/tools/tui/readline/draw.go b/tools/tui/readline/draw.go index f407536c5..f5f183460 100644 --- a/tools/tui/readline/draw.go +++ b/tools/tui/readline/draw.go @@ -9,26 +9,6 @@ import ( var _ = fmt.Print -func (self *Readline) write_line_with_prompt(line, prompt string, screen_width int) int { - self.loop.QueueWriteString(prompt) - self.loop.QueueWriteString(line) - w := wcswidth.Stringwidth(prompt) + wcswidth.Stringwidth(line) - num_lines := w / screen_width - if w%screen_width == 0 { - num_lines-- - } - return num_lines -} - -func (self *Readline) move_cursor_to_text_position(pos, screen_width int) int { - num_of_lines := pos / screen_width - self.loop.MoveCursorVertically(num_of_lines) - self.loop.QueueWriteString("\r") - x := pos % screen_width - self.loop.MoveCursorHorizontally(x) - return num_of_lines -} - func (self *Readline) update_current_screen_size() { screen_size, err := self.loop.ScreenSize() if err != nil { @@ -44,6 +24,52 @@ func (self *Readline) update_current_screen_size() { self.screen_width = int(screen_size.WidthCells) } +type ScreenLine struct { + ParentLineNumber, OffsetInParentLine, PromptLen int + TextLengthInCells, CursorCell, CursorTextPos int + Text string +} + +func (self *Readline) get_screen_lines() []*ScreenLine { + if self.screen_width == 0 { + self.update_current_screen_size() + } + ans := make([]*ScreenLine, 0, len(self.lines)) + found_cursor := false + for i, line := range self.lines { + plen := self.prompt_len + if i > 0 { + plen = self.continuation_prompt_len + } + offset := 0 + has_cursor := i == self.cursor.Y + for is_first := true; is_first || offset < len(line); is_first = false { + l, width := wcswidth.TruncateToVisualLengthWithWidth(line[offset:], self.screen_width-plen) + sl := ScreenLine{ + ParentLineNumber: i, OffsetInParentLine: offset, + PromptLen: plen, TextLengthInCells: width, + CursorCell: -1, Text: l, CursorTextPos: -1, + } + ans = append(ans, &sl) + if has_cursor && !found_cursor && offset <= self.cursor.X && self.cursor.X <= offset+len(l) { + found_cursor = true + ctpos := self.cursor.X - offset + ccell := plen + wcswidth.Stringwidth(l[:ctpos]) + if ccell >= self.screen_width { + ans = append(ans, &ScreenLine{OffsetInParentLine: len(line)}) + } else { + sl.CursorTextPos = ctpos + sl.CursorCell = ccell + } + } + plen = 0 + is_first = false + offset += len(l) + } + } + return ans +} + func (self *Readline) redraw() { if self.screen_width == 0 { self.update_current_screen_size() @@ -56,26 +82,36 @@ func (self *Readline) redraw() { } self.loop.QueueWriteString("\r") self.loop.ClearToEndOfScreen() - line_with_cursor := 0 - y := 0 - for i, line := range self.lines { - p := self.prompt + cursor_x := -1 + cursor_y := 0 + move_cursor_up_by := 0 + self.loop.AllowLineWrapping(false) + for i, sl := range self.get_screen_lines() { + self.loop.QueueWriteString("\r") if i > 0 { - y += 1 - self.loop.QueueWriteString("\r\n") - p = self.continuation_prompt + self.loop.QueueWriteString("\n") } - if i == self.cursor.Y { - line_with_cursor = y + if sl.PromptLen > 0 { + if i == 0 { + self.loop.QueueWriteString(self.prompt) + } else { + self.loop.QueueWriteString(self.continuation_prompt) + } } - y += self.write_line_with_prompt(line, p, self.screen_width) + self.loop.QueueWriteString(sl.Text) + if sl.CursorCell > -1 { + cursor_x = sl.CursorCell + } else if cursor_x > -1 { + move_cursor_up_by++ + } + cursor_y++ } - self.loop.MoveCursorVertically(-y + line_with_cursor) - line := self.lines[self.cursor.Y] - plen := self.prompt_len - if self.cursor.Y > 0 { - plen = self.continuation_prompt_len + self.loop.AllowLineWrapping(true) + self.loop.MoveCursorVertically(-move_cursor_up_by) + self.loop.QueueWriteString("\r") + self.loop.MoveCursorHorizontally(cursor_x) + self.cursor_y = 0 + if cursor_y > 0 { + self.cursor_y = cursor_y - 1 } - line_with_cursor += self.move_cursor_to_text_position(plen+wcswidth.Stringwidth(line[:self.cursor.X]), self.screen_width) - self.cursor_y = line_with_cursor }