From f2ae4e53edb200a2c312a7b86a63c89d37897152 Mon Sep 17 00:00:00 2001 From: Mateusz Drewniak Date: Wed, 5 Jul 2023 09:45:08 +0200 Subject: [PATCH 01/21] Add support for navigating in multiline input using left/right arrows --- buffer.go | 4 +- document.go | 40 ++++++---- document_test.go | 34 ++++++--- go.mod | 5 +- go.sum | 35 +++++++++ position.go | 93 +++++++++++++++++++++++ position_test.go | 189 +++++++++++++++++++++++++++++++++++++++++++++++ render.go | 76 +++++++++---------- 8 files changed, 408 insertions(+), 68 deletions(-) create mode 100644 position.go create mode 100644 position_test.go diff --git a/buffer.go b/buffer.go index 996af7c3..5eca4db9 100644 --- a/buffer.go +++ b/buffer.go @@ -37,8 +37,8 @@ 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() int { - return b.Document().DisplayCursorPosition() +func (b *Buffer) DisplayCursorPosition(columns int) Position { + return b.Document().DisplayCursorPosition(columns) } // InsertText insert string from current line. diff --git a/document.go b/document.go index b1996455..2e4bb880 100644 --- a/document.go +++ b/document.go @@ -6,7 +6,7 @@ import ( "github.com/elk-language/go-prompt/internal/bisect" istrings "github.com/elk-language/go-prompt/internal/strings" - runewidth "github.com/mattn/go-runewidth" + "golang.org/x/exp/utf8string" ) // Document has text displayed in terminal and cursor position. @@ -34,13 +34,9 @@ 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() int { - var position int - runes := []rune(d.Text)[:d.cursorPosition] - for i := range runes { - position += runewidth.RuneWidth(runes[i]) - } - return position +func (d *Document) DisplayCursorPosition(columns int) Position { + str := utf8string.NewString(d.Text).Slice(0, d.cursorPosition) + return positionAtEndOfString(str, columns) } // GetCharRelativeToCursor return character relative to cursor position, or empty string @@ -129,7 +125,7 @@ func (d *Document) GetWordAfterCursorUntilSeparatorIgnoreNextToCursor(sep string // pointing to the start of the previous word. Return 0 if nothing was found. func (d *Document) FindStartOfPreviousWord() int { x := d.TextBeforeCursor() - i := strings.LastIndexByte(x, ' ') + i := strings.LastIndexAny(x, " \n") if i != -1 { return i + 1 } @@ -324,10 +320,17 @@ func (d *Document) GetCursorLeftPosition(count int) int { if count < 0 { return d.GetCursorRightPosition(-count) } - if d.CursorPositionCol() > count { - return -count + runeSlice := []rune(d.Text) + counter := 0 + targetPosition := d.cursorPosition - count + if targetPosition < 0 { + targetPosition = 0 + } + for range runeSlice[targetPosition:d.cursorPosition] { + counter-- } - return -d.CursorPositionCol() + + return counter } // GetCursorRightPosition returns relative position for cursor right. @@ -335,10 +338,17 @@ func (d *Document) GetCursorRightPosition(count int) int { if count < 0 { return d.GetCursorLeftPosition(-count) } - if len(d.CurrentLineAfterCursor()) > count { - return count + runeSlice := []rune(d.Text) + counter := 0 + targetPosition := d.cursorPosition + count + if targetPosition > len(runeSlice) { + targetPosition = len(runeSlice) } - return len(d.CurrentLineAfterCursor()) + for range runeSlice[d.cursorPosition:targetPosition] { + counter++ + } + + return counter } // GetCursorUpPosition return the relative cursor position (character index) where we would be diff --git a/document_test.go b/document_test.go index dd4d1022..b14601fa 100644 --- a/document_test.go +++ b/document_test.go @@ -26,9 +26,9 @@ func ExampleDocument_DisplayCursorPosition() { Text: `Hello! my name is c-bata.`, cursorPosition: len(`Hello`), } - fmt.Println("DisplayCursorPosition", d.DisplayCursorPosition()) + fmt.Println("DisplayCursorPosition", d.DisplayCursorPosition(50)) // Output: - // DisplayCursorPosition 5 + // DisplayCursorPosition {5 0} } func ExampleDocument_CursorPositionRow() { @@ -94,9 +94,9 @@ func ExampleDocument_DisplayCursorPosition_withJapanese() { Text: `こんにちは、芝田 将です。`, cursorPosition: 3, } - fmt.Println("DisplayCursorPosition", d.DisplayCursorPosition()) + fmt.Println("DisplayCursorPosition", d.DisplayCursorPosition(30)) // Output: - // DisplayCursorPosition 6 + // DisplayCursorPosition {6 0} } func ExampleDocument_CurrentLineBeforeCursor() { @@ -222,21 +222,21 @@ func ExampleDocument_GetWordAfterCursorUntilSeparatorIgnoreNextToCursor() { func TestDocument_DisplayCursorPosition(t *testing.T) { patterns := []struct { document *Document - expected int + expected Position }{ { document: &Document{ Text: "hello", cursorPosition: 2, }, - expected: 2, + expected: Position{X: 2}, }, { document: &Document{ Text: "こんにちは", cursorPosition: 2, }, - expected: 4, + expected: Position{X: 4}, }, { // If you're facing test failure on this test case and your terminal is iTerm2, @@ -247,12 +247,12 @@ func TestDocument_DisplayCursorPosition(t *testing.T) { Text: "Добрый день", cursorPosition: 3, }, - expected: 3, + expected: Position{X: 3}, }, } for _, p := range patterns { - ac := p.document.DisplayCursorPosition() + ac := p.document.DisplayCursorPosition(30) if ac != p.expected { t.Errorf("Should be %#v, got %#v", p.expected, ac) } @@ -1105,7 +1105,13 @@ func TestDocument_GetCursorLeftPosition(t *testing.T) { t.Errorf("Should be %#v, got %#v", ex, ac) } ac = d.GetCursorLeftPosition(10) - ex = -3 + ex = -10 + if ac != ex { + t.Errorf("Should be %#v, got %#v", ex, ac) + } + + ac = d.GetCursorLeftPosition(30) + ex = -17 if ac != ex { t.Errorf("Should be %#v, got %#v", ex, ac) } @@ -1158,7 +1164,13 @@ func TestDocument_GetCursorRightPosition(t *testing.T) { t.Errorf("Should be %#v, got %#v", ex, ac) } ac = d.GetCursorRightPosition(10) - ex = 3 + ex = 10 + if ac != ex { + t.Errorf("Should be %#v, got %#v", ex, ac) + } + + ac = d.GetCursorRightPosition(30) + ex = 11 if ac != ex { t.Errorf("Should be %#v, got %#v", ex, ac) } diff --git a/go.mod b/go.mod index d559da46..6a03d321 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/elk-language/go-prompt -go 1.14 +go 1.19 require ( github.com/google/go-cmp v0.5.9 // indirect @@ -8,5 +8,6 @@ require ( github.com/mattn/go-runewidth v0.0.9 github.com/mattn/go-tty v0.0.3 github.com/pkg/term v1.2.0-beta.2 - golang.org/x/sys v0.0.0-20200918174421-af09f7315aff + golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect + golang.org/x/sys v0.1.0 ) diff --git a/go.sum b/go.sum index a0af5319..916c9d7d 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -14,7 +15,23 @@ github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI= github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= github.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw= github.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -23,3 +40,21 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200918174421-af09f7315aff h1:1CPUrky56AcgSpxz/KfgzQWzfG09u5YOL8MvPYBlrL8= golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/position.go b/position.go new file mode 100644 index 00000000..58365669 --- /dev/null +++ b/position.go @@ -0,0 +1,93 @@ +package prompt + +import ( + "io" + "strings" + + "github.com/mattn/go-runewidth" +) + +// Position stores the coordinates +// of a p. +// +// (0, 0) represents the top-left corner of the prompt, +// while (n, n) the bottom-right corner. +type Position struct { + X, Y int +} + +// Join two positions and return a new position. +func (p Position) Join(other Position) Position { + if other.Y == 0 { + p.X += other.X + } else { + p.X = other.X + p.Y += other.Y + } + return p +} + +// Add two positions and return a new position. +func (p Position) Add(other Position) Position { + return Position{ + X: p.X + other.X, + Y: p.Y + other.Y, + } +} + +// Subtract two positions and return a new position. +func (p Position) Subtract(other Position) Position { + return Position{ + X: p.X - other.X, + Y: p.Y - other.Y, + } +} + +// 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) + 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 + +charLoop: + for { + char, _, err := reader.ReadRune() + if err != nil { + break charLoop + } + + switch char { + case '\r': + char, _, err := reader.ReadRune() + if err != nil { + break charLoop + } + + if char == '\n' { + down++ + right = 0 + } + case '\n': + down++ + right = 0 + default: + right += runewidth.RuneWidth(char) + if right == columns { + right = 0 + down++ + } + } + } + + return Position{ + X: right, + Y: down, + } +} diff --git a/position_test.go b/position_test.go new file mode 100644 index 00000000..1de7defb --- /dev/null +++ b/position_test.go @@ -0,0 +1,189 @@ +//go:build !windows +// +build !windows + +package prompt + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestPositionAtEndOfString(t *testing.T) { + tests := map[string]struct { + input string + columns int + want Position + }{ + "empty": { + input: "", + columns: 20, + want: Position{ + X: 0, + Y: 0, + }, + }, + "one letter": { + input: "f", + columns: 20, + want: Position{ + X: 1, + Y: 0, + }, + }, + "one-line fits in columns": { + input: "foo bar", + columns: 20, + want: Position{ + X: 7, + Y: 0, + }, + }, + "multiline": { + input: "foo\nbar\n", + columns: 20, + want: Position{ + X: 0, + Y: 2, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + got := positionAtEndOfString(tc.input, tc.columns) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf(diff) + } + }) + } +} + +func TestPositionAdd(t *testing.T) { + tests := map[string]struct { + left Position + right Position + want Position + }{ + "empty": { + left: Position{}, + right: Position{}, + want: Position{}, + }, + "only X": { + left: Position{X: 1}, + right: Position{X: 2}, + want: Position{X: 3}, + }, + "only Y": { + left: Position{Y: 1}, + right: Position{Y: 2}, + want: Position{Y: 3}, + }, + "different coordinates": { + left: Position{X: 1}, + right: Position{Y: 2}, + want: Position{X: 1, Y: 2}, + }, + "both X and Y": { + left: Position{X: 1, Y: 5}, + right: Position{X: 10, Y: 2}, + want: Position{X: 11, Y: 7}, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + got := tc.left.Add(tc.right) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf(diff) + } + }) + } +} + +func TestPositionSubtract(t *testing.T) { + tests := map[string]struct { + left Position + right Position + want Position + }{ + "empty": { + left: Position{}, + right: Position{}, + want: Position{}, + }, + "only X": { + left: Position{X: 1}, + right: Position{X: 2}, + want: Position{X: -1}, + }, + "only Y": { + left: Position{Y: 5}, + right: Position{Y: 2}, + want: Position{Y: 3}, + }, + "different coordinates": { + left: Position{X: 1}, + right: Position{Y: 2}, + want: Position{X: 1, Y: -2}, + }, + "both X and Y": { + left: Position{X: 1, Y: 5}, + right: Position{X: 10, Y: 2}, + want: Position{X: -9, Y: 3}, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + got := tc.left.Subtract(tc.right) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf(diff) + } + }) + } +} + +func TestPositionJoin(t *testing.T) { + tests := map[string]struct { + left Position + right Position + want Position + }{ + "empty": { + left: Position{}, + right: Position{}, + want: Position{}, + }, + "only X": { + left: Position{X: 1}, + right: Position{X: 2}, + want: Position{X: 3}, + }, + "only Y": { + left: Position{Y: 1}, + right: Position{Y: 2}, + want: Position{Y: 3}, + }, + "different coordinates": { + left: Position{X: 5}, + right: Position{Y: 2}, + want: Position{X: 0, Y: 2}, + }, + "both X and Y": { + left: Position{X: 1, Y: 5}, + right: Position{X: 10, Y: 2}, + want: Position{X: 10, Y: 7}, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + got := tc.left.Join(tc.right) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf(diff) + } + }) + } +} diff --git a/render.go b/render.go index e865d137..f9ab8467 100644 --- a/render.go +++ b/render.go @@ -18,7 +18,7 @@ type Render struct { row uint16 col uint16 - previousCursor int + previousCursor Position // colors, prefixTextColor Color @@ -94,7 +94,7 @@ func (r *Render) renderWindowTooSmall() { func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) { suggestions := completions.GetSuggestions() - if len(completions.GetSuggestions()) == 0 { + if len(suggestions) == 0 { return } prefix := r.getCurrentPrefix() @@ -112,8 +112,8 @@ func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) { formatted = formatted[completions.verticalScroll : completions.verticalScroll+windowHeight] r.prepareArea(windowHeight) - cursor := runewidth.StringWidth(prefix) + runewidth.StringWidth(buf.Document().TextBeforeCursor()) - x, _ := r.toPos(cursor) + cursor := positionAtEndOfString(prefix+buf.Document().TextBeforeCursor(), int(r.col)) + x := cursor.X if x+width >= int(r.col) { cursor = r.backward(cursor, x+width-int(r.col)) } @@ -135,7 +135,7 @@ func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) { r.out.SetColor(White, Cyan, false) for i := 0; i < windowHeight; i++ { - alignNextLine(r, cursorColumnSpacing) + alignNextLine(r, cursorColumnSpacing.X) if i == selected { r.out.SetColor(r.selectedSuggestionTextColor, r.selectedSuggestionBGColor, true) @@ -159,8 +159,9 @@ func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) { r.out.WriteStr(" ") r.out.SetColor(DefaultColor, DefaultColor, false) - r.lineWrap(cursor + width) - r.backward(cursor+width, width) + c := cursor.Add(Position{X: width}) + r.lineWrap(&c) + r.backward(c, width) } if x+width >= int(r.col) { @@ -179,14 +180,15 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager, lexer Lex return } defer func() { debug.AssertNoError(r.out.Flush()) }() - r.move(r.previousCursor, 0) + r.clear(r.previousCursor) line := buffer.Text() prefix := r.getCurrentPrefix() - cursor := runewidth.StringWidth(prefix) + runewidth.StringWidth(line) + prefixWidth := runewidth.StringWidth(prefix) + cursor := positionAtEndOfString(prefix+line, int(r.col)) // prepare area - _, y := r.toPos(cursor) + y := cursor.Y h := y + 1 + int(completion.max) if h > int(r.row) || completionMargin > int(r.col) { @@ -209,11 +211,13 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager, lexer Lex r.out.SetColor(DefaultColor, DefaultColor, false) - r.lineWrap(cursor) + r.lineWrap(&cursor) - r.out.EraseDown() - - cursor = r.backward(cursor, runewidth.StringWidth(line)-buffer.DisplayCursorPosition()) + targetCursor := buffer.DisplayCursorPosition(int(r.col)) + if targetCursor.Y == 0 { + targetCursor.X += prefixWidth + } + cursor = r.move(cursor, targetCursor) r.renderCompletion(buffer, completion) if suggest, ok := completion.GetSelectedSuggestion(); ok { @@ -222,7 +226,8 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager, lexer Lex r.out.SetColor(r.previewSuggestionTextColor, r.previewSuggestionBGColor, false) r.out.WriteStr(suggest.Text) r.out.SetColor(DefaultColor, DefaultColor, false) - cursor += runewidth.StringWidth(suggest.Text) + cursor.X += runewidth.StringWidth(suggest.Text) + endOfSuggestionPos := cursor rest := buffer.Document().TextAfterCursor() @@ -234,10 +239,11 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager, lexer Lex r.out.SetColor(DefaultColor, DefaultColor, false) - cursor += runewidth.StringWidth(rest) - r.lineWrap(cursor) + cursor = cursor.Join(positionAtEndOfString(rest, int(r.col))) + + r.lineWrap(&cursor) - cursor = r.backward(cursor, runewidth.StringWidth(rest)) + cursor = r.move(cursor, endOfSuggestionPos) } r.previousCursor = cursor } @@ -265,7 +271,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 := runewidth.StringWidth(buffer.Document().TextBeforeCursor()) + runewidth.StringWidth(r.getCurrentPrefix()) + cursor := positionAtEndOfString(buffer.Document().TextBeforeCursor()+r.getCurrentPrefix(), int(r.col)) r.clear(cursor) r.renderPrefix() @@ -284,41 +290,35 @@ func (r *Render) BreakLine(buffer *Buffer, lexer Lexer) { r.breakLineCallback(buffer.Document()) } - r.previousCursor = 0 + r.previousCursor = Position{} } // clear erases the screen from a beginning of input // even if there is line break which means input length exceeds a window's width. -func (r *Render) clear(cursor int) { - r.move(cursor, 0) +func (r *Render) clear(cursor Position) { + r.move(cursor, Position{}) r.out.EraseDown() } // backward moves cursor to backward from a current cursor position // regardless there is a line break. -func (r *Render) backward(from, n int) int { - return r.move(from, from-n) +func (r *Render) backward(from Position, n int) Position { + return r.move(from, Position{X: from.X - n, Y: from.Y}) } // move moves cursor to specified position from the beginning of input // even if there is a line break. -func (r *Render) move(from, to int) int { - fromX, fromY := r.toPos(from) - toX, toY := r.toPos(to) - - r.out.CursorUp(fromY - toY) - r.out.CursorBackward(fromX - toX) +func (r *Render) move(from, to Position) Position { + newPosition := from.Subtract(to) + r.out.CursorUp(newPosition.Y) + r.out.CursorBackward(newPosition.X) return to } -// toPos returns the relative position from the beginning of the string. -func (r *Render) toPos(cursor int) (x, y int) { - col := int(r.col) - return cursor % col, cursor / col -} - -func (r *Render) lineWrap(cursor int) { - if runtime.GOOS != "windows" && cursor > 0 && cursor%int(r.col) == 0 { +func (r *Render) lineWrap(cursor *Position) { + if runtime.GOOS != "windows" && cursor.X > 0 && cursor.X%int(r.col) == 0 { + cursor.X = 0 + cursor.Y += 1 r.out.WriteRaw([]byte{'\n'}) } } From bce683f7bb8972477de267d1f7667dee49a50465 Mon Sep 17 00:00:00 2001 From: Mateusz Drewniak Date: Wed, 5 Jul 2023 10:30:05 +0200 Subject: [PATCH 02/21] Add support for navigating up an down multiline text with up/down arrows --- .gitignore | 4 ++++ buffer.go | 32 +++++++++----------------------- document.go | 10 ++++++++++ prompt.go | 44 +++++++++++++++++++++++++++++++++++--------- 4 files changed, 58 insertions(+), 32 deletions(-) diff --git a/.gitignore b/.gitignore index eba2b56a..a45a8758 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,7 @@ _testmain.go # Glide vendor/ + +# Logs +log +*.log diff --git a/buffer.go b/buffer.go index 5eca4db9..67b95cf7 100644 --- a/buffer.go +++ b/buffer.go @@ -8,12 +8,11 @@ import ( // Buffer emulates the console buffer. type Buffer struct { - workingLines []string // The working lines. Similar to history - workingIndex int - cursorPosition int - cacheDocument *Document - preferredColumn int // Remember the original column for the next up/down movement. - lastKeyStroke Key + workingLines []string // The working lines. Similar to history + workingIndex int + cursorPosition int + cacheDocument *Document + lastKeyStroke Key } // Text returns string of the current line. @@ -103,27 +102,15 @@ func (b *Buffer) CursorRight(count int) { // CursorUp move cursor to the previous line. // (for multi-line edit). func (b *Buffer) CursorUp(count int) { - orig := b.preferredColumn - if b.preferredColumn == -1 { // -1 means nil - orig = b.Document().CursorPositionCol() - } + orig := b.Document().CursorPositionCol() b.cursorPosition += b.Document().GetCursorUpPosition(count, orig) - - // Remember the original column for the next up/down movement. - b.preferredColumn = orig } // CursorDown move cursor to the next line. // (for multi-line edit). func (b *Buffer) CursorDown(count int) { - orig := b.preferredColumn - if b.preferredColumn == -1 { // -1 means nil - orig = b.Document().CursorPositionCol() - } + orig := b.Document().CursorPositionCol() b.cursorPosition += b.Document().GetCursorDownPosition(count, orig) - - // Remember the original column for the next up/down movement. - b.preferredColumn = orig } // DeleteBeforeCursor delete specified number of characters before cursor and return the deleted text. @@ -186,9 +173,8 @@ func (b *Buffer) SwapCharactersBeforeCursor() { // NewBuffer is constructor of Buffer struct. func NewBuffer() (b *Buffer) { b = &Buffer{ - workingLines: []string{""}, - workingIndex: 0, - preferredColumn: -1, // -1 means nil + workingLines: []string{""}, + workingIndex: 0, } return } diff --git a/document.go b/document.go index 2e4bb880..f2359b27 100644 --- a/document.go +++ b/document.go @@ -351,6 +351,16 @@ func (d *Document) GetCursorRightPosition(count int) int { return counter } +// Get the current cursor position. +func (d *Document) GetCursorPosition(columns int) Position { + return positionAtEndOfString(d.TextBeforeCursor(), columns) +} + +// Get the position of the end of the current text. +func (d *Document) GetEndOfTextPosition(columns int) 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 { diff --git a/prompt.go b/prompt.go index 4fc13cea..d0030a0e 100644 --- a/prompt.go +++ b/prompt.go @@ -117,6 +117,15 @@ func (p *Prompt) Run() { } } +// func Log(format string, a ...any) { +// f, err := os.OpenFile("log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) +// if err != nil { +// log.Fatalf("error opening file: %v", err) +// } +// defer f.Close() +// fmt.Fprintf(f, format, a...) +// } + func (p *Prompt) feed(b []byte) (shouldExit bool, exec *Exec) { key := GetKey(b) p.buf.lastKeyStroke = key @@ -138,18 +147,35 @@ func (p *Prompt) feed(b []byte) (shouldExit bool, exec *Exec) { p.buf = NewBuffer() p.history.Clear() case Up, ControlP: - if !completing { // Don't use p.completion.Completing() because it takes double operation when switch to selected=-1. - if newBuf, changed := p.history.Older(p.buf); changed { - p.buf = newBuf - } + cursor := p.buf.Document().GetCursorPosition(int(p.renderer.col)) + if cursor.Y != 0 { + p.buf.CursorUp(1) + break + } + if completing { + break } + + if newBuf, changed := p.history.Older(p.buf); changed { + p.buf = newBuf + } + case Down, ControlN: - if !completing { // Don't use p.completion.Completing() because it takes double operation when switch to selected=-1. - if newBuf, changed := p.history.Newer(p.buf); changed { - p.buf = newBuf - } - return + endOfTextCursor := p.buf.Document().GetEndOfTextPosition(int(p.renderer.col)) + cursor := p.buf.Document().GetCursorPosition(int(p.renderer.col)) + if endOfTextCursor.Y > cursor.Y { + p.buf.CursorDown(1) + break + } + + if completing { + break + } + + if newBuf, changed := p.history.Newer(p.buf); changed { + p.buf = newBuf } + return case ControlD: if p.buf.Text() == "" { shouldExit = true From 27e9a8c597651193278a20672dbec082456794e2 Mon Sep 17 00:00:00 2001 From: Mateusz Drewniak Date: Wed, 5 Jul 2023 20:57:28 +0200 Subject: [PATCH 03/21] Refactor type names and make interfaces more idiomatic --- _example/simple-echo/main.go | 7 ++- option.go => constructor.go | 14 ++--- internal/debug/log.go | 4 +- prompt.go | 61 +++++++++++--------- input.go => reader.go | 18 +++--- input_posix.go => reader_posix.go | 33 +++++------ input_test.go => reader_test.go | 0 input_windows.go => reader_windows.go | 39 ++++++------- render.go | 30 +++++----- shortcut.go | 30 +--------- output.go => writer.go | 23 ++++---- output_posix.go => writer_posix.go | 15 ++--- output_vt100.go => writer_vt100.go | 25 ++++---- output_vt100_test.go => writer_vt100_test.go | 8 +-- output_windows.go => writer_windows.go | 13 +++-- 15 files changed, 152 insertions(+), 168 deletions(-) rename option.go => constructor.go (95%) rename input.go => reader.go (96%) rename input_posix.go => reader_posix.go (60%) rename input_test.go => reader_test.go (100%) rename input_windows.go => reader_windows.go (55%) rename output.go => writer.go (90%) rename output_posix.go => writer_posix.go (74%) rename output_vt100.go => writer_vt100.go (94%) rename output_vt100_test.go => writer_vt100_test.go (88%) rename output_windows.go => writer_windows.go (69%) diff --git a/_example/simple-echo/main.go b/_example/simple-echo/main.go index 3330c0fc..62aaff0a 100644 --- a/_example/simple-echo/main.go +++ b/_example/simple-echo/main.go @@ -17,12 +17,15 @@ func completer(in prompt.Document) []prompt.Suggest { } func main() { - in := prompt.Input(">>> ", completer, + in := prompt.Input( + ">>> ", + completer, prompt.OptionTitle("sql-prompt"), prompt.OptionHistory([]string{"SELECT * FROM users;"}), prompt.OptionPrefixTextColor(prompt.Yellow), prompt.OptionPreviewSuggestionTextColor(prompt.Blue), prompt.OptionSelectedSuggestionBGColor(prompt.LightGray), - prompt.OptionSuggestionBGColor(prompt.DarkGray)) + prompt.OptionSuggestionBGColor(prompt.DarkGray), + ) fmt.Println("Your input: " + in) } diff --git a/option.go b/constructor.go similarity index 95% rename from option.go rename to constructor.go index 8aaca334..7ecfa462 100644 --- a/option.go +++ b/constructor.go @@ -4,18 +4,18 @@ package prompt // prompt.New accepts any number of options (this is functional option pattern). type Option func(prompt *Prompt) error -// OptionParser to set a custom ConsoleParser object. An argument should implement ConsoleParser interface. -func OptionParser(x ConsoleParser) Option { +// OptionParser to set a custom Reader object. An argument should implement Reader interface. +func OptionParser(x Reader) Option { return func(p *Prompt) error { p.in = x return nil } } -// OptionWriter to set a custom ConsoleWriter object. An argument should implement ConsoleWriter interface. -func OptionWriter(x ConsoleWriter) Option { +// OptionWriter to set a custom Writer object. An argument should implement Writer interface. +func OptionWriter(x Writer) Option { return func(p *Prompt) error { - registerConsoleWriter(x) + registerWriter(x) p.renderer.out = x return nil } @@ -277,10 +277,10 @@ func OptionSetLexer(lex Lexer) Option { // New returns a Prompt with powerful auto-completion. func New(executor Executor, completer Completer, opts ...Option) *Prompt { defaultWriter := NewStdoutWriter() - registerConsoleWriter(defaultWriter) + registerWriter(defaultWriter) pt := &Prompt{ - in: NewStandardInputParser(), + in: NewStdinReader(), renderer: &Render{ prefix: "> ", out: defaultWriter, diff --git a/internal/debug/log.go b/internal/debug/log.go index 51637862..c44cdf55 100644 --- a/internal/debug/log.go +++ b/internal/debug/log.go @@ -28,8 +28,8 @@ func init() { logger = log.New(ioutil.Discard, "", log.Llongfile) } -// Teardown to close logfile -func Teardown() { +// Close to close logfile +func Close() { if logfile == nil { return } diff --git a/prompt.go b/prompt.go index d0030a0e..a9cd0ad3 100644 --- a/prompt.go +++ b/prompt.go @@ -10,6 +10,8 @@ import ( "github.com/elk-language/go-prompt/internal/debug" ) +const inputBufferSize = 1024 + // Executor is called when the user // inputs a line of text. type Executor func(string) @@ -27,7 +29,7 @@ type Completer func(Document) []Suggest // Prompt is a core struct of go-prompt. type Prompt struct { - in ConsoleParser + in Reader buf *Buffer renderer *Render executor Executor @@ -39,21 +41,21 @@ type Prompt struct { keyBindMode KeyBindMode completionOnDown bool exitChecker ExitChecker - skipTearDown bool + skipClose bool } -// Exec is the struct that contains the user input context. -type Exec struct { +// UserInput is the struct that contains the user input context. +type UserInput struct { input string } // Run starts the prompt. func (p *Prompt) Run() { - p.skipTearDown = false - defer debug.Teardown() + p.skipClose = false + defer debug.Close() debug.Log("start prompt") - p.setUp() - defer p.tearDown() + p.setup() + defer p.Close() if p.completion.showAtStart { p.completion.Update(*p.buf.Document()) @@ -85,7 +87,7 @@ func (p *Prompt) Run() { // Unset raw mode // Reset to Blocking mode because returned EAGAIN when still set non-blocking mode. - debug.AssertNoError(p.in.TearDown()) + debug.AssertNoError(p.in.Close()) p.executor(e.input) p.completion.Update(*p.buf.Document()) @@ -93,11 +95,11 @@ func (p *Prompt) Run() { p.renderer.Render(p.buf, p.completion, p.lexer) if p.exitChecker != nil && p.exitChecker(e.input, true) { - p.skipTearDown = true + p.skipClose = true return } // Set raw mode - debug.AssertNoError(p.in.Setup()) + debug.AssertNoError(p.in.Open()) go p.readBuffer(bufCh, stopReadBufCh) go p.handleSignals(exitCh, winSizeCh, stopHandleSignalCh) } else { @@ -109,7 +111,7 @@ func (p *Prompt) Run() { p.renderer.Render(p.buf, p.completion, p.lexer) case code := <-exitCh: p.renderer.BreakLine(p.buf, p.lexer) - p.tearDown() + p.Close() os.Exit(code) default: time.Sleep(10 * time.Millisecond) @@ -126,7 +128,7 @@ func (p *Prompt) Run() { // fmt.Fprintf(f, format, a...) // } -func (p *Prompt) feed(b []byte) (shouldExit bool, exec *Exec) { +func (p *Prompt) feed(b []byte) (shouldExit bool, userInput *UserInput) { key := GetKey(b) p.buf.lastKeyStroke = key // completion @@ -137,10 +139,10 @@ func (p *Prompt) feed(b []byte) (shouldExit bool, exec *Exec) { case Enter, ControlJ, ControlM: p.renderer.BreakLine(p.buf, p.lexer) - exec = &Exec{input: p.buf.Text()} + userInput = &UserInput{input: p.buf.Text()} p.buf = NewBuffer() - if exec.input != "" { - p.history.Add(exec.input) + if userInput.input != "" { + p.history.Add(userInput.input) } case ControlC: p.renderer.BreakLine(p.buf, p.lexer) @@ -268,10 +270,10 @@ func (p *Prompt) handleASCIICodeBinding(b []byte) bool { // Input starts the prompt, lets the user // input a single line and returns this line as a string. func (p *Prompt) Input() string { - defer debug.Teardown() + defer debug.Close() debug.Log("start prompt") - p.setUp() - defer p.tearDown() + p.setup() + defer p.Close() if p.completion.showAtStart { p.completion.Update(*p.buf.Document()) @@ -311,8 +313,13 @@ func (p *Prompt) readBuffer(bufCh chan []byte, stopCh chan struct{}) { debug.Log("stop reading buffer") return default: - if bytes, err := p.in.Read(); err == nil && !(len(bytes) == 1 && bytes[0] == 0) { - // bufCh <- bytes + bytes := make([]byte, inputBufferSize) + n, err := p.in.Read(bytes) + if err != nil { + break + } + bytes = bytes[:n] + if len(bytes) != 1 || bytes[0] != 0 { newBytes := make([]byte, len(bytes)) for i, byt := range bytes { // translate raw mode \r into \n @@ -332,15 +339,15 @@ func (p *Prompt) readBuffer(bufCh chan []byte, stopCh chan struct{}) { } } -func (p *Prompt) setUp() { - debug.AssertNoError(p.in.Setup()) +func (p *Prompt) setup() { + debug.AssertNoError(p.in.Open()) p.renderer.Setup() p.renderer.UpdateWinSize(p.in.GetWinSize()) } -func (p *Prompt) tearDown() { - if !p.skipTearDown { - debug.AssertNoError(p.in.TearDown()) +func (p *Prompt) Close() { + if !p.skipClose { + debug.AssertNoError(p.in.Close()) } - p.renderer.TearDown() + p.renderer.Close() } diff --git a/input.go b/reader.go similarity index 96% rename from input.go rename to reader.go index 3c60ce58..a4a8b5f2 100644 --- a/input.go +++ b/reader.go @@ -1,6 +1,9 @@ package prompt -import "bytes" +import ( + "bytes" + "io" +) // WinSize represents the width and height of terminal. type WinSize struct { @@ -8,16 +11,13 @@ type WinSize struct { Col uint16 } -// ConsoleParser is an interface to abstract input layer. -type ConsoleParser interface { - // Setup should be called before starting input - Setup() error - // TearDown should be called after stopping input - TearDown() error +// Reader is an interface to abstract input layer. +type Reader interface { + // Open should be called before starting reading + Open() error // GetWinSize returns WinSize object to represent width and height of terminal. GetWinSize() *WinSize - // Read returns byte array. - Read() ([]byte, error) + io.ReadCloser } // GetKey returns Key correspond to input byte codes. diff --git a/input_posix.go b/reader_posix.go similarity index 60% rename from input_posix.go rename to reader_posix.go index afaa3dc0..d7676ff0 100644 --- a/input_posix.go +++ b/reader_posix.go @@ -11,15 +11,13 @@ import ( "golang.org/x/sys/unix" ) -const maxReadBytes = 1024 - -// PosixParser is a ConsoleParser implementation for POSIX environment. -type PosixParser struct { +// PosixReader is a Reader implementation for the POSIX environment. +type PosixReader struct { fd int } -// Setup should be called before starting input -func (t *PosixParser) Setup() error { +// Open should be called before starting input +func (t *PosixReader) Open() error { in, err := syscall.Open("/dev/tty", syscall.O_RDONLY, 0) if os.IsNotExist(err) { in = syscall.Stdin @@ -37,8 +35,8 @@ func (t *PosixParser) Setup() error { return nil } -// TearDown should be called after stopping input -func (t *PosixParser) TearDown() error { +// Close should be called after stopping input +func (t *PosixReader) Close() error { if err := syscall.Close(t.fd); err != nil { return err } @@ -49,17 +47,12 @@ func (t *PosixParser) TearDown() error { } // Read returns byte array. -func (t *PosixParser) Read() ([]byte, error) { - buf := make([]byte, maxReadBytes) - n, err := syscall.Read(t.fd, buf) - if err != nil { - return []byte{}, err - } - return buf[:n], nil +func (t *PosixReader) Read(buff []byte) (int, error) { + return syscall.Read(t.fd, buff) } // GetWinSize returns WinSize object to represent width and height of terminal. -func (t *PosixParser) GetWinSize() *WinSize { +func (t *PosixReader) GetWinSize() *WinSize { ws, err := unix.IoctlGetWinsize(t.fd, unix.TIOCGWINSZ) if err != nil { // If this errors, we simply return the default window size as @@ -75,9 +68,9 @@ func (t *PosixParser) GetWinSize() *WinSize { } } -var _ ConsoleParser = &PosixParser{} +var _ Reader = &PosixReader{} -// NewStandardInputParser returns ConsoleParser object to read from stdin. -func NewStandardInputParser() *PosixParser { - return &PosixParser{} +// NewStdinReader returns Reader object to read from stdin. +func NewStdinReader() *PosixReader { + return &PosixReader{} } diff --git a/input_test.go b/reader_test.go similarity index 100% rename from input_test.go rename to reader_test.go diff --git a/input_windows.go b/reader_windows.go similarity index 55% rename from input_windows.go rename to reader_windows.go index a0c3dc44..de2c76a7 100644 --- a/input_windows.go +++ b/reader_windows.go @@ -12,19 +12,17 @@ import ( tty "github.com/mattn/go-tty" ) -const maxReadBytes = 1024 - var kernel32 = syscall.NewLazyDLL("kernel32.dll") var procGetNumberOfConsoleInputEvents = kernel32.NewProc("GetNumberOfConsoleInputEvents") -// WindowsParser is a ConsoleParser implementation for Win32 console. -type WindowsParser struct { +// WindowsReader is a Reader implementation for Win32 console. +type WindowsReader struct { tty *tty.TTY } -// Setup should be called before starting input -func (p *WindowsParser) Setup() error { +// Open should be called before starting input +func (p *WindowsReader) Open() error { t, err := tty.Open() if err != nil { return err @@ -33,41 +31,40 @@ func (p *WindowsParser) Setup() error { return nil } -// TearDown should be called after stopping input -func (p *WindowsParser) TearDown() error { +// Close should be called after stopping input +func (p *WindowsReader) Close() error { return p.tty.Close() } // Read returns byte array. -func (p *WindowsParser) Read() ([]byte, error) { +func (p *WindowsReader) Read(buff []byte) (int, error) { var ev uint32 r0, _, err := procGetNumberOfConsoleInputEvents.Call(p.tty.Input().Fd(), uintptr(unsafe.Pointer(&ev))) if r0 == 0 { - return nil, err + return 0, err } if ev == 0 { - return nil, errors.New("EAGAIN") + return 0, errors.New("EAGAIN") } r, err := p.tty.ReadRune() if err != nil { - return nil, err + return 0, err } - buf := make([]byte, maxReadBytes) - n := utf8.EncodeRune(buf[:], r) - for p.tty.Buffered() && n < maxReadBytes { + n := utf8.EncodeRune(buff[:], r) + for p.tty.Buffered() && n < len(buff) { r, err := p.tty.ReadRune() if err != nil { break } n += utf8.EncodeRune(buf[n:], r) } - return buf[:n], nil + return n, nil } // GetWinSize returns WinSize object to represent width and height of terminal. -func (p *WindowsParser) GetWinSize() *WinSize { +func (p *WindowsReader) GetWinSize() *WinSize { w, h, err := p.tty.Size() if err != nil { panic(err) @@ -78,7 +75,9 @@ func (p *WindowsParser) GetWinSize() *WinSize { } } -// NewStandardInputParser returns ConsoleParser object to read from stdin. -func NewStandardInputParser() *WindowsParser { - return &WindowsParser{} +var _ Reader = &WindowsReader{} + +// NewStdinReader returns Reader object to read from stdin. +func NewStdinReader() *WindowsReader { + return &WindowsReader{} } diff --git a/render.go b/render.go index f9ab8467..a2d4f0df 100644 --- a/render.go +++ b/render.go @@ -10,7 +10,7 @@ import ( // Render to render prompt information from state of Buffer. type Render struct { - out ConsoleWriter + out Writer prefix string livePrefixCallback func() (prefix string, useLivePrefix bool) breakLineCallback func(*Document) @@ -58,13 +58,13 @@ func (r *Render) getCurrentPrefix() string { func (r *Render) renderPrefix() { r.out.SetColor(r.prefixTextColor, r.prefixBGColor, false) - r.out.WriteStr("\r") - r.out.WriteStr(r.getCurrentPrefix()) + r.out.WriteString("\r") + r.out.WriteString(r.getCurrentPrefix()) r.out.SetColor(DefaultColor, DefaultColor, false) } -// TearDown to clear title and erasing. -func (r *Render) TearDown() { +// Close to clear title and erasing. +func (r *Render) Close() { r.out.ClearTitle() r.out.EraseDown() debug.AssertNoError(r.out.Flush()) @@ -89,7 +89,7 @@ func (r *Render) renderWindowTooSmall() { r.out.CursorGoTo(0, 0) r.out.EraseScreen() r.out.SetColor(DarkRed, White, false) - r.out.WriteStr("Your console window is too small...") + r.out.WriteString("Your console window is too small...") } func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) { @@ -142,21 +142,21 @@ func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) { } else { r.out.SetColor(r.suggestionTextColor, r.suggestionBGColor, false) } - r.out.WriteStr(formatted[i].Text) + r.out.WriteString(formatted[i].Text) if i == selected { r.out.SetColor(r.selectedDescriptionTextColor, r.selectedDescriptionBGColor, false) } else { r.out.SetColor(r.descriptionTextColor, r.descriptionBGColor, false) } - r.out.WriteStr(formatted[i].Description) + r.out.WriteString(formatted[i].Description) if isScrollThumb(i) { r.out.SetColor(DefaultColor, r.scrollbarThumbColor, false) } else { r.out.SetColor(DefaultColor, r.scrollbarBGColor, false) } - r.out.WriteStr(" ") + r.out.WriteString(" ") r.out.SetColor(DefaultColor, DefaultColor, false) c := cursor.Add(Position{X: width}) @@ -206,7 +206,7 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager, lexer Lex r.lex(lexer, line) } else { r.out.SetColor(r.inputTextColor, r.inputBGColor, false) - r.out.WriteStr(line) + r.out.WriteString(line) } r.out.SetColor(DefaultColor, DefaultColor, false) @@ -224,7 +224,7 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager, lexer Lex cursor = r.backward(cursor, runewidth.StringWidth(buffer.Document().GetWordBeforeCursorUntilSeparator(completion.wordSeparator))) r.out.SetColor(r.previewSuggestionTextColor, r.previewSuggestionBGColor, false) - r.out.WriteStr(suggest.Text) + r.out.WriteString(suggest.Text) r.out.SetColor(DefaultColor, DefaultColor, false) cursor.X += runewidth.StringWidth(suggest.Text) endOfSuggestionPos := cursor @@ -234,7 +234,7 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager, lexer Lex if lexer != nil { r.lex(lexer, rest) } else { - r.out.WriteStr(rest) + r.out.WriteString(rest) } r.out.SetColor(DefaultColor, DefaultColor, false) @@ -264,7 +264,7 @@ func (r *Render) lex(lexer Lexer, input string) { s = strings.TrimPrefix(s, a[0]) r.out.SetColor(token.Color(), r.inputBGColor, false) - r.out.WriteStr(a[0]) + r.out.WriteString(a[0]) } } @@ -280,7 +280,7 @@ func (r *Render) BreakLine(buffer *Buffer, lexer Lexer) { r.lex(lexer, buffer.Document().Text+"\n") } else { r.out.SetColor(r.inputTextColor, r.inputBGColor, false) - r.out.WriteStr(buffer.Document().Text + "\n") + r.out.WriteString(buffer.Document().Text + "\n") } r.out.SetColor(DefaultColor, DefaultColor, false) @@ -336,6 +336,6 @@ func clamp(high, low, x float64) float64 { func alignNextLine(r *Render, col int) { r.out.CursorDown(1) - r.out.WriteStr("\r") + r.out.WriteString("\r") r.out.CursorForward(col) } diff --git a/shortcut.go b/shortcut.go index be8d4282..7e3cc424 100644 --- a/shortcut.go +++ b/shortcut.go @@ -1,10 +1,10 @@ package prompt -func dummyExecutor(in string) {} +func NoopExecutor(in string) {} // Input get the input data from the user and return it. func Input(prefix string, completer Completer, opts ...Option) string { - pt := New(dummyExecutor, completer) + pt := New(NoopExecutor, completer) pt.renderer.prefixTextColor = DefaultColor pt.renderer.prefix = prefix @@ -15,29 +15,3 @@ func Input(prefix string, completer Completer, opts ...Option) string { } return pt.Input() } - -// Choose to the shortcut of input function to select from string array. -// Deprecated: Maybe anyone want to use this. -func Choose(prefix string, choices []string, opts ...Option) string { - completer := newChoiceCompleter(choices, FilterHasPrefix) - pt := New(dummyExecutor, completer) - pt.renderer.prefixTextColor = DefaultColor - pt.renderer.prefix = prefix - - for _, opt := range opts { - if err := opt(pt); err != nil { - panic(err) - } - } - return pt.Input() -} - -func newChoiceCompleter(choices []string, filter Filter) Completer { - s := make([]Suggest, len(choices)) - for i := range choices { - s[i] = Suggest{Text: choices[i]} - } - return func(x Document) []Suggest { - return filter(s, x.GetWordBeforeCursor(), true) - } -} diff --git a/output.go b/writer.go similarity index 90% rename from output.go rename to writer.go index 42531b41..7d1df825 100644 --- a/output.go +++ b/writer.go @@ -1,13 +1,16 @@ package prompt -import "sync" +import ( + "io" + "sync" +) var ( consoleWriterMu sync.Mutex - consoleWriter ConsoleWriter + consoleWriter Writer ) -func registerConsoleWriter(f ConsoleWriter) { +func registerWriter(f Writer) { consoleWriterMu.Lock() defer consoleWriterMu.Unlock() consoleWriter = f @@ -87,18 +90,16 @@ const ( White ) -// ConsoleWriter is an interface to abstract output layer. -type ConsoleWriter interface { +// Writer is an interface to abstract the output layer. +type Writer interface { /* Write */ + io.Writer + io.StringWriter // WriteRaw to write raw byte array. WriteRaw(data []byte) - // Write to write safety byte array by removing control sequences. - Write(data []byte) - // WriteStr to write raw string. - WriteRawStr(data string) - // WriteStr to write safety string by removing control sequences. - WriteStr(data string) + // WriteString to write raw string. + WriteRawString(data string) // Flush to flush buffer. Flush() error diff --git a/output_posix.go b/writer_posix.go similarity index 74% rename from output_posix.go rename to writer_posix.go index 26c770cc..207da8ca 100644 --- a/output_posix.go +++ b/writer_posix.go @@ -1,3 +1,4 @@ +//go:build !windows // +build !windows package prompt @@ -8,7 +9,7 @@ import ( const flushMaxRetryCount = 3 -// PosixWriter is a ConsoleWriter implementation for POSIX environment. +// PosixWriter is a Writer implementation for POSIX environment. // To control terminal emulator, this outputs VT100 escape sequences. type PosixWriter struct { VT100Writer @@ -38,29 +39,29 @@ func (w *PosixWriter) Flush() error { return nil } -var _ ConsoleWriter = &PosixWriter{} +var _ Writer = &PosixWriter{} var ( - // NewStandardOutputWriter returns ConsoleWriter object to write to stdout. + // NewStandardOutputWriter returns Writer object to write to stdout. // This generates VT100 escape sequences because almost terminal emulators // in POSIX OS built on top of a VT100 specification. // Deprecated: Please use NewStdoutWriter NewStandardOutputWriter = NewStdoutWriter ) -// NewStdoutWriter returns ConsoleWriter object to write to stdout. +// NewStdoutWriter returns Writer object to write to stdout. // This generates VT100 escape sequences because almost terminal emulators // in POSIX OS built on top of a VT100 specification. -func NewStdoutWriter() ConsoleWriter { +func NewStdoutWriter() Writer { return &PosixWriter{ fd: syscall.Stdout, } } -// NewStderrWriter returns ConsoleWriter object to write to stderr. +// NewStderrWriter returns Writer object to write to stderr. // This generates VT100 escape sequences because almost terminal emulators // in POSIX OS built on top of a VT100 specification. -func NewStderrWriter() ConsoleWriter { +func NewStderrWriter() Writer { return &PosixWriter{ fd: syscall.Stderr, } diff --git a/output_vt100.go b/writer_vt100.go similarity index 94% rename from output_vt100.go rename to writer_vt100.go index 894b858e..7da31c21 100644 --- a/output_vt100.go +++ b/writer_vt100.go @@ -2,6 +2,7 @@ package prompt import ( "bytes" + "io" "strconv" ) @@ -10,26 +11,30 @@ type VT100Writer struct { buffer []byte } +var _ io.Writer = &VT100Writer{} +var _ io.StringWriter = &VT100Writer{} + +// Write to write safety byte array by removing control sequences. +func (w *VT100Writer) Write(data []byte) (int, error) { + w.WriteRaw(bytes.Replace(data, []byte{0x1b}, []byte{'?'}, -1)) + return len(data), nil +} + // WriteRaw to write raw byte array func (w *VT100Writer) WriteRaw(data []byte) { w.buffer = append(w.buffer, data...) } -// Write to write safety byte array by removing control sequences. -func (w *VT100Writer) Write(data []byte) { - w.WriteRaw(bytes.Replace(data, []byte{0x1b}, []byte{'?'}, -1)) +// WriteString to write safety string by removing control sequences. +func (w *VT100Writer) WriteString(data string) (int, error) { + return w.Write([]byte(data)) } -// WriteRawStr to write raw string -func (w *VT100Writer) WriteRawStr(data string) { +// WriteRawString to write raw string +func (w *VT100Writer) WriteRawString(data string) { w.WriteRaw([]byte(data)) } -// WriteStr to write safety string by removing control sequences. -func (w *VT100Writer) WriteStr(data string) { - w.Write([]byte(data)) -} - /* Erase */ // EraseScreen erases the screen with the background colour and moves the cursor to home. diff --git a/output_vt100_test.go b/writer_vt100_test.go similarity index 88% rename from output_vt100_test.go rename to writer_vt100_test.go index 591607ca..ec21cbcf 100644 --- a/output_vt100_test.go +++ b/writer_vt100_test.go @@ -30,7 +30,7 @@ func TestVT100WriterWrite(t *testing.T) { } } -func TestVT100WriterWriteStr(t *testing.T) { +func TestVT100WriterWriteString(t *testing.T) { scenarioTable := []struct { input string expected []byte @@ -47,7 +47,7 @@ func TestVT100WriterWriteStr(t *testing.T) { for _, s := range scenarioTable { pw := &VT100Writer{} - pw.WriteStr(s.input) + pw.WriteString(s.input) if !bytes.Equal(pw.buffer, s.expected) { t.Errorf("Should be %+#v, but got %+#v", pw.buffer, s.expected) @@ -55,7 +55,7 @@ func TestVT100WriterWriteStr(t *testing.T) { } } -func TestVT100WriterWriteRawStr(t *testing.T) { +func TestVT100WriterWriteRawString(t *testing.T) { scenarioTable := []struct { input string expected []byte @@ -72,7 +72,7 @@ func TestVT100WriterWriteRawStr(t *testing.T) { for _, s := range scenarioTable { pw := &VT100Writer{} - pw.WriteRawStr(s.input) + pw.WriteRawString(s.input) if !bytes.Equal(pw.buffer, s.expected) { t.Errorf("Should be %+#v, but got %+#v", pw.buffer, s.expected) diff --git a/output_windows.go b/writer_windows.go similarity index 69% rename from output_windows.go rename to writer_windows.go index 3b9c2769..b6a44f67 100644 --- a/output_windows.go +++ b/writer_windows.go @@ -1,3 +1,4 @@ +//go:build windows // +build windows package prompt @@ -8,7 +9,7 @@ import ( colorable "github.com/mattn/go-colorable" ) -// WindowsWriter is a ConsoleWriter implementation for Win32 console. +// WindowsWriter is a Writer implementation for Win32 console. // Output is converted from VT100 escape sequences by mattn/go-colorable. type WindowsWriter struct { VT100Writer @@ -25,24 +26,24 @@ func (w *WindowsWriter) Flush() error { return nil } -var _ ConsoleWriter = &WindowsWriter{} +var _ Writer = &WindowsWriter{} var ( // NewStandardOutputWriter is Deprecated: Please use NewStdoutWriter NewStandardOutputWriter = NewStdoutWriter ) -// NewStdoutWriter returns ConsoleWriter object to write to stdout. +// NewStdoutWriter returns Writer object to write to stdout. // This generates win32 control sequences. -func NewStdoutWriter() ConsoleWriter { +func NewStdoutWriter() Writer { return &WindowsWriter{ out: colorable.NewColorableStdout(), } } -// NewStderrWriter returns ConsoleWriter object to write to stderr. +// NewStderrWriter returns Writer object to write to stderr. // This generates win32 control sequences. -func NewStderrWriter() ConsoleWriter { +func NewStderrWriter() Writer { return &WindowsWriter{ out: colorable.NewColorableStderr(), } From 2fede7b2ee542292d834a7462961c944b62e30bb Mon Sep 17 00:00:00 2001 From: Mateusz Drewniak Date: Thu, 6 Jul 2023 11:31:11 +0200 Subject: [PATCH 04/21] Refactor the API --- _example/even-lexer/main.go | 7 +- _example/exec-command/main.go | 2 +- _example/http-prompt/main.go | 8 +- _example/live-prefix/main.go | 8 +- _example/simple-echo/cjk-cyrillic/main.go | 4 +- _example/simple-echo/main.go | 14 +- _tools/complete_file/main.go | 4 +- completer/file.go | 2 +- completion.go | 27 +++- constructor.go | 152 ++++++++++++---------- prompt.go | 14 +- shortcut.go | 4 +- signal_posix.go | 2 +- 13 files changed, 133 insertions(+), 115 deletions(-) diff --git a/_example/even-lexer/main.go b/_example/even-lexer/main.go index ea5f3375..ac82b5f4 100644 --- a/_example/even-lexer/main.go +++ b/_example/even-lexer/main.go @@ -10,8 +10,7 @@ import ( func main() { p := prompt.New( executor, - completer, - prompt.OptionSetLexer(prompt.NewEagerLexer(lexer)), + prompt.WithLexer(prompt.NewEagerLexer(lexer)), ) p.Run() @@ -38,10 +37,6 @@ func lexer(line string) []prompt.Token { return elements } -func completer(in prompt.Document) []prompt.Suggest { - return []prompt.Suggest{} -} - func executor(s string) { fmt.Println("You printed: " + s) } diff --git a/_example/exec-command/main.go b/_example/exec-command/main.go index 58de271d..1698593c 100644 --- a/_example/exec-command/main.go +++ b/_example/exec-command/main.go @@ -27,7 +27,7 @@ func completer(t prompt.Document) []prompt.Suggest { func main() { p := prompt.New( executor, - completer, + prompt.WithCompleter(completer) ) p.Run() } diff --git a/_example/http-prompt/main.go b/_example/http-prompt/main.go index 2824a22e..129b415a 100644 --- a/_example/http-prompt/main.go +++ b/_example/http-prompt/main.go @@ -183,10 +183,10 @@ func main() { p := prompt.New( executor, - completer, - prompt.OptionPrefix(u.String()+"> "), - prompt.OptionLivePrefix(livePrefix), - prompt.OptionTitle("http-prompt"), + prompt.WithPrefix(u.String()+"> "), + prompt.WithLivePrefix(livePrefix), + prompt.WithTitle("http-prompt"), + prompt.WithCompleter(completer), ) p.Run() } diff --git a/_example/live-prefix/main.go b/_example/live-prefix/main.go index 8a457dfa..121df1c6 100644 --- a/_example/live-prefix/main.go +++ b/_example/live-prefix/main.go @@ -39,10 +39,10 @@ func changeLivePrefix() (string, bool) { func main() { p := prompt.New( executor, - completer, - prompt.OptionPrefix(">>> "), - prompt.OptionLivePrefix(changeLivePrefix), - prompt.OptionTitle("live-prefix-example"), + prompt.WithPrefix(">>> "), + prompt.WithLivePrefix(changeLivePrefix), + prompt.WithTitle("live-prefix-example"), + prompt.WithCompleter(completer), ) p.Run() } diff --git a/_example/simple-echo/cjk-cyrillic/main.go b/_example/simple-echo/cjk-cyrillic/main.go index 27309723..e3bcc2d0 100644 --- a/_example/simple-echo/cjk-cyrillic/main.go +++ b/_example/simple-echo/cjk-cyrillic/main.go @@ -24,8 +24,8 @@ func main() { p := prompt.New( executor, completer, - prompt.OptionPrefix(">>> "), - prompt.OptionTitle("sql-prompt for multi width characters"), + prompt.WithPrefix(">>> "), + prompt.WithTitle("sql-prompt for multi width characters"), ) p.Run() } diff --git a/_example/simple-echo/main.go b/_example/simple-echo/main.go index 62aaff0a..42742d8e 100644 --- a/_example/simple-echo/main.go +++ b/_example/simple-echo/main.go @@ -19,13 +19,13 @@ func completer(in prompt.Document) []prompt.Suggest { func main() { in := prompt.Input( ">>> ", - completer, - prompt.OptionTitle("sql-prompt"), - prompt.OptionHistory([]string{"SELECT * FROM users;"}), - prompt.OptionPrefixTextColor(prompt.Yellow), - prompt.OptionPreviewSuggestionTextColor(prompt.Blue), - prompt.OptionSelectedSuggestionBGColor(prompt.LightGray), - prompt.OptionSuggestionBGColor(prompt.DarkGray), + prompt.WithTitle("sql-prompt"), + prompt.WithHistory([]string{"SELECT * FROM users;"}), + prompt.WithPrefixTextColor(prompt.Yellow), + prompt.WithPreviewSuggestionTextColor(prompt.Blue), + prompt.WithSelectedSuggestionBGColor(prompt.LightGray), + prompt.WithSuggestionBGColor(prompt.DarkGray), + prompt.WithCompleter(completer), ) fmt.Println("Your input: " + in) } diff --git a/_tools/complete_file/main.go b/_tools/complete_file/main.go index 0d7cb9fe..7992607f 100644 --- a/_tools/complete_file/main.go +++ b/_tools/complete_file/main.go @@ -36,8 +36,8 @@ func main() { p := prompt.New( executor, completerFunc, - prompt.OptionPrefix(">>> "), - prompt.OptionCompletionWordSeparator(completer.FilePathCompletionSeparator), + prompt.WithPrefix(">>> "), + prompt.WithCompletionWordSeparator(completer.FilePathCompletionSeparator), ) p.Run() } diff --git a/completer/file.go b/completer/file.go index 1b7a544a..2d5b3959 100644 --- a/completer/file.go +++ b/completer/file.go @@ -17,7 +17,7 @@ var ( ) // FilePathCompleter is a completer for your local file system. -// Please caution that you need to set OptionCompletionWordSeparator(completer.FilePathCompletionSeparator) +// Please caution that you need to set WithCompletionWordSeparator(completer.FilePathCompletionSeparator) // when you use this completer. type FilePathCompleter struct { Filter func(fi os.FileInfo) bool diff --git a/completion.go b/completion.go index 69183dbd..fd0d01b8 100644 --- a/completion.go +++ b/completion.go @@ -180,15 +180,30 @@ func formatSuggestions(suggests []Suggest, max int) (new []Suggest, width int) { return new, leftWidth + rightWidth } -// NewCompletionManager returns an initialized CompletionManager object. -func NewCompletionManager(completer Completer, max uint16) *CompletionManager { - return &CompletionManager{ - selected: -1, - max: max, - completer: completer, +// Constructor option for CompletionManager. +type CompletionManagerOption func(*CompletionManager) + +// Set a custom completer. +func CompletionManagerWithCompleter(completer Completer) CompletionManagerOption { + return func(c *CompletionManager) { + c.completer = completer + } +} +// NewCompletionManager returns an initialized CompletionManager object. +func NewCompletionManager(max uint16, opts ...CompletionManagerOption) *CompletionManager { + c := &CompletionManager{ + selected: -1, + max: max, + completer: NoopCompleter, verticalScroll: 0, } + + for _, opt := range opts { + opt(c) + } + + return c } var _ Completer = NoopCompleter diff --git a/constructor.go b/constructor.go index 7ecfa462..e39f1898 100644 --- a/constructor.go +++ b/constructor.go @@ -4,16 +4,24 @@ package prompt // prompt.New accepts any number of options (this is functional option pattern). type Option func(prompt *Prompt) error -// OptionParser to set a custom Reader object. An argument should implement Reader interface. -func OptionParser(x Reader) Option { +// WithCompleter is an option that sets a custom Completer object. +func WithCompleter(c Completer) Option { return func(p *Prompt) error { - p.in = x + p.completion.completer = c return nil } } -// OptionWriter to set a custom Writer object. An argument should implement Writer interface. -func OptionWriter(x Writer) Option { +// WithReader to set a custom Reader object. An argument should implement Reader interface. +func WithReader(x Reader) Option { + return func(p *Prompt) error { + p.reader = x + return nil + } +} + +// WithWriter to set a custom Writer object. An argument should implement Writer interface. +func WithWriter(x Writer) Option { return func(p *Prompt) error { registerWriter(x) p.renderer.out = x @@ -21,184 +29,184 @@ func OptionWriter(x Writer) Option { } } -// OptionTitle to set title displayed at the header bar of terminal. -func OptionTitle(x string) Option { +// WithTitle to set title displayed at the header bar of terminal. +func WithTitle(x string) Option { return func(p *Prompt) error { p.renderer.title = x return nil } } -// OptionPrefix to set prefix string. -func OptionPrefix(x string) Option { +// WithPrefix to set prefix string. +func WithPrefix(x string) Option { return func(p *Prompt) error { p.renderer.prefix = x return nil } } -// OptionInitialBufferText to set the initial buffer text -func OptionInitialBufferText(x string) Option { +// WithInitialBufferText to set the initial buffer text +func WithInitialBufferText(x string) Option { return func(p *Prompt) error { p.buf.InsertText(x, false, true) return nil } } -// OptionCompletionWordSeparator to set word separators. Enable only ' ' if empty. -func OptionCompletionWordSeparator(x string) Option { +// WithCompletionWordSeparator to set word separators. Enable only ' ' if empty. +func WithCompletionWordSeparator(x string) Option { return func(p *Prompt) error { p.completion.wordSeparator = x return nil } } -// OptionLivePrefix to change the prefix dynamically by callback function -func OptionLivePrefix(f func() (prefix string, useLivePrefix bool)) Option { +// WithLivePrefix to change the prefix dynamically by callback function +func WithLivePrefix(f func() (prefix string, useLivePrefix bool)) Option { return func(p *Prompt) error { p.renderer.livePrefixCallback = f return nil } } -// OptionPrefixTextColor change a text color of prefix string -func OptionPrefixTextColor(x Color) Option { +// WithPrefixTextColor change a text color of prefix string +func WithPrefixTextColor(x Color) Option { return func(p *Prompt) error { p.renderer.prefixTextColor = x return nil } } -// OptionPrefixBackgroundColor to change a background color of prefix string -func OptionPrefixBackgroundColor(x Color) Option { +// WithPrefixBackgroundColor to change a background color of prefix string +func WithPrefixBackgroundColor(x Color) Option { return func(p *Prompt) error { p.renderer.prefixBGColor = x return nil } } -// OptionInputTextColor to change a color of text which is input by user -func OptionInputTextColor(x Color) Option { +// WithInputTextColor to change a color of text which is input by user +func WithInputTextColor(x Color) Option { return func(p *Prompt) error { p.renderer.inputTextColor = x return nil } } -// OptionInputBGColor to change a color of background which is input by user -func OptionInputBGColor(x Color) Option { +// WithInputBGColor to change a color of background which is input by user +func WithInputBGColor(x Color) Option { return func(p *Prompt) error { p.renderer.inputBGColor = x return nil } } -// OptionPreviewSuggestionTextColor to change a text color which is completed -func OptionPreviewSuggestionTextColor(x Color) Option { +// WithPreviewSuggestionTextColor to change a text color which is completed +func WithPreviewSuggestionTextColor(x Color) Option { return func(p *Prompt) error { p.renderer.previewSuggestionTextColor = x return nil } } -// OptionPreviewSuggestionBGColor to change a background color which is completed -func OptionPreviewSuggestionBGColor(x Color) Option { +// WithPreviewSuggestionBGColor to change a background color which is completed +func WithPreviewSuggestionBGColor(x Color) Option { return func(p *Prompt) error { p.renderer.previewSuggestionBGColor = x return nil } } -// OptionSuggestionTextColor to change a text color in drop down suggestions. -func OptionSuggestionTextColor(x Color) Option { +// WithSuggestionTextColor to change a text color in drop down suggestions. +func WithSuggestionTextColor(x Color) Option { return func(p *Prompt) error { p.renderer.suggestionTextColor = x return nil } } -// OptionSuggestionBGColor change a background color in drop down suggestions. -func OptionSuggestionBGColor(x Color) Option { +// WithSuggestionBGColor change a background color in drop down suggestions. +func WithSuggestionBGColor(x Color) Option { return func(p *Prompt) error { p.renderer.suggestionBGColor = x return nil } } -// OptionSelectedSuggestionTextColor to change a text color for completed text which is selected inside suggestions drop down box. -func OptionSelectedSuggestionTextColor(x Color) Option { +// WithSelectedSuggestionTextColor to change a text color for completed text which is selected inside suggestions drop down box. +func WithSelectedSuggestionTextColor(x Color) Option { return func(p *Prompt) error { p.renderer.selectedSuggestionTextColor = x return nil } } -// OptionSelectedSuggestionBGColor to change a background color for completed text which is selected inside suggestions drop down box. -func OptionSelectedSuggestionBGColor(x Color) Option { +// WithSelectedSuggestionBGColor to change a background color for completed text which is selected inside suggestions drop down box. +func WithSelectedSuggestionBGColor(x Color) Option { return func(p *Prompt) error { p.renderer.selectedSuggestionBGColor = x return nil } } -// OptionDescriptionTextColor to change a background color of description text in drop down suggestions. -func OptionDescriptionTextColor(x Color) Option { +// WithDescriptionTextColor to change a background color of description text in drop down suggestions. +func WithDescriptionTextColor(x Color) Option { return func(p *Prompt) error { p.renderer.descriptionTextColor = x return nil } } -// OptionDescriptionBGColor to change a background color of description text in drop down suggestions. -func OptionDescriptionBGColor(x Color) Option { +// WithDescriptionBGColor to change a background color of description text in drop down suggestions. +func WithDescriptionBGColor(x Color) Option { return func(p *Prompt) error { p.renderer.descriptionBGColor = x return nil } } -// OptionSelectedDescriptionTextColor to change a text color of description which is selected inside suggestions drop down box. -func OptionSelectedDescriptionTextColor(x Color) Option { +// WithSelectedDescriptionTextColor to change a text color of description which is selected inside suggestions drop down box. +func WithSelectedDescriptionTextColor(x Color) Option { return func(p *Prompt) error { p.renderer.selectedDescriptionTextColor = x return nil } } -// OptionSelectedDescriptionBGColor to change a background color of description which is selected inside suggestions drop down box. -func OptionSelectedDescriptionBGColor(x Color) Option { +// WithSelectedDescriptionBGColor to change a background color of description which is selected inside suggestions drop down box. +func WithSelectedDescriptionBGColor(x Color) Option { return func(p *Prompt) error { p.renderer.selectedDescriptionBGColor = x return nil } } -// OptionScrollbarThumbColor to change a thumb color on scrollbar. -func OptionScrollbarThumbColor(x Color) Option { +// WithScrollbarThumbColor to change a thumb color on scrollbar. +func WithScrollbarThumbColor(x Color) Option { return func(p *Prompt) error { p.renderer.scrollbarThumbColor = x return nil } } -// OptionScrollbarBGColor to change a background color of scrollbar. -func OptionScrollbarBGColor(x Color) Option { +// WithScrollbarBGColor to change a background color of scrollbar. +func WithScrollbarBGColor(x Color) Option { return func(p *Prompt) error { p.renderer.scrollbarBGColor = x return nil } } -// OptionMaxSuggestion specify the max number of displayed suggestions. -func OptionMaxSuggestion(x uint16) Option { +// WithMaxSuggestion specify the max number of displayed suggestions. +func WithMaxSuggestion(x uint16) Option { return func(p *Prompt) error { p.completion.max = x return nil } } -// OptionHistory to set history expressed by string array. -func OptionHistory(x []string) Option { +// WithHistory to set history expressed by string array. +func WithHistory(x []string) Option { return func(p *Prompt) error { p.history.histories = x p.history.Clear() @@ -206,16 +214,16 @@ func OptionHistory(x []string) Option { } } -// OptionSwitchKeyBindMode set a key bind mode. -func OptionSwitchKeyBindMode(m KeyBindMode) Option { +// WithSwitchKeyBindMode set a key bind mode. +func WithSwitchKeyBindMode(m KeyBindMode) Option { return func(p *Prompt) error { p.keyBindMode = m return nil } } -// OptionCompletionOnDown allows for Down arrow key to trigger completion. -func OptionCompletionOnDown() Option { +// WithCompletionOnDown allows for Down arrow key to trigger completion. +func WithCompletionOnDown() Option { return func(p *Prompt) error { p.completionOnDown = true return nil @@ -223,51 +231,51 @@ func OptionCompletionOnDown() Option { } // SwitchKeyBindMode to set a key bind mode. -// Deprecated: Please use OptionSwitchKeyBindMode. -var SwitchKeyBindMode = OptionSwitchKeyBindMode +// Deprecated: Please use WithSwitchKeyBindMode. +var SwitchKeyBindMode = WithSwitchKeyBindMode -// OptionAddKeyBind to set a custom key bind. -func OptionAddKeyBind(b ...KeyBind) Option { +// WithAddKeyBind to set a custom key bind. +func WithAddKeyBind(b ...KeyBind) Option { return func(p *Prompt) error { p.keyBindings = append(p.keyBindings, b...) return nil } } -// OptionAddASCIICodeBind to set a custom key bind. -func OptionAddASCIICodeBind(b ...ASCIICodeBind) Option { +// WithAddASCIICodeBind to set a custom key bind. +func WithAddASCIICodeBind(b ...ASCIICodeBind) Option { return func(p *Prompt) error { p.ASCIICodeBindings = append(p.ASCIICodeBindings, b...) return nil } } -// OptionShowCompletionAtStart to set completion window is open at start. -func OptionShowCompletionAtStart() Option { +// WithShowCompletionAtStart to set completion window is open at start. +func WithShowCompletionAtStart() Option { return func(p *Prompt) error { p.completion.showAtStart = true return nil } } -// OptionBreakLineCallback to run a callback at every break line -func OptionBreakLineCallback(fn func(*Document)) Option { +// WithBreakLineCallback to run a callback at every break line +func WithBreakLineCallback(fn func(*Document)) Option { return func(p *Prompt) error { p.renderer.breakLineCallback = fn return nil } } -// OptionSetExitCheckerOnInput set an exit function which checks if go-prompt exits its Run loop -func OptionSetExitCheckerOnInput(fn ExitChecker) Option { +// WithExitChecker set an exit function which checks if go-prompt exits its Run loop +func WithExitChecker(fn ExitChecker) Option { return func(p *Prompt) error { p.exitChecker = fn return nil } } -// OptionSetLexer set lexer function and enable it. -func OptionSetLexer(lex Lexer) Option { +// WithLexer set lexer function and enable it. +func WithLexer(lex Lexer) Option { return func(p *Prompt) error { p.lexer = lex return nil @@ -275,12 +283,12 @@ func OptionSetLexer(lex Lexer) Option { } // New returns a Prompt with powerful auto-completion. -func New(executor Executor, completer Completer, opts ...Option) *Prompt { +func New(executor Executor, opts ...Option) *Prompt { defaultWriter := NewStdoutWriter() registerWriter(defaultWriter) pt := &Prompt{ - in: NewStdinReader(), + reader: NewStdinReader(), renderer: &Render{ prefix: "> ", out: defaultWriter, @@ -305,7 +313,7 @@ func New(executor Executor, completer Completer, opts ...Option) *Prompt { buf: NewBuffer(), executor: executor, history: NewHistory(), - completion: NewCompletionManager(completer, 6), + completion: NewCompletionManager(6), keyBindMode: EmacsKeyBind, // All the above assume that bash is running in the default Emacs setting } diff --git a/prompt.go b/prompt.go index a9cd0ad3..daf4e968 100644 --- a/prompt.go +++ b/prompt.go @@ -29,7 +29,7 @@ type Completer func(Document) []Suggest // Prompt is a core struct of go-prompt. type Prompt struct { - in Reader + reader Reader buf *Buffer renderer *Render executor Executor @@ -87,7 +87,7 @@ func (p *Prompt) Run() { // Unset raw mode // Reset to Blocking mode because returned EAGAIN when still set non-blocking mode. - debug.AssertNoError(p.in.Close()) + debug.AssertNoError(p.reader.Close()) p.executor(e.input) p.completion.Update(*p.buf.Document()) @@ -99,7 +99,7 @@ func (p *Prompt) Run() { return } // Set raw mode - debug.AssertNoError(p.in.Open()) + debug.AssertNoError(p.reader.Open()) go p.readBuffer(bufCh, stopReadBufCh) go p.handleSignals(exitCh, winSizeCh, stopHandleSignalCh) } else { @@ -314,7 +314,7 @@ func (p *Prompt) readBuffer(bufCh chan []byte, stopCh chan struct{}) { return default: bytes := make([]byte, inputBufferSize) - n, err := p.in.Read(bytes) + n, err := p.reader.Read(bytes) if err != nil { break } @@ -340,14 +340,14 @@ func (p *Prompt) readBuffer(bufCh chan []byte, stopCh chan struct{}) { } func (p *Prompt) setup() { - debug.AssertNoError(p.in.Open()) + debug.AssertNoError(p.reader.Open()) p.renderer.Setup() - p.renderer.UpdateWinSize(p.in.GetWinSize()) + p.renderer.UpdateWinSize(p.reader.GetWinSize()) } func (p *Prompt) Close() { if !p.skipClose { - debug.AssertNoError(p.in.Close()) + debug.AssertNoError(p.reader.Close()) } p.renderer.Close() } diff --git a/shortcut.go b/shortcut.go index 7e3cc424..15b62ecd 100644 --- a/shortcut.go +++ b/shortcut.go @@ -3,8 +3,8 @@ package prompt func NoopExecutor(in string) {} // Input get the input data from the user and return it. -func Input(prefix string, completer Completer, opts ...Option) string { - pt := New(NoopExecutor, completer) +func Input(prefix string, opts ...Option) string { + pt := New(NoopExecutor) pt.renderer.prefixTextColor = DefaultColor pt.renderer.prefix = prefix diff --git a/signal_posix.go b/signal_posix.go index fe27a99a..fc6db611 100644 --- a/signal_posix.go +++ b/signal_posix.go @@ -12,7 +12,7 @@ import ( ) func (p *Prompt) handleSignals(exitCh chan int, winSizeCh chan *WinSize, stop chan struct{}) { - in := p.in + in := p.reader sigCh := make(chan os.Signal, 1) signal.Notify( sigCh, From 194a1982392fdf7ca6199d3c2f47f6e200969792 Mon Sep 17 00:00:00 2001 From: Mateusz Drewniak Date: Fri, 7 Jul 2023 10:40:42 +0200 Subject: [PATCH 05/21] Add `ExecuteOnEnterCallback` --- _example/bang-executor/main.go | 26 +++++++++++ _example/http-prompt/main.go | 13 +++--- _example/live-prefix/main.go | 18 +++----- constructor.go | 79 +++++++++++++++++++++------------- prompt.go | 40 +++++++++++------ render.go | 18 +++----- render_test.go | 3 +- shortcut.go | 3 +- 8 files changed, 124 insertions(+), 76 deletions(-) create mode 100644 _example/bang-executor/main.go diff --git a/_example/bang-executor/main.go b/_example/bang-executor/main.go new file mode 100644 index 00000000..0f24d9d2 --- /dev/null +++ b/_example/bang-executor/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "fmt" + "unicode/utf8" + + "github.com/elk-language/go-prompt" +) + +func main() { + p := prompt.New( + executor, + prompt.WithExecuteOnEnterCallback(ExecuteOnEnter), + ) + + p.Run() +} + +func ExecuteOnEnter(input string) bool { + char, _ := utf8.DecodeLastRuneInString(input) + return char == '!' +} + +func executor(s string) { + fmt.Println("You printed: " + s) +} diff --git a/_example/http-prompt/main.go b/_example/http-prompt/main.go index 129b415a..d80e0a25 100644 --- a/_example/http-prompt/main.go +++ b/_example/http-prompt/main.go @@ -92,11 +92,13 @@ var suggestions = []prompt.Suggest{ {"X-XSRF-TOKEN", "Prevent cross-site request forgery"}, } -func livePrefix() (string, bool) { - if ctx.url.Path == "/" { - return "", false +func livePrefix(defaultPrefix string) prompt.PrefixCallback { + return func() string { + if ctx.url.Path == "/" { + return defaultPrefix + } + return ctx.url.String() + "> " } - return ctx.url.String() + "> ", true } func executor(in string) { @@ -183,8 +185,7 @@ func main() { p := prompt.New( executor, - prompt.WithPrefix(u.String()+"> "), - prompt.WithLivePrefix(livePrefix), + prompt.WithPrefixCallback(livePrefix(u.String()+"> ")), prompt.WithTitle("http-prompt"), prompt.WithCompleter(completer), ) diff --git a/_example/live-prefix/main.go b/_example/live-prefix/main.go index 121df1c6..4fe160a5 100644 --- a/_example/live-prefix/main.go +++ b/_example/live-prefix/main.go @@ -6,20 +6,15 @@ import ( prompt "github.com/elk-language/go-prompt" ) -var LivePrefixState struct { - LivePrefix string - IsEnable bool -} +var LivePrefix string = ">>> " func executor(in string) { fmt.Println("Your input: " + in) if in == "" { - LivePrefixState.IsEnable = false - LivePrefixState.LivePrefix = in + LivePrefix = ">>> " return } - LivePrefixState.LivePrefix = in + "> " - LivePrefixState.IsEnable = true + LivePrefix = in + "> " } func completer(in prompt.Document) []prompt.Suggest { @@ -32,15 +27,14 @@ func completer(in prompt.Document) []prompt.Suggest { return prompt.FilterHasPrefix(s, in.GetWordBeforeCursor(), true) } -func changeLivePrefix() (string, bool) { - return LivePrefixState.LivePrefix, LivePrefixState.IsEnable +func changeLivePrefix() string { + return LivePrefix } func main() { p := prompt.New( executor, - prompt.WithPrefix(">>> "), - prompt.WithLivePrefix(changeLivePrefix), + prompt.WithPrefixCallback(changeLivePrefix), prompt.WithTitle("live-prefix-example"), prompt.WithCompleter(completer), ) diff --git a/constructor.go b/constructor.go index e39f1898..16a8cd77 100644 --- a/constructor.go +++ b/constructor.go @@ -4,6 +4,9 @@ package prompt // prompt.New accepts any number of options (this is functional option pattern). type Option func(prompt *Prompt) error +// Callback function that returns a prompt prefix. +type PrefixCallback func() (prefix string) + // WithCompleter is an option that sets a custom Completer object. func WithCompleter(c Completer) Option { return func(p *Prompt) error { @@ -12,59 +15,59 @@ func WithCompleter(c Completer) Option { } } -// WithReader to set a custom Reader object. An argument should implement Reader interface. -func WithReader(x Reader) Option { +// WithReader can be used to set a custom Reader object. +func WithReader(r Reader) Option { return func(p *Prompt) error { - p.reader = x + p.reader = r return nil } } -// WithWriter to set a custom Writer object. An argument should implement Writer interface. -func WithWriter(x Writer) Option { +// WithWriter can be used to set a custom Writer object. +func WithWriter(w Writer) Option { return func(p *Prompt) error { - registerWriter(x) - p.renderer.out = x + registerWriter(w) + p.renderer.out = w return nil } } -// WithTitle to set title displayed at the header bar of terminal. -func WithTitle(x string) Option { +// WithTitle can be used to set the title displayed at the header bar of the terminal. +func WithTitle(t string) Option { return func(p *Prompt) error { - p.renderer.title = x + p.renderer.title = t return nil } } -// WithPrefix to set prefix string. -func WithPrefix(x string) Option { +// WithPrefix can be used to set a prefix string for the prompt. +func WithPrefix(prefix string) Option { return func(p *Prompt) error { - p.renderer.prefix = x + p.renderer.prefixCallback = func() string { return prefix } return nil } } -// WithInitialBufferText to set the initial buffer text -func WithInitialBufferText(x string) Option { +// WithInitialText can be used to set the initial buffer text. +func WithInitialText(text string) Option { return func(p *Prompt) error { - p.buf.InsertText(x, false, true) + p.buf.InsertText(text, false, true) return nil } } -// WithCompletionWordSeparator to set word separators. Enable only ' ' if empty. -func WithCompletionWordSeparator(x string) Option { +// WithCompletionWordSeparator can be used to set word separators. Enable only ' ' if empty. +func WithCompletionWordSeparator(sep string) Option { return func(p *Prompt) error { - p.completion.wordSeparator = x + p.completion.wordSeparator = sep return nil } } -// WithLivePrefix to change the prefix dynamically by callback function -func WithLivePrefix(f func() (prefix string, useLivePrefix bool)) Option { +// WithPrefixCallback can be used to change the prefix dynamically by a callback function. +func WithPrefixCallback(f PrefixCallback) Option { return func(p *Prompt) error { - p.renderer.livePrefixCallback = f + p.renderer.prefixCallback = f return nil } } @@ -282,6 +285,24 @@ func WithLexer(lex Lexer) Option { } } +// WithExecuteOnEnterCallback can be used to set +// a custom callback function that determines whether an Enter key +// should trigger the Executor or add a newline to the user input buffer. +func WithExecuteOnEnterCallback(fn ExecuteOnEnterCallback) Option { + return func(p *Prompt) error { + p.executeOnEnterCallback = fn + return nil + } +} + +func DefaultExecuteOnEnterCallback(input string) bool { + return true +} + +func DefaultPrefixCallback() string { + return "> " +} + // New returns a Prompt with powerful auto-completion. func New(executor Executor, opts ...Option) *Prompt { defaultWriter := NewStdoutWriter() @@ -290,9 +311,8 @@ func New(executor Executor, opts ...Option) *Prompt { pt := &Prompt{ reader: NewStdinReader(), renderer: &Render{ - prefix: "> ", out: defaultWriter, - livePrefixCallback: func() (string, bool) { return "", false }, + prefixCallback: DefaultPrefixCallback, prefixTextColor: Blue, prefixBGColor: DefaultColor, inputTextColor: DefaultColor, @@ -310,11 +330,12 @@ func New(executor Executor, opts ...Option) *Prompt { scrollbarThumbColor: DarkGray, scrollbarBGColor: Cyan, }, - buf: NewBuffer(), - executor: executor, - history: NewHistory(), - completion: NewCompletionManager(6), - keyBindMode: EmacsKeyBind, // All the above assume that bash is running in the default Emacs setting + buf: NewBuffer(), + executor: executor, + history: NewHistory(), + completion: NewCompletionManager(6), + executeOnEnterCallback: DefaultExecuteOnEnterCallback, + keyBindMode: EmacsKeyBind, // All the above assume that bash is running in the default Emacs setting } for _, opt := range opts { diff --git a/prompt.go b/prompt.go index daf4e968..ecc43ed9 100644 --- a/prompt.go +++ b/prompt.go @@ -23,25 +23,33 @@ type Executor func(string) // Exit means exit go-prompt (not the overall Go program) type ExitChecker func(in string, breakline bool) bool +// ExecuteOnEnterCallback is a function that receives +// user input after Enter has been pressed +// and determines whether the input should be executed. +// If this function returns true, the Executor callback will be called +// otherwise a newline will be added to the buffer containing user input. +type ExecuteOnEnterCallback func(input string) bool + // Completer is a function that returns // a slice of suggestions for the given Document. type Completer func(Document) []Suggest // Prompt is a core struct of go-prompt. type Prompt struct { - reader Reader - buf *Buffer - renderer *Render - executor Executor - history *History - lexer Lexer - completion *CompletionManager - keyBindings []KeyBind - ASCIICodeBindings []ASCIICodeBind - keyBindMode KeyBindMode - completionOnDown bool - exitChecker ExitChecker - skipClose bool + reader Reader + buf *Buffer + renderer *Render + executor Executor + history *History + lexer Lexer + completion *CompletionManager + keyBindings []KeyBind + ASCIICodeBindings []ASCIICodeBind + keyBindMode KeyBindMode + completionOnDown bool + exitChecker ExitChecker + executeOnEnterCallback ExecuteOnEnterCallback + skipClose bool } // UserInput is the struct that contains the user input context. @@ -137,8 +145,12 @@ func (p *Prompt) feed(b []byte) (shouldExit bool, userInput *UserInput) { switch key { case Enter, ControlJ, ControlM: - p.renderer.BreakLine(p.buf, p.lexer) + if !p.executeOnEnterCallback(p.buf.Text()) { + p.buf.NewLine(false) + break + } + p.renderer.BreakLine(p.buf, p.lexer) userInput = &UserInput{input: p.buf.Text()} p.buf = NewBuffer() if userInput.input != "" { diff --git a/render.go b/render.go index a2d4f0df..97b9d6c4 100644 --- a/render.go +++ b/render.go @@ -10,13 +10,12 @@ import ( // Render to render prompt information from state of Buffer. type Render struct { - out Writer - prefix string - livePrefixCallback func() (prefix string, useLivePrefix bool) - breakLineCallback func(*Document) - title string - row uint16 - col uint16 + out Writer + prefixCallback PrefixCallback + breakLineCallback func(*Document) + title string + row uint16 + col uint16 previousCursor Position @@ -50,10 +49,7 @@ func (r *Render) Setup() { // getCurrentPrefix to get current prefix. // If live-prefix is enabled, return live-prefix. func (r *Render) getCurrentPrefix() string { - if prefix, ok := r.livePrefixCallback(); ok { - return prefix - } - return r.prefix + return r.prefixCallback() } func (r *Render) renderPrefix() { diff --git a/render_test.go b/render_test.go index 14edcd22..81e0f643 100644 --- a/render_test.go +++ b/render_test.go @@ -73,11 +73,10 @@ func TestFormatCompletion(t *testing.T) { func TestBreakLineCallback(t *testing.T) { var i int r := &Render{ - prefix: "> ", out: &PosixWriter{ fd: syscall.Stdin, // "write" to stdin just so we don't mess with the output of the tests }, - livePrefixCallback: func() (string, bool) { return "", false }, + prefixCallback: DefaultPrefixCallback, prefixTextColor: Blue, prefixBGColor: DefaultColor, inputTextColor: DefaultColor, diff --git a/shortcut.go b/shortcut.go index 15b62ecd..4c4c8691 100644 --- a/shortcut.go +++ b/shortcut.go @@ -3,10 +3,9 @@ package prompt func NoopExecutor(in string) {} // Input get the input data from the user and return it. -func Input(prefix string, opts ...Option) string { +func Input(opts ...Option) string { pt := New(NoopExecutor) pt.renderer.prefixTextColor = DefaultColor - pt.renderer.prefix = prefix for _, opt := range opts { if err := opt(pt); err != nil { From 049ed0140237962fc7de0d719e8a7e371792c9d3 Mon Sep 17 00:00:00 2001 From: Mateusz Drewniak Date: Fri, 7 Jul 2023 14:04:19 +0200 Subject: [PATCH 06/21] Fix meta-left and meta-right word navigation --- document.go | 31 +++++++++++++++++++++++++++++++ emacs.go | 4 ++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/document.go b/document.go index f2359b27..26daa903 100644 --- a/document.go +++ b/document.go @@ -6,6 +6,7 @@ import ( "github.com/elk-language/go-prompt/internal/bisect" istrings "github.com/elk-language/go-prompt/internal/strings" + runewidth "github.com/mattn/go-runewidth" "golang.org/x/exp/utf8string" ) @@ -132,6 +133,13 @@ func (d *Document) FindStartOfPreviousWord() int { return 0 } +// Returns the string width (as visible in the terminal) +// of the text before the cursor until the start of the previous word. +func (d *Document) FindStringWidthUntilStartOfPreviousWord() int { + x := d.TextBeforeCursor() + return runewidth.StringWidth(x[d.FindStartOfPreviousWordWithSpace():]) +} + // FindStartOfPreviousWordWithSpace is almost the same as FindStartOfPreviousWord. // The only difference is to ignore contiguous spaces. func (d *Document) FindStartOfPreviousWordWithSpace() int { @@ -211,6 +219,29 @@ func (d *Document) FindEndOfCurrentWordWithSpace() int { return start + end } +// 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 { + t := d.TextAfterCursor() + width := 0 + nonSpaceCharSeen := false + for _, char := range t { + if !nonSpaceCharSeen && char == ' ' { + width += 1 + continue + } + + if nonSpaceCharSeen && char == ' ' { + break + } + + nonSpaceCharSeen = true + width += runewidth.RuneWidth(char) + } + + return width +} + // 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 { diff --git a/emacs.go b/emacs.go index 636606bd..be34b6c4 100644 --- a/emacs.go +++ b/emacs.go @@ -98,7 +98,7 @@ var emacsKeyBindings = []KeyBind{ { Key: AltRight, Fn: func(buf *Buffer) { - buf.CursorRight(buf.Document().FindEndOfCurrentWordWithSpace()) + buf.CursorRight(buf.Document().FindStringWidthUntilEndOfCurrentWord()) }, }, // Left allow: Backward one character @@ -112,7 +112,7 @@ var emacsKeyBindings = []KeyBind{ { Key: AltLeft, Fn: func(buf *Buffer) { - buf.CursorLeft(len([]rune(buf.Document().TextBeforeCursor())) - buf.Document().FindStartOfPreviousWordWithSpace()) + buf.CursorLeft(buf.Document().FindStringWidthUntilStartOfPreviousWord()) }, }, // Cut the Word before the cursor. From bd0e5818739d5fd5b5e486ba01ea4ba310e7afe0 Mon Sep 17 00:00:00 2001 From: Mateusz Drewniak Date: Fri, 7 Jul 2023 14:24:26 +0200 Subject: [PATCH 07/21] Fix `(*Document).GetCursorUpPosition` etc --- document.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/document.go b/document.go index 26daa903..3d7d1f68 100644 --- a/document.go +++ b/document.go @@ -304,7 +304,7 @@ func (d *Document) lineStartIndexes() []int { lc := d.LineCount() lengths := make([]int, lc) for i, l := range d.Lines() { - lengths[i] = len(l) + lengths[i] = len([]rune(l)) } // Calculate cumulative sums. @@ -339,8 +339,6 @@ func (d *Document) CursorPositionRow() (row int) { // CursorPositionCol returns the current column. (0-based.) func (d *Document) CursorPositionCol() (col int) { - // Don't use self.text_before_cursor to calculate this. Creating substrings - // and splitting is too expensive for getting the cursor position. _, index := d.findLineStartIndex(d.cursorPosition) col = d.cursorPosition - index return @@ -452,7 +450,7 @@ func (d *Document) TranslateRowColToIndex(row int, column int) (index int) { row = len(indexes) - 1 } index = indexes[row] - line := d.Lines()[row] + line := []rune(d.Lines()[row]) // python) result += max(0, min(col, len(line))) if column > 0 || len(line) > 0 { @@ -463,11 +461,12 @@ func (d *Document) TranslateRowColToIndex(row int, column int) (index int) { } } + text := []rune(d.Text) // 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(d.Text) { - index = len(d.Text) + if index > len(text) { + index = len(text) } if index < 0 { index = 0 From f68d3a303a3787e44c59c5fb7427a472b254ae54 Mon Sep 17 00:00:00 2001 From: Mateusz Drewniak Date: Fri, 7 Jul 2023 14:25:15 +0200 Subject: [PATCH 08/21] change the plural of index to indices --- document.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/document.go b/document.go index 3d7d1f68..84ae47fa 100644 --- a/document.go +++ b/document.go @@ -296,7 +296,7 @@ func (d *Document) CurrentLine() string { return d.CurrentLineBeforeCursor() + d.CurrentLineAfterCursor() } -// Array pointing to the start indexes of all the lines. +// Array pointing to the start indices of all the lines. func (d *Document) lineStartIndexes() []int { // TODO: Cache, because this is often reused. // (If it is used, it's often used many times. @@ -308,26 +308,26 @@ func (d *Document) lineStartIndexes() []int { } // Calculate cumulative sums. - indexes := make([]int, lc+1) - indexes[0] = 0 // https://github.com/jonathanslenders/python-prompt-toolkit/blob/master/prompt_toolkit/document.py#L189 + indices := make([]int, lc+1) + indices[0] = 0 // https://github.com/jonathanslenders/python-prompt-toolkit/blob/master/prompt_toolkit/document.py#L189 pos := 0 for i, l := range lengths { pos += l + 1 - indexes[i+1] = pos + indices[i+1] = pos } if lc > 1 { // Pop the last item. (This is not a new line.) - indexes = indexes[:lc] + indices = indices[:lc] } - return indexes + return indices } // 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) { - indexes := d.lineStartIndexes() - pos = bisect.Right(indexes, index) - 1 - lineStartIndex = indexes[pos] + indices := d.lineStartIndexes() + pos = bisect.Right(indices, index) - 1 + lineStartIndex = indices[pos] return } @@ -443,13 +443,13 @@ func (d *Document) TranslateIndexToPosition(index int) (row int, col int) { // 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) { - indexes := d.lineStartIndexes() + indices := d.lineStartIndexes() if row < 0 { row = 0 - } else if row > len(indexes) { - row = len(indexes) - 1 + } else if row > len(indices) { + row = len(indices) - 1 } - index = indexes[row] + index = indices[row] line := []rune(d.Lines()[row]) // python) result += max(0, min(col, len(line))) From facef81f11511ad144a264b9127514a7aff94ffd Mon Sep 17 00:00:00 2001 From: Mateusz Drewniak Date: Tue, 11 Jul 2023 01:29:18 +0200 Subject: [PATCH 09/21] Introduce more strict numeric types for different kinds of indices --- buffer.go | 60 +++++---- buffer_test.go | 34 ++--- completion.go | 5 +- completion_test.go | 10 +- document.go | 140 ++++++++++----------- document_test.go | 242 ++++++++++++++++++------------------ emacs.go | 21 ++-- emacs_test.go | 10 +- internal/bisect/bisect.go | 8 +- internal/strings/index.go | 21 ++++ internal/strings/strings.go | 34 +++-- key_bind_func.go | 14 ++- position.go | 14 ++- position_test.go | 3 +- prompt.go | 9 +- render.go | 39 +++--- render_test.go | 4 +- 17 files changed, 368 insertions(+), 300 deletions(-) create mode 100644 internal/strings/index.go 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: "", From aaad8c4ffd0d44b25ee8c8ba157ce521697d3b30 Mon Sep 17 00:00:00 2001 From: Mateusz Drewniak Date: Tue, 11 Jul 2023 23:24:20 +0200 Subject: [PATCH 10/21] translate tabs into double spaces --- prompt.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/prompt.go b/prompt.go index 19c3d135..31a76ea8 100644 --- a/prompt.go +++ b/prompt.go @@ -333,16 +333,20 @@ func (p *Prompt) readBuffer(bufCh chan []byte, stopCh chan struct{}) { } bytes = bytes[:n] if len(bytes) != 1 || bytes[0] != 0 { - newBytes := make([]byte, len(bytes)) - for i, byt := range bytes { + newBytes := make([]byte, 0, len(bytes)) + for _, byt := range bytes { + switch byt { // translate raw mode \r into \n // to make pasting multiline text // work properly - switch byt { case '\r': - newBytes[i] = '\n' + newBytes = append(newBytes, '\n') + // translate \t into two spaces + // to avoid problems with cursor positions + case '\t': + newBytes = append(newBytes, ' ', ' ') default: - newBytes[i] = byt + newBytes = append(newBytes, byt) } } bufCh <- newBytes From dce9de45f8edfb561bb087b021ec32cfd35a9b2c Mon Sep 17 00:00:00 2001 From: Mateusz Drewniak Date: Tue, 11 Jul 2023 23:45:30 +0200 Subject: [PATCH 11/21] Fix alt-left alt-right navigation --- document.go | 19 +++++++++---------- emacs.go | 8 ++++---- key_bind_func.go | 15 --------------- 3 files changed, 13 insertions(+), 29 deletions(-) diff --git a/document.go b/document.go index e01b8675..adf8f3b8 100644 --- a/document.go +++ b/document.go @@ -6,7 +6,6 @@ import ( "github.com/elk-language/go-prompt/internal/bisect" istrings "github.com/elk-language/go-prompt/internal/strings" - runewidth "github.com/mattn/go-runewidth" "golang.org/x/exp/utf8string" ) @@ -133,11 +132,11 @@ func (d *Document) FindStartOfPreviousWord() istrings.ByteIndex { return 0 } -// Returns the string width (as visible in the terminal) +// Returns the rune count // of the text before the cursor until the start of the previous word. -func (d *Document) FindStringWidthUntilStartOfPreviousWord() int { +func (d *Document) FindRuneCountUntilStartOfPreviousWord() istrings.RuneCount { x := d.TextBeforeCursor() - return runewidth.StringWidth(x[d.FindStartOfPreviousWordWithSpace():]) + return istrings.RuneLen(x[d.FindStartOfPreviousWordWithSpace():]) } // FindStartOfPreviousWordWithSpace is almost the same as FindStartOfPreviousWord. @@ -219,15 +218,15 @@ func (d *Document) FindEndOfCurrentWordWithSpace() istrings.ByteIndex { return start + end } -// Returns the string width (as visible in the terminal) +// Returns the number of runes // of the text after the cursor until the end of the current word. -func (d *Document) FindStringWidthUntilEndOfCurrentWord() istrings.StringWidth { +func (d *Document) FindRuneCountUntilEndOfCurrentWord() istrings.RuneCount { t := d.TextAfterCursor() - var width istrings.StringWidth + var count istrings.RuneCount nonSpaceCharSeen := false for _, char := range t { if !nonSpaceCharSeen && char == ' ' { - width += 1 + count += 1 continue } @@ -236,10 +235,10 @@ func (d *Document) FindStringWidthUntilEndOfCurrentWord() istrings.StringWidth { } nonSpaceCharSeen = true - width += istrings.StringWidth(runewidth.RuneWidth(char)) + count += 1 } - return width + return count } // FindEndOfCurrentWordUntilSeparator is almost the same as FindEndOfCurrentWord. diff --git a/emacs.go b/emacs.go index 2123cb52..4c0e2a76 100644 --- a/emacs.go +++ b/emacs.go @@ -101,7 +101,7 @@ var emacsKeyBindings = []KeyBind{ { Key: AltRight, Fn: func(buf *Buffer) { - buf.CursorRight(istrings.RuneIndex(buf.Document().FindStringWidthUntilEndOfCurrentWord())) // WARN + buf.CursorRight(buf.Document().FindRuneCountUntilEndOfCurrentWord()) }, }, // Left allow: Backward one character @@ -115,20 +115,20 @@ var emacsKeyBindings = []KeyBind{ { Key: AltLeft, Fn: func(buf *Buffer) { - buf.CursorLeft(istrings.RuneIndex(buf.Document().FindStringWidthUntilStartOfPreviousWord())) // WARN + buf.CursorLeft(buf.Document().FindRuneCountUntilStartOfPreviousWord()) }, }, // Cut the Word before the cursor. { Key: ControlW, Fn: func(buf *Buffer) { - buf.DeleteBeforeCursor(istrings.RuneIndex(len([]rune(buf.Document().GetWordBeforeCursorWithSpace())))) + buf.DeleteBeforeCursor(istrings.RuneLen(buf.Document().GetWordBeforeCursorWithSpace())) }, }, { Key: AltBackspace, Fn: func(buf *Buffer) { - buf.DeleteBeforeCursor(istrings.RuneIndex(len([]rune(buf.Document().GetWordBeforeCursorWithSpace())))) + buf.DeleteBeforeCursor(istrings.RuneLen(buf.Document().GetWordBeforeCursorWithSpace())) }, }, // Clear the Screen, similar to the clear command diff --git a/key_bind_func.go b/key_bind_func.go index 00bd3066..cfcefe6a 100644 --- a/key_bind_func.go +++ b/key_bind_func.go @@ -21,11 +21,6 @@ func DeleteChar(buf *Buffer) { buf.Delete(1) } -// DeleteWord Delete word before the cursor -func DeleteWord(buf *Buffer) { - buf.DeleteBeforeCursor(istrings.RuneCount(len([]rune(buf.Document().TextBeforeCursor()))) - istrings.RuneCount(buf.Document().FindStartOfPreviousWordWithSpace())) // WARN -} - // DeleteBeforeChar Go to Backspace func DeleteBeforeChar(buf *Buffer) { buf.DeleteBeforeCursor(1) @@ -40,13 +35,3 @@ func GoRightChar(buf *Buffer) { func GoLeftChar(buf *Buffer) { buf.CursorLeft(1) } - -// GoRightWord Forward one word -func GoRightWord(buf *Buffer) { - buf.CursorRight(istrings.RuneCount(buf.Document().FindEndOfCurrentWordWithSpace())) // WARN -} - -// GoLeftWord Backward one word -func GoLeftWord(buf *Buffer) { - buf.CursorLeft(istrings.RuneCount(len([]rune(buf.Document().TextBeforeCursor()))) - istrings.RuneCount(buf.Document().FindStartOfPreviousWordWithSpace())) // WARN -} From 04658149d515fa5b4bd5832242b84f54161098a2 Mon Sep 17 00:00:00 2001 From: Mateusz Drewniak Date: Wed, 12 Jul 2023 10:05:26 +0200 Subject: [PATCH 12/21] Fix keybinds for multiline input --- document.go | 33 +++++++++++++++++++++++++++++++-- emacs.go | 12 ++++-------- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/document.go b/document.go index adf8f3b8..e316e9f9 100644 --- a/document.go +++ b/document.go @@ -2,6 +2,7 @@ package prompt import ( "strings" + "unicode" "unicode/utf8" "github.com/elk-language/go-prompt/internal/bisect" @@ -479,8 +480,36 @@ func (d *Document) OnLastLine() bool { } // GetEndOfLinePosition returns relative position for the end of this line. -func (d *Document) GetEndOfLinePosition() istrings.RuneIndex { - return istrings.RuneIndex(len([]rune(d.CurrentLineAfterCursor()))) +func (d *Document) GetEndOfLinePosition() istrings.RuneCount { + return istrings.RuneLen(d.CurrentLineAfterCursor()) +} + +// GetStartOfLinePosition returns relative position for the start of this line. +func (d *Document) GetStartOfLinePosition() istrings.RuneCount { + return istrings.RuneLen(d.CurrentLineBeforeCursor()) +} + +// GetStartOfLinePosition returns relative position for the start of this line. +func (d *Document) FindStartOfFirstWordOfLine() istrings.RuneCount { + line := d.CurrentLineBeforeCursor() + var counter istrings.RuneCount + var nonSpaceCharSeen bool + for _, char := range line { + if !nonSpaceCharSeen && unicode.IsSpace(char) { + continue + } + + if !nonSpaceCharSeen { + nonSpaceCharSeen = true + } + counter++ + } + + if counter == 0 { + return istrings.RuneLen(line) + } + + return counter } func (d *Document) leadingWhitespaceInCurrentLine() (margin string) { diff --git a/emacs.go b/emacs.go index 4c0e2a76..f43e1774 100644 --- a/emacs.go +++ b/emacs.go @@ -46,32 +46,28 @@ var emacsKeyBindings = []KeyBind{ { Key: ControlE, Fn: func(buf *Buffer) { - x := []rune(buf.Document().TextAfterCursor()) - buf.CursorRight(istrings.RuneCount(len(x))) + buf.CursorRight(istrings.RuneLen(buf.Document().CurrentLineAfterCursor())) }, }, // Go to the beginning of the line { Key: ControlA, Fn: func(buf *Buffer) { - x := []rune(buf.Document().TextBeforeCursor()) - buf.CursorLeft(istrings.RuneCount(len(x))) + buf.CursorLeft(buf.Document().FindStartOfFirstWordOfLine()) }, }, // Cut the Line after the cursor { Key: ControlK, Fn: func(buf *Buffer) { - x := []rune(buf.Document().TextAfterCursor()) - buf.Delete(istrings.RuneCount(len(x))) + buf.Delete(istrings.RuneLen(buf.Document().CurrentLineAfterCursor())) }, }, // Cut/delete the Line before the cursor { Key: ControlU, Fn: func(buf *Buffer) { - x := []rune(buf.Document().TextBeforeCursor()) - buf.DeleteBeforeCursor(istrings.RuneCount(len(x))) + buf.DeleteBeforeCursor(istrings.RuneLen(buf.Document().CurrentLineBeforeCursor())) }, }, // Delete character under the cursor From 14a5059ca1dd78ea6e9f7a8454631dc3fa139411 Mon Sep 17 00:00:00 2001 From: Mateusz Drewniak Date: Wed, 12 Jul 2023 10:14:56 +0200 Subject: [PATCH 13/21] Change KeyBindMode to a numeric type --- key_bind.go | 6 +++--- prompt.go | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/key_bind.go b/key_bind.go index 42669e7f..0332e27d 100644 --- a/key_bind.go +++ b/key_bind.go @@ -16,13 +16,13 @@ type ASCIICodeBind struct { } // KeyBindMode to switch a key binding flexibly. -type KeyBindMode string +type KeyBindMode uint8 const ( // CommonKeyBind is a mode without any keyboard shortcut - CommonKeyBind KeyBindMode = "common" + CommonKeyBind KeyBindMode = iota // EmacsKeyBind is a mode to use emacs-like keyboard shortcut - EmacsKeyBind KeyBindMode = "emacs" + EmacsKeyBind ) var commonKeyBindings = []KeyBind{ diff --git a/prompt.go b/prompt.go index 31a76ea8..5d6d50c8 100644 --- a/prompt.go +++ b/prompt.go @@ -247,7 +247,8 @@ func (p *Prompt) handleKeyBinding(key Key) bool { } } - if p.keyBindMode == EmacsKeyBind { + switch p.keyBindMode { + case EmacsKeyBind: for i := range emacsKeyBindings { kb := emacsKeyBindings[i] if kb.Key == key { From 116b3679bb801932c3fc6a86c0d6d8cfcc2a7e56 Mon Sep 17 00:00:00 2001 From: Mateusz Drewniak Date: Wed, 12 Jul 2023 22:47:09 +0200 Subject: [PATCH 14/21] Refactor internal packages --- _tools/vt100_debug/main.go | 2 +- {internal/bisect => bisect}/bisect.go | 0 {internal/bisect => bisect}/bisect_test.go | 2 +- buffer.go | 30 +-- buffer_test.go | 30 +-- completer/file.go | 2 +- completion.go | 4 +- completion_test.go | 2 +- {internal/debug => debug}/assert.go | 0 {internal/debug => debug}/log.go | 0 document.go | 132 ++++++------- document_test.go | 184 +++++++++--------- emacs.go | 18 +- emacs_test.go | 6 +- key_bind_func.go | 6 +- position.go | 2 +- position_test.go | 2 +- prompt.go | 6 +- reader_posix.go | 2 +- render.go | 4 +- render_test.go | 2 +- signal_posix.go | 2 +- signal_windows.go | 2 +- {internal/strings => strings}/strings.go | 28 +-- {internal/strings => strings}/strings_test.go | 2 +- internal/strings/index.go => strings/units.go | 20 +- {internal/term => term}/raw.go | 0 {internal/term => term}/term.go | 0 28 files changed, 241 insertions(+), 249 deletions(-) rename {internal/bisect => bisect}/bisect.go (100%) rename {internal/bisect => bisect}/bisect_test.go (95%) rename {internal/debug => debug}/assert.go (100%) rename {internal/debug => debug}/log.go (100%) rename {internal/strings => strings}/strings.go (78%) rename {internal/strings => strings}/strings_test.go (94%) rename internal/strings/index.go => strings/units.go (53%) rename {internal/term => term}/raw.go (100%) rename {internal/term => term}/term.go (100%) diff --git a/_tools/vt100_debug/main.go b/_tools/vt100_debug/main.go index 9dc6a483..3b46ce7c 100644 --- a/_tools/vt100_debug/main.go +++ b/_tools/vt100_debug/main.go @@ -8,7 +8,7 @@ import ( "syscall" prompt "github.com/elk-language/go-prompt" - "github.com/elk-language/go-prompt/internal/term" + "github.com/elk-language/go-prompt/term" ) func main() { diff --git a/internal/bisect/bisect.go b/bisect/bisect.go similarity index 100% rename from internal/bisect/bisect.go rename to bisect/bisect.go diff --git a/internal/bisect/bisect_test.go b/bisect/bisect_test.go similarity index 95% rename from internal/bisect/bisect_test.go rename to bisect/bisect_test.go index 51a62452..332921bf 100644 --- a/internal/bisect/bisect_test.go +++ b/bisect/bisect_test.go @@ -5,7 +5,7 @@ import ( "math/rand" "testing" - "github.com/elk-language/go-prompt/internal/bisect" + "github.com/elk-language/go-prompt/bisect" ) func Example() { diff --git a/buffer.go b/buffer.go index 163a4aee..a1d44e6e 100644 --- a/buffer.go +++ b/buffer.go @@ -3,15 +3,15 @@ package prompt import ( "strings" - "github.com/elk-language/go-prompt/internal/debug" - istrings "github.com/elk-language/go-prompt/internal/strings" + "github.com/elk-language/go-prompt/debug" + istrings "github.com/elk-language/go-prompt/strings" ) // Buffer emulates the console buffer. type Buffer struct { workingLines []string // The working lines. Similar to history workingIndex int // index of the current line - cursorPosition istrings.RuneIndex + cursorPosition istrings.RuneNumber cacheDocument *Document lastKeyStroke Key } @@ -49,18 +49,18 @@ func (b *Buffer) InsertText(text string, overwrite bool, moveCursor bool) { if overwrite { overwritten := string(currentTextRunes[cursor:]) if len(overwritten) >= int(cursor)+len(text) { - overwritten = string(currentTextRunes[cursor : cursor+istrings.RuneLen(text)]) + overwritten = string(currentTextRunes[cursor : cursor+istrings.RuneCount(text)]) } if i := strings.IndexAny(overwritten, "\n"); i != -1 { overwritten = overwritten[:i] } - b.setText(string(currentTextRunes[:cursor]) + text + string(currentTextRunes[cursor+istrings.RuneLen(overwritten):])) + b.setText(string(currentTextRunes[:cursor]) + text + string(currentTextRunes[cursor+istrings.RuneCount(overwritten):])) } else { b.setText(string(currentTextRunes[:cursor]) + text + string(currentTextRunes[cursor:])) } if moveCursor { - b.cursorPosition += istrings.RuneLen(text) + b.cursorPosition += istrings.RuneCount(text) } } @@ -68,12 +68,12 @@ func (b *Buffer) InsertText(text string, overwrite bool, moveCursor 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) { - debug.Assert(b.cursorPosition <= istrings.RuneLen(text), "length of input should be shorter than cursor position") + debug.Assert(b.cursorPosition <= istrings.RuneCount(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 istrings.RuneIndex) { +func (b *Buffer) setCursorPosition(p istrings.RuneNumber) { 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 istrings.RuneCount) { +func (b *Buffer) CursorLeft(count istrings.RuneNumber) { l := b.Document().GetCursorLeftPosition(count) b.cursorPosition += l } // CursorRight move to right on the current line. -func (b *Buffer) CursorRight(count istrings.RuneCount) { +func (b *Buffer) CursorRight(count istrings.RuneNumber) { 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 istrings.RuneCount) (deleted string) { +func (b *Buffer) DeleteBeforeCursor(count istrings.RuneNumber) (deleted string) { debug.Assert(count >= 0, "count should be positive") r := []rune(b.Text()) @@ -126,7 +126,7 @@ func (b *Buffer) DeleteBeforeCursor(count istrings.RuneCount) (deleted string) { deleted = string(r[start:b.cursorPosition]) b.setDocument(&Document{ Text: string(r[:start]) + string(r[b.cursorPosition:]), - cursorPosition: b.cursorPosition - istrings.RuneIndex(len([]rune(deleted))), + cursorPosition: b.cursorPosition - istrings.RuneNumber(len([]rune(deleted))), }) } return @@ -142,13 +142,13 @@ func (b *Buffer) NewLine(copyMargin bool) { } // Delete specified number of characters and Return the deleted text. -func (b *Buffer) Delete(count istrings.RuneCount) string { +func (b *Buffer) Delete(count istrings.RuneNumber) string { r := []rune(b.Text()) - if b.cursorPosition < istrings.RuneIndex(len(r)) { + if b.cursorPosition < istrings.RuneNumber(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)):])) + b.setText(string(r[:b.cursorPosition]) + string(r[b.cursorPosition+istrings.RuneNumber(len(deletedRunes)):])) deleted := string(deletedRunes) return deleted diff --git a/buffer_test.go b/buffer_test.go index 86878749..644a5be7 100644 --- a/buffer_test.go +++ b/buffer_test.go @@ -4,7 +4,7 @@ import ( "reflect" "testing" - istrings "github.com/elk-language/go-prompt/internal/strings" + istrings "github.com/elk-language/go-prompt/strings" ) func TestNewBuffer(t *testing.T) { @@ -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.RuneLen("some_text") { - t.Errorf("cursorPosition should be %#v, got %#v", istrings.RuneLen("some_text"), b.cursorPosition) + if b.cursorPosition != istrings.RuneCount("some_text") { + t.Errorf("cursorPosition should be %#v, got %#v", istrings.RuneCount("some_text"), b.cursorPosition) } } @@ -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.RuneLen("ABC") { - t.Errorf("cursorPosition should be %#v, got %#v", istrings.RuneLen("ABC"), b.cursorPosition) + if b.cursorPosition != istrings.RuneCount("ABC") { + t.Errorf("cursorPosition should be %#v, got %#v", istrings.RuneCount("ABC"), b.cursorPosition) } b.CursorLeft(1) @@ -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.RuneLen("some_teA") { - t.Errorf("Text should be %#v, got %#v", istrings.RuneLen("some_teA"), b.cursorPosition) + if b.cursorPosition != istrings.RuneCount("some_teA") { + t.Errorf("Text should be %#v, got %#v", istrings.RuneCount("some_teA"), b.cursorPosition) } // Moving over left character counts. @@ -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.RuneLen("A") { - t.Errorf("Text should be %#v, got %#v", istrings.RuneLen("some_teA"), b.cursorPosition) + if b.cursorPosition != istrings.RuneCount("A") { + t.Errorf("Text should be %#v, got %#v", istrings.RuneCount("some_teA"), b.cursorPosition) } // TODO: Going right already at right end. @@ -147,8 +147,8 @@ func TestBuffer_CursorDown(t *testing.T) { // Normally going down b.CursorDown(1) - if b.Document().cursorPosition != istrings.RuneLen("line1\nlin") { - t.Errorf("Should be %#v, got %#v", istrings.RuneLen("line1\nlin"), b.Document().cursorPosition) + if b.Document().cursorPosition != istrings.RuneCount("line1\nlin") { + t.Errorf("Should be %#v, got %#v", istrings.RuneCount("line1\nlin"), b.Document().cursorPosition) } // Going down to a line that's storter. @@ -156,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 != istrings.RuneLen("long line1\na") { - t.Errorf("Should be %#v, got %#v", istrings.RuneLen("long line1\na"), b.Document().cursorPosition) + if b.Document().cursorPosition != istrings.RuneCount("long line1\na") { + t.Errorf("Should be %#v, got %#v", istrings.RuneCount("long line1\na"), b.Document().cursorPosition) } } @@ -173,8 +173,8 @@ func TestBuffer_DeleteBeforeCursor(t *testing.T) { if deleted != "e" { t.Errorf("Should be %#v, got %#v", deleted, "e") } - if b.cursorPosition != istrings.RuneLen("some_t") { - t.Errorf("Should be %#v, got %#v", istrings.RuneLen("some_t"), b.cursorPosition) + if b.cursorPosition != istrings.RuneCount("some_t") { + t.Errorf("Should be %#v, got %#v", istrings.RuneCount("some_t"), b.cursorPosition) } // Delete over the characters length before cursor. diff --git a/completer/file.go b/completer/file.go index 2d5b3959..dd8f4434 100644 --- a/completer/file.go +++ b/completer/file.go @@ -8,7 +8,7 @@ import ( "runtime" prompt "github.com/elk-language/go-prompt" - "github.com/elk-language/go-prompt/internal/debug" + "github.com/elk-language/go-prompt/debug" ) var ( diff --git a/completion.go b/completion.go index 5589689a..1ee6713c 100644 --- a/completion.go +++ b/completion.go @@ -3,8 +3,8 @@ package prompt import ( "strings" - "github.com/elk-language/go-prompt/internal/debug" - istrings "github.com/elk-language/go-prompt/internal/strings" + "github.com/elk-language/go-prompt/debug" + istrings "github.com/elk-language/go-prompt/strings" runewidth "github.com/mattn/go-runewidth" ) diff --git a/completion_test.go b/completion_test.go index 6746fafe..c2d13446 100644 --- a/completion_test.go +++ b/completion_test.go @@ -4,7 +4,7 @@ import ( "reflect" "testing" - istrings "github.com/elk-language/go-prompt/internal/strings" + istrings "github.com/elk-language/go-prompt/strings" ) func TestFormatShortSuggestion(t *testing.T) { diff --git a/internal/debug/assert.go b/debug/assert.go similarity index 100% rename from internal/debug/assert.go rename to debug/assert.go diff --git a/internal/debug/log.go b/debug/log.go similarity index 100% rename from internal/debug/log.go rename to debug/log.go diff --git a/document.go b/document.go index e316e9f9..642c098c 100644 --- a/document.go +++ b/document.go @@ -5,8 +5,8 @@ import ( "unicode" "unicode/utf8" - "github.com/elk-language/go-prompt/internal/bisect" - istrings "github.com/elk-language/go-prompt/internal/strings" + "github.com/elk-language/go-prompt/bisect" + istrings "github.com/elk-language/go-prompt/strings" "golang.org/x/exp/utf8string" ) @@ -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 istrings.RuneIndex + cursorPosition istrings.RuneNumber lastKey Key } @@ -41,14 +41,14 @@ func (d *Document) DisplayCursorPosition(columns istrings.StringWidth) Position } // GetCharRelativeToCursor return character relative to cursor position, or empty string -func (d *Document) GetCharRelativeToCursor(offset istrings.RuneCount) (r rune) { +func (d *Document) GetCharRelativeToCursor(offset istrings.RuneNumber) (r rune) { s := d.Text - var cnt istrings.RuneIndex + var cnt istrings.RuneNumber for len(s) > 0 { cnt++ r, size := utf8.DecodeRuneInString(s) - if cnt == d.cursorPosition+istrings.RuneIndex(offset) { + if cnt == d.cursorPosition+istrings.RuneNumber(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() istrings.ByteIndex { +func (d *Document) FindStartOfPreviousWord() istrings.ByteNumber { x := d.TextBeforeCursor() - i := istrings.ByteIndex(strings.LastIndexAny(x, " \n")) + i := istrings.ByteNumber(strings.LastIndexAny(x, " \n")) if i != -1 { return i + 1 } @@ -135,21 +135,21 @@ func (d *Document) FindStartOfPreviousWord() istrings.ByteIndex { // Returns the rune count // of the text before the cursor until the start of the previous word. -func (d *Document) FindRuneCountUntilStartOfPreviousWord() istrings.RuneCount { +func (d *Document) FindRuneNumberUntilStartOfPreviousWord() istrings.RuneNumber { x := d.TextBeforeCursor() - return istrings.RuneLen(x[d.FindStartOfPreviousWordWithSpace():]) + return istrings.RuneCount(x[d.FindStartOfPreviousWordWithSpace():]) } // FindStartOfPreviousWordWithSpace is almost the same as FindStartOfPreviousWord. // The only difference is to ignore contiguous spaces. -func (d *Document) FindStartOfPreviousWordWithSpace() istrings.ByteIndex { +func (d *Document) FindStartOfPreviousWordWithSpace() istrings.ByteNumber { x := d.TextBeforeCursor() end := istrings.LastIndexNotByte(x, ' ') if end == -1 { return 0 } - start := istrings.ByteIndex(strings.LastIndexByte(x[:end], ' ')) + start := istrings.ByteNumber(strings.LastIndexByte(x[:end], ' ')) if start == -1 { return 0 } @@ -158,13 +158,13 @@ func (d *Document) FindStartOfPreviousWordWithSpace() istrings.ByteIndex { // FindStartOfPreviousWordUntilSeparator is almost the same as FindStartOfPreviousWord. // But this can specify Separator. Return 0 if nothing was found. -func (d *Document) FindStartOfPreviousWordUntilSeparator(sep string) istrings.ByteIndex { +func (d *Document) FindStartOfPreviousWordUntilSeparator(sep string) istrings.ByteNumber { if sep == "" { return d.FindStartOfPreviousWord() } x := d.TextBeforeCursor() - i := istrings.ByteIndex(strings.LastIndexAny(x, sep)) + i := istrings.ByteNumber(strings.LastIndexAny(x, sep)) if i != -1 { return i + 1 } @@ -173,7 +173,7 @@ func (d *Document) FindStartOfPreviousWordUntilSeparator(sep string) istrings.By // FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor is almost the same as FindStartOfPreviousWordWithSpace. // But this can specify Separator. Return 0 if nothing was found. -func (d *Document) FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor(sep string) istrings.ByteIndex { +func (d *Document) FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor(sep string) istrings.ByteNumber { if sep == "" { return d.FindStartOfPreviousWordWithSpace() } @@ -183,7 +183,7 @@ func (d *Document) FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor(sep s if end == -1 { return 0 } - start := istrings.ByteIndex(strings.LastIndexAny(x[:end], sep)) + start := istrings.ByteNumber(strings.LastIndexAny(x[:end], sep)) if start == -1 { return 0 } @@ -192,28 +192,28 @@ func (d *Document) FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor(sep s // 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() istrings.ByteIndex { +func (d *Document) FindEndOfCurrentWord() istrings.ByteNumber { x := d.TextAfterCursor() - i := istrings.ByteIndex(strings.IndexByte(x, ' ')) + i := istrings.ByteNumber(strings.IndexByte(x, ' ')) if i != -1 { return i } - return istrings.ByteIndex(len(x)) + return istrings.ByteNumber(len(x)) } // FindEndOfCurrentWordWithSpace is almost the same as FindEndOfCurrentWord. // The only difference is to ignore contiguous spaces. -func (d *Document) FindEndOfCurrentWordWithSpace() istrings.ByteIndex { +func (d *Document) FindEndOfCurrentWordWithSpace() istrings.ByteNumber { x := d.TextAfterCursor() start := istrings.IndexNotByte(x, ' ') if start == -1 { - return istrings.ByteIndex(len(x)) + return istrings.ByteNumber(len(x)) } - end := istrings.ByteIndex(strings.IndexByte(x[start:], ' ')) + end := istrings.ByteNumber(strings.IndexByte(x[start:], ' ')) if end == -1 { - return istrings.ByteIndex(len(x)) + return istrings.ByteNumber(len(x)) } return start + end @@ -221,9 +221,9 @@ func (d *Document) FindEndOfCurrentWordWithSpace() istrings.ByteIndex { // Returns the number of runes // of the text after the cursor until the end of the current word. -func (d *Document) FindRuneCountUntilEndOfCurrentWord() istrings.RuneCount { +func (d *Document) FindRuneNumberUntilEndOfCurrentWord() istrings.RuneNumber { t := d.TextAfterCursor() - var count istrings.RuneCount + var count istrings.RuneNumber nonSpaceCharSeen := false for _, char := range t { if !nonSpaceCharSeen && char == ' ' { @@ -244,22 +244,22 @@ func (d *Document) FindRuneCountUntilEndOfCurrentWord() istrings.RuneCount { // FindEndOfCurrentWordUntilSeparator is almost the same as FindEndOfCurrentWord. // But this can specify Separator. Return 0 if nothing was found. -func (d *Document) FindEndOfCurrentWordUntilSeparator(sep string) istrings.ByteIndex { +func (d *Document) FindEndOfCurrentWordUntilSeparator(sep string) istrings.ByteNumber { if sep == "" { return d.FindEndOfCurrentWord() } x := d.TextAfterCursor() - i := istrings.ByteIndex(strings.IndexAny(x, sep)) + i := istrings.ByteNumber(strings.IndexAny(x, sep)) if i != -1 { return i } - return istrings.ByteIndex(len(x)) + return istrings.ByteNumber(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) istrings.ByteIndex { +func (d *Document) FindEndOfCurrentWordUntilSeparatorIgnoreNextToCursor(sep string) istrings.ByteNumber { if sep == "" { return d.FindEndOfCurrentWordWithSpace() } @@ -268,12 +268,12 @@ func (d *Document) FindEndOfCurrentWordUntilSeparatorIgnoreNextToCursor(sep stri start := istrings.IndexNotAny(x, sep) if start == -1 { - return istrings.ByteIndex(len(x)) + return istrings.ByteNumber(len(x)) } - end := istrings.ByteIndex(strings.IndexAny(x[start:], sep)) + end := istrings.ByteNumber(strings.IndexAny(x[start:], sep)) if end == -1 { - return istrings.ByteIndex(len(x)) + return istrings.ByteNumber(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) lineStartIndices() []istrings.RuneIndex { +func (d *Document) lineStartIndices() []istrings.RuneNumber { // 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([]istrings.RuneCount, lc) + lengths := make([]istrings.RuneNumber, lc) for i, l := range d.Lines() { - lengths[i] = istrings.RuneCount(len([]rune(l))) + lengths[i] = istrings.RuneNumber(len([]rune(l))) } // Calculate cumulative sums. - indices := make([]istrings.RuneIndex, lc+1) + indices := make([]istrings.RuneNumber, lc+1) indices[0] = 0 // https://github.com/jonathanslenders/python-prompt-toolkit/blob/master/prompt_toolkit/document.py#L189 - var pos istrings.RuneCount + var pos istrings.RuneNumber for i, l := range lengths { pos += l + 1 - indices[i+1] = istrings.RuneIndex(pos) + indices[i+1] = istrings.RuneNumber(pos) } if lc > 1 { // Pop the last item. (This is not a new line.) @@ -324,7 +324,7 @@ func (d *Document) lineStartIndices() []istrings.RuneIndex { // 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 istrings.RuneIndex) (pos, lineStartIndex istrings.RuneIndex) { +func (d *Document) findLineStartIndex(index istrings.RuneNumber) (pos, lineStartIndex istrings.RuneNumber) { indices := d.lineStartIndices() pos = bisect.Right(indices, index) - 1 lineStartIndex = indices[pos] @@ -332,25 +332,25 @@ func (d *Document) findLineStartIndex(index istrings.RuneIndex) (pos, lineStartI } // CursorPositionRow returns the current row. (0-based.) -func (d *Document) CursorPositionRow() (row istrings.RuneIndex) { +func (d *Document) CursorPositionRow() (row istrings.RuneNumber) { row, _ = d.findLineStartIndex(d.cursorPosition) return } // CursorPositionCol returns the current column. (0-based.) -func (d *Document) CursorPositionCol() (col istrings.RuneIndex) { +func (d *Document) CursorPositionCol() (col istrings.RuneNumber) { _, index := d.findLineStartIndex(d.cursorPosition) col = d.cursorPosition - index return } // GetCursorLeftPosition returns the relative position for cursor left. -func (d *Document) GetCursorLeftPosition(count istrings.RuneCount) istrings.RuneCount { +func (d *Document) GetCursorLeftPosition(count istrings.RuneNumber) istrings.RuneNumber { if count < 0 { return d.GetCursorRightPosition(-count) } runeSlice := []rune(d.Text) - var counter istrings.RuneCount + var counter istrings.RuneNumber targetPosition := d.cursorPosition - count if targetPosition < 0 { targetPosition = 0 @@ -363,15 +363,15 @@ func (d *Document) GetCursorLeftPosition(count istrings.RuneCount) istrings.Rune } // GetCursorRightPosition returns relative position for cursor right. -func (d *Document) GetCursorRightPosition(count istrings.RuneCount) istrings.RuneCount { +func (d *Document) GetCursorRightPosition(count istrings.RuneNumber) istrings.RuneNumber { if count < 0 { return d.GetCursorLeftPosition(-count) } runeSlice := []rune(d.Text) - var counter istrings.RuneCount + var counter istrings.RuneNumber targetPosition := d.cursorPosition + count - if targetPosition > istrings.RuneCount(len(runeSlice)) { - targetPosition = istrings.RuneCount(len(runeSlice)) + if targetPosition > istrings.RuneNumber(len(runeSlice)) { + targetPosition = istrings.RuneNumber(len(runeSlice)) } for range runeSlice[d.cursorPosition:targetPosition] { counter++ @@ -392,8 +392,8 @@ func (d *Document) GetEndOfTextPosition(columns istrings.StringWidth) Position { // 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 istrings.RuneIndex) istrings.RuneIndex { - var col istrings.RuneIndex +func (d *Document) GetCursorUpPosition(count int, preferredColumn istrings.RuneNumber) istrings.RuneNumber { + var col istrings.RuneNumber if preferredColumn == -1 { // -1 means nil col = d.CursorPositionCol() } else { @@ -409,8 +409,8 @@ func (d *Document) GetCursorUpPosition(count int, preferredColumn istrings.RuneI // 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 istrings.RuneIndex) istrings.RuneIndex { - var col istrings.RuneIndex +func (d *Document) GetCursorDownPosition(count int, preferredColumn istrings.RuneNumber) istrings.RuneNumber { + var col istrings.RuneNumber if preferredColumn == -1 { // -1 means nil col = d.CursorPositionCol() } else { @@ -434,7 +434,7 @@ 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 istrings.RuneIndex) (int, int) { +func (d *Document) TranslateIndexToPosition(index istrings.RuneNumber) (int, int) { r, rowIndex := d.findLineStartIndex(index) c := index - rowIndex return int(r), int(c) @@ -442,7 +442,7 @@ func (d *Document) TranslateIndexToPosition(index istrings.RuneIndex) (int, int) // TranslateRowColToIndex given a (row, col), return the corresponding index. // (Row and col params are 0-based.) -func (d *Document) TranslateRowColToIndex(row int, column istrings.RuneIndex) (index istrings.RuneIndex) { +func (d *Document) TranslateRowColToIndex(row int, column istrings.RuneNumber) (index istrings.RuneNumber) { indices := d.lineStartIndices() if row < 0 { row = 0 @@ -454,10 +454,10 @@ func (d *Document) TranslateRowColToIndex(row int, column istrings.RuneIndex) (i // python) result += max(0, min(col, len(line))) if column > 0 || len(line) > 0 { - if column > istrings.RuneIndex(len(line)) { - index += istrings.RuneCount(len(line)) + if column > istrings.RuneNumber(len(line)) { + index += istrings.RuneNumber(len(line)) } else { - index += istrings.RuneIndex(column) + index += istrings.RuneNumber(column) } } @@ -465,8 +465,8 @@ func (d *Document) TranslateRowColToIndex(row int, column istrings.RuneIndex) (i // 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 > istrings.RuneCount(len(text)) { - index = istrings.RuneIndex(len(text)) + if index > istrings.RuneNumber(len(text)) { + index = istrings.RuneNumber(len(text)) } if index < 0 { index = 0 @@ -476,23 +476,23 @@ func (d *Document) TranslateRowColToIndex(row int, column istrings.RuneIndex) (i // OnLastLine returns true when we are at the last line. func (d *Document) OnLastLine() bool { - return d.CursorPositionRow() == istrings.RuneIndex(d.LineCount()-1) + return d.CursorPositionRow() == istrings.RuneNumber(d.LineCount()-1) } // GetEndOfLinePosition returns relative position for the end of this line. -func (d *Document) GetEndOfLinePosition() istrings.RuneCount { - return istrings.RuneLen(d.CurrentLineAfterCursor()) +func (d *Document) GetEndOfLinePosition() istrings.RuneNumber { + return istrings.RuneCount(d.CurrentLineAfterCursor()) } // GetStartOfLinePosition returns relative position for the start of this line. -func (d *Document) GetStartOfLinePosition() istrings.RuneCount { - return istrings.RuneLen(d.CurrentLineBeforeCursor()) +func (d *Document) GetStartOfLinePosition() istrings.RuneNumber { + return istrings.RuneCount(d.CurrentLineBeforeCursor()) } // GetStartOfLinePosition returns relative position for the start of this line. -func (d *Document) FindStartOfFirstWordOfLine() istrings.RuneCount { +func (d *Document) FindStartOfFirstWordOfLine() istrings.RuneNumber { line := d.CurrentLineBeforeCursor() - var counter istrings.RuneCount + var counter istrings.RuneNumber var nonSpaceCharSeen bool for _, char := range line { if !nonSpaceCharSeen && unicode.IsSpace(char) { @@ -506,7 +506,7 @@ func (d *Document) FindStartOfFirstWordOfLine() istrings.RuneCount { } if counter == 0 { - return istrings.RuneLen(line) + return istrings.RuneCount(line) } return counter diff --git a/document_test.go b/document_test.go index dbe9d59d..56648d48 100644 --- a/document_test.go +++ b/document_test.go @@ -6,7 +6,7 @@ import ( "testing" "unicode/utf8" - istrings "github.com/elk-language/go-prompt/internal/strings" + istrings "github.com/elk-language/go-prompt/strings" ) func ExampleDocument_CurrentLine() { @@ -15,7 +15,7 @@ func ExampleDocument_CurrentLine() { This is a example of Document component. This component has texts displayed in terminal and cursor position. `, - cursorPosition: istrings.RuneLen(`Hello! my name is c-bata. + cursorPosition: istrings.RuneCount(`Hello! my name is c-bata. This is a exam`), } fmt.Println(d.CurrentLine()) @@ -26,7 +26,7 @@ This is a exam`), func ExampleDocument_DisplayCursorPosition() { d := &Document{ Text: `Hello! my name is c-bata.`, - cursorPosition: istrings.RuneLen(`Hello`), + cursorPosition: istrings.RuneCount(`Hello`), } fmt.Println("DisplayCursorPosition", d.DisplayCursorPosition(50)) // Output: @@ -39,7 +39,7 @@ func ExampleDocument_CursorPositionRow() { This is a example of Document component. This component has texts displayed in terminal and cursor position. `, - cursorPosition: istrings.RuneLen(`Hello! my name is c-bata. + cursorPosition: istrings.RuneCount(`Hello! my name is c-bata. This is a exam`), } fmt.Println("CursorPositionRow", d.CursorPositionRow()) @@ -53,7 +53,7 @@ func ExampleDocument_CursorPositionCol() { This is a example of Document component. This component has texts displayed in terminal and cursor position. `, - cursorPosition: istrings.RuneLen(`Hello! my name is c-bata. + cursorPosition: istrings.RuneCount(`Hello! my name is c-bata. This is a exam`), } fmt.Println("CursorPositionCol", d.CursorPositionCol()) @@ -67,7 +67,7 @@ func ExampleDocument_TextBeforeCursor() { This is a example of Document component. This component has texts displayed in terminal and cursor position. `, - cursorPosition: istrings.RuneLen(`Hello! my name is c-bata. + cursorPosition: istrings.RuneCount(`Hello! my name is c-bata. This is a exam`), } fmt.Println(d.TextBeforeCursor()) @@ -82,7 +82,7 @@ func ExampleDocument_TextAfterCursor() { This is a example of Document component. This component has texts displayed in terminal and cursor position. `, - cursorPosition: istrings.RuneLen(`Hello! my name is c-bata. + cursorPosition: istrings.RuneCount(`Hello! my name is c-bata. This is a exam`), } fmt.Println(d.TextAfterCursor()) @@ -107,7 +107,7 @@ func ExampleDocument_CurrentLineBeforeCursor() { This is a example of Document component. This component has texts displayed in terminal and cursor position. `, - cursorPosition: istrings.RuneLen(`Hello! my name is c-bata. + cursorPosition: istrings.RuneCount(`Hello! my name is c-bata. This is a exam`), } fmt.Println(d.CurrentLineBeforeCursor()) @@ -121,7 +121,7 @@ func ExampleDocument_CurrentLineAfterCursor() { This is a example of Document component. This component has texts displayed in terminal and cursor position. `, - cursorPosition: istrings.RuneLen(`Hello! my name is c-bata. + cursorPosition: istrings.RuneCount(`Hello! my name is c-bata. This is a exam`), } fmt.Println(d.CurrentLineAfterCursor()) @@ -134,7 +134,7 @@ func ExampleDocument_GetWordBeforeCursor() { Text: `Hello! my name is c-bata. This is a example of Document component. `, - cursorPosition: istrings.RuneLen(`Hello! my name is c-bata. + cursorPosition: istrings.RuneCount(`Hello! my name is c-bata. This is a exam`), } fmt.Println(d.GetWordBeforeCursor()) @@ -147,7 +147,7 @@ func ExampleDocument_GetWordAfterCursor() { Text: `Hello! my name is c-bata. This is a example of Document component. `, - cursorPosition: istrings.RuneLen(`Hello! my name is c-bata. + cursorPosition: istrings.RuneCount(`Hello! my name is c-bata. This is a exam`), } fmt.Println(d.GetWordAfterCursor()) @@ -160,7 +160,7 @@ func ExampleDocument_GetWordBeforeCursorWithSpace() { Text: `Hello! my name is c-bata. This is a example of Document component. `, - cursorPosition: istrings.RuneLen(`Hello! my name is c-bata. + cursorPosition: istrings.RuneCount(`Hello! my name is c-bata. This is a example `), } fmt.Println(d.GetWordBeforeCursorWithSpace()) @@ -173,7 +173,7 @@ func ExampleDocument_GetWordAfterCursorWithSpace() { Text: `Hello! my name is c-bata. This is a example of Document component. `, - cursorPosition: istrings.RuneLen(`Hello! my name is c-bata. + cursorPosition: istrings.RuneCount(`Hello! my name is c-bata. This is a`), } fmt.Println(d.GetWordAfterCursorWithSpace()) @@ -184,7 +184,7 @@ This is a`), func ExampleDocument_GetWordBeforeCursorUntilSeparator() { d := &Document{ Text: `hello,i am c-bata`, - cursorPosition: istrings.RuneLen(`hello,i am c`), + cursorPosition: istrings.RuneCount(`hello,i am c`), } fmt.Println(d.GetWordBeforeCursorUntilSeparator(",")) // Output: @@ -194,7 +194,7 @@ func ExampleDocument_GetWordBeforeCursorUntilSeparator() { func ExampleDocument_GetWordAfterCursorUntilSeparator() { d := &Document{ Text: `hello,i am c-bata,thank you for using go-prompt`, - cursorPosition: istrings.RuneLen(`hello,i a`), + cursorPosition: istrings.RuneCount(`hello,i a`), } fmt.Println(d.GetWordAfterCursorUntilSeparator(",")) // Output: @@ -204,7 +204,7 @@ func ExampleDocument_GetWordAfterCursorUntilSeparator() { func ExampleDocument_GetWordBeforeCursorUntilSeparatorIgnoreNextToCursor() { d := &Document{ Text: `hello,i am c-bata,thank you for using go-prompt`, - cursorPosition: istrings.RuneLen(`hello,i am c-bata,`), + cursorPosition: istrings.RuneCount(`hello,i am c-bata,`), } fmt.Println(d.GetWordBeforeCursorUntilSeparatorIgnoreNextToCursor(",")) // Output: @@ -214,7 +214,7 @@ func ExampleDocument_GetWordBeforeCursorUntilSeparatorIgnoreNextToCursor() { func ExampleDocument_GetWordAfterCursorUntilSeparatorIgnoreNextToCursor() { d := &Document{ Text: `hello,i am c-bata,thank you for using go-prompt`, - cursorPosition: istrings.RuneLen(`hello`), + cursorPosition: istrings.RuneCount(`hello`), } fmt.Println(d.GetWordAfterCursorUntilSeparatorIgnoreNextToCursor(",")) // Output: @@ -269,7 +269,7 @@ func TestDocument_GetCharRelativeToCursor(t *testing.T) { { document: &Document{ Text: "line 1\nline 2\nline 3\nline 4\n", - cursorPosition: istrings.RuneLen("line 1\n" + "lin"), + cursorPosition: istrings.RuneCount("line 1\n" + "lin"), }, expected: "e", }, @@ -306,7 +306,7 @@ func TestDocument_TextBeforeCursor(t *testing.T) { { document: &Document{ Text: "line 1\nline 2\nline 3\nline 4\n", - cursorPosition: istrings.RuneLen("line 1\n" + "lin"), + cursorPosition: istrings.RuneCount("line 1\n" + "lin"), }, expected: "line 1\nlin", }, @@ -341,7 +341,7 @@ func TestDocument_TextAfterCursor(t *testing.T) { { document: &Document{ Text: "line 1\nline 2\nline 3\nline 4\n", - cursorPosition: istrings.RuneLen("line 1\n" + "lin"), + cursorPosition: istrings.RuneCount("line 1\n" + "lin"), }, expected: "e 2\nline 3\nline 4\n", }, @@ -385,14 +385,14 @@ func TestDocument_GetWordBeforeCursor(t *testing.T) { { document: &Document{ Text: "apple bana", - cursorPosition: istrings.RuneLen("apple bana"), + cursorPosition: istrings.RuneCount("apple bana"), }, expected: "bana", }, { document: &Document{ Text: "apply -f ./file/foo.json", - cursorPosition: istrings.RuneLen("apply -f ./file/foo.json"), + cursorPosition: istrings.RuneCount("apply -f ./file/foo.json"), }, expected: "foo.json", sep: " /", @@ -400,14 +400,14 @@ func TestDocument_GetWordBeforeCursor(t *testing.T) { { document: &Document{ Text: "apple banana orange", - cursorPosition: istrings.RuneLen("apple ba"), + cursorPosition: istrings.RuneCount("apple ba"), }, expected: "ba", }, { document: &Document{ Text: "apply -f ./file/foo.json", - cursorPosition: istrings.RuneLen("apply -f ./fi"), + cursorPosition: istrings.RuneCount("apply -f ./fi"), }, expected: "fi", sep: " /", @@ -415,7 +415,7 @@ func TestDocument_GetWordBeforeCursor(t *testing.T) { { document: &Document{ Text: "apple ", - cursorPosition: istrings.RuneLen("apple "), + cursorPosition: istrings.RuneCount("apple "), }, expected: "", }, @@ -463,14 +463,14 @@ func TestDocument_GetWordBeforeCursorWithSpace(t *testing.T) { { document: &Document{ Text: "apple bana ", - cursorPosition: istrings.RuneLen("apple bana "), + cursorPosition: istrings.RuneCount("apple bana "), }, expected: "bana ", }, { document: &Document{ Text: "apply -f /path/to/file/", - cursorPosition: istrings.RuneLen("apply -f /path/to/file/"), + cursorPosition: istrings.RuneCount("apply -f /path/to/file/"), }, expected: "file/", sep: " /", @@ -478,14 +478,14 @@ func TestDocument_GetWordBeforeCursorWithSpace(t *testing.T) { { document: &Document{ Text: "apple ", - cursorPosition: istrings.RuneLen("apple "), + cursorPosition: istrings.RuneCount("apple "), }, expected: "apple ", }, { document: &Document{ Text: "path/", - cursorPosition: istrings.RuneLen("path/"), + cursorPosition: istrings.RuneCount("path/"), }, expected: "path/", sep: " /", @@ -528,20 +528,20 @@ func TestDocument_GetWordBeforeCursorWithSpace(t *testing.T) { func TestDocument_FindStartOfPreviousWord(t *testing.T) { pattern := []struct { document *Document - expected istrings.ByteIndex + expected istrings.ByteNumber sep string }{ { document: &Document{ Text: "apple bana", - cursorPosition: istrings.RuneLen("apple bana"), + cursorPosition: istrings.RuneCount("apple bana"), }, expected: istrings.Len("apple "), }, { document: &Document{ Text: "apply -f ./file/foo.json", - cursorPosition: istrings.RuneLen("apply -f ./file/foo.json"), + cursorPosition: istrings.RuneCount("apply -f ./file/foo.json"), }, expected: istrings.Len("apply -f ./file/"), sep: " /", @@ -549,14 +549,14 @@ func TestDocument_FindStartOfPreviousWord(t *testing.T) { { document: &Document{ Text: "apple ", - cursorPosition: istrings.RuneLen("apple "), + cursorPosition: istrings.RuneCount("apple "), }, expected: istrings.Len("apple "), }, { document: &Document{ Text: "apply -f ./file/foo.json", - cursorPosition: istrings.RuneLen("apply -f ./"), + cursorPosition: istrings.RuneCount("apply -f ./"), }, expected: istrings.Len("apply -f ./"), sep: " /", @@ -599,20 +599,20 @@ func TestDocument_FindStartOfPreviousWord(t *testing.T) { func TestDocument_FindStartOfPreviousWordWithSpace(t *testing.T) { pattern := []struct { document *Document - expected istrings.ByteIndex + expected istrings.ByteNumber sep string }{ { document: &Document{ Text: "apple bana ", - cursorPosition: istrings.RuneLen("apple bana "), + cursorPosition: istrings.RuneCount("apple bana "), }, expected: istrings.Len("apple "), }, { document: &Document{ Text: "apply -f /file/foo/", - cursorPosition: istrings.RuneLen("apply -f /file/foo/"), + cursorPosition: istrings.RuneCount("apply -f /file/foo/"), }, expected: istrings.Len("apply -f /file/"), sep: " /", @@ -620,14 +620,14 @@ func TestDocument_FindStartOfPreviousWordWithSpace(t *testing.T) { { document: &Document{ Text: "apple ", - cursorPosition: istrings.RuneLen("apple "), + cursorPosition: istrings.RuneCount("apple "), }, expected: istrings.Len(""), }, { document: &Document{ Text: "file/", - cursorPosition: istrings.RuneLen("file/"), + cursorPosition: istrings.RuneCount("file/"), }, expected: istrings.Len(""), sep: " /", @@ -676,14 +676,14 @@ func TestDocument_GetWordAfterCursor(t *testing.T) { { document: &Document{ Text: "apple bana", - cursorPosition: istrings.RuneLen("apple bana"), + cursorPosition: istrings.RuneCount("apple bana"), }, expected: "", }, { document: &Document{ Text: "apply -f ./file/foo.json", - cursorPosition: istrings.RuneLen("apply -f ./fi"), + cursorPosition: istrings.RuneCount("apply -f ./fi"), }, expected: "le", sep: " /", @@ -691,21 +691,21 @@ func TestDocument_GetWordAfterCursor(t *testing.T) { { document: &Document{ Text: "apple bana", - cursorPosition: istrings.RuneLen("apple "), + cursorPosition: istrings.RuneCount("apple "), }, expected: "bana", }, { document: &Document{ Text: "apple bana", - cursorPosition: istrings.RuneLen("apple"), + cursorPosition: istrings.RuneCount("apple"), }, expected: "", }, { document: &Document{ Text: "apply -f ./file/foo.json", - cursorPosition: istrings.RuneLen("apply -f ."), + cursorPosition: istrings.RuneCount("apply -f ."), }, expected: "", sep: " /", @@ -713,7 +713,7 @@ func TestDocument_GetWordAfterCursor(t *testing.T) { { document: &Document{ Text: "apple bana", - cursorPosition: istrings.RuneLen("ap"), + cursorPosition: istrings.RuneCount("ap"), }, expected: "ple", }, @@ -761,21 +761,21 @@ func TestDocument_GetWordAfterCursorWithSpace(t *testing.T) { { document: &Document{ Text: "apple bana", - cursorPosition: istrings.RuneLen("apple bana"), + cursorPosition: istrings.RuneCount("apple bana"), }, expected: "", }, { document: &Document{ Text: "apple bana", - cursorPosition: istrings.RuneLen("apple "), + cursorPosition: istrings.RuneCount("apple "), }, expected: "bana", }, { document: &Document{ Text: "/path/to", - cursorPosition: istrings.RuneLen("/path/"), + cursorPosition: istrings.RuneCount("/path/"), }, expected: "to", sep: " /", @@ -783,7 +783,7 @@ func TestDocument_GetWordAfterCursorWithSpace(t *testing.T) { { document: &Document{ Text: "/path/to/file", - cursorPosition: istrings.RuneLen("/path/"), + cursorPosition: istrings.RuneCount("/path/"), }, expected: "to", sep: " /", @@ -791,14 +791,14 @@ func TestDocument_GetWordAfterCursorWithSpace(t *testing.T) { { document: &Document{ Text: "apple bana", - cursorPosition: istrings.RuneLen("apple"), + cursorPosition: istrings.RuneCount("apple"), }, expected: " bana", }, { document: &Document{ Text: "path/to", - cursorPosition: istrings.RuneLen("path"), + cursorPosition: istrings.RuneCount("path"), }, expected: "/to", sep: " /", @@ -806,7 +806,7 @@ func TestDocument_GetWordAfterCursorWithSpace(t *testing.T) { { document: &Document{ Text: "apple bana", - cursorPosition: istrings.RuneLen("ap"), + cursorPosition: istrings.RuneCount("ap"), }, expected: "ple", }, @@ -848,27 +848,27 @@ func TestDocument_GetWordAfterCursorWithSpace(t *testing.T) { func TestDocument_FindEndOfCurrentWord(t *testing.T) { pattern := []struct { document *Document - expected istrings.ByteIndex + expected istrings.ByteNumber sep string }{ { document: &Document{ Text: "apple bana", - cursorPosition: istrings.RuneLen("apple bana"), + cursorPosition: istrings.RuneCount("apple bana"), }, expected: istrings.Len(""), }, { document: &Document{ Text: "apple bana", - cursorPosition: istrings.RuneLen("apple "), + cursorPosition: istrings.RuneCount("apple "), }, expected: istrings.Len("bana"), }, { document: &Document{ Text: "apply -f ./file/foo.json", - cursorPosition: istrings.RuneLen("apply -f ./"), + cursorPosition: istrings.RuneCount("apply -f ./"), }, expected: istrings.Len("file"), sep: " /", @@ -876,14 +876,14 @@ func TestDocument_FindEndOfCurrentWord(t *testing.T) { { document: &Document{ Text: "apple bana", - cursorPosition: istrings.RuneLen("apple"), + cursorPosition: istrings.RuneCount("apple"), }, expected: istrings.Len(""), }, { document: &Document{ Text: "apply -f ./file/foo.json", - cursorPosition: istrings.RuneLen("apply -f ."), + cursorPosition: istrings.RuneCount("apply -f ."), }, expected: istrings.Len(""), sep: " /", @@ -891,7 +891,7 @@ func TestDocument_FindEndOfCurrentWord(t *testing.T) { { document: &Document{ Text: "apple bana", - cursorPosition: istrings.RuneLen("ap"), + cursorPosition: istrings.RuneCount("ap"), }, expected: istrings.Len("ple"), }, @@ -942,27 +942,27 @@ func TestDocument_FindEndOfCurrentWord(t *testing.T) { func TestDocument_FindEndOfCurrentWordWithSpace(t *testing.T) { pattern := []struct { document *Document - expected istrings.ByteIndex + expected istrings.ByteNumber sep string }{ { document: &Document{ Text: "apple bana", - cursorPosition: istrings.RuneLen("apple bana"), + cursorPosition: istrings.RuneCount("apple bana"), }, expected: istrings.Len(""), }, { document: &Document{ Text: "apple bana", - cursorPosition: istrings.RuneLen("apple "), + cursorPosition: istrings.RuneCount("apple "), }, expected: istrings.Len("bana"), }, { document: &Document{ Text: "apply -f /file/foo.json", - cursorPosition: istrings.RuneLen("apply -f /"), + cursorPosition: istrings.RuneCount("apply -f /"), }, expected: istrings.Len("file"), sep: " /", @@ -970,14 +970,14 @@ func TestDocument_FindEndOfCurrentWordWithSpace(t *testing.T) { { document: &Document{ Text: "apple bana", - cursorPosition: istrings.RuneLen("apple"), + cursorPosition: istrings.RuneCount("apple"), }, expected: istrings.Len(" bana"), }, { document: &Document{ Text: "apply -f /path/to", - cursorPosition: istrings.RuneLen("apply -f /path"), + cursorPosition: istrings.RuneCount("apply -f /path"), }, expected: istrings.Len("/to"), sep: " /", @@ -985,7 +985,7 @@ func TestDocument_FindEndOfCurrentWordWithSpace(t *testing.T) { { document: &Document{ Text: "apple bana", - cursorPosition: istrings.RuneLen("ap"), + cursorPosition: istrings.RuneCount("ap"), }, expected: istrings.Len("ple"), }, @@ -1034,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: istrings.RuneLen("line 1\n" + "lin"), + cursorPosition: istrings.RuneCount("line 1\n" + "lin"), } ac := d.CurrentLineBeforeCursor() ex := "lin" @@ -1046,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: istrings.RuneLen("line 1\n" + "lin"), + cursorPosition: istrings.RuneCount("line 1\n" + "lin"), } ac := d.CurrentLineAfterCursor() ex := "e 2" @@ -1058,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: istrings.RuneLen("line 1\n" + "lin"), + cursorPosition: istrings.RuneCount("line 1\n" + "lin"), } ac := d.CurrentLine() ex := "line 2" @@ -1070,11 +1070,11 @@ func TestDocument_CurrentLine(t *testing.T) { func TestDocument_CursorPositionRowAndCol(t *testing.T) { var cursorPositionTests = []struct { document *Document - expectedRow istrings.RuneIndex - expectedCol istrings.RuneIndex + expectedRow istrings.RuneNumber + expectedCol istrings.RuneNumber }{ { - document: &Document{Text: "line 1\nline 2\nline 3\n", cursorPosition: istrings.RuneLen("line 1\n" + "lin")}, + document: &Document{Text: "line 1\nline 2\nline 3\n", cursorPosition: istrings.RuneCount("line 1\n" + "lin")}, expectedRow: 1, expectedCol: 3, }, @@ -1099,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: istrings.RuneLen("line 1\n" + "line 2\n" + "lin"), + cursorPosition: istrings.RuneCount("line 1\n" + "line 2\n" + "lin"), } ac := d.GetCursorLeftPosition(2) - var ex istrings.RuneIndex = -2 + var ex istrings.RuneNumber = -2 if ac != ex { t.Errorf("Should be %#v, got %#v", ex, ac) } @@ -1122,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: istrings.RuneLen("line 1\n" + "line 2\n" + "lin"), + cursorPosition: istrings.RuneCount("line 1\n" + "line 2\n" + "lin"), } ac := d.GetCursorUpPosition(2, -1) - ex := istrings.RuneLen("lin") - istrings.RuneLen("line 1\n"+"line 2\n"+"lin") + ex := istrings.RuneCount("lin") - istrings.RuneCount("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 = istrings.RuneLen("lin") - istrings.RuneLen("line 1\n"+"line 2\n"+"lin") + ex = istrings.RuneCount("lin") - istrings.RuneCount("line 1\n"+"line 2\n"+"lin") if ac != ex { t.Errorf("Should be %#v, got %#v", ex, ac) } @@ -1140,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: istrings.RuneLen("lin"), + cursorPosition: istrings.RuneCount("lin"), } ac := d.GetCursorDownPosition(2, -1) - ex := istrings.RuneLen("line 1\n"+"line 2\n"+"lin") - istrings.RuneLen("lin") + ex := istrings.RuneCount("line 1\n"+"line 2\n"+"lin") - istrings.RuneCount("lin") if ac != ex { t.Errorf("Should be %#v, got %#v", ex, ac) } ac = d.GetCursorDownPosition(100, -1) - ex = istrings.RuneLen("line 1\n"+"line 2\n"+"line 3\n"+"line 4\n") - istrings.RuneLen("lin") + ex = istrings.RuneCount("line 1\n"+"line 2\n"+"line 3\n"+"line 4\n") - istrings.RuneCount("lin") if ac != ex { t.Errorf("Should be %#v, got %#v", ex, ac) } @@ -1158,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: istrings.RuneLen("line 1\n" + "line 2\n" + "lin"), + cursorPosition: istrings.RuneCount("line 1\n" + "line 2\n" + "lin"), } ac := d.GetCursorRightPosition(2) - var ex istrings.RuneIndex = 2 + var ex istrings.RuneNumber = 2 if ac != ex { t.Errorf("Should be %#v, got %#v", ex, ac) } @@ -1181,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: istrings.RuneLen("line 1\n" + "lin"), + cursorPosition: istrings.RuneCount("line 1\n" + "lin"), } ac := d.Lines() ex := []string{"line 1", "line 2", "line 3", "line 4", ""} @@ -1193,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: istrings.RuneLen("line 1\n" + "lin"), + cursorPosition: istrings.RuneCount("line 1\n" + "lin"), } ac := d.LineCount() ex := 5 @@ -1205,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: istrings.RuneLen("line 1\n" + "lin"), + cursorPosition: istrings.RuneCount("line 1\n" + "lin"), } - row, col := d.TranslateIndexToPosition(istrings.RuneLen("line 1\nline 2\nlin")) + row, col := d.TranslateIndexToPosition(istrings.RuneCount("line 1\nline 2\nlin")) if row != 2 { t.Errorf("Should be %#v, got %#v", 2, row) } @@ -1226,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: istrings.RuneLen("line 1\n" + "lin"), + cursorPosition: istrings.RuneCount("line 1\n" + "lin"), } ac := d.TranslateRowColToIndex(2, 3) - ex := istrings.RuneLen("line 1\nline 2\nlin") + ex := istrings.RuneCount("line 1\nline 2\nlin") if ac != ex { t.Errorf("Should be %#v, got %#v", ex, ac) } @@ -1243,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: istrings.RuneLen("line 1\nline"), + cursorPosition: istrings.RuneCount("line 1\nline"), } ac := d.OnLastLine() if ac { t.Errorf("Should be %#v, got %#v", false, ac) } - d.cursorPosition = istrings.RuneLen("line 1\nline 2\nline") + d.cursorPosition = istrings.RuneCount("line 1\nline 2\nline") ac = d.OnLastLine() if !ac { t.Errorf("Should be %#v, got %#v", true, ac) @@ -1259,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: istrings.RuneLen("line 1\nli"), + cursorPosition: istrings.RuneCount("line 1\nli"), } ac := d.GetEndOfLinePosition() - ex := istrings.RuneLen("ne 2") + ex := istrings.RuneCount("ne 2") if ac != ex { t.Errorf("Should be %#v, got %#v", ex, ac) } diff --git a/emacs.go b/emacs.go index f43e1774..4770a360 100644 --- a/emacs.go +++ b/emacs.go @@ -1,8 +1,8 @@ package prompt import ( - "github.com/elk-language/go-prompt/internal/debug" - istrings "github.com/elk-language/go-prompt/internal/strings" + "github.com/elk-language/go-prompt/debug" + istrings "github.com/elk-language/go-prompt/strings" ) /* @@ -46,7 +46,7 @@ var emacsKeyBindings = []KeyBind{ { Key: ControlE, Fn: func(buf *Buffer) { - buf.CursorRight(istrings.RuneLen(buf.Document().CurrentLineAfterCursor())) + buf.CursorRight(istrings.RuneCount(buf.Document().CurrentLineAfterCursor())) }, }, // Go to the beginning of the line @@ -60,14 +60,14 @@ var emacsKeyBindings = []KeyBind{ { Key: ControlK, Fn: func(buf *Buffer) { - buf.Delete(istrings.RuneLen(buf.Document().CurrentLineAfterCursor())) + buf.Delete(istrings.RuneCount(buf.Document().CurrentLineAfterCursor())) }, }, // Cut/delete the Line before the cursor { Key: ControlU, Fn: func(buf *Buffer) { - buf.DeleteBeforeCursor(istrings.RuneLen(buf.Document().CurrentLineBeforeCursor())) + buf.DeleteBeforeCursor(istrings.RuneCount(buf.Document().CurrentLineBeforeCursor())) }, }, // Delete character under the cursor @@ -97,7 +97,7 @@ var emacsKeyBindings = []KeyBind{ { Key: AltRight, Fn: func(buf *Buffer) { - buf.CursorRight(buf.Document().FindRuneCountUntilEndOfCurrentWord()) + buf.CursorRight(buf.Document().FindRuneNumberUntilEndOfCurrentWord()) }, }, // Left allow: Backward one character @@ -111,20 +111,20 @@ var emacsKeyBindings = []KeyBind{ { Key: AltLeft, Fn: func(buf *Buffer) { - buf.CursorLeft(buf.Document().FindRuneCountUntilStartOfPreviousWord()) + buf.CursorLeft(buf.Document().FindRuneNumberUntilStartOfPreviousWord()) }, }, // Cut the Word before the cursor. { Key: ControlW, Fn: func(buf *Buffer) { - buf.DeleteBeforeCursor(istrings.RuneLen(buf.Document().GetWordBeforeCursorWithSpace())) + buf.DeleteBeforeCursor(istrings.RuneCount(buf.Document().GetWordBeforeCursorWithSpace())) }, }, { Key: AltBackspace, Fn: func(buf *Buffer) { - buf.DeleteBeforeCursor(istrings.RuneLen(buf.Document().GetWordBeforeCursorWithSpace())) + buf.DeleteBeforeCursor(istrings.RuneCount(buf.Document().GetWordBeforeCursorWithSpace())) }, }, // Clear the Screen, similar to the clear command diff --git a/emacs_test.go b/emacs_test.go index 20e9a0e4..605c3bd8 100644 --- a/emacs_test.go +++ b/emacs_test.go @@ -3,13 +3,13 @@ package prompt import ( "testing" - istrings "github.com/elk-language/go-prompt/internal/strings" + istrings "github.com/elk-language/go-prompt/strings" ) func TestEmacsKeyBindings(t *testing.T) { buf := NewBuffer() buf.InsertText("abcde", false, true) - if buf.cursorPosition != istrings.RuneIndex(len("abcde")) { + if buf.cursorPosition != istrings.RuneNumber(len("abcde")) { t.Errorf("Want %d, but got %d", len("abcde"), buf.cursorPosition) } @@ -21,7 +21,7 @@ func TestEmacsKeyBindings(t *testing.T) { // Go to the end of the line applyEmacsKeyBind(buf, ControlE) - if buf.cursorPosition != istrings.RuneIndex(len("abcde")) { + if buf.cursorPosition != istrings.RuneNumber(len("abcde")) { t.Errorf("Want %d, but got %d", len("abcde"), buf.cursorPosition) } } diff --git a/key_bind_func.go b/key_bind_func.go index cfcefe6a..7b49f713 100644 --- a/key_bind_func.go +++ b/key_bind_func.go @@ -1,19 +1,19 @@ package prompt import ( - istrings "github.com/elk-language/go-prompt/internal/strings" + istrings "github.com/elk-language/go-prompt/strings" ) // GoLineEnd Go to the End of the line func GoLineEnd(buf *Buffer) { x := []rune(buf.Document().TextAfterCursor()) - buf.CursorRight(istrings.RuneCount(len(x))) + buf.CursorRight(istrings.RuneNumber(len(x))) } // GoLineBeginning Go to the beginning of the line func GoLineBeginning(buf *Buffer) { x := []rune(buf.Document().TextBeforeCursor()) - buf.CursorLeft(istrings.RuneCount(len(x))) + buf.CursorLeft(istrings.RuneNumber(len(x))) } // DeleteChar Delete character under the cursor diff --git a/position.go b/position.go index ac6d834b..decccd40 100644 --- a/position.go +++ b/position.go @@ -4,7 +4,7 @@ import ( "io" "strings" - istrings "github.com/elk-language/go-prompt/internal/strings" + istrings "github.com/elk-language/go-prompt/strings" "github.com/mattn/go-runewidth" ) diff --git a/position_test.go b/position_test.go index 44d14567..49984d0b 100644 --- a/position_test.go +++ b/position_test.go @@ -6,7 +6,7 @@ package prompt import ( "testing" - istrings "github.com/elk-language/go-prompt/internal/strings" + istrings "github.com/elk-language/go-prompt/strings" "github.com/google/go-cmp/cmp" ) diff --git a/prompt.go b/prompt.go index 5d6d50c8..4d838aac 100644 --- a/prompt.go +++ b/prompt.go @@ -7,8 +7,8 @@ import ( "unicode" "unicode/utf8" - "github.com/elk-language/go-prompt/internal/debug" - istrings "github.com/elk-language/go-prompt/internal/strings" + "github.com/elk-language/go-prompt/debug" + istrings "github.com/elk-language/go-prompt/strings" ) const inputBufferSize = 1024 @@ -230,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(istrings.RuneCount(len([]rune(w)))) + p.buf.DeleteBeforeCursor(istrings.RuneNumber(len([]rune(w)))) } p.buf.InsertText(s.Text, false, true) } diff --git a/reader_posix.go b/reader_posix.go index d7676ff0..a22d3590 100644 --- a/reader_posix.go +++ b/reader_posix.go @@ -7,7 +7,7 @@ import ( "os" "syscall" - "github.com/elk-language/go-prompt/internal/term" + "github.com/elk-language/go-prompt/term" "golang.org/x/sys/unix" ) diff --git a/render.go b/render.go index f68298da..e22dcbb6 100644 --- a/render.go +++ b/render.go @@ -4,8 +4,8 @@ import ( "runtime" "strings" - "github.com/elk-language/go-prompt/internal/debug" - istrings "github.com/elk-language/go-prompt/internal/strings" + "github.com/elk-language/go-prompt/debug" + istrings "github.com/elk-language/go-prompt/strings" runewidth "github.com/mattn/go-runewidth" ) diff --git a/render_test.go b/render_test.go index ab349520..d67a7ef2 100644 --- a/render_test.go +++ b/render_test.go @@ -8,7 +8,7 @@ import ( "syscall" "testing" - istrings "github.com/elk-language/go-prompt/internal/strings" + istrings "github.com/elk-language/go-prompt/strings" ) func TestFormatCompletion(t *testing.T) { diff --git a/signal_posix.go b/signal_posix.go index fc6db611..b63c78fd 100644 --- a/signal_posix.go +++ b/signal_posix.go @@ -8,7 +8,7 @@ import ( "os/signal" "syscall" - "github.com/elk-language/go-prompt/internal/debug" + "github.com/elk-language/go-prompt/debug" ) func (p *Prompt) handleSignals(exitCh chan int, winSizeCh chan *WinSize, stop chan struct{}) { diff --git a/signal_windows.go b/signal_windows.go index 2b79c39c..a1191332 100644 --- a/signal_windows.go +++ b/signal_windows.go @@ -8,7 +8,7 @@ import ( "os/signal" "syscall" - "github.com/elk-language/go-prompt/internal/debug" + "github.com/elk-language/go-prompt/debug" ) func (p *Prompt) handleSignals(exitCh chan int, winSizeCh chan *WinSize, stop chan struct{}) { diff --git a/internal/strings/strings.go b/strings/strings.go similarity index 78% rename from internal/strings/strings.go rename to strings/strings.go index 7800becf..d89ec60f 100644 --- a/internal/strings/strings.go +++ b/strings/strings.go @@ -5,31 +5,31 @@ import ( ) // Get the length of the string in bytes. -func Len(s string) ByteCount { - return ByteCount(len(s)) +func Len(s string) ByteNumber { + return ByteNumber(len(s)) } // Get the length of the string in runes. -func RuneLen(s string) RuneCount { - return RuneCount(utf8.RuneCountInString(s)) +func RuneCount(s string) RuneNumber { + return RuneNumber(utf8.RuneCountInString(s)) } // IndexNotByte is similar with strings.IndexByte but showing the opposite behavior. -func IndexNotByte(s string, c byte) ByteIndex { +func IndexNotByte(s string, c byte) ByteNumber { n := len(s) for i := 0; i < n; i++ { if s[i] != c { - return ByteIndex(i) + return ByteNumber(i) } } return -1 } // LastIndexNotByte is similar with strings.LastIndexByte but showing the opposite behavior. -func LastIndexNotByte(s string, c byte) ByteIndex { +func LastIndexNotByte(s string, c byte) ByteNumber { for i := len(s) - 1; i >= 0; i-- { if s[i] != c { - return ByteIndex(i) + return ByteNumber(i) } } return -1 @@ -53,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) ByteIndex { +func IndexNotAny(s, chars string) ByteNumber { 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 ByteIndex(i) + return ByteNumber(i) } } return -1 @@ -70,7 +70,7 @@ func IndexNotAny(s, chars string) ByteIndex { for i, c := range s { for j, m := range chars { if c != m && j == len(chars)-1 { - return ByteIndex(i) + return ByteNumber(i) } else if c != m { continue } else { @@ -83,13 +83,13 @@ func IndexNotAny(s, chars string) ByteIndex { } // LastIndexNotAny is similar with strings.LastIndexAny but showing the opposite behavior. -func LastIndexNotAny(s, chars string) ByteIndex { +func LastIndexNotAny(s, chars string) ByteNumber { 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 ByteIndex(i) + return ByteNumber(i) } } return -1 @@ -101,7 +101,7 @@ func LastIndexNotAny(s, chars string) ByteIndex { i -= size for j, m := range chars { if r != m && j == len(chars)-1 { - return ByteIndex(i) + return ByteNumber(i) } else if r != m { continue } else { diff --git a/internal/strings/strings_test.go b/strings/strings_test.go similarity index 94% rename from internal/strings/strings_test.go rename to strings/strings_test.go index 9782da0c..8266054c 100644 --- a/internal/strings/strings_test.go +++ b/strings/strings_test.go @@ -3,7 +3,7 @@ package strings_test import ( "fmt" - "github.com/elk-language/go-prompt/internal/strings" + "github.com/elk-language/go-prompt/strings" ) func ExampleIndexNotByte() { diff --git a/internal/strings/index.go b/strings/units.go similarity index 53% rename from internal/strings/index.go rename to strings/units.go index e711af0f..106b3432 100644 --- a/internal/strings/index.go +++ b/strings/units.go @@ -1,21 +1,13 @@ 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 +type ByteNumber int // Numeric type that represents the amount // of runes in a string, array or slice. -type RuneCount = RuneIndex +type RuneNumber int + +// Numeric type that represents the visible +// width of characters in a string as seen in a terminal emulator. +type StringWidth int diff --git a/internal/term/raw.go b/term/raw.go similarity index 100% rename from internal/term/raw.go rename to term/raw.go diff --git a/internal/term/term.go b/term/term.go similarity index 100% rename from internal/term/term.go rename to term/term.go From fd9ace21171fe20ff1948c1b370392f7ddfb4603 Mon Sep 17 00:00:00 2001 From: Mateusz Drewniak Date: Wed, 12 Jul 2023 23:27:01 +0200 Subject: [PATCH 15/21] Add indentation to ExecuteOnEnterCallback --- _example/bang-executor/main.go | 4 ++-- constructor.go | 16 ++++++++++++++-- prompt.go | 23 +++++++++++++++++++---- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/_example/bang-executor/main.go b/_example/bang-executor/main.go index 0f24d9d2..786ef551 100644 --- a/_example/bang-executor/main.go +++ b/_example/bang-executor/main.go @@ -16,9 +16,9 @@ func main() { p.Run() } -func ExecuteOnEnter(input string) bool { +func ExecuteOnEnter(input string, indentSize int) (int, bool) { char, _ := utf8.DecodeLastRuneInString(input) - return char == '!' + return 1, char == '!' } func executor(s string) { diff --git a/constructor.go b/constructor.go index 16a8cd77..1727dffa 100644 --- a/constructor.go +++ b/constructor.go @@ -7,6 +7,17 @@ type Option func(prompt *Prompt) error // Callback function that returns a prompt prefix. type PrefixCallback func() (prefix string) +const DefaultIndentSize = 2 + +// WithIndentSize is an option that sets the amount of spaces +// that constitute a single indentation level. +func WithIndentSize(i int) Option { + return func(p *Prompt) error { + p.indentSize = i + return nil + } +} + // WithCompleter is an option that sets a custom Completer object. func WithCompleter(c Completer) Option { return func(p *Prompt) error { @@ -295,8 +306,8 @@ func WithExecuteOnEnterCallback(fn ExecuteOnEnterCallback) Option { } } -func DefaultExecuteOnEnterCallback(input string) bool { - return true +func DefaultExecuteOnEnterCallback(input string, indentSize int) (int, bool) { + return 0, true } func DefaultPrefixCallback() string { @@ -335,6 +346,7 @@ func New(executor Executor, opts ...Option) *Prompt { history: NewHistory(), completion: NewCompletionManager(6), executeOnEnterCallback: DefaultExecuteOnEnterCallback, + indentSize: DefaultIndentSize, keyBindMode: EmacsKeyBind, // All the above assume that bash is running in the default Emacs setting } diff --git a/prompt.go b/prompt.go index 4d838aac..59309321 100644 --- a/prompt.go +++ b/prompt.go @@ -3,6 +3,7 @@ package prompt import ( "bytes" "os" + "strings" "time" "unicode" "unicode/utf8" @@ -28,8 +29,9 @@ type ExitChecker func(in string, breakline bool) bool // user input after Enter has been pressed // and determines whether the input should be executed. // If this function returns true, the Executor callback will be called -// otherwise a newline will be added to the buffer containing user input. -type ExecuteOnEnterCallback func(input string) bool +// otherwise a newline will be added to the buffer containing user input +// and optionally indentation made up of `indentSize * indent` spaces. +type ExecuteOnEnterCallback func(input string, indentSize int) (indent int, execute bool) // Completer is a function that returns // a slice of suggestions for the given Document. @@ -48,6 +50,7 @@ type Prompt struct { ASCIICodeBindings []ASCIICodeBind keyBindMode KeyBindMode completionOnDown bool + indentSize int // How many spaces constitute a single indentation level exitChecker ExitChecker executeOnEnterCallback ExecuteOnEnterCallback skipClose bool @@ -146,8 +149,15 @@ func (p *Prompt) feed(b []byte) (shouldExit bool, userInput *UserInput) { switch key { case Enter, ControlJ, ControlM: - if !p.executeOnEnterCallback(p.buf.Text()) { + indent, execute := p.executeOnEnterCallback(p.buf.Text(), p.indentSize) + if !execute { p.buf.NewLine(false) + var indentStrBuilder strings.Builder + indentUnitCount := indent * p.indentSize + for i := 0; i < indentUnitCount; i++ { + indentStrBuilder.WriteRune(IndentUnit) + } + p.buf.InsertText(indentStrBuilder.String(), false, true) break } @@ -319,6 +329,9 @@ func (p *Prompt) Input() string { } } +const IndentUnit = ' ' +const IndentUnitString = string(IndentUnit) + func (p *Prompt) readBuffer(bufCh chan []byte, stopCh chan struct{}) { debug.Log("start reading buffer") for { @@ -345,7 +358,9 @@ func (p *Prompt) readBuffer(bufCh chan []byte, stopCh chan struct{}) { // translate \t into two spaces // to avoid problems with cursor positions case '\t': - newBytes = append(newBytes, ' ', ' ') + for i := 0; i < p.indentSize; i++ { + newBytes = append(newBytes, IndentUnit) + } default: newBytes = append(newBytes, byt) } From 0b52d99da46cf6c8da3d55f42613ecc593e2b795 Mon Sep 17 00:00:00 2001 From: Mateusz Drewniak Date: Fri, 14 Jul 2023 22:17:46 +0200 Subject: [PATCH 16/21] Document changes --- CHANGELOG.md | 184 +++++++++++++++++++++++++++++++++------------ _example/README.md | 14 +++- _example/build.sh | 1 + constructor.go | 52 ++++++------- 4 files changed, 176 insertions(+), 75 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9e21f1e..083a53b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,79 +1,171 @@ -# Change Log +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [1.0.0] + +This release contains a major refactoring of the codebase. +It's the first release of the [elk-language/go-prompt](https://github.com/elk-language/go-prompt) fork. + +The original library has been abandoned for at least 2 years now (although serious development has stopped 5 years ago). + +This release aims to make the code a bit cleaner, fix a couple of bugs and provide new, essential functionality such as syntax highlighting, dynamic Enter and multiline edit support. + +### Added + +- `prompt.New` constructor options: + - `prompt.WithLexer` let's you set a custom lexer for providing syntax highlighting + - `prompt.WithCompleter` for setting a custom `Completer` (completer is no longer a required argument in `prompt.New`) + - `prompt.WithIndentSize` let's you customise how many spaces should constitute a single indentation level + - `prompt.WithExecuteOnEnterCallback` + +- `prompt.Position` -- represents the cursor's position in 2D +- `prompt.Lexer`, `prompt.Token`, `prompt.SimpleToken`, `prompt.EagerLexer`, `prompt.LexerFunc` -- new syntax highlighting functionality +- `prompt.ExecuteOnEnterCallback` -- new dynamic Enter functionality (decide whether to insert a newline and indent or execute the input) + +- `_example/bang-executor` -- a sample program which uses the new `ExecuteOnEnterCallback`. Pressing Enter will insert a newline unless the input ends with an exclamation point `!` (then it gets executed). +- `_example/even-lexer` -- a sample program which shows how to use the new lexer feature. It implements a simple lexer which colours every character with an even index green. + +### Changed + +- Update Go from 1.16 to 1.19 +- The cursor can move in 2D. +- The Up arrow key will jump to the line above if the cursor is beyond the first line, but it will replace the input with the previous history entry if it's on the first line (like in Ruby's irb) +- The Down arrow key will jump to the line below if the cursor is before the last line, but it will replace the input with the next history entry if it's on the last line (like in Ruby's irb) +- Make `Completer` optional when creating a new `prompt.Prompt`. Change the signature of `prompt.New` from `func New(Executor, Completer, ...Option) *Prompt` to `func New(Executor, ...Option) *Prompt` +- Rename `prompt.ConsoleParser` to `prompt.Reader` and make it embed `io.ReadCloser` +- Rename `prompt.ConsoleWriter` to `prompt.Writer` and make it embed `io.Writer` and `io.StringWriter` +- Rename `prompt.OptionTitle` to `prompt.WithTitle` +- Rename `prompt.OptionPrefix` to `prompt.WithPrefix` +- Rename `prompt.OptionInitialBufferText` to `prompt.WithInitialText` +- Rename `prompt.OptionCompletionWordSeparator` to `prompt.WithCompletionWordSeparator` +- Replace `prompt.OptionLivePrefix` with `prompt.WithPrefixCallback` -- `func() string`. The prefix is always determined by a callback function which should always return a `string`. +- Rename `prompt.OptionPrefixTextColor` to `prompt.WithPrefixTextColor` +- Rename `prompt.OptionPrefixBackgroundColor` to `prompt.WithPrefixBackgroundColor` +- Rename `prompt.OptionInputTextColor` to `prompt.WithInputTextColor` +- Rename `prompt.OptionInputBGColor` to `prompt.WithInputBGColor` +- Rename `prompt.OptionPreviewSuggestionTextColor` to `prompt.WithPreviewSuggestionTextColor` +- Rename `prompt.OptionSuggestionTextColor` to `prompt.WithSuggestionTextColor` +- Rename `prompt.OptionSuggestionBGColor` to `prompt.WithSuggestionBGColor` +- Rename `prompt.OptionSelectedSuggestionTextColor` to `prompt.WithSelectedSuggestionTextColor` +- Rename `prompt.OptionSelectedSuggestionBGColor` to `prompt.WithSelectedSuggestionBGColor` +- Rename `prompt.OptionDescriptionTextColor` to `prompt.WithDescriptionTextColor` +- Rename `prompt.OptionDescriptionBGColor` to `prompt.WithDescriptionBGColor` +- Rename `prompt.OptionSelectedDescriptionTextColor` to `prompt.WithSelectedDescriptionTextColor` +- Rename `prompt.OptionSelectedDescriptionBGColor` to `prompt.WithSelectedDescriptionBGColor` +- Rename `prompt.OptionScrollbarThumbColor` to `prompt.WithScrollbarThumbColor` +- Rename `prompt.OptionScrollbarBGColor` to `prompt.WithScrollbarBGColor` +- Rename `prompt.OptionMaxSuggestion` to `prompt.WithMaxSuggestion` +- Rename `prompt.OptionHistory` to `prompt.WithHistory` +- Rename `prompt.OptionSwitchKeyBindMode` to `prompt.WithKeyBindMode` +- Rename `prompt.OptionCompletionOnDown` to `prompt.WithCompletionOnDown` +- Rename `prompt.OptionAddKeyBind` to `prompt.WithKeyBind` +- Rename `prompt.OptionAddASCIICodeBind` to `prompt.WithASCIICodeBind` +- Rename `prompt.OptionShowCompletionAtStart` to `prompt.WithShowCompletionAtStart` +- Rename `prompt.OptionBreakLineCallback` to `prompt.WithBreakLineCallback` +- Rename `prompt.OptionExitChecker` to `prompt.WithExitChecker` -## v0.3.0 (2018/??/??) +### Fixed -next release. +- Make pasting multiline text work properly +- Make pasting text with tabs work properly (tabs get replaced with spaces) +- Introduce `strings.ByteNumber`, `strings.RuneNumber`, `strings.StringWidth` to reduce the ambiguity of when to use which of the three main units used by this library to measure string length and index parts of strings. Several subtle bugs (using the wrong unit) causing panics have been fixed this way. +- Remove a `/dev/tty` leak in `PosixReader` (old `PosixParser`) -## v0.2.3 (2018/10/25) +### Removed -### What's new? +- `prompt.SwitchKeyBindMode` -* Add `prompt.FuzzyFilter` for fuzzy matching at [#92](https://github.com/c-bata/go-prompt/pull/92). -* Add `OptionShowCompletionAtStart` to show completion at start at [#100](https://github.com/c-bata/go-prompt/pull/100). -* Add `prompt.NewStderrWriter` at [#102](https://github.com/c-bata/go-prompt/pull/102). +## [0.2.6] - 2021-03-03 -### Fixed +### Changed -* Fix resetting display attributes (please see [pull #104](https://github.com/c-bata/go-prompt/pull/104) for more details). -* Fix error handling of Flush function in ConsoleWriter (please see [pull #97](https://github.com/c-bata/go-prompt/pull/97) for more details). -* Fix panic problem when reading from stdin before starting the prompt (please see [issue #88](https://github.com/c-bata/go-prompt/issues/88) for more details). +- Update pkg/term to 1.2.0 -### Removed or Deprecated -* `prompt.NewStandardOutputWriter` is deprecated. Please use `prompt.NewStdoutWriter`. +## [0.2.5] - 2020-09-19 -## v0.2.2 (2018/06/28) +### Changed -### What's new? +- Upgrade all dependencies to latest -* Support CJK(Chinese, Japanese and Korean) and Cyrillic characters. -* Add OptionCompletionWordSeparator(x string) to customize insertion points for completions. - * To support this, text query functions by arbitrary word separator are added in Document (please see [here](https://github.com/c-bata/go-prompt/pull/79) for more details). -* Add FilePathCompleter to complete file path on your system. -* Add option to customize ascii code key bindings. -* Add GetWordAfterCursor method in Document. -### Removed or Deprecated +## [0.2.4] - 2020-09-18 -* prompt.Choose shortcut function is deprecated. +### Changed -## v0.2.1 (2018/02/14) +- Update pkg/term module to latest and use unix.Termios -### What's New? -* ~~It seems that windows support is almost perfect.~~ - * A critical bug is found :( When you change a terminal window size, the layout will be broken because current implementation cannot catch signal for updating window size on Windows. +## [0.2.3] - 2018-10-25 + +### Added + +* `prompt.FuzzyFilter` for fuzzy matching at [#92](https://github.com/c-bata/go-prompt/pull/92). +* `OptionShowCompletionAtStart` to show completion at start at [#100](https://github.com/c-bata/go-prompt/pull/100). +* `prompt.NewStderrWriter` at [#102](https://github.com/c-bata/go-prompt/pull/102). ### Fixed -* Fix a Shift+Tab handling on Windows. -* Fix 4-dimension arrow keys handling on Windows. +* reset display attributes (please see [pull #104](https://github.com/c-bata/go-prompt/pull/104) for more details). +* handle errors of Flush function in ConsoleWriter (please see [pull #97](https://github.com/c-bata/go-prompt/pull/97) for more details). +* don't panic problem when reading from stdin before starting the prompt (please see [issue #88](https://github.com/c-bata/go-prompt/issues/88) for more details). + +### Deprecated + +* `prompt.NewStandardOutputWriter` -- please use `prompt.NewStdoutWriter`. + + +## [0.2.2] - 2018-06-28 + +### Added -## v0.2.0 (2018/02/13) +* Support CJK (Chinese, Japanese and Korean) and Cyrillic characters. +* `OptionCompletionWordSeparator(x string)` to customize insertion points for completions. + * To support this, text query functions by arbitrary word separator are added in `Document` (please see [here](https://github.com/c-bata/go-prompt/pull/79) for more details). +* `FilePathCompleter` to complete file path on your system. +* `option` to customize ascii code key bindings. +* `GetWordAfterCursor` method in `Document`. -### What's New? +### Deprecated -* Supports scrollbar when there are too many matched suggestions -* Windows support (but please caution because this is still not perfect). -* Add OptionLivePrefix to update the prefix dynamically -* Implement clear screen by `Ctrl+L`. +* `prompt.Choose` shortcut function is deprecated. + + +## [0.2.1] - 2018-02-14 + +### Added + +* ~~It seems that windows support is almost perfect.~~ + * A critical bug is found :( When you change a terminal window size, the layout will be broken because current implementation cannot catch signal for updating window size on Windows. ### Fixed -* Fix the behavior of `Ctrl+W` keybind. -* Fix the panic because when running on a docker container (please see [here](https://github.com/c-bata/go-prompt/pull/32) for details). -* Fix panic when making terminal window small size after input 2 lines of texts. See [here](https://github.com/c-bata/go-prompt/issues/37) for details). -* And also fixed many bugs that layout is broken when using Terminal.app, GNU Terminal and a Goland(IntelliJ). +* Shift + Tab handling on Windows. +* 4-dimension arrow keys handling on Windows. + + +## [0.2.0] - 2018-02-13 -### News +### Added -New core developers are joined (alphabetical order). +* Support scrollbar when there are too many matched suggestions +* Support Windows (but please caution because this is still not perfect). +* `OptionLivePrefix` to update the prefix dynamically +* Clear screen by Ctrl + L. + +### Fixed -* Nao Yonashiro (Github @orisano) -* Ryoma Abe (Github @Allajah) -* Yusuke Nakamura (Github @unasuke) +* Improve the Ctrl + W keybind. +* Don't panic because when running in a docker container (please see [here](https://github.com/c-bata/go-prompt/pull/32) for details). +* Don't panic when making terminal window small size after input 2 lines of texts. See [here](https://github.com/c-bata/go-prompt/issues/37) for details). +* Get rid of many bugs that layout is broken when using Terminal.app, GNU Terminal and a Goland(IntelliJ). -## v0.1.0 (2017/08/15) +## [0.1.0] - 2017-08-15 Initial Release diff --git a/_example/README.md b/_example/README.md index 759b0e3f..02fe6057 100644 --- a/_example/README.md +++ b/_example/README.md @@ -3,6 +3,18 @@ This directory includes some examples using go-prompt. These examples are useful to know the usage of go-prompt and check behavior for development. +## even-lexer + +Uses a custom lexer that colours every character with an even index green. + +Shows you how to hook up a custom lexer for syntax highlighting. + +## bang-executor + +Inserts a newline when the Enter key is pressed unless the input ends with an exclamation point `!` (then it gets printed). + +Shows you how to define a custom callback which determines whether the input is complete and should be executed or a newline should be inserted (after Enter has been pressed). + ## simple-echo ![simple-input](https://github.com/c-bata/assets/raw/master/go-prompt/examples/input.gif) @@ -19,7 +31,7 @@ A simple [http-prompt](https://github.com/eliangcs/http-prompt) implementation u ![live-prefix](https://github.com/c-bata/assets/raw/master/go-prompt/examples/live-prefix.gif) -A example application which changes a prefix string dynamically. +A example application which changes the prefix string dynamically. This feature is used like [ktr0731/evans](https://github.com/ktr0731/evans) which is interactive gRPC client using go-prompt. ## exec-command diff --git a/_example/build.sh b/_example/build.sh index 7b71a4f5..8c561684 100755 --- a/_example/build.sh +++ b/_example/build.sh @@ -11,3 +11,4 @@ go build -o ${BIN_DIR}/live-prefix ${DIR}/live-prefix/main.go go build -o ${BIN_DIR}/simple-echo ${DIR}/simple-echo/main.go go build -o ${BIN_DIR}/simple-echo-cjk-cyrillic ${DIR}/simple-echo/cjk-cyrillic/main.go go build -o ${BIN_DIR}/even-lexer ${DIR}/even-lexer/main.go +go build -o ${BIN_DIR}/bang-executor ${DIR}/bang-executor/main.go diff --git a/constructor.go b/constructor.go index 1727dffa..ecb18a43 100644 --- a/constructor.go +++ b/constructor.go @@ -18,6 +18,24 @@ func WithIndentSize(i int) Option { } } +// WithLexer set lexer function and enable it. +func WithLexer(lex Lexer) Option { + return func(p *Prompt) error { + p.lexer = lex + return nil + } +} + +// WithExecuteOnEnterCallback can be used to set +// a custom callback function that determines whether an Enter key +// should trigger the Executor or add a newline to the user input buffer. +func WithExecuteOnEnterCallback(fn ExecuteOnEnterCallback) Option { + return func(p *Prompt) error { + p.executeOnEnterCallback = fn + return nil + } +} + // WithCompleter is an option that sets a custom Completer object. func WithCompleter(c Completer) Option { return func(p *Prompt) error { @@ -228,8 +246,8 @@ func WithHistory(x []string) Option { } } -// WithSwitchKeyBindMode set a key bind mode. -func WithSwitchKeyBindMode(m KeyBindMode) Option { +// WithKeyBindMode set a key bind mode. +func WithKeyBindMode(m KeyBindMode) Option { return func(p *Prompt) error { p.keyBindMode = m return nil @@ -244,20 +262,16 @@ func WithCompletionOnDown() Option { } } -// SwitchKeyBindMode to set a key bind mode. -// Deprecated: Please use WithSwitchKeyBindMode. -var SwitchKeyBindMode = WithSwitchKeyBindMode - -// WithAddKeyBind to set a custom key bind. -func WithAddKeyBind(b ...KeyBind) Option { +// WithKeyBind to set a custom key bind. +func WithKeyBind(b ...KeyBind) Option { return func(p *Prompt) error { p.keyBindings = append(p.keyBindings, b...) return nil } } -// WithAddASCIICodeBind to set a custom key bind. -func WithAddASCIICodeBind(b ...ASCIICodeBind) Option { +// WithASCIICodeBind to set a custom key bind. +func WithASCIICodeBind(b ...ASCIICodeBind) Option { return func(p *Prompt) error { p.ASCIICodeBindings = append(p.ASCIICodeBindings, b...) return nil @@ -288,24 +302,6 @@ func WithExitChecker(fn ExitChecker) Option { } } -// WithLexer set lexer function and enable it. -func WithLexer(lex Lexer) Option { - return func(p *Prompt) error { - p.lexer = lex - return nil - } -} - -// WithExecuteOnEnterCallback can be used to set -// a custom callback function that determines whether an Enter key -// should trigger the Executor or add a newline to the user input buffer. -func WithExecuteOnEnterCallback(fn ExecuteOnEnterCallback) Option { - return func(p *Prompt) error { - p.executeOnEnterCallback = fn - return nil - } -} - func DefaultExecuteOnEnterCallback(input string, indentSize int) (int, bool) { return 0, true } From 2cae24f22edd765f2de1f468d1e66367ce592911 Mon Sep 17 00:00:00 2001 From: Mateusz Drewniak Date: Fri, 14 Jul 2023 22:30:54 +0200 Subject: [PATCH 17/21] Add diffs to changelog entries --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 083a53b7..bbf2031a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.0.0] +[Diff](https://github.com/elk-language/go-prompt/compare/v0.2.6...elk-language:go-prompt:v1.0.0) + This release contains a major refactoring of the codebase. It's the first release of the [elk-language/go-prompt](https://github.com/elk-language/go-prompt) fork. @@ -80,8 +82,11 @@ This release aims to make the code a bit cleaner, fix a couple of bugs and provi - `prompt.SwitchKeyBindMode` + ## [0.2.6] - 2021-03-03 +[Diff](https://github.com/elk-language/go-prompt/compare/v0.2.5...elk-language:go-prompt:v0.2.6) + ### Changed - Update pkg/term to 1.2.0 @@ -89,6 +94,8 @@ This release aims to make the code a bit cleaner, fix a couple of bugs and provi ## [0.2.5] - 2020-09-19 +[Diff](https://github.com/elk-language/go-prompt/compare/v0.2.4...elk-language:go-prompt:v0.2.5) + ### Changed - Upgrade all dependencies to latest @@ -96,6 +103,8 @@ This release aims to make the code a bit cleaner, fix a couple of bugs and provi ## [0.2.4] - 2020-09-18 +[Diff](https://github.com/elk-language/go-prompt/compare/v0.2.3...elk-language:go-prompt:v0.2.4) + ### Changed - Update pkg/term module to latest and use unix.Termios @@ -103,6 +112,8 @@ This release aims to make the code a bit cleaner, fix a couple of bugs and provi ## [0.2.3] - 2018-10-25 +[Diff](https://github.com/elk-language/go-prompt/compare/v0.2.2...elk-language:go-prompt:v0.2.3) + ### Added * `prompt.FuzzyFilter` for fuzzy matching at [#92](https://github.com/c-bata/go-prompt/pull/92). @@ -122,6 +133,8 @@ This release aims to make the code a bit cleaner, fix a couple of bugs and provi ## [0.2.2] - 2018-06-28 +[Diff](https://github.com/elk-language/go-prompt/compare/v0.2.1...elk-language:go-prompt:v0.2.2) + ### Added * Support CJK (Chinese, Japanese and Korean) and Cyrillic characters. @@ -138,6 +151,8 @@ This release aims to make the code a bit cleaner, fix a couple of bugs and provi ## [0.2.1] - 2018-02-14 +[Diff](https://github.com/elk-language/go-prompt/compare/v0.2.0...elk-language:go-prompt:v0.2.1) + ### Added * ~~It seems that windows support is almost perfect.~~ @@ -151,6 +166,8 @@ This release aims to make the code a bit cleaner, fix a couple of bugs and provi ## [0.2.0] - 2018-02-13 +[Diff](https://github.com/elk-language/go-prompt/compare/v0.1.0...elk-language:go-prompt:v0.2.0) + ### Added * Support scrollbar when there are too many matched suggestions From a9f18e20bf53aa92797693a0dad08658656894a8 Mon Sep 17 00:00:00 2001 From: Mateusz Drewniak Date: Sat, 15 Jul 2023 10:23:10 +0200 Subject: [PATCH 18/21] Fix WindowsReader --- go.mod | 6 ++++-- go.sum | 33 --------------------------------- reader_windows.go | 2 +- 3 files changed, 5 insertions(+), 36 deletions(-) diff --git a/go.mod b/go.mod index 6a03d321..0db5cac3 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,13 @@ module github.com/elk-language/go-prompt go 1.19 require ( - github.com/google/go-cmp v0.5.9 // indirect + github.com/google/go-cmp v0.5.9 github.com/mattn/go-colorable v0.1.7 github.com/mattn/go-runewidth v0.0.9 github.com/mattn/go-tty v0.0.3 github.com/pkg/term v1.2.0-beta.2 - golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect + golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df golang.org/x/sys v0.1.0 ) + +require github.com/mattn/go-isatty v0.0.12 // indirect diff --git a/go.sum b/go.sum index 916c9d7d..8b177a46 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,3 @@ -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -15,46 +14,14 @@ github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI= github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= github.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw= github.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= -golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200918174421-af09f7315aff h1:1CPUrky56AcgSpxz/KfgzQWzfG09u5YOL8MvPYBlrL8= -golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/reader_windows.go b/reader_windows.go index de2c76a7..f6f64d87 100644 --- a/reader_windows.go +++ b/reader_windows.go @@ -58,7 +58,7 @@ func (p *WindowsReader) Read(buff []byte) (int, error) { if err != nil { break } - n += utf8.EncodeRune(buf[n:], r) + n += utf8.EncodeRune(buff[n:], r) } return n, nil } From 8b93249a4789dae93703ee88df2b82960c390bc7 Mon Sep 17 00:00:00 2001 From: Mateusz Drewniak Date: Sat, 15 Jul 2023 10:32:30 +0200 Subject: [PATCH 19/21] Fix linter warnings --- completer/file.go | 9 ++++++--- debug/log.go | 4 ++-- render.go | 48 +++++++++++++++++++++++++++++++++----------- writer_vt100_test.go | 8 ++++++-- 4 files changed, 50 insertions(+), 19 deletions(-) diff --git a/completer/file.go b/completer/file.go index dd8f4434..d0862337 100644 --- a/completer/file.go +++ b/completer/file.go @@ -1,7 +1,6 @@ package completer import ( - "io/ioutil" "os" "os/user" "path/filepath" @@ -70,7 +69,7 @@ func (c *FilePathCompleter) Complete(d prompt.Document) []prompt.Suggest { return prompt.FilterHasPrefix(cached, base, c.IgnoreCase) } - files, err := ioutil.ReadDir(dir) + files, err := os.ReadDir(dir) if err != nil && os.IsNotExist(err) { return nil } else if err != nil { @@ -80,7 +79,11 @@ func (c *FilePathCompleter) Complete(d prompt.Document) []prompt.Suggest { suggests := make([]prompt.Suggest, 0, len(files)) for _, f := range files { - if c.Filter != nil && !c.Filter(f) { + fileInfo, err := f.Info() + if err != nil { + panic(err) + } + if c.Filter != nil && !c.Filter(fileInfo) { continue } suggests = append(suggests, prompt.Suggest{Text: f.Name()}) diff --git a/debug/log.go b/debug/log.go index c44cdf55..8d20d444 100644 --- a/debug/log.go +++ b/debug/log.go @@ -1,7 +1,7 @@ package debug import ( - "io/ioutil" + "io" "log" "os" ) @@ -25,7 +25,7 @@ func init() { return } } - logger = log.New(ioutil.Discard, "", log.Llongfile) + logger = log.New(io.Discard, "", log.Llongfile) } // Close to close logfile diff --git a/render.go b/render.go index e22dcbb6..7cc69e05 100644 --- a/render.go +++ b/render.go @@ -55,8 +55,12 @@ func (r *Render) getCurrentPrefix() string { func (r *Render) renderPrefix() { r.out.SetColor(r.prefixTextColor, r.prefixBGColor, false) - r.out.WriteString("\r") - r.out.WriteString(r.getCurrentPrefix()) + if _, err := r.out.WriteString("\r"); err != nil { + panic(err) + } + if _, err := r.out.WriteString(r.getCurrentPrefix()); err != nil { + panic(err) + } r.out.SetColor(DefaultColor, DefaultColor, false) } @@ -86,7 +90,9 @@ func (r *Render) renderWindowTooSmall() { r.out.CursorGoTo(0, 0) r.out.EraseScreen() r.out.SetColor(DarkRed, White, false) - r.out.WriteString("Your console window is too small...") + if _, err := r.out.WriteString("Your console window is too small..."); err != nil { + panic(err) + } } func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) { @@ -139,21 +145,27 @@ func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) { } else { r.out.SetColor(r.suggestionTextColor, r.suggestionBGColor, false) } - r.out.WriteString(formatted[i].Text) + if _, err := r.out.WriteString(formatted[i].Text); err != nil { + panic(err) + } if i == selected { r.out.SetColor(r.selectedDescriptionTextColor, r.selectedDescriptionBGColor, false) } else { r.out.SetColor(r.descriptionTextColor, r.descriptionBGColor, false) } - r.out.WriteString(formatted[i].Description) + if _, err := r.out.WriteString(formatted[i].Description); err != nil { + panic(err) + } if isScrollThumb(i) { r.out.SetColor(DefaultColor, r.scrollbarThumbColor, false) } else { r.out.SetColor(DefaultColor, r.scrollbarBGColor, false) } - r.out.WriteString(" ") + if _, err := r.out.WriteString(" "); err != nil { + panic(err) + } r.out.SetColor(DefaultColor, DefaultColor, false) c := cursor.Add(Position{X: width}) @@ -203,7 +215,9 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager, lexer Lex r.lex(lexer, line) } else { r.out.SetColor(r.inputTextColor, r.inputBGColor, false) - r.out.WriteString(line) + if _, err := r.out.WriteString(line); err != nil { + panic(err) + } } r.out.SetColor(DefaultColor, DefaultColor, false) @@ -221,7 +235,9 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager, lexer Lex 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) + if _, err := r.out.WriteString(suggest.Text); err != nil { + panic(err) + } r.out.SetColor(DefaultColor, DefaultColor, false) cursor.X += istrings.StringWidth(runewidth.StringWidth(suggest.Text)) endOfSuggestionPos := cursor @@ -231,7 +247,9 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager, lexer Lex if lexer != nil { r.lex(lexer, rest) } else { - r.out.WriteString(rest) + if _, err := r.out.WriteString(rest); err != nil { + panic(err) + } } r.out.SetColor(DefaultColor, DefaultColor, false) @@ -261,7 +279,9 @@ func (r *Render) lex(lexer Lexer, input string) { s = strings.TrimPrefix(s, a[0]) r.out.SetColor(token.Color(), r.inputBGColor, false) - r.out.WriteString(a[0]) + if _, err := r.out.WriteString(a[0]); err != nil { + panic(err) + } } } @@ -277,7 +297,9 @@ func (r *Render) BreakLine(buffer *Buffer, lexer Lexer) { r.lex(lexer, buffer.Document().Text+"\n") } else { r.out.SetColor(r.inputTextColor, r.inputBGColor, false) - r.out.WriteString(buffer.Document().Text + "\n") + if _, err := r.out.WriteString(buffer.Document().Text + "\n"); err != nil { + panic(err) + } } r.out.SetColor(DefaultColor, DefaultColor, false) @@ -333,6 +355,8 @@ func clamp(high, low, x float64) float64 { func alignNextLine(r *Render, col istrings.StringWidth) { r.out.CursorDown(1) - r.out.WriteString("\r") + if _, err := r.out.WriteString("\r"); err != nil { + panic(err) + } r.out.CursorForward(int(col)) } diff --git a/writer_vt100_test.go b/writer_vt100_test.go index ec21cbcf..b0ea1c1e 100644 --- a/writer_vt100_test.go +++ b/writer_vt100_test.go @@ -22,7 +22,9 @@ func TestVT100WriterWrite(t *testing.T) { for _, s := range scenarioTable { pw := &VT100Writer{} - pw.Write(s.input) + if _, err := pw.Write(s.input); err != nil { + panic(err) + } if !bytes.Equal(pw.buffer, s.expected) { t.Errorf("Should be %+#v, but got %+#v", pw.buffer, s.expected) @@ -47,7 +49,9 @@ func TestVT100WriterWriteString(t *testing.T) { for _, s := range scenarioTable { pw := &VT100Writer{} - pw.WriteString(s.input) + if _, err := pw.WriteString(s.input); err != nil { + panic(err) + } if !bytes.Equal(pw.buffer, s.expected) { t.Errorf("Should be %+#v, but got %+#v", pw.buffer, s.expected) From 5e93cb04993c85924045fc9f6e6850c9e157e8b1 Mon Sep 17 00:00:00 2001 From: Mateusz Drewniak Date: Sat, 15 Jul 2023 11:26:39 +0200 Subject: [PATCH 20/21] Fix `Tab` and `Shift` + `Tab` --- CHANGELOG.md | 3 ++ _example/exec-command/main.go | 17 +++++---- _example/simple-echo/cjk-cyrillic/main.go | 2 +- _example/simple-echo/main.go | 2 +- prompt.go | 45 +++++++++++++++++++++-- 5 files changed, 55 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbf2031a..e0c16452 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,7 +38,10 @@ This release aims to make the code a bit cleaner, fix a couple of bugs and provi - The cursor can move in 2D. - The Up arrow key will jump to the line above if the cursor is beyond the first line, but it will replace the input with the previous history entry if it's on the first line (like in Ruby's irb) - The Down arrow key will jump to the line below if the cursor is before the last line, but it will replace the input with the next history entry if it's on the last line (like in Ruby's irb) +- Tab will insert a single indentation level when there are no suggestions +- Shift + Tab will delete a single indentation level when there are no suggestions and the line before the cursors consists only of indentation (spaces) - Make `Completer` optional when creating a new `prompt.Prompt`. Change the signature of `prompt.New` from `func New(Executor, Completer, ...Option) *Prompt` to `func New(Executor, ...Option) *Prompt` +- Make `prefix` optional in `prompt.Input`. Change the signature of `prompt.Input` from `func Input(string, ...Option) string` to `func Input(...Option) string`. - Rename `prompt.ConsoleParser` to `prompt.Reader` and make it embed `io.ReadCloser` - Rename `prompt.ConsoleWriter` to `prompt.Writer` and make it embed `io.Writer` and `io.StringWriter` - Rename `prompt.OptionTitle` to `prompt.WithTitle` diff --git a/_example/exec-command/main.go b/_example/exec-command/main.go index 1698593c..02766bf7 100644 --- a/_example/exec-command/main.go +++ b/_example/exec-command/main.go @@ -8,14 +8,15 @@ import ( ) func executor(t string) { - if t == "bash" { - cmd := exec.Command("bash") - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Run() + if t != "bash" { + return } - return + + cmd := exec.Command("bash") + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Run() } func completer(t prompt.Document) []prompt.Suggest { @@ -27,7 +28,7 @@ func completer(t prompt.Document) []prompt.Suggest { func main() { p := prompt.New( executor, - prompt.WithCompleter(completer) + prompt.WithCompleter(completer), ) p.Run() } diff --git a/_example/simple-echo/cjk-cyrillic/main.go b/_example/simple-echo/cjk-cyrillic/main.go index e3bcc2d0..27f65248 100644 --- a/_example/simple-echo/cjk-cyrillic/main.go +++ b/_example/simple-echo/cjk-cyrillic/main.go @@ -23,7 +23,7 @@ func completer(in prompt.Document) []prompt.Suggest { func main() { p := prompt.New( executor, - completer, + prompt.WithCompleter(completer), prompt.WithPrefix(">>> "), prompt.WithTitle("sql-prompt for multi width characters"), ) diff --git a/_example/simple-echo/main.go b/_example/simple-echo/main.go index 42742d8e..62be416e 100644 --- a/_example/simple-echo/main.go +++ b/_example/simple-echo/main.go @@ -18,7 +18,7 @@ func completer(in prompt.Document) []prompt.Suggest { func main() { in := prompt.Input( - ">>> ", + prompt.WithPrefix(">>> "), prompt.WithTitle("sql-prompt"), prompt.WithHistory([]string{"SELECT * FROM users;"}), prompt.WithPrefixTextColor(prompt.Yellow), diff --git a/prompt.go b/prompt.go index 59309321..217b8fb4 100644 --- a/prompt.go +++ b/prompt.go @@ -147,6 +147,7 @@ func (p *Prompt) feed(b []byte) (shouldExit bool, userInput *UserInput) { completing := p.completion.Completing() p.handleCompletionKeyBinding(key, completing) +keySwitch: switch key { case Enter, ControlJ, ControlM: indent, execute := p.executeOnEnterCallback(p.buf.Text(), p.indentSize) @@ -167,6 +168,40 @@ func (p *Prompt) feed(b []byte) (shouldExit bool, userInput *UserInput) { if userInput.input != "" { p.history.Add(userInput.input) } + case Tab: + if len(p.completion.GetSuggestions()) > 0 { + // If there are any suggestions, select the next one + p.completion.Next() + break + } + + // if there are no suggestions insert indentation + newBytes := make([]byte, 0, len(b)) + for _, byt := range b { + switch byt { + case '\t': + for i := 0; i < p.indentSize; i++ { + newBytes = append(newBytes, IndentUnit) + } + default: + newBytes = append(newBytes, byt) + } + } + p.buf.InsertText(string(newBytes), false, true) + case BackTab: + if len(p.completion.GetSuggestions()) > 0 { + // If there are any suggestions, select the previous one + p.completion.Previous() + break + } + + text := p.buf.Document().CurrentLineBeforeCursor() + for _, char := range text { + if char != IndentUnit { + break keySwitch + } + } + p.buf.DeleteBeforeCursor(istrings.RuneNumber(p.indentSize)) case ControlC: p.renderer.BreakLine(p.buf, p.lexer) p.buf = NewBuffer() @@ -228,14 +263,12 @@ func (p *Prompt) handleCompletionKeyBinding(key Key, completing bool) { if completing || p.completionOnDown { p.completion.Next() } - case Tab, ControlI: + case ControlI: p.completion.Next() case Up: if completing { p.completion.Previous() } - case BackTab: - p.completion.Previous() default: if s, ok := p.completion.GetSelectedSuggestion(); ok { w := p.buf.Document().GetWordBeforeCursorUntilSeparator(p.completion.wordSeparator) @@ -346,7 +379,11 @@ func (p *Prompt) readBuffer(bufCh chan []byte, stopCh chan struct{}) { break } bytes = bytes[:n] - if len(bytes) != 1 || bytes[0] != 0 { + if len(bytes) == 1 && bytes[0] == '\t' { + // if only a single Tab key has been pressed + // handle it as a keybind + bufCh <- bytes + } else if len(bytes) != 1 || bytes[0] != 0 { newBytes := make([]byte, 0, len(bytes)) for _, byt := range bytes { switch byt { From 68485be048772dae6b6ac133efa1eebae4d9d824 Mon Sep 17 00:00:00 2001 From: Mateusz Drewniak Date: Sat, 15 Jul 2023 11:36:41 +0200 Subject: [PATCH 21/21] Update the setup-go action in CI --- .github/workflows/test.yml | 4 ++-- CHANGELOG.md | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0e71cb7e..d279c86c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,7 @@ jobs: go: ['1.19', '1.20'] steps: - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: go-version: ${{ matrix.go }} id: go @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: go-version: '1.20' id: go diff --git a/CHANGELOG.md b/CHANGELOG.md index e0c16452..816fa3ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,17 +29,18 @@ This release aims to make the code a bit cleaner, fix a couple of bugs and provi - `prompt.Lexer`, `prompt.Token`, `prompt.SimpleToken`, `prompt.EagerLexer`, `prompt.LexerFunc` -- new syntax highlighting functionality - `prompt.ExecuteOnEnterCallback` -- new dynamic Enter functionality (decide whether to insert a newline and indent or execute the input) -- `_example/bang-executor` -- a sample program which uses the new `ExecuteOnEnterCallback`. Pressing Enter will insert a newline unless the input ends with an exclamation point `!` (then it gets executed). -- `_example/even-lexer` -- a sample program which shows how to use the new lexer feature. It implements a simple lexer which colours every character with an even index green. +- examples: + - `_example/bang-executor` -- a sample program which uses the new `ExecuteOnEnterCallback`. Pressing Enter will insert a newline unless the input ends with an exclamation point `!` (then it gets executed). + - `_example/even-lexer` -- a sample program which shows how to use the new lexer feature. It implements a simple lexer which colours every character with an even index green. ### Changed - Update Go from 1.16 to 1.19 -- The cursor can move in 2D. +- The cursor can move in 2D (left-right, up-down) - The Up arrow key will jump to the line above if the cursor is beyond the first line, but it will replace the input with the previous history entry if it's on the first line (like in Ruby's irb) - The Down arrow key will jump to the line below if the cursor is before the last line, but it will replace the input with the next history entry if it's on the last line (like in Ruby's irb) - Tab will insert a single indentation level when there are no suggestions -- Shift + Tab will delete a single indentation level when there are no suggestions and the line before the cursors consists only of indentation (spaces) +- Shift + Tab will delete a single indentation level when there are no suggestions and the line before the cursor consists only of indentation (spaces) - Make `Completer` optional when creating a new `prompt.Prompt`. Change the signature of `prompt.New` from `func New(Executor, Completer, ...Option) *Prompt` to `func New(Executor, ...Option) *Prompt` - Make `prefix` optional in `prompt.Input`. Change the signature of `prompt.Input` from `func Input(string, ...Option) string` to `func Input(...Option) string`. - Rename `prompt.ConsoleParser` to `prompt.Reader` and make it embed `io.ReadCloser` @@ -77,9 +78,9 @@ This release aims to make the code a bit cleaner, fix a couple of bugs and provi ### Fixed - Make pasting multiline text work properly -- Make pasting text with tabs work properly (tabs get replaced with spaces) +- Make pasting text with tabs work properly (tabs get replaced with indentation -- spaces) - Introduce `strings.ByteNumber`, `strings.RuneNumber`, `strings.StringWidth` to reduce the ambiguity of when to use which of the three main units used by this library to measure string length and index parts of strings. Several subtle bugs (using the wrong unit) causing panics have been fixed this way. -- Remove a `/dev/tty` leak in `PosixReader` (old `PosixParser`) +- Remove a `/dev/tty` leak in `prompt.PosixReader` (old `prompt.PosixParser`) ### Removed