more work on history search

This commit is contained in:
Kovid Goyal
2022-11-08 06:21:02 +05:30
parent ffea66357a
commit 0c82832356
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 { func (self *Readline) text_upto_cursor_pos() string {
buf := strings.Builder{} buf := strings.Builder{}
buf.Grow(1024) buf.Grow(1024)
for i, line := range self.lines { for i, line := range self.input_state.lines {
if i == self.cursor.Y { if i == self.input_state.cursor.Y {
buf.WriteString(line[:utils.Min(len(line), self.cursor.X)]) buf.WriteString(line[:utils.Min(len(line), self.input_state.cursor.X)])
break break
} else { } else {
buf.WriteString(line) buf.WriteString(line)
@@ -32,11 +32,11 @@ func (self *Readline) text_upto_cursor_pos() string {
func (self *Readline) text_after_cursor_pos() string { func (self *Readline) text_after_cursor_pos() string {
buf := strings.Builder{} buf := strings.Builder{}
buf.Grow(1024) buf.Grow(1024)
for i, line := range self.lines { for i, line := range self.input_state.lines {
if i == self.cursor.Y { if i == self.input_state.cursor.Y {
buf.WriteString(line[utils.Min(len(line), self.cursor.X):]) buf.WriteString(line[utils.Min(len(line), self.input_state.cursor.X):])
buf.WriteString("\n") buf.WriteString("\n")
} else if i > self.cursor.Y { } else if i > self.input_state.cursor.Y {
buf.WriteString(line) buf.WriteString(line)
buf.WriteString("\n") buf.WriteString("\n")
} }
@@ -49,35 +49,35 @@ func (self *Readline) text_after_cursor_pos() string {
} }
func (self *Readline) all_text() 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) { func (self *Readline) add_text(text string) {
new_lines := make([]string, 0, len(self.lines)+4) new_lines := make([]string, 0, len(self.input_state.lines)+4)
new_lines = append(new_lines, self.lines[:self.cursor.Y]...) new_lines = append(new_lines, self.input_state.lines[:self.input_state.cursor.Y]...)
var lines_after []string var lines_after []string
if len(self.lines) > self.cursor.Y+1 { if len(self.input_state.lines) > self.input_state.cursor.Y+1 {
lines_after = self.lines[self.cursor.Y+1:] lines_after = self.input_state.lines[self.input_state.cursor.Y+1:]
} }
has_trailing_newline := strings.HasSuffix(text, "\n") has_trailing_newline := strings.HasSuffix(text, "\n")
add_line_break := func(line string) { add_line_break := func(line string) {
new_lines = append(new_lines, line) new_lines = append(new_lines, line)
self.cursor.X = len(line) self.input_state.cursor.X = len(line)
self.cursor.Y += 1 self.input_state.cursor.Y += 1
} }
cline := self.lines[self.cursor.Y] cline := self.input_state.lines[self.input_state.cursor.Y]
before_first_line := cline[:self.cursor.X] before_first_line := cline[:self.input_state.cursor.X]
after_first_line := "" after_first_line := ""
if self.cursor.X < len(cline) { if self.input_state.cursor.X < len(cline) {
after_first_line = cline[self.cursor.X:] after_first_line = cline[self.input_state.cursor.X:]
} }
for i, line := range utils.Splitlines(text) { for i, line := range utils.Splitlines(text) {
if i > 0 { if i > 0 {
add_line_break(line) add_line_break(line)
} else { } else {
line := before_first_line + line line := before_first_line + line
self.cursor.X = len(line) self.input_state.cursor.X = len(line)
new_lines = append(new_lines, line) new_lines = append(new_lines, line)
} }
} }
@@ -93,23 +93,23 @@ func (self *Readline) add_text(text string) {
if len(lines_after) > 0 { if len(lines_after) > 0 {
new_lines = append(new_lines, lines_after...) 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) { func (self *Readline) move_cursor_left(amt uint, traverse_line_breaks bool) (amt_moved uint) {
for amt_moved < amt { for amt_moved < amt {
if self.cursor.X == 0 { if self.input_state.cursor.X == 0 {
if !traverse_line_breaks || self.cursor.Y == 0 { if !traverse_line_breaks || self.input_state.cursor.Y == 0 {
return amt_moved return amt_moved
} }
self.cursor.Y -= 1 self.input_state.cursor.Y -= 1
self.cursor.X = len(self.lines[self.cursor.Y]) self.input_state.cursor.X = len(self.input_state.lines[self.input_state.cursor.Y])
amt_moved++ amt_moved++
continue continue
} }
line := self.lines[self.cursor.Y] line := self.input_state.lines[self.input_state.cursor.Y]
for ci := wcswidth.NewCellIterator(line[:self.cursor.X]).GotoEnd(); amt_moved < amt && ci.Backward(); amt_moved++ { for ci := wcswidth.NewCellIterator(line[:self.input_state.cursor.X]).GotoEnd(); amt_moved < amt && ci.Backward(); amt_moved++ {
self.cursor.X -= len(ci.Current()) self.input_state.cursor.X -= len(ci.Current())
} }
} }
return amt_moved 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) { func (self *Readline) move_cursor_right(amt uint, traverse_line_breaks bool) (amt_moved uint) {
for amt_moved < amt { for amt_moved < amt {
line := self.lines[self.cursor.Y] line := self.input_state.lines[self.input_state.cursor.Y]
if self.cursor.X >= len(line) { if self.input_state.cursor.X >= len(line) {
if !traverse_line_breaks || self.cursor.Y == len(self.lines)-1 { if !traverse_line_breaks || self.input_state.cursor.Y == len(self.input_state.lines)-1 {
return amt_moved return amt_moved
} }
self.cursor.Y += 1 self.input_state.cursor.Y += 1
self.cursor.X = 0 self.input_state.cursor.X = 0
amt_moved++ amt_moved++
continue continue
} }
for ci := wcswidth.NewCellIterator(line[self.cursor.X:]); amt_moved < amt && ci.Forward(); amt_moved++ { for ci := wcswidth.NewCellIterator(line[self.input_state.cursor.X:]); amt_moved < amt && ci.Forward(); amt_moved++ {
self.cursor.X += len(ci.Current()) self.input_state.cursor.X += len(ci.Current())
} }
} }
return amt_moved 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) { func (self *Readline) move_cursor_to_target_line(source_line, target_line *ScreenLine) {
if source_line != target_line { if source_line != target_line {
visual_distance_into_text := source_line.CursorCell - source_line.Prompt.Length 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) 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 { func (self *Readline) move_to_start_of_line() bool {
if self.cursor.X > 0 { if self.input_state.cursor.X > 0 {
self.cursor.X = 0 self.input_state.cursor.X = 0
return true return true
} }
return false return false
} }
func (self *Readline) move_to_end_of_line() bool { func (self *Readline) move_to_end_of_line() bool {
line := self.lines[self.cursor.Y] line := self.input_state.lines[self.input_state.cursor.Y]
if self.cursor.X >= len(line) { if self.input_state.cursor.X >= len(line) {
return false return false
} }
self.cursor.X = len(line) self.input_state.cursor.X = len(line)
return true return true
} }
func (self *Readline) move_to_start() bool { 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 return false
} }
self.cursor.Y = 0 self.input_state.cursor.Y = 0
self.move_to_start_of_line() self.move_to_start_of_line()
return true return true
} }
func (self *Readline) move_to_end() bool { func (self *Readline) move_to_end() bool {
line := self.lines[self.cursor.Y] line := self.input_state.lines[self.input_state.cursor.Y]
if self.cursor.Y == len(self.lines)-1 && self.cursor.X >= len(line) { if self.input_state.cursor.Y == len(self.input_state.lines)-1 && self.input_state.cursor.X >= len(line) {
return false 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() self.move_to_end_of_line()
return true return true
} }
@@ -214,68 +214,68 @@ func (self *Readline) erase_between(start, end Position) string {
} }
buf := strings.Builder{} buf := strings.Builder{}
if start.Y == end.Y { if start.Y == end.Y {
line := self.lines[start.Y] line := self.input_state.lines[start.Y]
buf.WriteString(line[start.X:end.X]) buf.WriteString(line[start.X:end.X])
self.lines[start.Y] = line[:start.X] + line[end.X:] self.input_state.lines[start.Y] = line[:start.X] + line[end.X:]
if self.cursor.Y == start.Y && self.cursor.X >= start.X { if self.input_state.cursor.Y == start.Y && self.input_state.cursor.X >= start.X {
if self.cursor.X < end.X { if self.input_state.cursor.X < end.X {
self.cursor.X = start.X self.input_state.cursor.X = start.X
} else { } else {
self.cursor.X -= end.X - start.X self.input_state.cursor.X -= end.X - start.X
} }
} }
return buf.String() return buf.String()
} }
lines := make([]string, 0, len(self.lines)) lines := make([]string, 0, len(self.input_state.lines))
for i, line := range self.lines { for i, line := range self.input_state.lines {
if i < start.Y || i > end.Y { if i < start.Y || i > end.Y {
lines = append(lines, line) lines = append(lines, line)
} else if i == start.Y { } else if i == start.Y {
lines = append(lines, line[:start.X]) lines = append(lines, line[:start.X])
buf.WriteString(line[start.X:]) buf.WriteString(line[start.X:])
if self.cursor.Y == i && self.cursor.X > start.X { if self.input_state.cursor.Y == i && self.input_state.cursor.X > start.X {
self.cursor.X = start.X self.input_state.cursor.X = start.X
} }
} else if i == end.Y { } else if i == end.Y {
lines[len(lines)-1] += line[end.X:] lines[len(lines)-1] += line[end.X:]
buf.WriteString(line[:end.X]) buf.WriteString(line[:end.X])
if i == self.cursor.Y { if i == self.input_state.cursor.Y {
self.cursor.Y = start.Y self.input_state.cursor.Y = start.Y
if self.cursor.X < end.X { if self.input_state.cursor.X < end.X {
self.cursor.X = start.X self.input_state.cursor.X = start.X
} else { } else {
self.cursor.X -= end.X - start.X self.input_state.cursor.X -= end.X - start.X
} }
} }
} else { } else {
if i == self.cursor.Y { if i == self.input_state.cursor.Y {
self.cursor = start self.input_state.cursor = start
} }
buf.WriteString(line) buf.WriteString(line)
buf.WriteString("\n") buf.WriteString("\n")
} }
} }
self.lines = lines self.input_state.lines = lines
return buf.String() return buf.String()
} }
func (self *Readline) erase_chars_before_cursor(amt uint, traverse_line_breaks bool) uint { 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) num := self.move_cursor_left(amt, traverse_line_breaks)
if num == 0 { if num == 0 {
return num return num
} }
self.erase_between(self.cursor, pos) self.erase_between(self.input_state.cursor, pos)
return num return num
} }
func (self *Readline) erase_chars_after_cursor(amt uint, traverse_line_breaks bool) uint { 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) num := self.move_cursor_right(amt, traverse_line_breaks)
if num == 0 { if num == 0 {
return num return num
} }
self.erase_between(pos, self.cursor) self.erase_between(pos, self.input_state.cursor)
return num return num
} }
@@ -292,9 +292,9 @@ func (self *Readline) move_to_end_of_word(amt uint, traverse_line_breaks bool, i
if amt == 0 { if amt == 0 {
return 0 return 0
} }
line := self.lines[self.cursor.Y] line := self.input_state.lines[self.input_state.cursor.Y]
in_word := false in_word := false
ci := wcswidth.NewCellIterator(line[self.cursor.X:]) ci := wcswidth.NewCellIterator(line[self.input_state.cursor.X:])
sz := 0 sz := 0
for ci.Forward() { 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 { if current_is_word_char {
in_word = true in_word = true
} else if in_word { } else if in_word {
self.cursor.X += plen self.input_state.cursor.X += plen
amt-- amt--
num_of_words_moved++ num_of_words_moved++
if amt == 0 { 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++ num_of_words_moved++
} }
if amt > 0 { if amt > 0 {
if traverse_line_breaks && self.cursor.Y < len(self.lines)-1 { if traverse_line_breaks && self.input_state.cursor.Y < len(self.input_state.lines)-1 {
self.cursor.Y++ self.input_state.cursor.Y++
self.cursor.X = 0 self.input_state.cursor.X = 0
num_of_words_moved += self.move_to_end_of_word(amt, traverse_line_breaks, is_part_of_word) 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 { if amt == 0 {
return 0 return 0
} }
line := self.lines[self.cursor.Y] line := self.input_state.lines[self.input_state.cursor.Y]
in_word := false in_word := false
ci := wcswidth.NewCellIterator(line[:self.cursor.X]).GotoEnd() ci := wcswidth.NewCellIterator(line[:self.input_state.cursor.X]).GotoEnd()
sz := 0 sz := 0
for ci.Backward() { 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 { if current_is_word_char {
in_word = true in_word = true
} else if in_word { } else if in_word {
self.cursor.X -= plen self.input_state.cursor.X -= plen
amt-- amt--
num_of_words_moved++ num_of_words_moved++
if amt == 0 { 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++ num_of_words_moved++
} }
if amt > 0 { if amt > 0 {
if traverse_line_breaks && self.cursor.Y > 0 { if traverse_line_breaks && self.input_state.cursor.Y > 0 {
self.cursor.Y-- self.input_state.cursor.Y--
self.cursor.X = len(self.lines[self.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) 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 { func (self *Readline) kill_to_end_of_line() bool {
line := self.lines[self.cursor.Y] line := self.input_state.lines[self.input_state.cursor.Y]
if self.cursor.X >= len(line) { if self.input_state.cursor.X >= len(line) {
return false return false
} }
self.lines[self.cursor.Y] = line[:self.cursor.X] self.input_state.lines[self.input_state.cursor.Y] = line[:self.input_state.cursor.X]
self.kill_text(line[self.cursor.X:]) self.kill_text(line[self.input_state.cursor.X:])
return true return true
} }
func (self *Readline) kill_to_start_of_line() bool { func (self *Readline) kill_to_start_of_line() bool {
line := self.lines[self.cursor.Y] line := self.input_state.lines[self.input_state.cursor.Y]
if self.cursor.X <= 0 { if self.input_state.cursor.X <= 0 {
return false return false
} }
self.lines[self.cursor.Y] = line[self.cursor.X:] self.input_state.lines[self.input_state.cursor.Y] = line[self.input_state.cursor.X:]
self.kill_text(line[:self.cursor.X]) self.kill_text(line[:self.input_state.cursor.X])
self.cursor.X = 0 self.input_state.cursor.X = 0
return true return true
} }
func (self *Readline) kill_next_word(amt uint, traverse_line_breaks bool) (num_killed uint) { 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) num_killed = self.move_to_end_of_word(amt, traverse_line_breaks, has_word_chars)
if num_killed > 0 { 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 return num_killed
} }
func (self *Readline) kill_previous_word(amt uint, traverse_line_breaks bool) (num_killed uint) { 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) num_killed = self.move_to_start_of_word(amt, traverse_line_breaks, has_word_chars)
if num_killed > 0 { 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 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) { 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) num_killed = self.move_to_start_of_word(amt, traverse_line_breaks, has_no_space_chars)
if num_killed > 0 { 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 return num_killed
} }
func (self *Readline) ensure_position_in_bounds(pos *Position) *Position { func (self *Readline) ensure_position_in_bounds(pos *Position) *Position {
pos.Y = utils.Max(0, utils.Min(pos.Y, len(self.lines)-1)) pos.Y = utils.Max(0, utils.Min(pos.Y, len(self.input_state.lines)-1))
line := self.lines[pos.Y] line := self.input_state.lines[pos.Y]
pos.X = utils.Max(0, utils.Min(pos.X, len(line))) pos.X = utils.Max(0, utils.Min(pos.X, len(line)))
return pos return pos
} }
@@ -451,24 +451,24 @@ func (self *Readline) yank(repeat_count uint, pop bool) bool {
if text == "" { if text == "" {
return false return false
} }
before := self.cursor before := self.input_state.cursor
if pop { if pop {
self.ensure_position_in_bounds(&self.last_yank_extent.start) self.ensure_position_in_bounds(&self.last_yank_extent.start)
self.ensure_position_in_bounds(&self.last_yank_extent.end) self.ensure_position_in_bounds(&self.last_yank_extent.end)
self.erase_between(self.last_yank_extent.start, 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 self.input_state.cursor = self.last_yank_extent.start
before = self.cursor before = self.input_state.cursor
} }
self.add_text(text) self.add_text(text)
self.last_yank_extent.start = before self.last_yank_extent.start = before
self.last_yank_extent.end = self.cursor self.last_yank_extent.end = self.input_state.cursor
return true return true
} }
func (self *Readline) apply_history_text(text string) { func (self *Readline) apply_history_text(text string) {
self.lines = utils.Splitlines(text) self.input_state.lines = utils.Splitlines(text)
if len(self.lines) == 0 { if len(self.input_state.lines) == 0 {
self.lines = []string{""} 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 }() defer func() { self.last_action = ac }()
switch ac { switch ac {
case ActionBackspace: case ActionBackspace:
if self.erase_chars_before_cursor(repeat_count, true) > 0 { if self.history_search != nil {
return 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: case ActionDelete:
if self.erase_chars_after_cursor(repeat_count, true) > 0 { 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 return nil
} }
case ActionEndInput: case ActionEndInput:
line := self.lines[self.cursor.Y] line := self.input_state.lines[self.input_state.cursor.Y]
if line == "" { if line == "" {
return io.EOF return io.EOF
} }
@@ -672,6 +678,16 @@ func (self *Readline) perform_action(ac Action, repeat_count uint) error {
self.add_text(text) self.add_text(text)
} }
return nil 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 return ErrCouldNotPerformAction
} }

View File

@@ -48,15 +48,15 @@ func TestAddText(t *testing.T) {
dt("test", nil, "test", "", "test") dt("test", nil, "test", "", "test")
dt("1234\n", nil, "1234\n", "", "1234\n") dt("1234\n", nil, "1234\n", "", "1234\n")
dt("abcd", func(rl *Readline) { dt("abcd", func(rl *Readline) {
rl.cursor.X = 2 rl.input_state.cursor.X = 2
rl.add_text("12") rl.add_text("12")
}, "ab12", "cd", "ab12cd") }, "ab12", "cd", "ab12cd")
dt("abcd", func(rl *Readline) { dt("abcd", func(rl *Readline) {
rl.cursor.X = 2 rl.input_state.cursor.X = 2
rl.add_text("12\n34") rl.add_text("12\n34")
}, "ab12\n34", "cd", "ab12\n34cd") }, "ab12\n34", "cd", "ab12\n34cd")
dt("abcd\nxyz", func(rl *Readline) { dt("abcd\nxyz", func(rl *Readline) {
rl.cursor.X = 2 rl.input_state.cursor.X = 2
rl.add_text("12\n34") rl.add_text("12\n34")
}, "abcd\nxy12\n34", "z", "abcd\nxy12\n34z") }, "abcd\nxy12\n34", "z", "abcd\nxy12\n34z")
} }
@@ -80,7 +80,7 @@ func TestGetScreenLines(t *testing.T) {
actual[i] = *x actual[i] = *x
} }
if diff := cmp.Diff(expected, actual); diff != "" { 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}) 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{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"}, 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( tsl(
ScreenLine{Prompt: p(true), CursorCell: 5, Text: "123", CursorTextPos: 2, TextLengthInCells: 3}, 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{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"}, 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( tsl(
ScreenLine{Prompt: p(true), CursorCell: -1, Text: "123", CursorTextPos: -1, TextLengthInCells: 3}, 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{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"}, 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( tsl(
ScreenLine{Prompt: p(true), CursorCell: -1, Text: "123", CursorTextPos: -1, TextLengthInCells: 3}, 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}, 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.ResetText()
rl.add_text("1234567\nabc") rl.add_text("1234567\nabc")
rl.cursor = Position{X: 7} rl.input_state.cursor = Position{X: 7}
tsl( tsl(
ScreenLine{Prompt: p(true), CursorCell: -1, Text: "1234567", CursorTextPos: -1, TextLengthInCells: 7}, 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}, ScreenLine{ParentLineNumber: 1, Prompt: p(false), Text: "abc", CursorCell: 2, TextLengthInCells: 3, CursorTextPos: 0},
@@ -164,8 +164,8 @@ func TestCursorMovement(t *testing.T) {
}, "one", "à") }, "one", "à")
right := func(rl *Readline, amt uint, moved_amt uint, traverse_line_breaks bool) { right := func(rl *Readline, amt uint, moved_amt uint, traverse_line_breaks bool) {
rl.cursor.Y = 0 rl.input_state.cursor.Y = 0
rl.cursor.X = 0 rl.input_state.cursor.X = 0
actual := rl.move_cursor_right(amt, traverse_line_breaks) actual := rl.move_cursor_right(amt, traverse_line_breaks)
if actual != moved_amt { if actual != moved_amt {
t.Fatalf("Failed to move cursor by %d\nactual != expected: %d != %d", amt, 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 { if len(initials) > 0 {
initial = initials[0] initial = initials[0]
} }
rl.cursor = initial rl.input_state.cursor = initial
actual := rl.move_cursor_vertically(amt) actual := rl.move_cursor_vertically(amt)
if actual != moved_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) 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") rl.add_text("o\u0300ne two three\nfour five")
wf := func(amt uint, expected_amt uint, text_before_cursor string) { 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) actual_amt := rl.move_to_end_of_word(amt, true, has_word_chars)
if actual_amt != expected_amt { if actual_amt != expected_amt {
t.Fatalf("Failed to move to word end, expected amt (%d) != actual amt (%d)", expected_amt, actual_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) 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")
wf(1, 1, "òne two") wf(1, 1, "òne two")
wf(1, 1, "òne two three") wf(1, 1, "òne two three")
wf(1, 1, "òne two three\nfour") wf(1, 1, "òne two three\nfour")
wf(1, 1, "òne two three\nfour five") wf(1, 1, "òne two three\nfour five")
wf(1, 0, "ò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") 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") wf(1, 1, "òne two")
wb := func(amt uint, expected_amt uint, text_before_cursor string) { 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) actual_amt := rl.move_to_start_of_word(amt, true, has_word_chars)
if actual_amt != expected_amt { if actual_amt != expected_amt {
t.Fatalf("Failed to move to word end, expected amt (%d) != actual amt (%d)", expected_amt, actual_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) 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, "") 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\nfour ")
wb(1, 1, "òne two three\n") wb(1, 1, "òne two three\n")
wb(1, 1, "òne two ") wb(1, 1, "òne two ")
wb(1, 1, "òne ") wb(1, 1, "òne ")
wb(1, 1, "") wb(1, 1, "")
wb(1, 0, "") wb(1, 0, "")
rl.cursor = Position{X: 8, Y: 1} rl.input_state.cursor = Position{X: 8, Y: 1}
wb(5, 5, "") wb(5, 5, "")
rl.cursor = Position{X: 5} rl.input_state.cursor = Position{X: 5}
wb(1, 1, "") wb(1, 1, "")
} }
@@ -330,11 +330,11 @@ func TestEraseChars(t *testing.T) {
backspace(rl, 2, 2, false) backspace(rl, 2, 2, false)
}, "one\nt", "") }, "one\nt", "")
dt("one\ntwo", func(rl *Readline) { dt("one\ntwo", func(rl *Readline) {
rl.cursor.X = 1 rl.input_state.cursor.X = 1
backspace(rl, 2, 1, false) backspace(rl, 2, 1, false)
}, "one\n", "wo") }, "one\n", "wo")
dt("one\ntwo", func(rl *Readline) { dt("one\ntwo", func(rl *Readline) {
rl.cursor.X = 1 rl.input_state.cursor.X = 1
backspace(rl, 2, 2, true) backspace(rl, 2, 2, true)
}, "one", "wo") }, "one", "wo")
dt("a😀", func(rl *Readline) { dt("a😀", func(rl *Readline) {
@@ -345,8 +345,8 @@ func TestEraseChars(t *testing.T) {
}, "b", "") }, "b", "")
del := func(rl *Readline, amt uint, erased_amt uint, traverse_line_breaks bool) { del := func(rl *Readline, amt uint, erased_amt uint, traverse_line_breaks bool) {
rl.cursor.Y = 0 rl.input_state.cursor.Y = 0
rl.cursor.X = 0 rl.input_state.cursor.X = 0
actual := rl.erase_chars_after_cursor(amt, traverse_line_breaks) actual := rl.erase_chars_after_cursor(amt, traverse_line_breaks)
if actual != erased_amt { if actual != erased_amt {
t.Fatalf("Failed to move cursor by %#v\nactual != expected: %d != %d", amt, 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}) rl.erase_between(Position{X: 1}, Position{X: 2, Y: 2})
}, "oree", "") }, "oree", "")
dt("one\ntwo\nthree", func(rl *Readline) { 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}) rl.erase_between(Position{X: 1}, Position{X: 2, Y: 2})
}, "o", "ree") }, "o", "ree")
dt("one\ntwo\nthree", func(rl *Readline) { 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}) rl.erase_between(Position{X: 1}, Position{X: 2, Y: 2})
}, "o", "ree") }, "o", "ree")
dt("one\ntwo\nthree", func(rl *Readline) { 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}) rl.erase_between(Position{X: 1}, Position{X: 2, Y: 2})
}, "o", "ree") }, "o", "ree")
dt("one\ntwo\nthree", func(rl *Readline) { 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}) rl.erase_between(Position{X: 1}, Position{X: 2, Y: 2})
}, "", "oree") }, "", "oree")
} }

View File

@@ -17,6 +17,8 @@ var _ = fmt.Print
const ST = "\x1b\\" const ST = "\x1b\\"
const PROMPT_MARK = "\x1b]133;" const PROMPT_MARK = "\x1b]133;"
type SyntaxHighlightFunction func(text string, x, y int) string
type RlInit struct { type RlInit struct {
Prompt string Prompt string
HistoryPath string HistoryPath string
@@ -24,6 +26,7 @@ type RlInit struct {
ContinuationPrompt string ContinuationPrompt string
EmptyContinuationPrompt bool EmptyContinuationPrompt bool
DontMarkPrompts bool DontMarkPrompts bool
SyntaxHighlighter SyntaxHighlightFunction
} }
type Position struct { 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) return self.Y < other.Y || (self.Y == other.Y && self.X < other.X)
} }
// Actions {{{
type Action uint type Action uint
const ( const (
@@ -61,6 +65,8 @@ const (
ActionHistoryLast ActionHistoryLast
ActionHistoryIncrementalSearchBackwards ActionHistoryIncrementalSearchBackwards
ActionHistoryIncrementalSearchForwards ActionHistoryIncrementalSearchForwards
ActionTerminateHistorySearchAndApply
ActionTerminateHistorySearchAndRestore
ActionClearScreen ActionClearScreen
ActionAddText ActionAddText
ActionAbortCurrentLine ActionAbortCurrentLine
@@ -89,6 +95,8 @@ const (
ActionNumericArgumentDigitMinus ActionNumericArgumentDigitMinus
) )
// }}}
type kill_ring struct { type kill_ring struct {
items *list.List items *list.List
} }
@@ -133,6 +141,28 @@ type Prompt struct {
Length int 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 { type Readline struct {
prompt, continuation_prompt Prompt prompt, continuation_prompt Prompt
@@ -141,13 +171,10 @@ type Readline struct {
history *History history *History
kill_ring kill_ring kill_ring kill_ring
input_state InputState
// The number of lines after the initial line on the screen // The number of lines after the initial line on the screen
cursor_y int cursor_y int
screen_width int screen_width int
// Input lines
lines []string
// The cursor position in the text
cursor Position
last_yank_extent struct { last_yank_extent struct {
start, end Position start, end Position
} }
@@ -158,6 +185,7 @@ type Readline struct {
keyboard_state KeyboardState keyboard_state KeyboardState
fmt_ctx *markup.Context fmt_ctx *markup.Context
text_to_be_added string text_to_be_added string
syntax_highlighted syntax_highlighted
} }
func (self *Readline) make_prompt(text string, is_secondary bool) Prompt { 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{ ans := &Readline{
mark_prompts: !r.DontMarkPrompts, fmt_ctx: markup.New(true), 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) ans.prompt = ans.make_prompt(r.Prompt, false)
t := "" t := ""
@@ -201,11 +231,10 @@ func (self *Readline) AddHistoryItem(hi HistoryItem) {
} }
func (self *Readline) ResetText() { func (self *Readline) ResetText() {
self.lines = []string{""} self.input_state = InputState{lines: []string{""}}
self.cursor = Position{}
self.cursor_y = 0
self.last_action = ActionNil self.last_action = ActionNil
self.keyboard_state = KeyboardState{} self.keyboard_state = KeyboardState{}
self.history_search = nil
} }
func (self *Readline) ChangeLoopAndResetText(lp *loop.Loop) { func (self *Readline) ChangeLoopAndResetText(lp *loop.Loop) {
@@ -278,7 +307,7 @@ func (self *Readline) AllText() string {
} }
func (self *Readline) CursorAtEndOfLine() bool { 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 { func (self *Readline) OnResize(old_size loop.ScreenSize, new_size loop.ScreenSize) error {

View File

@@ -4,7 +4,9 @@ package readline
import ( import (
"fmt" "fmt"
"kitty/tools/utils"
"kitty/tools/wcswidth" "kitty/tools/wcswidth"
"strings"
) )
var _ = fmt.Print 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 { 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 != "" { 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) 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 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 { func (self *Readline) get_screen_lines() []*ScreenLine {
if self.screen_width == 0 { if self.screen_width == 0 {
self.update_current_screen_size() 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 found_cursor := false
cursor_at_start_of_next_line := 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) prompt := self.prompt_for_line_number(i)
offset := 0 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 { for is_first := true; is_first || offset < len(line); is_first = false {
l, width := wcswidth.TruncateToVisualLengthWithWidth(line[offset:], self.screen_width-prompt.Length) l, width := wcswidth.TruncateToVisualLengthWithWidth(line[offset:], self.screen_width-prompt.Length)
sl := ScreenLine{ sl := ScreenLine{
@@ -73,12 +106,12 @@ func (self *Readline) get_screen_lines() []*ScreenLine {
sl.CursorTextPos = 0 sl.CursorTextPos = 0
} }
ans = append(ans, &sl) 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 found_cursor = true
ctpos := self.cursor.X - offset ctpos := cursor.X - offset
ccell := prompt.Length + wcswidth.Stringwidth(l[:ctpos]) ccell := prompt.Length + wcswidth.Stringwidth(l[:ctpos])
if ccell >= self.screen_width { 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 cursor_at_start_of_next_line = true
} else { } else {
ans = append(ans, &ScreenLine{ParentLineNumber: i, OffsetInParentLine: len(line)}) ans = append(ans, &ScreenLine{ParentLineNumber: i, OffsetInParentLine: len(line)})

View File

@@ -12,6 +12,8 @@ import (
"kitty/tools/utils" "kitty/tools/utils"
"kitty/tools/wcswidth" "kitty/tools/wcswidth"
"github.com/google/shlex"
) )
var _ = fmt.Print var _ = fmt.Print
@@ -31,13 +33,12 @@ type HistoryMatches struct {
} }
type HistorySearch struct { type HistorySearch struct {
query string query string
tokens []string tokens []string
items []*HistoryItem items []*HistoryItem
current_idx int current_idx int
backwards bool backwards bool
original_lines []string original_input_state InputState
original_cursor Position
} }
type History struct { 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) { 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() self.markup_history_search()
} }
func (self *Readline) end_history_search(accept bool) { func (self *Readline) end_history_search(accept bool) {
self.cursor = Position{}
if accept && self.history_search.current_idx < len(self.history_search.items) { 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.input_state.lines = utils.Splitlines(self.history_search.items[self.history_search.current_idx].Cmd)
self.cursor.Y = len(self.lines) - 1 self.input_state.cursor.Y = len(self.input_state.lines) - 1
self.cursor.X = len(self.lines[self.cursor.Y]) self.input_state.cursor.X = len(self.input_state.lines[self.input_state.cursor.Y])
} else { } else {
self.lines = self.history_search.original_lines self.input_state = self.history_search.original_input_state
self.cursor = self.history_search.original_cursor
} }
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() { func (self *Readline) markup_history_search() {
if len(self.history_search.items) == 0 { if len(self.history_search.items) == 0 {
if len(self.history_search.tokens) == 0 { if len(self.history_search.tokens) == 0 {
self.lines = []string{""} self.input_state.lines = []string{""}
} else { } 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 return
} }
lines := utils.Splitlines(self.history_search.items[self.history_search.current_idx].Cmd) 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 _, tok := range self.history_search.tokens {
for i, line := range lines { for i, line := range lines {
if idx := strings.Index(line, tok); idx > -1 { 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} q := Position{Y: i, X: idx}
if q.Less(cursor) { if q.Less(cursor) {
cursor = q cursor = q
@@ -252,31 +253,64 @@ func (self *Readline) markup_history_search() {
} }
} }
} }
self.lines = lines self.input_state.lines = lines
self.cursor = *self.ensure_position_in_bounds(&cursor) 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) { func (self *Readline) add_text_to_history_search(text string) {
self.history_search.query += text 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 var current_item *HistoryItem
if len(self.history_search.items) > 0 { if len(self.history_search.items) > 0 {
current_item = self.history_search.items[self.history_search.current_idx] current_item = self.history_search.items[self.history_search.current_idx]
} }
items := make([]*HistoryItem, len(self.history.items)) if len(self.history_search.tokens) == 0 {
for i, x := range self.history.items { self.history_search.items = []*HistoryItem{}
items[i] = &x } else {
} items := make([]*HistoryItem, len(self.history.items))
for _, token := range self.history_search.tokens { for i, x := range self.history.items {
matches := make([]*HistoryItem, 0, len(items)) items[i] = &x
for _, item := range items {
if strings.Contains(item.Cmd, token) {
matches = append(matches, item)
}
} }
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 idx := -1
for i, item := range self.history_search.items { for i, item := range self.history_search.items {
if item == current_item { if item == current_item {

View File

@@ -121,18 +121,69 @@ func default_shortcuts() *ShortcutMap {
return _default_shortcuts return _default_shortcuts
} }
func (self *Readline) action_for_key_event(event *loop.KeyEvent, shortcuts map[string]Action) Action { var _history_search_shortcuts *ShortcutMap
for sc, ac := range shortcuts {
if event.MatchesPressOrRepeat(sc) { func history_search_shortcuts() *ShortcutMap {
return ac 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 ErrCouldNotPerformAction = errors.New("Could not perform the specified action")
var ErrAcceptInput = errors.New("Accept input") 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) { func (self *Readline) handle_numeric_arg(ac Action) {
t := "-" t := "-"
num := int(ac - ActionNumericArgumentDigit0) num := int(ac - ActionNumericArgumentDigit0)