Implement word killing actions

This commit is contained in:
Kovid Goyal 2022-10-25 17:52:18 +05:30
parent ea583f60b3
commit 3237db00fc
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
4 changed files with 80 additions and 17 deletions

View File

@ -207,12 +207,14 @@ func (self *Readline) move_to_end() bool {
return true return true
} }
func (self *Readline) erase_between(start, end Position) { func (self *Readline) erase_between(start, end Position) string {
if end.Less(start) { if end.Less(start) {
start, end = end, start start, end = end, start
} }
buf := strings.Builder{}
if start.Y == end.Y { if start.Y == end.Y {
line := self.lines[start.Y] line := self.lines[start.Y]
buf.WriteString(line[start.X:end.X])
self.lines[start.Y] = line[:start.X] + line[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.Y == start.Y && self.cursor.X >= start.X {
if self.cursor.X < end.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 self.cursor.X -= end.X - start.X
} }
} }
return return buf.String()
} }
lines := make([]string, 0, len(self.lines)) lines := make([]string, 0, len(self.lines))
for i, line := range self.lines { for i, line := range self.lines {
@ -229,11 +231,13 @@ func (self *Readline) erase_between(start, end Position) {
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:])
if self.cursor.Y == i && self.cursor.X > start.X { if self.cursor.Y == i && self.cursor.X > start.X {
self.cursor.X = start.X self.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])
if i == self.cursor.Y { if i == self.cursor.Y {
self.cursor.Y = start.Y self.cursor.Y = start.Y
if self.cursor.X < end.X { 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 self.cursor.X -= end.X - start.X
} }
} }
} else if i == self.cursor.Y { } else {
self.cursor = start if i == self.cursor.Y {
self.cursor = start
}
buf.WriteString(line)
buf.WriteString("\n")
} }
} }
self.lines = lines self.lines = lines
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 {
@ -278,7 +287,7 @@ func has_word_chars(text string) bool {
return false 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 { if amt == 0 {
return 0 return 0
} }
@ -288,7 +297,7 @@ func (self *Readline) move_to_end_of_word(amt uint, traverse_line_breaks bool) (
sz := 0 sz := 0
for ci.Forward() { for ci.Forward() {
current_is_word_char := has_word_chars(ci.Current()) current_is_word_char := is_part_of_word(ci.Current())
plen := sz plen := sz
sz += len(ci.Current()) sz += len(ci.Current())
if current_is_word_char { 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 { if traverse_line_breaks && self.cursor.Y < len(self.lines)-1 {
self.cursor.Y++ self.cursor.Y++
self.cursor.X = 0 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 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 { if amt == 0 {
return 0 return 0
} }
@ -327,7 +336,7 @@ func (self *Readline) move_to_start_of_word(amt uint, traverse_line_breaks bool)
sz := 0 sz := 0
for ci.Backward() { for ci.Backward() {
current_is_word_char := has_word_chars(ci.Current()) current_is_word_char := is_part_of_word(ci.Current())
plen := sz plen := sz
sz += len(ci.Current()) sz += len(ci.Current())
if current_is_word_char { 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 { if traverse_line_breaks && self.cursor.Y > 0 {
self.cursor.Y-- self.cursor.Y--
self.cursor.X = len(self.lines[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 return
@ -385,6 +394,42 @@ func (self *Readline) kill_to_start_of_line() bool {
return true 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 { 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 {
@ -405,11 +450,11 @@ func (self *Readline) perform_action(ac Action, repeat_count uint) error {
return nil return nil
} }
case ActionMoveToEndOfWord: 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 return nil
} }
case ActionMoveToStartOfWord: 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 return nil
} }
case ActionMoveToStartOfDocument: case ActionMoveToStartOfDocument:
@ -474,6 +519,18 @@ func (self *Readline) perform_action(ac Action, repeat_count uint) error {
if self.kill_to_start_of_line() { if self.kill_to_start_of_line() {
return nil 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 return ErrCouldNotPerformAction
} }

View File

@ -207,7 +207,7 @@ func TestCursorMovement(t *testing.T) {
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.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 { 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)
} }
@ -229,7 +229,7 @@ func TestCursorMovement(t *testing.T) {
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.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 { 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)
} }

View File

@ -62,6 +62,9 @@ const (
ActionStartKillActions ActionStartKillActions
ActionKillToEndOfLine ActionKillToEndOfLine
ActionKillToStartOfLine ActionKillToStartOfLine
ActionKillNextWord
ActionKillPreviousWord
ActionKillPreviousSpaceDelimitedWord
ActionEndKillActions ActionEndKillActions
) )

View File

@ -40,9 +40,12 @@ var default_shortcuts = map[string]Action{
"ctrl+d": ActionEndInput, "ctrl+d": ActionEndInput,
"enter": ActionAcceptInput, "enter": ActionAcceptInput,
"ctrl+k": ActionKillToEndOfLine, "ctrl+k": ActionKillToEndOfLine,
"ctrl+x": ActionKillToStartOfLine, "ctrl+x": ActionKillToStartOfLine,
"ctrl+u": 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 { func action_for_key_event(event *loop.KeyEvent, shortcuts map[string]Action) Action {