more work on history search

This commit is contained in:
Kovid Goyal 2022-11-08 06:21:02 +05:30
parent ffea66357a
commit 0c82832356
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
6 changed files with 356 additions and 193 deletions

View File

@ -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
}

View File

@ -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")
}

View File

@ -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 {

View File

@ -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)})

View File

@ -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 {

View File

@ -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)