diff --git a/buffer.go b/buffer.go index 67b95cf7..163a4aee 100644 --- a/buffer.go +++ b/buffer.go @@ -4,13 +4,14 @@ import ( "strings" "github.com/elk-language/go-prompt/internal/debug" + istrings "github.com/elk-language/go-prompt/internal/strings" ) // Buffer emulates the console buffer. type Buffer struct { workingLines []string // The working lines. Similar to history - workingIndex int - cursorPosition int + workingIndex int // index of the current line + cursorPosition istrings.RuneIndex cacheDocument *Document lastKeyStroke Key } @@ -36,44 +37,43 @@ func (b *Buffer) Document() (d *Document) { // DisplayCursorPosition returns the cursor position on rendered text on terminal emulators. // So if Document is "日本(cursor)語", DisplayedCursorPosition returns 4 because '日' and '本' are double width characters. -func (b *Buffer) DisplayCursorPosition(columns int) Position { +func (b *Buffer) DisplayCursorPosition(columns istrings.StringWidth) Position { return b.Document().DisplayCursorPosition(columns) } // InsertText insert string from current line. -func (b *Buffer) InsertText(v string, overwrite bool, moveCursor bool) { - or := []rune(b.Text()) - oc := b.cursorPosition +func (b *Buffer) InsertText(text string, overwrite bool, moveCursor bool) { + currentTextRunes := []rune(b.Text()) + cursor := b.cursorPosition if overwrite { - overwritten := string(or[oc:]) - if len(overwritten) >= oc+len(v) { - overwritten = string(or[oc : oc+len(v)]) + overwritten := string(currentTextRunes[cursor:]) + if len(overwritten) >= int(cursor)+len(text) { + overwritten = string(currentTextRunes[cursor : cursor+istrings.RuneLen(text)]) } - if strings.Contains(overwritten, "\n") { - i := strings.IndexAny(overwritten, "\n") + if i := strings.IndexAny(overwritten, "\n"); i != -1 { overwritten = overwritten[:i] } - b.setText(string(or[:oc]) + v + string(or[oc+len(overwritten):])) + b.setText(string(currentTextRunes[:cursor]) + text + string(currentTextRunes[cursor+istrings.RuneLen(overwritten):])) } else { - b.setText(string(or[:oc]) + v + string(or[oc:])) + b.setText(string(currentTextRunes[:cursor]) + text + string(currentTextRunes[cursor:])) } if moveCursor { - b.cursorPosition += len([]rune(v)) + b.cursorPosition += istrings.RuneLen(text) } } // SetText method to set text and update cursorPosition. // (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(v string) { - debug.Assert(b.cursorPosition <= len([]rune(v)), "length of input should be shorter than cursor position") - b.workingLines[b.workingIndex] = v +func (b *Buffer) setText(text string) { + debug.Assert(b.cursorPosition <= istrings.RuneLen(text), "length of input should be shorter than cursor position") + b.workingLines[b.workingIndex] = text } // Set cursor position. Return whether it changed. -func (b *Buffer) setCursorPosition(p int) { +func (b *Buffer) setCursorPosition(p istrings.RuneIndex) { if p > 0 { b.cursorPosition = p } else { @@ -88,13 +88,13 @@ func (b *Buffer) setDocument(d *Document) { } // CursorLeft move to left on the current line. -func (b *Buffer) CursorLeft(count int) { +func (b *Buffer) CursorLeft(count istrings.RuneCount) { l := b.Document().GetCursorLeftPosition(count) b.cursorPosition += l } // CursorRight move to right on the current line. -func (b *Buffer) CursorRight(count int) { +func (b *Buffer) CursorRight(count istrings.RuneCount) { l := b.Document().GetCursorRightPosition(count) b.cursorPosition += l } @@ -114,7 +114,7 @@ func (b *Buffer) CursorDown(count int) { } // DeleteBeforeCursor delete specified number of characters before cursor and return the deleted text. -func (b *Buffer) DeleteBeforeCursor(count int) (deleted string) { +func (b *Buffer) DeleteBeforeCursor(count istrings.RuneCount) (deleted string) { debug.Assert(count >= 0, "count should be positive") r := []rune(b.Text()) @@ -126,7 +126,7 @@ func (b *Buffer) DeleteBeforeCursor(count int) (deleted string) { deleted = string(r[start:b.cursorPosition]) b.setDocument(&Document{ Text: string(r[:start]) + string(r[b.cursorPosition:]), - cursorPosition: b.cursorPosition - len([]rune(deleted)), + cursorPosition: b.cursorPosition - istrings.RuneIndex(len([]rune(deleted))), }) } return @@ -142,13 +142,19 @@ func (b *Buffer) NewLine(copyMargin bool) { } // Delete specified number of characters and Return the deleted text. -func (b *Buffer) Delete(count int) (deleted string) { +func (b *Buffer) Delete(count istrings.RuneCount) string { r := []rune(b.Text()) - if b.cursorPosition < len(r) { - deleted = b.Document().TextAfterCursor()[:count] - b.setText(string(r[:b.cursorPosition]) + string(r[b.cursorPosition+len(deleted):])) + if b.cursorPosition < istrings.RuneIndex(len(r)) { + textAfterCursor := b.Document().TextAfterCursor() + textAfterCursorRunes := []rune(textAfterCursor) + deletedRunes := textAfterCursorRunes[:count] + b.setText(string(r[:b.cursorPosition]) + string(r[b.cursorPosition+istrings.RuneCount(len(deletedRunes)):])) + + deleted := string(deletedRunes) + return deleted } - return + + return "" } // JoinNextLine joins the next line to the current one by deleting the line ending after the current line. diff --git a/buffer_test.go b/buffer_test.go index e6859f13..86878749 100644 --- a/buffer_test.go +++ b/buffer_test.go @@ -3,6 +3,8 @@ package prompt import ( "reflect" "testing" + + istrings "github.com/elk-language/go-prompt/internal/strings" ) func TestNewBuffer(t *testing.T) { @@ -23,8 +25,8 @@ func TestBuffer_InsertText(t *testing.T) { t.Errorf("Text should be %#v, got %#v", "some_text", b.Text()) } - if b.cursorPosition != len("some_text") { - t.Errorf("cursorPosition should be %#v, got %#v", len("some_text"), b.cursorPosition) + if b.cursorPosition != istrings.RuneLen("some_text") { + t.Errorf("cursorPosition should be %#v, got %#v", istrings.RuneLen("some_text"), b.cursorPosition) } } @@ -36,8 +38,8 @@ func TestBuffer_InsertText_Overwrite(t *testing.T) { t.Errorf("Text should be %#v, got %#v", "ABC", b.Text()) } - if b.cursorPosition != len("ABC") { - t.Errorf("cursorPosition should be %#v, got %#v", len("ABC"), b.cursorPosition) + if b.cursorPosition != istrings.RuneLen("ABC") { + t.Errorf("cursorPosition should be %#v, got %#v", istrings.RuneLen("ABC"), b.cursorPosition) } b.CursorLeft(1) @@ -85,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 != len("some_teA") { - t.Errorf("Text should be %#v, got %#v", len("some_teA"), b.cursorPosition) + if b.cursorPosition != istrings.RuneLen("some_teA") { + t.Errorf("Text should be %#v, got %#v", istrings.RuneLen("some_teA"), b.cursorPosition) } // Moving over left character counts. @@ -95,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 != len("A") { - t.Errorf("Text should be %#v, got %#v", len("some_teA"), b.cursorPosition) + if b.cursorPosition != istrings.RuneLen("A") { + t.Errorf("Text should be %#v, got %#v", istrings.RuneLen("some_teA"), b.cursorPosition) } // TODO: Going right already at right end. @@ -109,6 +111,10 @@ func TestBuffer_CursorMovement_WithMultiByte(t *testing.T) { if l := b.Document().TextAfterCursor(); l != "お" { t.Errorf("Should be 'お', but got %s", l) } + b.InsertText("żółć", true, true) + if b.Text() != "あいうえżółć" { + t.Errorf("Text should be %#v, got %#v", "あいうえżółć", b.Text()) + } } func TestBuffer_CursorUp(t *testing.T) { @@ -141,8 +147,8 @@ func TestBuffer_CursorDown(t *testing.T) { // Normally going down b.CursorDown(1) - if b.Document().cursorPosition != len("line1\nlin") { - t.Errorf("Should be %#v, got %#v", len("line1\nlin"), b.Document().cursorPosition) + if b.Document().cursorPosition != istrings.RuneLen("line1\nlin") { + t.Errorf("Should be %#v, got %#v", istrings.RuneLen("line1\nlin"), b.Document().cursorPosition) } // Going down to a line that's storter. @@ -150,8 +156,8 @@ func TestBuffer_CursorDown(t *testing.T) { b.InsertText("long line1\na\nb", false, true) b.cursorPosition = 3 b.CursorDown(1) - if b.Document().cursorPosition != len("long line1\na") { - t.Errorf("Should be %#v, got %#v", len("long line1\na"), b.Document().cursorPosition) + if b.Document().cursorPosition != istrings.RuneLen("long line1\na") { + t.Errorf("Should be %#v, got %#v", istrings.RuneLen("long line1\na"), b.Document().cursorPosition) } } @@ -167,8 +173,8 @@ func TestBuffer_DeleteBeforeCursor(t *testing.T) { if deleted != "e" { t.Errorf("Should be %#v, got %#v", deleted, "e") } - if b.cursorPosition != len("some_t") { - t.Errorf("Should be %#v, got %#v", len("some_t"), b.cursorPosition) + if b.cursorPosition != istrings.RuneLen("some_t") { + t.Errorf("Should be %#v, got %#v", istrings.RuneLen("some_t"), b.cursorPosition) } // Delete over the characters length before cursor. diff --git a/completion.go b/completion.go index fd0d01b8..5589689a 100644 --- a/completion.go +++ b/completion.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/elk-language/go-prompt/internal/debug" + istrings "github.com/elk-language/go-prompt/internal/strings" runewidth "github.com/mattn/go-runewidth" ) @@ -155,7 +156,7 @@ func formatTexts(o []string, max int, prefix, suffix string) (new []string, widt return n, lenPrefix + width + lenSuffix } -func formatSuggestions(suggests []Suggest, max int) (new []Suggest, width int) { +func formatSuggestions(suggests []Suggest, max int) (new []Suggest, width istrings.StringWidth) { num := len(suggests) new = make([]Suggest, num) @@ -177,7 +178,7 @@ func formatSuggestions(suggests []Suggest, max int) (new []Suggest, width int) { for i := 0; i < num; i++ { new[i] = Suggest{Text: left[i], Description: right[i]} } - return new, leftWidth + rightWidth + return new, istrings.StringWidth(leftWidth + rightWidth) } // Constructor option for CompletionManager. diff --git a/completion_test.go b/completion_test.go index c17c1d91..6746fafe 100644 --- a/completion_test.go +++ b/completion_test.go @@ -3,6 +3,8 @@ package prompt import ( "reflect" "testing" + + istrings "github.com/elk-language/go-prompt/internal/strings" ) func TestFormatShortSuggestion(t *testing.T) { @@ -10,7 +12,7 @@ func TestFormatShortSuggestion(t *testing.T) { in []Suggest expected []Suggest max int - exWidth int + exWidth istrings.StringWidth }{ { in: []Suggest{ @@ -38,7 +40,7 @@ func TestFormatShortSuggestion(t *testing.T) { {Text: " coconut ", Description: " This is coconut. "}, }, max: 100, - exWidth: len(" apple " + " This is apple. "), + exWidth: istrings.StringWidth(len(" apple " + " This is apple. ")), }, { in: []Suggest{ @@ -82,7 +84,7 @@ func TestFormatShortSuggestion(t *testing.T) { {Text: " --include-extended-apis ", Description: " --------------... "}, }, max: 50, - exWidth: len(" --include-extended-apis " + " ---------------..."), + exWidth: istrings.StringWidth(len(" --include-extended-apis " + " ---------------...")), }, { in: []Suggest{ @@ -102,7 +104,7 @@ func TestFormatShortSuggestion(t *testing.T) { {Text: " --include-extended-apis ", Description: " If true, include definitions of new APIs via calls to the API server. [default true] "}, }, max: 500, - exWidth: len(" --include-extended-apis " + " If true, include definitions of new APIs via calls to the API server. [default true] "), + exWidth: istrings.StringWidth(len(" --include-extended-apis " + " If true, include definitions of new APIs via calls to the API server. [default true] ")), }, } diff --git a/document.go b/document.go index 84ae47fa..e01b8675 100644 --- a/document.go +++ b/document.go @@ -16,7 +16,7 @@ type Document struct { // This represents a index in a rune array of Document.Text. // So if Document is "日本(cursor)語", cursorPosition is 2. // But DisplayedCursorPosition returns 4 because '日' and '本' are double width characters. - cursorPosition int + cursorPosition istrings.RuneIndex lastKey Key } @@ -35,20 +35,20 @@ func (d *Document) LastKeyStroke() Key { // DisplayCursorPosition returns the cursor position on rendered text on terminal emulators. // So if Document is "日本(cursor)語", DisplayedCursorPosition returns 4 because '日' and '本' are double width characters. -func (d *Document) DisplayCursorPosition(columns int) Position { - str := utf8string.NewString(d.Text).Slice(0, d.cursorPosition) +func (d *Document) DisplayCursorPosition(columns istrings.StringWidth) Position { + str := utf8string.NewString(d.Text).Slice(0, int(d.cursorPosition)) return positionAtEndOfString(str, columns) } // GetCharRelativeToCursor return character relative to cursor position, or empty string -func (d *Document) GetCharRelativeToCursor(offset int) (r rune) { +func (d *Document) GetCharRelativeToCursor(offset istrings.RuneCount) (r rune) { s := d.Text - cnt := 0 + var cnt istrings.RuneIndex for len(s) > 0 { cnt++ r, size := utf8.DecodeRuneInString(s) - if cnt == d.cursorPosition+offset { + if cnt == d.cursorPosition+istrings.RuneIndex(offset) { return r } s = s[size:] @@ -124,9 +124,9 @@ func (d *Document) GetWordAfterCursorUntilSeparatorIgnoreNextToCursor(sep string // FindStartOfPreviousWord returns an index relative to the cursor position // pointing to the start of the previous word. Return 0 if nothing was found. -func (d *Document) FindStartOfPreviousWord() int { +func (d *Document) FindStartOfPreviousWord() istrings.ByteIndex { x := d.TextBeforeCursor() - i := strings.LastIndexAny(x, " \n") + i := istrings.ByteIndex(strings.LastIndexAny(x, " \n")) if i != -1 { return i + 1 } @@ -142,14 +142,14 @@ func (d *Document) FindStringWidthUntilStartOfPreviousWord() int { // FindStartOfPreviousWordWithSpace is almost the same as FindStartOfPreviousWord. // The only difference is to ignore contiguous spaces. -func (d *Document) FindStartOfPreviousWordWithSpace() int { +func (d *Document) FindStartOfPreviousWordWithSpace() istrings.ByteIndex { x := d.TextBeforeCursor() end := istrings.LastIndexNotByte(x, ' ') if end == -1 { return 0 } - start := strings.LastIndexByte(x[:end], ' ') + start := istrings.ByteIndex(strings.LastIndexByte(x[:end], ' ')) if start == -1 { return 0 } @@ -158,13 +158,13 @@ func (d *Document) FindStartOfPreviousWordWithSpace() int { // FindStartOfPreviousWordUntilSeparator is almost the same as FindStartOfPreviousWord. // But this can specify Separator. Return 0 if nothing was found. -func (d *Document) FindStartOfPreviousWordUntilSeparator(sep string) int { +func (d *Document) FindStartOfPreviousWordUntilSeparator(sep string) istrings.ByteIndex { if sep == "" { return d.FindStartOfPreviousWord() } x := d.TextBeforeCursor() - i := strings.LastIndexAny(x, sep) + i := istrings.ByteIndex(strings.LastIndexAny(x, sep)) if i != -1 { return i + 1 } @@ -173,7 +173,7 @@ func (d *Document) FindStartOfPreviousWordUntilSeparator(sep string) int { // FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor is almost the same as FindStartOfPreviousWordWithSpace. // But this can specify Separator. Return 0 if nothing was found. -func (d *Document) FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor(sep string) int { +func (d *Document) FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor(sep string) istrings.ByteIndex { if sep == "" { return d.FindStartOfPreviousWordWithSpace() } @@ -183,37 +183,37 @@ func (d *Document) FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor(sep s if end == -1 { return 0 } - start := strings.LastIndexAny(x[:end], sep) + start := istrings.ByteIndex(strings.LastIndexAny(x[:end], sep)) if start == -1 { return 0 } return start + 1 } -// FindEndOfCurrentWord returns an index relative to the cursor position. +// FindEndOfCurrentWord returns a byte index relative to the cursor position. // pointing to the end of the current word. Return 0 if nothing was found. -func (d *Document) FindEndOfCurrentWord() int { +func (d *Document) FindEndOfCurrentWord() istrings.ByteIndex { x := d.TextAfterCursor() - i := strings.IndexByte(x, ' ') + i := istrings.ByteIndex(strings.IndexByte(x, ' ')) if i != -1 { return i } - return len(x) + return istrings.ByteIndex(len(x)) } // FindEndOfCurrentWordWithSpace is almost the same as FindEndOfCurrentWord. // The only difference is to ignore contiguous spaces. -func (d *Document) FindEndOfCurrentWordWithSpace() int { +func (d *Document) FindEndOfCurrentWordWithSpace() istrings.ByteIndex { x := d.TextAfterCursor() start := istrings.IndexNotByte(x, ' ') if start == -1 { - return len(x) + return istrings.ByteIndex(len(x)) } - end := strings.IndexByte(x[start:], ' ') + end := istrings.ByteIndex(strings.IndexByte(x[start:], ' ')) if end == -1 { - return len(x) + return istrings.ByteIndex(len(x)) } return start + end @@ -221,9 +221,9 @@ func (d *Document) FindEndOfCurrentWordWithSpace() int { // Returns the string width (as visible in the terminal) // of the text after the cursor until the end of the current word. -func (d *Document) FindStringWidthUntilEndOfCurrentWord() int { +func (d *Document) FindStringWidthUntilEndOfCurrentWord() istrings.StringWidth { t := d.TextAfterCursor() - width := 0 + var width istrings.StringWidth nonSpaceCharSeen := false for _, char := range t { if !nonSpaceCharSeen && char == ' ' { @@ -236,7 +236,7 @@ func (d *Document) FindStringWidthUntilEndOfCurrentWord() int { } nonSpaceCharSeen = true - width += runewidth.RuneWidth(char) + width += istrings.StringWidth(runewidth.RuneWidth(char)) } return width @@ -244,22 +244,22 @@ func (d *Document) FindStringWidthUntilEndOfCurrentWord() int { // FindEndOfCurrentWordUntilSeparator is almost the same as FindEndOfCurrentWord. // But this can specify Separator. Return 0 if nothing was found. -func (d *Document) FindEndOfCurrentWordUntilSeparator(sep string) int { +func (d *Document) FindEndOfCurrentWordUntilSeparator(sep string) istrings.ByteIndex { if sep == "" { return d.FindEndOfCurrentWord() } x := d.TextAfterCursor() - i := strings.IndexAny(x, sep) + i := istrings.ByteIndex(strings.IndexAny(x, sep)) if i != -1 { return i } - return len(x) + return istrings.ByteIndex(len(x)) } // FindEndOfCurrentWordUntilSeparatorIgnoreNextToCursor is almost the same as FindEndOfCurrentWordWithSpace. // But this can specify Separator. Return 0 if nothing was found. -func (d *Document) FindEndOfCurrentWordUntilSeparatorIgnoreNextToCursor(sep string) int { +func (d *Document) FindEndOfCurrentWordUntilSeparatorIgnoreNextToCursor(sep string) istrings.ByteIndex { if sep == "" { return d.FindEndOfCurrentWordWithSpace() } @@ -268,12 +268,12 @@ func (d *Document) FindEndOfCurrentWordUntilSeparatorIgnoreNextToCursor(sep stri start := istrings.IndexNotAny(x, sep) if start == -1 { - return len(x) + return istrings.ByteIndex(len(x)) } - end := strings.IndexAny(x[start:], sep) + end := istrings.ByteIndex(strings.IndexAny(x[start:], sep)) if end == -1 { - return len(x) + return istrings.ByteIndex(len(x)) } return start + end @@ -297,23 +297,23 @@ func (d *Document) CurrentLine() string { } // Array pointing to the start indices of all the lines. -func (d *Document) lineStartIndexes() []int { +func (d *Document) lineStartIndices() []istrings.RuneIndex { // TODO: Cache, because this is often reused. // (If it is used, it's often used many times. // And this has to be fast for editing big documents!) lc := d.LineCount() - lengths := make([]int, lc) + lengths := make([]istrings.RuneCount, lc) for i, l := range d.Lines() { - lengths[i] = len([]rune(l)) + lengths[i] = istrings.RuneCount(len([]rune(l))) } // Calculate cumulative sums. - indices := make([]int, lc+1) + indices := make([]istrings.RuneIndex, lc+1) indices[0] = 0 // https://github.com/jonathanslenders/python-prompt-toolkit/blob/master/prompt_toolkit/document.py#L189 - pos := 0 + var pos istrings.RuneCount for i, l := range lengths { pos += l + 1 - indices[i+1] = pos + indices[i+1] = istrings.RuneIndex(pos) } if lc > 1 { // Pop the last item. (This is not a new line.) @@ -324,33 +324,33 @@ func (d *Document) lineStartIndexes() []int { // For the index of a character at a certain line, calculate the index of // the first character on that line. -func (d *Document) findLineStartIndex(index int) (pos int, lineStartIndex int) { - indices := d.lineStartIndexes() +func (d *Document) findLineStartIndex(index istrings.RuneIndex) (pos, lineStartIndex istrings.RuneIndex) { + indices := d.lineStartIndices() pos = bisect.Right(indices, index) - 1 lineStartIndex = indices[pos] return } // CursorPositionRow returns the current row. (0-based.) -func (d *Document) CursorPositionRow() (row int) { +func (d *Document) CursorPositionRow() (row istrings.RuneIndex) { row, _ = d.findLineStartIndex(d.cursorPosition) return } // CursorPositionCol returns the current column. (0-based.) -func (d *Document) CursorPositionCol() (col int) { +func (d *Document) CursorPositionCol() (col istrings.RuneIndex) { _, index := d.findLineStartIndex(d.cursorPosition) col = d.cursorPosition - index return } // GetCursorLeftPosition returns the relative position for cursor left. -func (d *Document) GetCursorLeftPosition(count int) int { +func (d *Document) GetCursorLeftPosition(count istrings.RuneCount) istrings.RuneCount { if count < 0 { return d.GetCursorRightPosition(-count) } runeSlice := []rune(d.Text) - counter := 0 + var counter istrings.RuneCount targetPosition := d.cursorPosition - count if targetPosition < 0 { targetPosition = 0 @@ -363,15 +363,15 @@ func (d *Document) GetCursorLeftPosition(count int) int { } // GetCursorRightPosition returns relative position for cursor right. -func (d *Document) GetCursorRightPosition(count int) int { +func (d *Document) GetCursorRightPosition(count istrings.RuneCount) istrings.RuneCount { if count < 0 { return d.GetCursorLeftPosition(-count) } runeSlice := []rune(d.Text) - counter := 0 + var counter istrings.RuneCount targetPosition := d.cursorPosition + count - if targetPosition > len(runeSlice) { - targetPosition = len(runeSlice) + if targetPosition > istrings.RuneCount(len(runeSlice)) { + targetPosition = istrings.RuneCount(len(runeSlice)) } for range runeSlice[d.cursorPosition:targetPosition] { counter++ @@ -381,26 +381,26 @@ func (d *Document) GetCursorRightPosition(count int) int { } // Get the current cursor position. -func (d *Document) GetCursorPosition(columns int) Position { +func (d *Document) GetCursorPosition(columns istrings.StringWidth) Position { return positionAtEndOfString(d.TextBeforeCursor(), columns) } // Get the position of the end of the current text. -func (d *Document) GetEndOfTextPosition(columns int) Position { +func (d *Document) GetEndOfTextPosition(columns istrings.StringWidth) Position { return positionAtEndOfString(d.Text, columns) } // GetCursorUpPosition return the relative cursor position (character index) where we would be // if the user pressed the arrow-up button. -func (d *Document) GetCursorUpPosition(count int, preferredColumn int) int { - var col int +func (d *Document) GetCursorUpPosition(count int, preferredColumn istrings.RuneIndex) istrings.RuneIndex { + var col istrings.RuneIndex if preferredColumn == -1 { // -1 means nil col = d.CursorPositionCol() } else { col = preferredColumn } - row := d.CursorPositionRow() - count + row := int(d.CursorPositionRow()) - count if row < 0 { row = 0 } @@ -409,14 +409,14 @@ func (d *Document) GetCursorUpPosition(count int, preferredColumn int) int { // GetCursorDownPosition return the relative cursor position (character index) where we would be if the // user pressed the arrow-down button. -func (d *Document) GetCursorDownPosition(count int, preferredColumn int) int { - var col int +func (d *Document) GetCursorDownPosition(count int, preferredColumn istrings.RuneIndex) istrings.RuneIndex { + var col istrings.RuneIndex if preferredColumn == -1 { // -1 means nil col = d.CursorPositionCol() } else { col = preferredColumn } - row := d.CursorPositionRow() + count + row := int(d.CursorPositionRow()) + count return d.TranslateRowColToIndex(row, col) - d.cursorPosition } @@ -434,16 +434,16 @@ func (d *Document) LineCount() int { // TranslateIndexToPosition given an index for the text, return the corresponding (row, col) tuple. // (0-based. Returns (0, 0) for index=0.) -func (d *Document) TranslateIndexToPosition(index int) (row int, col int) { - row, rowIndex := d.findLineStartIndex(index) - col = index - rowIndex - return +func (d *Document) TranslateIndexToPosition(index istrings.RuneIndex) (int, int) { + r, rowIndex := d.findLineStartIndex(index) + c := index - rowIndex + return int(r), int(c) } // TranslateRowColToIndex given a (row, col), return the corresponding index. // (Row and col params are 0-based.) -func (d *Document) TranslateRowColToIndex(row int, column int) (index int) { - indices := d.lineStartIndexes() +func (d *Document) TranslateRowColToIndex(row int, column istrings.RuneIndex) (index istrings.RuneIndex) { + indices := d.lineStartIndices() if row < 0 { row = 0 } else if row > len(indices) { @@ -454,10 +454,10 @@ func (d *Document) TranslateRowColToIndex(row int, column int) (index int) { // python) result += max(0, min(col, len(line))) if column > 0 || len(line) > 0 { - if column > len(line) { - index += len(line) + if column > istrings.RuneIndex(len(line)) { + index += istrings.RuneCount(len(line)) } else { - index += column + index += istrings.RuneIndex(column) } } @@ -465,8 +465,8 @@ func (d *Document) TranslateRowColToIndex(row int, column int) (index int) { // Keep in range. (len(self.text) is included, because the cursor can be // right after the end of the text as well.) // python) result = max(0, min(result, len(self.text))) - if index > len(text) { - index = len(text) + if index > istrings.RuneCount(len(text)) { + index = istrings.RuneIndex(len(text)) } if index < 0 { index = 0 @@ -476,12 +476,12 @@ func (d *Document) TranslateRowColToIndex(row int, column int) (index int) { // OnLastLine returns true when we are at the last line. func (d *Document) OnLastLine() bool { - return d.CursorPositionRow() == (d.LineCount() - 1) + return d.CursorPositionRow() == istrings.RuneIndex(d.LineCount()-1) } // GetEndOfLinePosition returns relative position for the end of this line. -func (d *Document) GetEndOfLinePosition() int { - return len([]rune(d.CurrentLineAfterCursor())) +func (d *Document) GetEndOfLinePosition() istrings.RuneIndex { + return istrings.RuneIndex(len([]rune(d.CurrentLineAfterCursor()))) } func (d *Document) leadingWhitespaceInCurrentLine() (margin string) { diff --git a/document_test.go b/document_test.go index b14601fa..dbe9d59d 100644 --- a/document_test.go +++ b/document_test.go @@ -5,6 +5,8 @@ import ( "reflect" "testing" "unicode/utf8" + + istrings "github.com/elk-language/go-prompt/internal/strings" ) func ExampleDocument_CurrentLine() { @@ -13,7 +15,7 @@ func ExampleDocument_CurrentLine() { This is a example of Document component. This component has texts displayed in terminal and cursor position. `, - cursorPosition: len(`Hello! my name is c-bata. + cursorPosition: istrings.RuneLen(`Hello! my name is c-bata. This is a exam`), } fmt.Println(d.CurrentLine()) @@ -24,7 +26,7 @@ This is a exam`), func ExampleDocument_DisplayCursorPosition() { d := &Document{ Text: `Hello! my name is c-bata.`, - cursorPosition: len(`Hello`), + cursorPosition: istrings.RuneLen(`Hello`), } fmt.Println("DisplayCursorPosition", d.DisplayCursorPosition(50)) // Output: @@ -37,7 +39,7 @@ func ExampleDocument_CursorPositionRow() { This is a example of Document component. This component has texts displayed in terminal and cursor position. `, - cursorPosition: len(`Hello! my name is c-bata. + cursorPosition: istrings.RuneLen(`Hello! my name is c-bata. This is a exam`), } fmt.Println("CursorPositionRow", d.CursorPositionRow()) @@ -51,7 +53,7 @@ func ExampleDocument_CursorPositionCol() { This is a example of Document component. This component has texts displayed in terminal and cursor position. `, - cursorPosition: len(`Hello! my name is c-bata. + cursorPosition: istrings.RuneLen(`Hello! my name is c-bata. This is a exam`), } fmt.Println("CursorPositionCol", d.CursorPositionCol()) @@ -65,7 +67,7 @@ func ExampleDocument_TextBeforeCursor() { This is a example of Document component. This component has texts displayed in terminal and cursor position. `, - cursorPosition: len(`Hello! my name is c-bata. + cursorPosition: istrings.RuneLen(`Hello! my name is c-bata. This is a exam`), } fmt.Println(d.TextBeforeCursor()) @@ -80,7 +82,7 @@ func ExampleDocument_TextAfterCursor() { This is a example of Document component. This component has texts displayed in terminal and cursor position. `, - cursorPosition: len(`Hello! my name is c-bata. + cursorPosition: istrings.RuneLen(`Hello! my name is c-bata. This is a exam`), } fmt.Println(d.TextAfterCursor()) @@ -105,7 +107,7 @@ func ExampleDocument_CurrentLineBeforeCursor() { This is a example of Document component. This component has texts displayed in terminal and cursor position. `, - cursorPosition: len(`Hello! my name is c-bata. + cursorPosition: istrings.RuneLen(`Hello! my name is c-bata. This is a exam`), } fmt.Println(d.CurrentLineBeforeCursor()) @@ -119,7 +121,7 @@ func ExampleDocument_CurrentLineAfterCursor() { This is a example of Document component. This component has texts displayed in terminal and cursor position. `, - cursorPosition: len(`Hello! my name is c-bata. + cursorPosition: istrings.RuneLen(`Hello! my name is c-bata. This is a exam`), } fmt.Println(d.CurrentLineAfterCursor()) @@ -132,7 +134,7 @@ func ExampleDocument_GetWordBeforeCursor() { Text: `Hello! my name is c-bata. This is a example of Document component. `, - cursorPosition: len(`Hello! my name is c-bata. + cursorPosition: istrings.RuneLen(`Hello! my name is c-bata. This is a exam`), } fmt.Println(d.GetWordBeforeCursor()) @@ -145,7 +147,7 @@ func ExampleDocument_GetWordAfterCursor() { Text: `Hello! my name is c-bata. This is a example of Document component. `, - cursorPosition: len(`Hello! my name is c-bata. + cursorPosition: istrings.RuneLen(`Hello! my name is c-bata. This is a exam`), } fmt.Println(d.GetWordAfterCursor()) @@ -158,7 +160,7 @@ func ExampleDocument_GetWordBeforeCursorWithSpace() { Text: `Hello! my name is c-bata. This is a example of Document component. `, - cursorPosition: len(`Hello! my name is c-bata. + cursorPosition: istrings.RuneLen(`Hello! my name is c-bata. This is a example `), } fmt.Println(d.GetWordBeforeCursorWithSpace()) @@ -171,7 +173,7 @@ func ExampleDocument_GetWordAfterCursorWithSpace() { Text: `Hello! my name is c-bata. This is a example of Document component. `, - cursorPosition: len(`Hello! my name is c-bata. + cursorPosition: istrings.RuneLen(`Hello! my name is c-bata. This is a`), } fmt.Println(d.GetWordAfterCursorWithSpace()) @@ -182,7 +184,7 @@ This is a`), func ExampleDocument_GetWordBeforeCursorUntilSeparator() { d := &Document{ Text: `hello,i am c-bata`, - cursorPosition: len(`hello,i am c`), + cursorPosition: istrings.RuneLen(`hello,i am c`), } fmt.Println(d.GetWordBeforeCursorUntilSeparator(",")) // Output: @@ -192,7 +194,7 @@ func ExampleDocument_GetWordBeforeCursorUntilSeparator() { func ExampleDocument_GetWordAfterCursorUntilSeparator() { d := &Document{ Text: `hello,i am c-bata,thank you for using go-prompt`, - cursorPosition: len(`hello,i a`), + cursorPosition: istrings.RuneLen(`hello,i a`), } fmt.Println(d.GetWordAfterCursorUntilSeparator(",")) // Output: @@ -202,7 +204,7 @@ func ExampleDocument_GetWordAfterCursorUntilSeparator() { func ExampleDocument_GetWordBeforeCursorUntilSeparatorIgnoreNextToCursor() { d := &Document{ Text: `hello,i am c-bata,thank you for using go-prompt`, - cursorPosition: len(`hello,i am c-bata,`), + cursorPosition: istrings.RuneLen(`hello,i am c-bata,`), } fmt.Println(d.GetWordBeforeCursorUntilSeparatorIgnoreNextToCursor(",")) // Output: @@ -212,7 +214,7 @@ func ExampleDocument_GetWordBeforeCursorUntilSeparatorIgnoreNextToCursor() { func ExampleDocument_GetWordAfterCursorUntilSeparatorIgnoreNextToCursor() { d := &Document{ Text: `hello,i am c-bata,thank you for using go-prompt`, - cursorPosition: len(`hello`), + cursorPosition: istrings.RuneLen(`hello`), } fmt.Println(d.GetWordAfterCursorUntilSeparatorIgnoreNextToCursor(",")) // Output: @@ -267,7 +269,7 @@ func TestDocument_GetCharRelativeToCursor(t *testing.T) { { document: &Document{ Text: "line 1\nline 2\nline 3\nline 4\n", - cursorPosition: len([]rune("line 1\n" + "lin")), + cursorPosition: istrings.RuneLen("line 1\n" + "lin"), }, expected: "e", }, @@ -304,7 +306,7 @@ func TestDocument_TextBeforeCursor(t *testing.T) { { document: &Document{ Text: "line 1\nline 2\nline 3\nline 4\n", - cursorPosition: len("line 1\n" + "lin"), + cursorPosition: istrings.RuneLen("line 1\n" + "lin"), }, expected: "line 1\nlin", }, @@ -339,7 +341,7 @@ func TestDocument_TextAfterCursor(t *testing.T) { { document: &Document{ Text: "line 1\nline 2\nline 3\nline 4\n", - cursorPosition: len("line 1\n" + "lin"), + cursorPosition: istrings.RuneLen("line 1\n" + "lin"), }, expected: "e 2\nline 3\nline 4\n", }, @@ -383,14 +385,14 @@ func TestDocument_GetWordBeforeCursor(t *testing.T) { { document: &Document{ Text: "apple bana", - cursorPosition: len("apple bana"), + cursorPosition: istrings.RuneLen("apple bana"), }, expected: "bana", }, { document: &Document{ Text: "apply -f ./file/foo.json", - cursorPosition: len("apply -f ./file/foo.json"), + cursorPosition: istrings.RuneLen("apply -f ./file/foo.json"), }, expected: "foo.json", sep: " /", @@ -398,14 +400,14 @@ func TestDocument_GetWordBeforeCursor(t *testing.T) { { document: &Document{ Text: "apple banana orange", - cursorPosition: len("apple ba"), + cursorPosition: istrings.RuneLen("apple ba"), }, expected: "ba", }, { document: &Document{ Text: "apply -f ./file/foo.json", - cursorPosition: len("apply -f ./fi"), + cursorPosition: istrings.RuneLen("apply -f ./fi"), }, expected: "fi", sep: " /", @@ -413,7 +415,7 @@ func TestDocument_GetWordBeforeCursor(t *testing.T) { { document: &Document{ Text: "apple ", - cursorPosition: len("apple "), + cursorPosition: istrings.RuneLen("apple "), }, expected: "", }, @@ -461,14 +463,14 @@ func TestDocument_GetWordBeforeCursorWithSpace(t *testing.T) { { document: &Document{ Text: "apple bana ", - cursorPosition: len("apple bana "), + cursorPosition: istrings.RuneLen("apple bana "), }, expected: "bana ", }, { document: &Document{ Text: "apply -f /path/to/file/", - cursorPosition: len("apply -f /path/to/file/"), + cursorPosition: istrings.RuneLen("apply -f /path/to/file/"), }, expected: "file/", sep: " /", @@ -476,14 +478,14 @@ func TestDocument_GetWordBeforeCursorWithSpace(t *testing.T) { { document: &Document{ Text: "apple ", - cursorPosition: len("apple "), + cursorPosition: istrings.RuneLen("apple "), }, expected: "apple ", }, { document: &Document{ Text: "path/", - cursorPosition: len("path/"), + cursorPosition: istrings.RuneLen("path/"), }, expected: "path/", sep: " /", @@ -526,37 +528,37 @@ func TestDocument_GetWordBeforeCursorWithSpace(t *testing.T) { func TestDocument_FindStartOfPreviousWord(t *testing.T) { pattern := []struct { document *Document - expected int + expected istrings.ByteIndex sep string }{ { document: &Document{ Text: "apple bana", - cursorPosition: len("apple bana"), + cursorPosition: istrings.RuneLen("apple bana"), }, - expected: len("apple "), + expected: istrings.Len("apple "), }, { document: &Document{ Text: "apply -f ./file/foo.json", - cursorPosition: len("apply -f ./file/foo.json"), + cursorPosition: istrings.RuneLen("apply -f ./file/foo.json"), }, - expected: len("apply -f ./file/"), + expected: istrings.Len("apply -f ./file/"), sep: " /", }, { document: &Document{ Text: "apple ", - cursorPosition: len("apple "), + cursorPosition: istrings.RuneLen("apple "), }, - expected: len("apple "), + expected: istrings.Len("apple "), }, { document: &Document{ Text: "apply -f ./file/foo.json", - cursorPosition: len("apply -f ./"), + cursorPosition: istrings.RuneLen("apply -f ./"), }, - expected: len("apply -f ./"), + expected: istrings.Len("apply -f ./"), sep: " /", }, { @@ -564,14 +566,14 @@ func TestDocument_FindStartOfPreviousWord(t *testing.T) { Text: "あいうえお かきくけこ さしすせそ", cursorPosition: 8, // between 'き' and 'く' }, - expected: len("あいうえお "), // this function returns index byte in string + expected: istrings.Len("あいうえお "), // this function returns index byte in string }, { document: &Document{ Text: "Добрый день Добрый день", cursorPosition: 9, }, - expected: len("Добрый "), // this function returns index byte in string + expected: istrings.Len("Добрый "), // this function returns index byte in string }, } @@ -597,37 +599,37 @@ func TestDocument_FindStartOfPreviousWord(t *testing.T) { func TestDocument_FindStartOfPreviousWordWithSpace(t *testing.T) { pattern := []struct { document *Document - expected int + expected istrings.ByteIndex sep string }{ { document: &Document{ Text: "apple bana ", - cursorPosition: len("apple bana "), + cursorPosition: istrings.RuneLen("apple bana "), }, - expected: len("apple "), + expected: istrings.Len("apple "), }, { document: &Document{ Text: "apply -f /file/foo/", - cursorPosition: len("apply -f /file/foo/"), + cursorPosition: istrings.RuneLen("apply -f /file/foo/"), }, - expected: len("apply -f /file/"), + expected: istrings.Len("apply -f /file/"), sep: " /", }, { document: &Document{ Text: "apple ", - cursorPosition: len("apple "), + cursorPosition: istrings.RuneLen("apple "), }, - expected: len(""), + expected: istrings.Len(""), }, { document: &Document{ Text: "file/", - cursorPosition: len("file/"), + cursorPosition: istrings.RuneLen("file/"), }, - expected: len(""), + expected: istrings.Len(""), sep: " /", }, { @@ -635,14 +637,14 @@ func TestDocument_FindStartOfPreviousWordWithSpace(t *testing.T) { Text: "あいうえお かきくけこ ", cursorPosition: 12, // cursor points to last }, - expected: len("あいうえお "), // this function returns index byte in string + expected: istrings.Len("あいうえお "), // this function returns index byte in string }, { document: &Document{ Text: "Добрый день ", cursorPosition: 12, }, - expected: len("Добрый "), // this function returns index byte in string + expected: istrings.Len("Добрый "), // this function returns index byte in string }, } @@ -674,14 +676,14 @@ func TestDocument_GetWordAfterCursor(t *testing.T) { { document: &Document{ Text: "apple bana", - cursorPosition: len("apple bana"), + cursorPosition: istrings.RuneLen("apple bana"), }, expected: "", }, { document: &Document{ Text: "apply -f ./file/foo.json", - cursorPosition: len("apply -f ./fi"), + cursorPosition: istrings.RuneLen("apply -f ./fi"), }, expected: "le", sep: " /", @@ -689,21 +691,21 @@ func TestDocument_GetWordAfterCursor(t *testing.T) { { document: &Document{ Text: "apple bana", - cursorPosition: len("apple "), + cursorPosition: istrings.RuneLen("apple "), }, expected: "bana", }, { document: &Document{ Text: "apple bana", - cursorPosition: len("apple"), + cursorPosition: istrings.RuneLen("apple"), }, expected: "", }, { document: &Document{ Text: "apply -f ./file/foo.json", - cursorPosition: len("apply -f ."), + cursorPosition: istrings.RuneLen("apply -f ."), }, expected: "", sep: " /", @@ -711,7 +713,7 @@ func TestDocument_GetWordAfterCursor(t *testing.T) { { document: &Document{ Text: "apple bana", - cursorPosition: len("ap"), + cursorPosition: istrings.RuneLen("ap"), }, expected: "ple", }, @@ -759,21 +761,21 @@ func TestDocument_GetWordAfterCursorWithSpace(t *testing.T) { { document: &Document{ Text: "apple bana", - cursorPosition: len("apple bana"), + cursorPosition: istrings.RuneLen("apple bana"), }, expected: "", }, { document: &Document{ Text: "apple bana", - cursorPosition: len("apple "), + cursorPosition: istrings.RuneLen("apple "), }, expected: "bana", }, { document: &Document{ Text: "/path/to", - cursorPosition: len("/path/"), + cursorPosition: istrings.RuneLen("/path/"), }, expected: "to", sep: " /", @@ -781,7 +783,7 @@ func TestDocument_GetWordAfterCursorWithSpace(t *testing.T) { { document: &Document{ Text: "/path/to/file", - cursorPosition: len("/path/"), + cursorPosition: istrings.RuneLen("/path/"), }, expected: "to", sep: " /", @@ -789,14 +791,14 @@ func TestDocument_GetWordAfterCursorWithSpace(t *testing.T) { { document: &Document{ Text: "apple bana", - cursorPosition: len("apple"), + cursorPosition: istrings.RuneLen("apple"), }, expected: " bana", }, { document: &Document{ Text: "path/to", - cursorPosition: len("path"), + cursorPosition: istrings.RuneLen("path"), }, expected: "/to", sep: " /", @@ -804,7 +806,7 @@ func TestDocument_GetWordAfterCursorWithSpace(t *testing.T) { { document: &Document{ Text: "apple bana", - cursorPosition: len("ap"), + cursorPosition: istrings.RuneLen("ap"), }, expected: "ple", }, @@ -846,52 +848,52 @@ func TestDocument_GetWordAfterCursorWithSpace(t *testing.T) { func TestDocument_FindEndOfCurrentWord(t *testing.T) { pattern := []struct { document *Document - expected int + expected istrings.ByteIndex sep string }{ { document: &Document{ Text: "apple bana", - cursorPosition: len("apple bana"), + cursorPosition: istrings.RuneLen("apple bana"), }, - expected: len(""), + expected: istrings.Len(""), }, { document: &Document{ Text: "apple bana", - cursorPosition: len("apple "), + cursorPosition: istrings.RuneLen("apple "), }, - expected: len("bana"), + expected: istrings.Len("bana"), }, { document: &Document{ Text: "apply -f ./file/foo.json", - cursorPosition: len("apply -f ./"), + cursorPosition: istrings.RuneLen("apply -f ./"), }, - expected: len("file"), + expected: istrings.Len("file"), sep: " /", }, { document: &Document{ Text: "apple bana", - cursorPosition: len("apple"), + cursorPosition: istrings.RuneLen("apple"), }, - expected: len(""), + expected: istrings.Len(""), }, { document: &Document{ Text: "apply -f ./file/foo.json", - cursorPosition: len("apply -f ."), + cursorPosition: istrings.RuneLen("apply -f ."), }, - expected: len(""), + expected: istrings.Len(""), sep: " /", }, { document: &Document{ Text: "apple bana", - cursorPosition: len("ap"), + cursorPosition: istrings.RuneLen("ap"), }, - expected: len("ple"), + expected: istrings.Len("ple"), }, { // りん(cursor)ご ばなな @@ -899,7 +901,7 @@ func TestDocument_FindEndOfCurrentWord(t *testing.T) { Text: "りんご ばなな", cursorPosition: 2, }, - expected: len("ご"), + expected: istrings.Len("ご"), }, { document: &Document{ @@ -914,7 +916,7 @@ func TestDocument_FindEndOfCurrentWord(t *testing.T) { Text: "Добрый день", cursorPosition: 3, }, - expected: len("рый"), + expected: istrings.Len("рый"), }, } @@ -940,73 +942,73 @@ func TestDocument_FindEndOfCurrentWord(t *testing.T) { func TestDocument_FindEndOfCurrentWordWithSpace(t *testing.T) { pattern := []struct { document *Document - expected int + expected istrings.ByteIndex sep string }{ { document: &Document{ Text: "apple bana", - cursorPosition: len("apple bana"), + cursorPosition: istrings.RuneLen("apple bana"), }, - expected: len(""), + expected: istrings.Len(""), }, { document: &Document{ Text: "apple bana", - cursorPosition: len("apple "), + cursorPosition: istrings.RuneLen("apple "), }, - expected: len("bana"), + expected: istrings.Len("bana"), }, { document: &Document{ Text: "apply -f /file/foo.json", - cursorPosition: len("apply -f /"), + cursorPosition: istrings.RuneLen("apply -f /"), }, - expected: len("file"), + expected: istrings.Len("file"), sep: " /", }, { document: &Document{ Text: "apple bana", - cursorPosition: len("apple"), + cursorPosition: istrings.RuneLen("apple"), }, - expected: len(" bana"), + expected: istrings.Len(" bana"), }, { document: &Document{ Text: "apply -f /path/to", - cursorPosition: len("apply -f /path"), + cursorPosition: istrings.RuneLen("apply -f /path"), }, - expected: len("/to"), + expected: istrings.Len("/to"), sep: " /", }, { document: &Document{ Text: "apple bana", - cursorPosition: len("ap"), + cursorPosition: istrings.RuneLen("ap"), }, - expected: len("ple"), + expected: istrings.Len("ple"), }, { document: &Document{ Text: "あいうえお かきくけこ", cursorPosition: 6, }, - expected: len("かきくけこ"), + expected: istrings.Len("かきくけこ"), }, { document: &Document{ Text: "あいうえお かきくけこ", cursorPosition: 5, }, - expected: len(" かきくけこ"), + expected: istrings.Len(" かきくけこ"), }, { document: &Document{ Text: "Добрый день", cursorPosition: 6, }, - expected: len(" день"), + expected: istrings.Len(" день"), }, } @@ -1032,7 +1034,7 @@ func TestDocument_FindEndOfCurrentWordWithSpace(t *testing.T) { func TestDocument_CurrentLineBeforeCursor(t *testing.T) { d := &Document{ Text: "line 1\nline 2\nline 3\nline 4\n", - cursorPosition: len("line 1\n" + "lin"), + cursorPosition: istrings.RuneLen("line 1\n" + "lin"), } ac := d.CurrentLineBeforeCursor() ex := "lin" @@ -1044,7 +1046,7 @@ func TestDocument_CurrentLineBeforeCursor(t *testing.T) { func TestDocument_CurrentLineAfterCursor(t *testing.T) { d := &Document{ Text: "line 1\nline 2\nline 3\nline 4\n", - cursorPosition: len("line 1\n" + "lin"), + cursorPosition: istrings.RuneLen("line 1\n" + "lin"), } ac := d.CurrentLineAfterCursor() ex := "e 2" @@ -1056,7 +1058,7 @@ func TestDocument_CurrentLineAfterCursor(t *testing.T) { func TestDocument_CurrentLine(t *testing.T) { d := &Document{ Text: "line 1\nline 2\nline 3\nline 4\n", - cursorPosition: len("line 1\n" + "lin"), + cursorPosition: istrings.RuneLen("line 1\n" + "lin"), } ac := d.CurrentLine() ex := "line 2" @@ -1068,11 +1070,11 @@ func TestDocument_CurrentLine(t *testing.T) { func TestDocument_CursorPositionRowAndCol(t *testing.T) { var cursorPositionTests = []struct { document *Document - expectedRow int - expectedCol int + expectedRow istrings.RuneIndex + expectedCol istrings.RuneIndex }{ { - document: &Document{Text: "line 1\nline 2\nline 3\n", cursorPosition: len("line 1\n" + "lin")}, + document: &Document{Text: "line 1\nline 2\nline 3\n", cursorPosition: istrings.RuneLen("line 1\n" + "lin")}, expectedRow: 1, expectedCol: 3, }, @@ -1097,10 +1099,10 @@ func TestDocument_CursorPositionRowAndCol(t *testing.T) { func TestDocument_GetCursorLeftPosition(t *testing.T) { d := &Document{ Text: "line 1\nline 2\nline 3\nline 4\n", - cursorPosition: len("line 1\n" + "line 2\n" + "lin"), + cursorPosition: istrings.RuneLen("line 1\n" + "line 2\n" + "lin"), } ac := d.GetCursorLeftPosition(2) - ex := -2 + var ex istrings.RuneIndex = -2 if ac != ex { t.Errorf("Should be %#v, got %#v", ex, ac) } @@ -1120,16 +1122,16 @@ func TestDocument_GetCursorLeftPosition(t *testing.T) { func TestDocument_GetCursorUpPosition(t *testing.T) { d := &Document{ Text: "line 1\nline 2\nline 3\nline 4\n", - cursorPosition: len("line 1\n" + "line 2\n" + "lin"), + cursorPosition: istrings.RuneLen("line 1\n" + "line 2\n" + "lin"), } ac := d.GetCursorUpPosition(2, -1) - ex := len("lin") - len("line 1\n"+"line 2\n"+"lin") + ex := istrings.RuneLen("lin") - istrings.RuneLen("line 1\n"+"line 2\n"+"lin") if ac != ex { t.Errorf("Should be %#v, got %#v", ex, ac) } ac = d.GetCursorUpPosition(100, -1) - ex = len("lin") - len("line 1\n"+"line 2\n"+"lin") + ex = istrings.RuneLen("lin") - istrings.RuneLen("line 1\n"+"line 2\n"+"lin") if ac != ex { t.Errorf("Should be %#v, got %#v", ex, ac) } @@ -1138,16 +1140,16 @@ func TestDocument_GetCursorUpPosition(t *testing.T) { func TestDocument_GetCursorDownPosition(t *testing.T) { d := &Document{ Text: "line 1\nline 2\nline 3\nline 4\n", - cursorPosition: len("lin"), + cursorPosition: istrings.RuneLen("lin"), } ac := d.GetCursorDownPosition(2, -1) - ex := len("line 1\n"+"line 2\n"+"lin") - len("lin") + ex := istrings.RuneLen("line 1\n"+"line 2\n"+"lin") - istrings.RuneLen("lin") if ac != ex { t.Errorf("Should be %#v, got %#v", ex, ac) } ac = d.GetCursorDownPosition(100, -1) - ex = len("line 1\n"+"line 2\n"+"line 3\n"+"line 4\n") - len("lin") + ex = istrings.RuneLen("line 1\n"+"line 2\n"+"line 3\n"+"line 4\n") - istrings.RuneLen("lin") if ac != ex { t.Errorf("Should be %#v, got %#v", ex, ac) } @@ -1156,10 +1158,10 @@ func TestDocument_GetCursorDownPosition(t *testing.T) { func TestDocument_GetCursorRightPosition(t *testing.T) { d := &Document{ Text: "line 1\nline 2\nline 3\nline 4\n", - cursorPosition: len("line 1\n" + "line 2\n" + "lin"), + cursorPosition: istrings.RuneLen("line 1\n" + "line 2\n" + "lin"), } ac := d.GetCursorRightPosition(2) - ex := 2 + var ex istrings.RuneIndex = 2 if ac != ex { t.Errorf("Should be %#v, got %#v", ex, ac) } @@ -1179,7 +1181,7 @@ func TestDocument_GetCursorRightPosition(t *testing.T) { func TestDocument_Lines(t *testing.T) { d := &Document{ Text: "line 1\nline 2\nline 3\nline 4\n", - cursorPosition: len("line 1\n" + "lin"), + cursorPosition: istrings.RuneLen("line 1\n" + "lin"), } ac := d.Lines() ex := []string{"line 1", "line 2", "line 3", "line 4", ""} @@ -1191,7 +1193,7 @@ func TestDocument_Lines(t *testing.T) { func TestDocument_LineCount(t *testing.T) { d := &Document{ Text: "line 1\nline 2\nline 3\nline 4\n", - cursorPosition: len("line 1\n" + "lin"), + cursorPosition: istrings.RuneLen("line 1\n" + "lin"), } ac := d.LineCount() ex := 5 @@ -1203,9 +1205,9 @@ func TestDocument_LineCount(t *testing.T) { func TestDocument_TranslateIndexToPosition(t *testing.T) { d := &Document{ Text: "line 1\nline 2\nline 3\nline 4\n", - cursorPosition: len("line 1\n" + "lin"), + cursorPosition: istrings.RuneLen("line 1\n" + "lin"), } - row, col := d.TranslateIndexToPosition(len("line 1\nline 2\nlin")) + row, col := d.TranslateIndexToPosition(istrings.RuneLen("line 1\nline 2\nlin")) if row != 2 { t.Errorf("Should be %#v, got %#v", 2, row) } @@ -1224,10 +1226,10 @@ func TestDocument_TranslateIndexToPosition(t *testing.T) { func TestDocument_TranslateRowColToIndex(t *testing.T) { d := &Document{ Text: "line 1\nline 2\nline 3\nline 4\n", - cursorPosition: len("line 1\n" + "lin"), + cursorPosition: istrings.RuneLen("line 1\n" + "lin"), } ac := d.TranslateRowColToIndex(2, 3) - ex := len("line 1\nline 2\nlin") + ex := istrings.RuneLen("line 1\nline 2\nlin") if ac != ex { t.Errorf("Should be %#v, got %#v", ex, ac) } @@ -1241,13 +1243,13 @@ func TestDocument_TranslateRowColToIndex(t *testing.T) { func TestDocument_OnLastLine(t *testing.T) { d := &Document{ Text: "line 1\nline 2\nline 3", - cursorPosition: len("line 1\nline"), + cursorPosition: istrings.RuneLen("line 1\nline"), } ac := d.OnLastLine() if ac { t.Errorf("Should be %#v, got %#v", false, ac) } - d.cursorPosition = len("line 1\nline 2\nline") + d.cursorPosition = istrings.RuneLen("line 1\nline 2\nline") ac = d.OnLastLine() if !ac { t.Errorf("Should be %#v, got %#v", true, ac) @@ -1257,10 +1259,10 @@ func TestDocument_OnLastLine(t *testing.T) { func TestDocument_GetEndOfLinePosition(t *testing.T) { d := &Document{ Text: "line 1\nline 2\nline 3", - cursorPosition: len("line 1\nli"), + cursorPosition: istrings.RuneLen("line 1\nli"), } ac := d.GetEndOfLinePosition() - ex := len("ne 2") + ex := istrings.RuneLen("ne 2") if ac != ex { t.Errorf("Should be %#v, got %#v", ex, ac) } diff --git a/emacs.go b/emacs.go index be34b6c4..2123cb52 100644 --- a/emacs.go +++ b/emacs.go @@ -1,6 +1,9 @@ package prompt -import "github.com/elk-language/go-prompt/internal/debug" +import ( + "github.com/elk-language/go-prompt/internal/debug" + istrings "github.com/elk-language/go-prompt/internal/strings" +) /* @@ -44,7 +47,7 @@ var emacsKeyBindings = []KeyBind{ Key: ControlE, Fn: func(buf *Buffer) { x := []rune(buf.Document().TextAfterCursor()) - buf.CursorRight(len(x)) + buf.CursorRight(istrings.RuneCount(len(x))) }, }, // Go to the beginning of the line @@ -52,7 +55,7 @@ var emacsKeyBindings = []KeyBind{ Key: ControlA, Fn: func(buf *Buffer) { x := []rune(buf.Document().TextBeforeCursor()) - buf.CursorLeft(len(x)) + buf.CursorLeft(istrings.RuneCount(len(x))) }, }, // Cut the Line after the cursor @@ -60,7 +63,7 @@ var emacsKeyBindings = []KeyBind{ Key: ControlK, Fn: func(buf *Buffer) { x := []rune(buf.Document().TextAfterCursor()) - buf.Delete(len(x)) + buf.Delete(istrings.RuneCount(len(x))) }, }, // Cut/delete the Line before the cursor @@ -68,7 +71,7 @@ var emacsKeyBindings = []KeyBind{ Key: ControlU, Fn: func(buf *Buffer) { x := []rune(buf.Document().TextBeforeCursor()) - buf.DeleteBeforeCursor(len(x)) + buf.DeleteBeforeCursor(istrings.RuneCount(len(x))) }, }, // Delete character under the cursor @@ -98,7 +101,7 @@ var emacsKeyBindings = []KeyBind{ { Key: AltRight, Fn: func(buf *Buffer) { - buf.CursorRight(buf.Document().FindStringWidthUntilEndOfCurrentWord()) + buf.CursorRight(istrings.RuneIndex(buf.Document().FindStringWidthUntilEndOfCurrentWord())) // WARN }, }, // Left allow: Backward one character @@ -112,20 +115,20 @@ var emacsKeyBindings = []KeyBind{ { Key: AltLeft, Fn: func(buf *Buffer) { - buf.CursorLeft(buf.Document().FindStringWidthUntilStartOfPreviousWord()) + buf.CursorLeft(istrings.RuneIndex(buf.Document().FindStringWidthUntilStartOfPreviousWord())) // WARN }, }, // Cut the Word before the cursor. { Key: ControlW, Fn: func(buf *Buffer) { - buf.DeleteBeforeCursor(len([]rune(buf.Document().GetWordBeforeCursorWithSpace()))) + buf.DeleteBeforeCursor(istrings.RuneIndex(len([]rune(buf.Document().GetWordBeforeCursorWithSpace())))) }, }, { Key: AltBackspace, Fn: func(buf *Buffer) { - buf.DeleteBeforeCursor(len([]rune(buf.Document().GetWordBeforeCursorWithSpace()))) + buf.DeleteBeforeCursor(istrings.RuneIndex(len([]rune(buf.Document().GetWordBeforeCursorWithSpace())))) }, }, // Clear the Screen, similar to the clear command diff --git a/emacs_test.go b/emacs_test.go index bfda59e6..20e9a0e4 100644 --- a/emacs_test.go +++ b/emacs_test.go @@ -1,11 +1,15 @@ package prompt -import "testing" +import ( + "testing" + + istrings "github.com/elk-language/go-prompt/internal/strings" +) func TestEmacsKeyBindings(t *testing.T) { buf := NewBuffer() buf.InsertText("abcde", false, true) - if buf.cursorPosition != len("abcde") { + if buf.cursorPosition != istrings.RuneIndex(len("abcde")) { t.Errorf("Want %d, but got %d", len("abcde"), buf.cursorPosition) } @@ -17,7 +21,7 @@ func TestEmacsKeyBindings(t *testing.T) { // Go to the end of the line applyEmacsKeyBind(buf, ControlE) - if buf.cursorPosition != len("abcde") { + if buf.cursorPosition != istrings.RuneIndex(len("abcde")) { t.Errorf("Want %d, but got %d", len("abcde"), buf.cursorPosition) } } diff --git a/internal/bisect/bisect.go b/internal/bisect/bisect.go index efe162fa..e8f25d74 100644 --- a/internal/bisect/bisect.go +++ b/internal/bisect/bisect.go @@ -3,13 +3,13 @@ package bisect import "sort" // Right to locate the insertion point for v in a to maintain sorted order. -func Right(a []int, v int) int { +func Right[T ~int](a []T, v T) T { return bisectRightRange(a, v, 0, len(a)) } -func bisectRightRange(a []int, v int, lo, hi int) int { +func bisectRightRange[T ~int](a []T, v T, lo, hi int) T { s := a[lo:hi] - return sort.Search(len(s), func(i int) bool { + return T(sort.Search(len(s), func(i int) bool { return s[i] > v - }) + })) } diff --git a/internal/strings/index.go b/internal/strings/index.go new file mode 100644 index 00000000..e711af0f --- /dev/null +++ b/internal/strings/index.go @@ -0,0 +1,21 @@ +package strings + +// Numeric type that represents an index +// of a single byte in a string, array or slice. +type ByteIndex int + +// Numeric type that represents an index +// of a single rune in a string, array or slice. +type RuneIndex int + +// Numeric type that represents the visible +// width of characters in a string as seen in a terminal emulator. +type StringWidth int + +// Numeric type that represents the amount +// of bytes in a string, array or slice. +type ByteCount = ByteIndex + +// Numeric type that represents the amount +// of runes in a string, array or slice. +type RuneCount = RuneIndex diff --git a/internal/strings/strings.go b/internal/strings/strings.go index f7d1886a..7800becf 100644 --- a/internal/strings/strings.go +++ b/internal/strings/strings.go @@ -1,23 +1,35 @@ package strings -import "unicode/utf8" +import ( + "unicode/utf8" +) + +// Get the length of the string in bytes. +func Len(s string) ByteCount { + return ByteCount(len(s)) +} + +// Get the length of the string in runes. +func RuneLen(s string) RuneCount { + return RuneCount(utf8.RuneCountInString(s)) +} // IndexNotByte is similar with strings.IndexByte but showing the opposite behavior. -func IndexNotByte(s string, c byte) int { +func IndexNotByte(s string, c byte) ByteIndex { n := len(s) for i := 0; i < n; i++ { if s[i] != c { - return i + return ByteIndex(i) } } return -1 } // LastIndexNotByte is similar with strings.LastIndexByte but showing the opposite behavior. -func LastIndexNotByte(s string, c byte) int { +func LastIndexNotByte(s string, c byte) ByteIndex { for i := len(s) - 1; i >= 0; i-- { if s[i] != c { - return i + return ByteIndex(i) } } return -1 @@ -41,13 +53,13 @@ func makeASCIISet(chars string) (as asciiSet, ok bool) { } // IndexNotAny is similar with strings.IndexAny but showing the opposite behavior. -func IndexNotAny(s, chars string) int { +func IndexNotAny(s, chars string) ByteIndex { if len(chars) > 0 { if len(s) > 8 { if as, isASCII := makeASCIISet(chars); isASCII { for i := 0; i < len(s); i++ { if as.notContains(s[i]) { - return i + return ByteIndex(i) } } return -1 @@ -58,7 +70,7 @@ func IndexNotAny(s, chars string) int { for i, c := range s { for j, m := range chars { if c != m && j == len(chars)-1 { - return i + return ByteIndex(i) } else if c != m { continue } else { @@ -71,13 +83,13 @@ func IndexNotAny(s, chars string) int { } // LastIndexNotAny is similar with strings.LastIndexAny but showing the opposite behavior. -func LastIndexNotAny(s, chars string) int { +func LastIndexNotAny(s, chars string) ByteIndex { if len(chars) > 0 { if len(s) > 8 { if as, isASCII := makeASCIISet(chars); isASCII { for i := len(s) - 1; i >= 0; i-- { if as.notContains(s[i]) { - return i + return ByteIndex(i) } } return -1 @@ -89,7 +101,7 @@ func LastIndexNotAny(s, chars string) int { i -= size for j, m := range chars { if r != m && j == len(chars)-1 { - return i + return ByteIndex(i) } else if r != m { continue } else { diff --git a/key_bind_func.go b/key_bind_func.go index 7b2ecdf6..00bd3066 100644 --- a/key_bind_func.go +++ b/key_bind_func.go @@ -1,15 +1,19 @@ package prompt +import ( + istrings "github.com/elk-language/go-prompt/internal/strings" +) + // GoLineEnd Go to the End of the line func GoLineEnd(buf *Buffer) { x := []rune(buf.Document().TextAfterCursor()) - buf.CursorRight(len(x)) + buf.CursorRight(istrings.RuneCount(len(x))) } // GoLineBeginning Go to the beginning of the line func GoLineBeginning(buf *Buffer) { x := []rune(buf.Document().TextBeforeCursor()) - buf.CursorLeft(len(x)) + buf.CursorLeft(istrings.RuneCount(len(x))) } // DeleteChar Delete character under the cursor @@ -19,7 +23,7 @@ func DeleteChar(buf *Buffer) { // DeleteWord Delete word before the cursor func DeleteWord(buf *Buffer) { - buf.DeleteBeforeCursor(len([]rune(buf.Document().TextBeforeCursor())) - buf.Document().FindStartOfPreviousWordWithSpace()) + buf.DeleteBeforeCursor(istrings.RuneCount(len([]rune(buf.Document().TextBeforeCursor()))) - istrings.RuneCount(buf.Document().FindStartOfPreviousWordWithSpace())) // WARN } // DeleteBeforeChar Go to Backspace @@ -39,10 +43,10 @@ func GoLeftChar(buf *Buffer) { // GoRightWord Forward one word func GoRightWord(buf *Buffer) { - buf.CursorRight(buf.Document().FindEndOfCurrentWordWithSpace()) + buf.CursorRight(istrings.RuneCount(buf.Document().FindEndOfCurrentWordWithSpace())) // WARN } // GoLeftWord Backward one word func GoLeftWord(buf *Buffer) { - buf.CursorLeft(len([]rune(buf.Document().TextBeforeCursor())) - buf.Document().FindStartOfPreviousWordWithSpace()) + buf.CursorLeft(istrings.RuneCount(len([]rune(buf.Document().TextBeforeCursor()))) - istrings.RuneCount(buf.Document().FindStartOfPreviousWordWithSpace())) // WARN } diff --git a/position.go b/position.go index 58365669..ac6d834b 100644 --- a/position.go +++ b/position.go @@ -4,6 +4,7 @@ import ( "io" "strings" + istrings "github.com/elk-language/go-prompt/internal/strings" "github.com/mattn/go-runewidth" ) @@ -13,7 +14,8 @@ import ( // (0, 0) represents the top-left corner of the prompt, // while (n, n) the bottom-right corner. type Position struct { - X, Y int + X istrings.StringWidth + Y int } // Join two positions and return a new position. @@ -45,16 +47,16 @@ func (p Position) Subtract(other Position) Position { // positionAtEndOfString calculates the position of the // p at the end of the given string. -func positionAtEndOfString(str string, columns int) Position { - // fmt.Printf("%q\n", str) +func positionAtEndOfString(str string, columns istrings.StringWidth) Position { pos := positionAtEndOfReader(strings.NewReader(str), columns) return pos } // positionAtEndOfReader calculates the position of the // p at the end of the given io.Reader. -func positionAtEndOfReader(reader io.RuneReader, columns int) Position { - var down, right int +func positionAtEndOfReader(reader io.RuneReader, columns istrings.StringWidth) Position { + var down int + var right istrings.StringWidth charLoop: for { @@ -78,7 +80,7 @@ charLoop: down++ right = 0 default: - right += runewidth.RuneWidth(char) + right += istrings.StringWidth(runewidth.RuneWidth(char)) if right == columns { right = 0 down++ diff --git a/position_test.go b/position_test.go index 1de7defb..44d14567 100644 --- a/position_test.go +++ b/position_test.go @@ -6,13 +6,14 @@ package prompt import ( "testing" + istrings "github.com/elk-language/go-prompt/internal/strings" "github.com/google/go-cmp/cmp" ) func TestPositionAtEndOfString(t *testing.T) { tests := map[string]struct { input string - columns int + columns istrings.StringWidth want Position }{ "empty": { diff --git a/prompt.go b/prompt.go index ecc43ed9..19c3d135 100644 --- a/prompt.go +++ b/prompt.go @@ -8,6 +8,7 @@ import ( "unicode/utf8" "github.com/elk-language/go-prompt/internal/debug" + istrings "github.com/elk-language/go-prompt/internal/strings" ) const inputBufferSize = 1024 @@ -161,7 +162,7 @@ func (p *Prompt) feed(b []byte) (shouldExit bool, userInput *UserInput) { p.buf = NewBuffer() p.history.Clear() case Up, ControlP: - cursor := p.buf.Document().GetCursorPosition(int(p.renderer.col)) + cursor := p.buf.Document().GetCursorPosition(p.renderer.col) if cursor.Y != 0 { p.buf.CursorUp(1) break @@ -175,8 +176,8 @@ func (p *Prompt) feed(b []byte) (shouldExit bool, userInput *UserInput) { } case Down, ControlN: - endOfTextCursor := p.buf.Document().GetEndOfTextPosition(int(p.renderer.col)) - cursor := p.buf.Document().GetCursorPosition(int(p.renderer.col)) + endOfTextCursor := p.buf.Document().GetEndOfTextPosition(p.renderer.col) + cursor := p.buf.Document().GetCursorPosition(p.renderer.col) if endOfTextCursor.Y > cursor.Y { p.buf.CursorDown(1) break @@ -229,7 +230,7 @@ func (p *Prompt) handleCompletionKeyBinding(key Key, completing bool) { if s, ok := p.completion.GetSelectedSuggestion(); ok { w := p.buf.Document().GetWordBeforeCursorUntilSeparator(p.completion.wordSeparator) if w != "" { - p.buf.DeleteBeforeCursor(len([]rune(w))) + p.buf.DeleteBeforeCursor(istrings.RuneCount(len([]rune(w)))) } p.buf.InsertText(s.Text, false, true) } diff --git a/render.go b/render.go index 97b9d6c4..f68298da 100644 --- a/render.go +++ b/render.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/elk-language/go-prompt/internal/debug" + istrings "github.com/elk-language/go-prompt/internal/strings" runewidth "github.com/mattn/go-runewidth" ) @@ -15,7 +16,7 @@ type Render struct { breakLineCallback func(*Document) title string row uint16 - col uint16 + col istrings.StringWidth previousCursor Position @@ -78,7 +79,7 @@ func (r *Render) prepareArea(lines int) { // UpdateWinSize called when window size is changed. func (r *Render) UpdateWinSize(ws *WinSize) { r.row = ws.Row - r.col = ws.Col + r.col = istrings.StringWidth(ws.Col) } func (r *Render) renderWindowTooSmall() { @@ -108,10 +109,10 @@ func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) { formatted = formatted[completions.verticalScroll : completions.verticalScroll+windowHeight] r.prepareArea(windowHeight) - cursor := positionAtEndOfString(prefix+buf.Document().TextBeforeCursor(), int(r.col)) + cursor := positionAtEndOfString(prefix+buf.Document().TextBeforeCursor(), r.col) x := cursor.X - if x+width >= int(r.col) { - cursor = r.backward(cursor, x+width-int(r.col)) + if x+width >= r.col { + cursor = r.backward(cursor, x+width-r.col) } contentHeight := len(completions.tmp) @@ -160,8 +161,8 @@ func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) { r.backward(c, width) } - if x+width >= int(r.col) { - r.out.CursorForward(x + width - int(r.col)) + if x+width >= r.col { + r.out.CursorForward(int(x + width - r.col)) } r.out.CursorUp(windowHeight) @@ -180,8 +181,8 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager, lexer Lex line := buffer.Text() prefix := r.getCurrentPrefix() - prefixWidth := runewidth.StringWidth(prefix) - cursor := positionAtEndOfString(prefix+line, int(r.col)) + prefixWidth := istrings.StringWidth(runewidth.StringWidth(prefix)) + cursor := positionAtEndOfString(prefix+line, r.col) // prepare area y := cursor.Y @@ -209,7 +210,7 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager, lexer Lex r.lineWrap(&cursor) - targetCursor := buffer.DisplayCursorPosition(int(r.col)) + targetCursor := buffer.DisplayCursorPosition(r.col) if targetCursor.Y == 0 { targetCursor.X += prefixWidth } @@ -217,12 +218,12 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager, lexer Lex r.renderCompletion(buffer, completion) if suggest, ok := completion.GetSelectedSuggestion(); ok { - cursor = r.backward(cursor, runewidth.StringWidth(buffer.Document().GetWordBeforeCursorUntilSeparator(completion.wordSeparator))) + cursor = r.backward(cursor, istrings.StringWidth(runewidth.StringWidth(buffer.Document().GetWordBeforeCursorUntilSeparator(completion.wordSeparator)))) r.out.SetColor(r.previewSuggestionTextColor, r.previewSuggestionBGColor, false) r.out.WriteString(suggest.Text) r.out.SetColor(DefaultColor, DefaultColor, false) - cursor.X += runewidth.StringWidth(suggest.Text) + cursor.X += istrings.StringWidth(runewidth.StringWidth(suggest.Text)) endOfSuggestionPos := cursor rest := buffer.Document().TextAfterCursor() @@ -235,7 +236,7 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager, lexer Lex r.out.SetColor(DefaultColor, DefaultColor, false) - cursor = cursor.Join(positionAtEndOfString(rest, int(r.col))) + cursor = cursor.Join(positionAtEndOfString(rest, r.col)) r.lineWrap(&cursor) @@ -267,7 +268,7 @@ func (r *Render) lex(lexer Lexer, input string) { // BreakLine to break line. func (r *Render) BreakLine(buffer *Buffer, lexer Lexer) { // Erasing and Render - cursor := positionAtEndOfString(buffer.Document().TextBeforeCursor()+r.getCurrentPrefix(), int(r.col)) + cursor := positionAtEndOfString(buffer.Document().TextBeforeCursor()+r.getCurrentPrefix(), r.col) r.clear(cursor) r.renderPrefix() @@ -298,7 +299,7 @@ func (r *Render) clear(cursor Position) { // backward moves cursor to backward from a current cursor position // regardless there is a line break. -func (r *Render) backward(from Position, n int) Position { +func (r *Render) backward(from Position, n istrings.StringWidth) Position { return r.move(from, Position{X: from.X - n, Y: from.Y}) } @@ -307,12 +308,12 @@ func (r *Render) backward(from Position, n int) Position { func (r *Render) move(from, to Position) Position { newPosition := from.Subtract(to) r.out.CursorUp(newPosition.Y) - r.out.CursorBackward(newPosition.X) + r.out.CursorBackward(int(newPosition.X)) return to } func (r *Render) lineWrap(cursor *Position) { - if runtime.GOOS != "windows" && cursor.X > 0 && cursor.X%int(r.col) == 0 { + if runtime.GOOS != "windows" && cursor.X > 0 && cursor.X%r.col == 0 { cursor.X = 0 cursor.Y += 1 r.out.WriteRaw([]byte{'\n'}) @@ -330,8 +331,8 @@ func clamp(high, low, x float64) float64 { } } -func alignNextLine(r *Render, col int) { +func alignNextLine(r *Render, col istrings.StringWidth) { r.out.CursorDown(1) r.out.WriteString("\r") - r.out.CursorForward(col) + r.out.CursorForward(int(col)) } diff --git a/render_test.go b/render_test.go index 81e0f643..ab349520 100644 --- a/render_test.go +++ b/render_test.go @@ -7,6 +7,8 @@ import ( "reflect" "syscall" "testing" + + istrings "github.com/elk-language/go-prompt/internal/strings" ) func TestFormatCompletion(t *testing.T) { @@ -17,7 +19,7 @@ func TestFormatCompletion(t *testing.T) { suffix string expected []Suggest maxWidth int - expectedWidth int + expectedWidth istrings.StringWidth }{ { scenario: "",