Skip to content

Commit

Permalink
prompt: fix some keybindings to work with multilines
Browse files Browse the repository at this point in the history
- The behavior of the `Home`/`End` keys has been changed:
now, the cursor moves to the beginning/end of the entire command
(instead of the line) when these keys are pressed.

- The behavior of the `alt-b`/`alt-f` keys has been changed:
now, the cursor moves one word backward in multiline commands, and
the word delimeter characters are `\n` and ` `.

- The associated emacs keybindings has been changed:
now `ctrl-b`/`ctrl-f` allow to move over the entire multiline command.
`ctrl-a`/`ctrl-e` same as `Home`/`End`.

- Added integration tests for emacs keyset.

Closes #6
  • Loading branch information
askalt committed Jul 18, 2023
1 parent 88e395d commit 898ae77
Show file tree
Hide file tree
Showing 8 changed files with 443 additions and 33 deletions.
30 changes: 30 additions & 0 deletions document.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,36 @@ func (d *Document) FindStartOfPreviousWord() int {
return 0
}

// FindStartOfPreviousWordCursor finds the start of the previous word and returns
// the associated cursor position.
// Determines whether a rune is part of a word using `isWordPart`.
func (d *Document) FindStartOfPreviousWordCursor(isWordPart func(rune) bool) int {
textBeforeCursor := []rune(d.TextBeforeCursor())
cursor := len(textBeforeCursor) - 1
for cursor >= 0 && !isWordPart(textBeforeCursor[cursor]) {
cursor--
}
for cursor >= 0 && isWordPart(textBeforeCursor[cursor]) {
cursor--
}
return cursor + 1
}

// FindEndOfCurrentWordCursor finds the end of the current word and returns
// the cursor position exactly after the end.
// Determines whether a rune is part of a word using `isWordPart`.
func (d *Document) FindEndOfCurrentWordCursor(isWordPart func(rune) bool) int {
textAfterCursor := []rune(d.TextAfterCursor())
cursor := 0
for cursor < len(textAfterCursor) && !isWordPart(textAfterCursor[cursor]) {
cursor++
}
for cursor < len(textAfterCursor) && isWordPart(textAfterCursor[cursor]) {
cursor++
}
return d.cursorPosition + cursor
}

// FindStartOfPreviousWordWithSpace is almost the same as FindStartOfPreviousWord.
// The only difference is to ignore contiguous spaces.
func (d *Document) FindStartOfPreviousWordWithSpace() int {
Expand Down
110 changes: 110 additions & 0 deletions document_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1476,3 +1476,113 @@ func Test_GetCustomCursorPosition(t *testing.T) {
})
}
}

func TestFindStartOfPreviousWordCursor(t *testing.T) {
isWordPart := func(r rune) bool {
return r != '\n' && r != ' '
}
cases := []struct {
description string
input string
cursor int
expectedCursor int
}{
{
"wo[r]d -> [w]ord",
"word",
2,
0,
},
{
" word[] -> [w]ord ",
" word ",
5,
1,
},
{
"строка 1\n[с]трока2\nстрока3 -> строка [1]\nстрока2\nстрока3",
"строка 1\nстрока2\nстрока3 ",
9,
7,
},
{
"слово\nслово2\n[с]лово3 -> слово\n[с]лово2\nслово3",
"слово\nслово2\nслово3",
13,
6,
},
{
"some long [] line -> some [l]ong line",
"some long line",
10,
5,
},
{
"[] -> []",
"",
0,
0,
},
{
"a\n\n\n\n\n\n[b]a -> [a]\n\n\n\n\n\nba",
"a\n\n\n\n\n\nba",
7,
0,
},
}
for _, tc := range cases {
t.Run(tc.description, func(t *testing.T) {
d := Document{Text: tc.input, cursorPosition: tc.cursor}
assert.Equal(t, tc.expectedCursor, d.FindStartOfPreviousWordCursor(isWordPart))
})
}
}

