Implement bindings for moving by simple word

This commit is contained in:
Kovid Goyal 2022-10-23 14:45:25 +05:30
parent 0068ae8f66
commit a008c627e3
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
4 changed files with 152 additions and 3 deletions

View File

@ -6,6 +6,7 @@ import (
"fmt"
"io"
"strings"
"unicode"
"kitty/tools/utils"
"kitty/tools/wcswidth"
@ -18,7 +19,7 @@ func (self *Readline) text_upto_cursor_pos() string {
buf.Grow(1024)
for i, line := range self.lines {
if i == self.cursor.Y {
buf.WriteString(line[:self.cursor.X])
buf.WriteString(line[:utils.Min(len(line), self.cursor.X)])
break
} else {
buf.WriteString(line)
@ -297,8 +298,91 @@ func (self *Readline) erase_chars_after_cursor(amt uint, traverse_line_breaks bo
return num
}
func (self *Readline) next_word_char_pos(traverse_line_breaks bool) int {
return 0
func has_word_chars(text string) bool {
for _, ch := range text {
if unicode.IsLetter(ch) || unicode.IsDigit(ch) {
return true
}
}
return false
}
func (self *Readline) move_to_end_of_word(amt uint, traverse_line_breaks bool) (num_of_words_moved uint) {
if amt == 0 {
return 0
}
line := self.lines[self.cursor.Y]
in_word := false
ci := wcswidth.NewCellIterator(line[self.cursor.X:])
sz := 0
for ci.Forward() {
current_is_word_char := has_word_chars(ci.Current())
plen := sz
sz += len(ci.Current())
if current_is_word_char {
in_word = true
} else if in_word {
self.cursor.X += plen
amt--
num_of_words_moved++
if amt == 0 {
return
}
in_word = false
}
}
if self.move_to_end_of_line() {
amt--
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
num_of_words_moved += self.move_to_end_of_word(amt, traverse_line_breaks)
}
}
return
}
func (self *Readline) move_to_start_of_word(amt uint, traverse_line_breaks bool) (num_of_words_moved uint) {
if amt == 0 {
return 0
}
line := self.lines[self.cursor.Y]
in_word := false
ci := wcswidth.NewCellIterator(line[:self.cursor.X]).GotoEnd()
sz := 0
for ci.Backward() {
current_is_word_char := has_word_chars(ci.Current())
plen := sz
sz += len(ci.Current())
if current_is_word_char {
in_word = true
} else if in_word {
self.cursor.X -= plen
amt--
num_of_words_moved++
if amt == 0 {
return
}
in_word = false
}
}
if self.move_to_start_of_line() {
amt--
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])
num_of_words_moved += self.move_to_start_of_word(amt, traverse_line_breaks)
}
}
return
}
func (self *Readline) perform_action(ac Action, repeat_count uint) error {
@ -319,6 +403,14 @@ func (self *Readline) perform_action(ac Action, repeat_count uint) error {
if self.move_to_end_of_line() {
return nil
}
case ActionMoveToEndOfWord:
if self.move_to_end_of_word(repeat_count, true) > 0 {
return nil
}
case ActionMoveToStartOfWord:
if self.move_to_start_of_word(repeat_count, true) > 0 {
return nil
}
case ActionMoveToStartOfDocument:
if self.move_to_start() {
return nil

View File

@ -201,6 +201,56 @@ func TestCursorMovement(t *testing.T) {
vert(-1, -1, "1234567xy\nabc", Position{X: 3, Y: 2})
vert(-2, -2, "1234567xy", Position{X: 3, Y: 2})
vert(-30, -3, "123", Position{X: 3, Y: 2})
rl.ResetText()
rl.add_text("o\u0300ne two three\nfour five")
wf := func(amt uint, expected_amt uint, text_before_cursor string) {
pos := rl.cursor
actual_amt := rl.move_to_end_of_word(amt, true)
if actual_amt != expected_amt {
t.Fatalf("Failed to move to word end, expected amt (%d) != actual amt (%d)", expected_amt, actual_amt)
}
if diff := cmp.Diff(text_before_cursor, rl.TextBeforeCursor()); diff != "" {
t.Fatalf("Did not get expected text before cursor for: %#v and cursor: %+v\n%s", rl.AllText(), pos, diff)
}
}
rl.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{}
wf(5, 5, "òne two three\nfour five")
rl.cursor = Position{X: 5}
wf(1, 1, "òne two")
wb := func(amt uint, expected_amt uint, text_before_cursor string) {
pos := rl.cursor
actual_amt := rl.move_to_start_of_word(amt, true)
if actual_amt != expected_amt {
t.Fatalf("Failed to move to word end, expected amt (%d) != actual amt (%d)", expected_amt, actual_amt)
}
if diff := cmp.Diff(text_before_cursor, rl.TextBeforeCursor()); 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}
wb(1, 1, "")
rl.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}
wb(5, 5, "")
rl.cursor = Position{X: 5}
wb(1, 1, "")
}
func TestEraseChars(t *testing.T) {

View File

@ -43,6 +43,8 @@ const (
ActionMoveToEndOfLine
ActionMoveToStartOfDocument
ActionMoveToEndOfDocument
ActionMoveToEndOfWord
ActionMoveToStartOfWord
ActionCursorLeft
ActionCursorRight
ActionEndInput

View File

@ -25,6 +25,11 @@ var default_shortcuts = map[string]Action{
"ctrl+home": ActionMoveToStartOfDocument,
"ctrl+end": ActionMoveToEndOfDocument,
"alt+f": ActionMoveToEndOfWord,
"ctrl+right": ActionMoveToEndOfWord,
"ctrl+left": ActionMoveToStartOfWord,
"alt+b": ActionMoveToStartOfWord,
"left": ActionCursorLeft,
"ctrl+b": ActionCursorLeft,
"right": ActionCursorRight,