Skip to content

Commit

Permalink
Introduce more strict numeric types for different kinds of indices
Browse files Browse the repository at this point in the history
  • Loading branch information
Verseth committed Jul 10, 2023
1 parent f68d3a3 commit facef81
Show file tree
Hide file tree
Showing 17 changed files with 368 additions and 300 deletions.
60 changes: 33 additions & 27 deletions buffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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 {
Expand All @@ -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
}
Expand All @@ -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())

Expand All @@ -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
Expand All @@ -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.
Expand Down
34 changes: 20 additions & 14 deletions buffer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package prompt
import (
"reflect"
"testing"

istrings "github.com/elk-language/go-prompt/internal/strings"
)

func TestNewBuffer(t *testing.T) {
Expand All @@ -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)
}
}

Expand All @@ -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)
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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) {
Expand Down Expand Up @@ -141,17 +147,17 @@ 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.
b = NewBuffer()
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)
}
}

Expand All @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)

Expand All @@ -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.
Expand Down
10 changes: 6 additions & 4 deletions completion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ package prompt
import (
"reflect"
"testing"

istrings "github.com/elk-language/go-prompt/internal/strings"
)

func TestFormatShortSuggestion(t *testing.T) {
var scenarioTable = []struct {
in []Suggest
expected []Suggest
max int
exWidth int
exWidth istrings.StringWidth
}{
{
in: []Suggest{
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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{
Expand All @@ -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] ")),
},
}

Expand Down
Loading

0 comments on commit facef81

Please sign in to comment.