diff --git a/tools/tui/readline/actions.go b/tools/tui/readline/actions.go index ad5306b80..adae2b79c 100644 --- a/tools/tui/readline/actions.go +++ b/tools/tui/readline/actions.go @@ -207,12 +207,14 @@ func (self *Readline) move_to_end() bool { return true } -func (self *Readline) erase_between(start, end Position) { +func (self *Readline) erase_between(start, end Position) string { if end.Less(start) { start, end = end, start } + buf := strings.Builder{} if start.Y == end.Y { line := self.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 { @@ -221,7 +223,7 @@ func (self *Readline) erase_between(start, end Position) { self.cursor.X -= end.X - start.X } } - return + return buf.String() } lines := make([]string, 0, len(self.lines)) for i, line := range self.lines { @@ -229,11 +231,13 @@ func (self *Readline) erase_between(start, end Position) { 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 } } 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 { @@ -242,11 +246,16 @@ func (self *Readline) erase_between(start, end Position) { self.cursor.X -= end.X - start.X } } - } else if i == self.cursor.Y { - self.cursor = start + } else { + if i == self.cursor.Y { + self.cursor = start + } + buf.WriteString(line) + buf.WriteString("\n") } } self.lines = lines + return buf.String() } func (self *Readline) erase_chars_before_cursor(amt uint, traverse_line_breaks bool) uint { @@ -278,7 +287,7 @@ func has_word_chars(text string) bool { return false } -func (self *Readline) move_to_end_of_word(amt uint, traverse_line_breaks bool) (num_of_words_moved uint) { +func (self *Readline) move_to_end_of_word(amt uint, traverse_line_breaks bool, is_part_of_word func(string) bool) (num_of_words_moved uint) { if amt == 0 { return 0 } @@ -288,7 +297,7 @@ func (self *Readline) move_to_end_of_word(amt uint, traverse_line_breaks bool) ( sz := 0 for ci.Forward() { - current_is_word_char := has_word_chars(ci.Current()) + current_is_word_char := is_part_of_word(ci.Current()) plen := sz sz += len(ci.Current()) if current_is_word_char { @@ -311,13 +320,13 @@ func (self *Readline) move_to_end_of_word(amt uint, traverse_line_breaks bool) ( if traverse_line_breaks && self.cursor.Y < len(self.lines)-1 { self.cursor.Y++ self.cursor.X = 0 - num_of_words_moved += self.move_to_end_of_word(amt, traverse_line_breaks) + num_of_words_moved += self.move_to_end_of_word(amt, traverse_line_breaks, is_part_of_word) } } return } -func (self *Readline) move_to_start_of_word(amt uint, traverse_line_breaks bool) (num_of_words_moved uint) { +func (self *Readline) move_to_start_of_word(amt uint, traverse_line_breaks bool, is_part_of_word func(string) bool) (num_of_words_moved uint) { if amt == 0 { return 0 } @@ -327,7 +336,7 @@ func (self *Readline) move_to_start_of_word(amt uint, traverse_line_breaks bool) sz := 0 for ci.Backward() { - current_is_word_char := has_word_chars(ci.Current()) + current_is_word_char := is_part_of_word(ci.Current()) plen := sz sz += len(ci.Current()) if current_is_word_char { @@ -350,7 +359,7 @@ func (self *Readline) move_to_start_of_word(amt uint, traverse_line_breaks bool) if traverse_line_breaks && self.cursor.Y > 0 { self.cursor.Y-- self.cursor.X = len(self.lines[self.cursor.Y]) - num_of_words_moved += self.move_to_start_of_word(amt, traverse_line_breaks) + num_of_words_moved += self.move_to_start_of_word(amt, traverse_line_breaks, has_word_chars) } } return @@ -385,6 +394,42 @@ func (self *Readline) kill_to_start_of_line() bool { return true } +func (self *Readline) kill_next_word(amt uint, traverse_line_breaks bool) (num_killed uint) { + before := self.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)) + } + return num_killed +} + +func (self *Readline) kill_previous_word(amt uint, traverse_line_breaks bool) (num_killed uint) { + before := self.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)) + } + return num_killed +} + +func has_no_space_chars(text string) bool { + for _, r := range text { + if unicode.IsSpace(r) { + return false + } + } + return true +} + +func (self *Readline) kill_previous_space_delimited_word(amt uint, traverse_line_breaks bool) (num_killed uint) { + before := self.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)) + } + return num_killed +} + func (self *Readline) perform_action(ac Action, repeat_count uint) error { defer func() { self.last_action = ac }() switch ac { @@ -405,11 +450,11 @@ func (self *Readline) perform_action(ac Action, repeat_count uint) error { return nil } case ActionMoveToEndOfWord: - if self.move_to_end_of_word(repeat_count, true) > 0 { + if self.move_to_end_of_word(repeat_count, true, has_word_chars) > 0 { return nil } case ActionMoveToStartOfWord: - if self.move_to_start_of_word(repeat_count, true) > 0 { + if self.move_to_start_of_word(repeat_count, true, has_word_chars) > 0 { return nil } case ActionMoveToStartOfDocument: @@ -474,6 +519,18 @@ func (self *Readline) perform_action(ac Action, repeat_count uint) error { if self.kill_to_start_of_line() { return nil } + case ActionKillNextWord: + if self.kill_next_word(repeat_count, true) > 0 { + return nil + } + case ActionKillPreviousWord: + if self.kill_previous_word(repeat_count, true) > 0 { + return nil + } + case ActionKillPreviousSpaceDelimitedWord: + if self.kill_previous_space_delimited_word(repeat_count, true) > 0 { + return nil + } } return ErrCouldNotPerformAction } diff --git a/tools/tui/readline/actions_test.go b/tools/tui/readline/actions_test.go index bfa2cbf91..4a8f39dd7 100644 --- a/tools/tui/readline/actions_test.go +++ b/tools/tui/readline/actions_test.go @@ -207,7 +207,7 @@ func TestCursorMovement(t *testing.T) { wf := func(amt uint, expected_amt uint, text_before_cursor string) { pos := rl.cursor - actual_amt := rl.move_to_end_of_word(amt, true) + 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) } @@ -229,7 +229,7 @@ func TestCursorMovement(t *testing.T) { wb := func(amt uint, expected_amt uint, text_before_cursor string) { pos := rl.cursor - actual_amt := rl.move_to_start_of_word(amt, true) + 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) } diff --git a/tools/tui/readline/api.go b/tools/tui/readline/api.go index 0a0c1a914..c988df662 100644 --- a/tools/tui/readline/api.go +++ b/tools/tui/readline/api.go @@ -62,6 +62,9 @@ const ( ActionStartKillActions ActionKillToEndOfLine ActionKillToStartOfLine + ActionKillNextWord + ActionKillPreviousWord + ActionKillPreviousSpaceDelimitedWord ActionEndKillActions ) diff --git a/tools/tui/readline/keys.go b/tools/tui/readline/keys.go index 0cca8010d..eaa05d461 100644 --- a/tools/tui/readline/keys.go +++ b/tools/tui/readline/keys.go @@ -40,9 +40,12 @@ var default_shortcuts = map[string]Action{ "ctrl+d": ActionEndInput, "enter": ActionAcceptInput, - "ctrl+k": ActionKillToEndOfLine, - "ctrl+x": ActionKillToStartOfLine, - "ctrl+u": ActionKillToStartOfLine, + "ctrl+k": ActionKillToEndOfLine, + "ctrl+x": ActionKillToStartOfLine, + "ctrl+u": ActionKillToStartOfLine, + "alt+d": ActionKillNextWord, + "alt+backspace": ActionKillPreviousWord, + "ctrl+w": ActionKillPreviousSpaceDelimitedWord, } func action_for_key_event(event *loop.KeyEvent, shortcuts map[string]Action) Action {