Skip to content

Commit

Permalink
Fix horizontal cursor movements when there are grapheme clusters
Browse files Browse the repository at this point in the history
  • Loading branch information
Verseth committed Jul 27, 2023
1 parent 87ef19e commit e5cbb07
Show file tree
Hide file tree
Showing 16 changed files with 337 additions and 237 deletions.
2 changes: 1 addition & 1 deletion _example/http-prompt/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ func completer(in prompt.Document) ([]prompt.Suggest, istrings.RuneNumber, istri
if w == "" {
return []prompt.Suggest{}, 0, 0
}
startIndex := endIndex - istrings.RuneCount(w)
startIndex := endIndex - istrings.RuneCountInString(w)
return prompt.FilterHasPrefix(suggestions, w, true), startIndex, endIndex
}

Expand Down
2 changes: 1 addition & 1 deletion _example/live-prefix/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func executor(in string) {
func completer(in prompt.Document) ([]prompt.Suggest, istrings.RuneNumber, istrings.RuneNumber) {
endIndex := in.CurrentRuneIndex()
w := in.GetWordBeforeCursor()
startIndex := endIndex - istrings.RuneCount(w)
startIndex := endIndex - istrings.RuneCountInString(w)

s := []prompt.Suggest{
{Text: "users", Description: "Store the username and age"},
Expand Down
40 changes: 26 additions & 14 deletions buffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,13 @@ func (b *Buffer) insertText(text string, columns istrings.Width, rows int, overw
if overwrite {
overwritten := string(currentTextRunes[cursor:])
if len(overwritten) >= int(cursor)+len(text) {
overwritten = string(currentTextRunes[cursor : cursor+istrings.RuneCount(text)])
overwritten = string(currentTextRunes[cursor : cursor+istrings.RuneCountInString(text)])
}
if i := strings.IndexAny(overwritten, "\n"); i != -1 {
overwritten = overwritten[:i]
}
b.setText(
string(currentTextRunes[:cursor])+text+string(currentTextRunes[cursor+istrings.RuneCount(overwritten):]),
string(currentTextRunes[:cursor])+text+string(currentTextRunes[cursor+istrings.RuneCountInString(overwritten):]),
columns,
rows,
)
Expand All @@ -88,7 +88,7 @@ func (b *Buffer) insertText(text string, columns istrings.Width, rows int, overw
}

if moveCursor {
b.cursorPosition += istrings.RuneCount(text)
b.cursorPosition += istrings.RuneCountInString(text)
b.recalculateStartLine(columns, rows)
b.updatePreferredColumn()
}
Expand Down Expand Up @@ -118,7 +118,7 @@ func (b *Buffer) recalculateStartLine(columns istrings.Width, rows int) bool {
// (When doing this, make sure that the cursor_position is valid for this text.
// text/cursor_position should be consistent at any time, otherwise set a Document instead.)
func (b *Buffer) setText(text string, col istrings.Width, row int) {
debug.Assert(b.cursorPosition <= istrings.RuneCount(text), "length of input should be shorter than cursor position")
debug.Assert(b.cursorPosition <= istrings.RuneCountInString(text), "length of input should be shorter than cursor position")
b.workingLines[b.workingIndex] = text
b.recalculateStartLine(col, row)
b.resetPreferredColumn()
Expand All @@ -141,20 +141,32 @@ func (b *Buffer) setDocument(d *Document, columns istrings.Width, rows int) {
b.resetPreferredColumn()
}

// Move to the left on the current line.
// Move to the left on the current line by the given amount of graphemes.
// Returns true when the view should be rerendered.
func (b *Buffer) CursorLeft(count istrings.RuneNumber, columns istrings.Width, rows int) bool {
l := b.Document().GetCursorLeftPosition(count)
b.cursorPosition += l
b.updatePreferredColumn()
return b.recalculateStartLine(columns, rows)
func (b *Buffer) CursorLeft(count istrings.GraphemeNumber, columns istrings.Width, rows int) bool {
return b.cursorHorizontalMove(b.Document().GetCursorLeftPosition(count), columns, rows)
}

// Move to the left on the current line by the given amount of runes.
// Returns true when the view should be rerendered.
func (b *Buffer) CursorLeftRunes(count istrings.RuneNumber, columns istrings.Width, rows int) bool {
return b.cursorHorizontalMove(b.Document().GetCursorLeftPositionRunes(count), columns, rows)
}

// Move to the right on the current line.
// Move to the right on the current line by the given amount of graphemes.
// Returns true when the view should be rerendered.
func (b *Buffer) CursorRight(count istrings.RuneNumber, columns istrings.Width, rows int) bool {
l := b.Document().GetCursorRightPosition(count)
b.cursorPosition += l
func (b *Buffer) CursorRight(count istrings.GraphemeNumber, columns istrings.Width, rows int) bool {
return b.cursorHorizontalMove(b.Document().GetCursorRightPosition(count), columns, rows)
}

// Move to the right on the current line by the given amount of runes.
// Returns true when the view should be rerendered.
func (b *Buffer) CursorRightRunes(count istrings.RuneNumber, columns istrings.Width, rows int) bool {
return b.cursorHorizontalMove(b.Document().GetCursorRightPositionRunes(count), columns, rows)
}

func (b *Buffer) cursorHorizontalMove(count istrings.RuneNumber, columns istrings.Width, rows int) bool {
b.cursorPosition += count
b.updatePreferredColumn()
return b.recalculateStartLine(columns, rows)
}
Expand Down
28 changes: 14 additions & 14 deletions buffer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ func TestBuffer_InsertText(t *testing.T) {
t.Errorf("Text should be %#v, got %#v", "some_text", b.Text())
}

if b.cursorPosition != istrings.RuneCount("some_text") {
t.Errorf("cursorPosition should be %#v, got %#v", istrings.RuneCount("some_text"), b.cursorPosition)
if b.cursorPosition != istrings.RuneCountInString("some_text") {
t.Errorf("cursorPosition should be %#v, got %#v", istrings.RuneCountInString("some_text"), b.cursorPosition)
}
}

Expand All @@ -38,8 +38,8 @@ func TestBuffer_InsertText_Overwrite(t *testing.T) {
t.Errorf("Text should be %#v, got %#v", "ABC", b.Text())
}

if b.cursorPosition != istrings.RuneCount("ABC") {
t.Errorf("cursorPosition should be %#v, got %#v", istrings.RuneCount("ABC"), b.cursorPosition)
if b.cursorPosition != istrings.RuneCountInString("ABC") {
t.Errorf("cursorPosition should be %#v, got %#v", istrings.RuneCountInString("ABC"), b.cursorPosition)
}

b.CursorLeft(1, DefColCount, DefRowCount)
Expand Down Expand Up @@ -87,8 +87,8 @@ func TestBuffer_CursorMovement(t *testing.T) {
if b.Text() != "some_teAxt" {
t.Errorf("Text should be %#v, got %#v", "some_teAxt", b.Text())
}
if b.cursorPosition != istrings.RuneCount("some_teA") {
t.Errorf("Text should be %#v, got %#v", istrings.RuneCount("some_teA"), b.cursorPosition)
if b.cursorPosition != istrings.RuneCountInString("some_teA") {
t.Errorf("Text should be %#v, got %#v", istrings.RuneCountInString("some_teA"), b.cursorPosition)
}

// Moving over left character counts.
Expand All @@ -97,8 +97,8 @@ func TestBuffer_CursorMovement(t *testing.T) {
if b.Text() != "Asome_teAxt" {
t.Errorf("Text should be %#v, got %#v", "some_teAxt", b.Text())
}
if b.cursorPosition != istrings.RuneCount("A") {
t.Errorf("Text should be %#v, got %#v", istrings.RuneCount("some_teA"), b.cursorPosition)
if b.cursorPosition != istrings.RuneCountInString("A") {
t.Errorf("Text should be %#v, got %#v", istrings.RuneCountInString("some_teA"), b.cursorPosition)
}

// TODO: Going right already at right end.
Expand Down Expand Up @@ -148,17 +148,17 @@ func TestBuffer_CursorDown(t *testing.T) {

// Normally going down
b.CursorDown(1, DefColCount, DefRowCount)
if b.Document().cursorPosition != istrings.RuneCount("line1\nlin") {
t.Errorf("Should be %#v, got %#v", istrings.RuneCount("line1\nlin"), b.Document().cursorPosition)
if b.Document().cursorPosition != istrings.RuneCountInString("line1\nlin") {
t.Errorf("Should be %#v, got %#v", istrings.RuneCountInString("line1\nlin"), b.Document().cursorPosition)
}

// Going down to a line that's storter.
b = NewBuffer()
b.InsertTextMoveCursor("long line1\na\nb", DefColCount, DefRowCount, false)
b.cursorPosition = 3
b.CursorDown(1, DefColCount, DefRowCount)
if b.Document().cursorPosition != istrings.RuneCount("long line1\na") {
t.Errorf("Should be %#v, got %#v", istrings.RuneCount("long line1\na"), b.Document().cursorPosition)
if b.Document().cursorPosition != istrings.RuneCountInString("long line1\na") {
t.Errorf("Should be %#v, got %#v", istrings.RuneCountInString("long line1\na"), b.Document().cursorPosition)
}
}

Expand All @@ -174,8 +174,8 @@ func TestBuffer_DeleteBeforeCursor(t *testing.T) {
if deleted != "e" {
t.Errorf("Should be %#v, got %#v", deleted, "e")
}
if b.cursorPosition != istrings.RuneCount("some_t") {
t.Errorf("Should be %#v, got %#v", istrings.RuneCount("some_t"), b.cursorPosition)
if b.cursorPosition != istrings.RuneCountInString("some_t") {
t.Errorf("Should be %#v, got %#v", istrings.RuneCountInString("some_t"), b.cursorPosition)
}

// Delete over the characters length before cursor.
Expand Down
77 changes: 68 additions & 9 deletions document.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/elk-language/go-prompt/bisect"
istrings "github.com/elk-language/go-prompt/strings"
"github.com/rivo/uniseg"
"golang.org/x/exp/utf8string"
)

Expand Down Expand Up @@ -142,7 +143,7 @@ func (d *Document) FindStartOfPreviousWord() istrings.ByteNumber {
// of the text before the cursor until the start of the previous word.
func (d *Document) FindRuneNumberUntilStartOfPreviousWord() istrings.RuneNumber {
x := d.TextBeforeCursor()
return istrings.RuneCount(x[d.FindStartOfPreviousWordWithSpace():])
return istrings.RuneCountInString(x[d.FindStartOfPreviousWordWithSpace():])
}

// FindStartOfPreviousWordWithSpace is almost the same as FindStartOfPreviousWord.
Expand Down Expand Up @@ -344,7 +345,7 @@ func (d *Document) CursorPositionRow() (row istrings.RuneNumber) {

// TextEndPositionRow returns the row of the end of the current text. (0-based.)
func (d *Document) TextEndPositionRow() (row istrings.RuneNumber) {
textLength := istrings.RuneCount(d.Text)
textLength := istrings.RuneCountInString(d.Text)
if textLength == 0 {
return 0
}
Expand All @@ -359,11 +360,41 @@ func (d *Document) CursorPositionCol() (col istrings.RuneNumber) {
return
}

// GetCursorLeftPosition returns the relative position for cursor left.
func (d *Document) GetCursorLeftPosition(count istrings.RuneNumber) istrings.RuneNumber {
// Returns the amount of runes that the cursors should be moved by.
// The `count` argument tells this function by how many graphemes (visible characters)
// the cursor should be moved (to the left).
func (d *Document) GetCursorLeftPosition(count istrings.GraphemeNumber) istrings.RuneNumber {
if count < 0 {
return d.GetCursorRightPosition(-count)
}
if d.cursorPosition == 0 {
return 0
}
text := d.TextBeforeCursor()
g := uniseg.NewGraphemes(text)
graphemeLength := istrings.GraphemeCount(text)
var currentGraphemeIndex istrings.GraphemeNumber
var currentPosition istrings.RuneNumber

for g.Next() {
if currentGraphemeIndex >= graphemeLength-count {
break
}
currentPosition += istrings.RuneNumber(len(g.Runes()))
currentGraphemeIndex++
}

result := d.cursorPosition - currentPosition
return -result
}

// Returns the amount of runes that the cursors should be moved by.
// The `count` argument tells this function by how many runes
// the cursor should be moved (to the left).
func (d *Document) GetCursorLeftPositionRunes(count istrings.RuneNumber) istrings.RuneNumber {
if count < 0 {
return d.GetCursorRightPositionRunes(-count)
}
runeSlice := []rune(d.Text)
var counter istrings.RuneNumber
targetPosition := d.cursorPosition - count
Expand All @@ -377,11 +408,39 @@ func (d *Document) GetCursorLeftPosition(count istrings.RuneNumber) istrings.Run
return counter
}

// GetCursorRightPosition returns relative position for cursor right.
func (d *Document) GetCursorRightPosition(count istrings.RuneNumber) istrings.RuneNumber {
// Returns the amount of runes that the cursors should be moved by.
// The `count` argument tells this function by how many graphemes (visible characters)
// the cursor should be moved (to the right).
func (d *Document) GetCursorRightPosition(count istrings.GraphemeNumber) istrings.RuneNumber {
if count < 0 {
return d.GetCursorLeftPosition(-count)
}
text := d.TextAfterCursor()
if len(text) == 0 {
return 0
}

g := uniseg.NewGraphemes(text)
var currentGraphemeIndex istrings.GraphemeNumber
var currentPosition istrings.RuneNumber

for g.Next() {
if currentGraphemeIndex >= count {
break
}
currentPosition += istrings.RuneNumber(len(g.Runes()))
currentGraphemeIndex++
}
return currentPosition
}

// Returns the amount of runes that the cursors should be moved by.
// The `count` argument tells this function by how many runes
// the cursor should be moved (to the right).
func (d *Document) GetCursorRightPositionRunes(count istrings.RuneNumber) istrings.RuneNumber {
if count < 0 {
return d.GetCursorLeftPositionRunes(-count)
}
runeSlice := []rune(d.Text)
var counter istrings.RuneNumber
targetPosition := d.cursorPosition + count
Expand Down Expand Up @@ -496,12 +555,12 @@ func (d *Document) OnLastLine() bool {

// GetEndOfLinePosition returns relative position for the end of this line.
func (d *Document) GetEndOfLinePosition() istrings.RuneNumber {
return istrings.RuneCount(d.CurrentLineAfterCursor())
return istrings.RuneCountInString(d.CurrentLineAfterCursor())
}

// GetStartOfLinePosition returns relative position for the start of this line.
func (d *Document) GetStartOfLinePosition() istrings.RuneNumber {
return istrings.RuneCount(d.CurrentLineBeforeCursor())
return istrings.RuneCountInString(d.CurrentLineBeforeCursor())
}

// GetStartOfLinePosition returns relative position for the start of this line.
Expand All @@ -521,7 +580,7 @@ func (d *Document) FindStartOfFirstWordOfLine() istrings.RuneNumber {
}

if counter == 0 {
return istrings.RuneCount(line)
return istrings.RuneCountInString(line)
}

return counter
Expand Down
Loading

0 comments on commit e5cbb07

Please sign in to comment.