func TestFindEndOfCurrentWordCursor(t *testing.T) {
isWordPart := func(r rune) bool {
return r != '\n' && r != ' '
}
cases := []struct {
description string
input string
cursor int
expectedCursor int
}{
{
"wo[r]d -> word[]",
"word",
2,
4,
},
{
" [] word -> word[]",
" word",
1,
8,
},
{
"строка1\n[]строка2\nстрока3 -> строка1\n строка2[\n]строка3",
"строка1\n строка2\nстрока3",
8,
16,
},
{
"[] -> []",
"",
0,
0,
},
{
"[\n]\n\n\nслово->\n\n\n\nслово[]",
"\n\n\n\nслово",
0,
9,
},
}
for _, tc := range cases {
t.Run(tc.description, func(t *testing.T) {
d := Document{Text: tc.input, cursorPosition: tc.cursor}
assert.Equal(t, tc.expectedCursor, d.FindEndOfCurrentWordCursor(isWordPart))
})
}
}
18 changes: 4 additions & 14 deletions emacs.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,12 @@ var emacsKeyBindings = []KeyBind{
// Go to the End of the line
{
Key: ControlE,
Fn: func(buf *Buffer) {
x := []rune(buf.Document().TextAfterCursor())
buf.CursorRight(len(x))
},
Fn: GoCmdEnd,
},
// Go to the beginning of the line
{
Key: ControlA,
Fn: func(buf *Buffer) {
x := []rune(buf.Document().TextBeforeCursor())
buf.CursorLeft(len(x))
},
Fn: GoCmdBeginning,
},
// Cut the Line after the cursor
{
Expand Down Expand Up @@ -90,16 +84,12 @@ var emacsKeyBindings = []KeyBind{
// Right allow: Forward one character
{
Key: ControlF,
Fn: func(buf *Buffer) {
buf.CursorRight(1)
},
Fn: GoRightChar,
},
// Left allow: Backward one character
{
Key: ControlB,
Fn: func(buf *Buffer) {
buf.CursorLeft(1)
},
Fn: GoLeftChar,
},
// Cut the Word before the cursor.
{
Expand Down
27 changes: 27 additions & 0 deletions internal/prompt_app/prompt_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ import (
"github.com/c-bata/go-prompt"
)

var (
ControlLeftBytes []byte
ControlRightBytes []byte
)

func init() {
ControlLeftBytes = []byte{0x1b, 0x62}
ControlRightBytes = []byte{0x1b, 0x66}
}

// Console describes the console.
type Console struct {
title string
Expand Down Expand Up @@ -81,6 +91,23 @@ func getPromptOptions(console *Console) []prompt.Option {

prompt.OptionDisableAutoHistory(),
prompt.OptionReverseSearch(),

prompt.OptionAddASCIICodeBind(
// Move to one word left.
prompt.ASCIICodeBind{
ASCIICode: ControlLeftBytes,
Fn: func(buf *prompt.Buffer) {
prompt.GoLeftWord(buf)
},
},
// Move to one word right.
prompt.ASCIICodeBind{
ASCIICode: ControlRightBytes,
Fn: func(buf *prompt.Buffer) {
prompt.GoRightWord(buf)
},
},
),
}
args := os.Args
if len(args) > 1 {
Expand Down
4 changes: 2 additions & 2 deletions key_bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ var commonKeyBindings = []KeyBind{
// Go to the End of the line
{
Key: End,
Fn: GoLineEnd,
Fn: GoCmdEnd,
},
// Go to the beginning of the line
{
Key: Home,
Fn: GoLineBeginning,
Fn: GoCmdBeginning,
},
// Delete character under the cursor
{
Expand Down
23 changes: 18 additions & 5 deletions key_bind_func.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,26 @@ func GoLeftChar(buf *Buffer) {
buf.CursorLeft(1)
}

// GoRightWord Forward one word.
// GoRightWord moves the cursor to the end of the next word.
func GoRightWord(buf *Buffer) {
buf.CursorRight(buf.Document().FindEndOfCurrentWordWithSpace())
buf.setCursorPosition(buf.Document().FindEndOfCurrentWordCursor(func(r rune) bool {
return r != '\n' && r != ' '
}))
}

// GoLeftWord Backward one word.
// GoLeftWord moves the cursor to the beginning of the previous word.
func GoLeftWord(buf *Buffer) {
buf.CursorLeft(len([]rune(buf.Document().TextBeforeCursor())) - buf.Document().
FindStartOfPreviousWordWithSpace())
buf.setCursorPosition(buf.Document().FindStartOfPreviousWordCursor(func(r rune) bool {
return r != '\n' && r != ' '
}))
}

// GoCmdBeginning moves the cursor to the beginning of the command.
func GoCmdBeginning(buf *Buffer) {
buf.setCursorPosition(0)
}

// GoCmdEnd moves the cursor to the end of the command.
func GoCmdEnd(buf *Buffer) {
buf.setCursorPosition(len([]rune(buf.Text())))
}
Loading

0 comments on commit 898ae77

Please sign in to comment.