diff --git a/Gopkg.lock b/Gopkg.lock index b6e96ff..1b6866b 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -13,27 +13,33 @@ revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" version = "v0.0.3" +[[projects]] + branch = "master" + name = "github.com/mattn/go-runewidth" + packages = ["."] + revision = "ce7b0b5c7b45a81508558cd1dba6bb1e4ddb51bb" + [[projects]] branch = "master" name = "github.com/mattn/go-tty" packages = ["."] - revision = "061c12e2dc3ef933c21c2249823e6f42e6935c40" + revision = "931426f7535ac39720c8909d70ece5a41a2502a6" [[projects]] branch = "master" name = "github.com/pkg/term" packages = ["termios"] - revision = "b1f72af2d63057363398bec5873d16a98b453312" + revision = "cda20d4ac917ad418d86e151eff439648b06185b" [[projects]] branch = "master" name = "golang.org/x/sys" packages = ["unix"] - revision = "37707fdb30a5b38865cfb95e5aab41707daec7fd" + revision = "ad87a3a340fa7f3bed189293fbfa7a9b7e021ae1" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "6c442f55617e93df75aa1ee2fd2b36cfab23003fded4723be56c6b6fd0545c56" + inputs-digest = "d6a0ea9e49092cfd8cb3d6077c97a938de7c39195b83828dae2a0befdd207ffd" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index fe31c57..903f53b 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -32,3 +32,7 @@ [[constraint]] branch = "master" name = "github.com/pkg/term" + +[[constraint]] + branch = "master" + name = "github.com/mattn/go-runewidth" diff --git a/_example/hello-cjk-cyrillic/main.go b/_example/hello-cjk-cyrillic/main.go new file mode 100644 index 0000000..f734e98 --- /dev/null +++ b/_example/hello-cjk-cyrillic/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "fmt" + + "github.com/c-bata/go-prompt" +) + +func executor(in string) { + fmt.Println("Your input: " + in) +} + +func completer(in prompt.Document) []prompt.Suggest { + s := []prompt.Suggest{ + {Text: "こんにちは", Description: "'こんにちは' means 'Hello' in Japanese"}, + {Text: "감사합니다", Description: "'안녕하세요' means 'Hello' in Korean."}, + {Text: "您好", Description: "'您好' means 'Hello' in Chinese."}, + {Text: "Добрый день", Description: "'Добрый день' means 'Hello' in Russian."}, + } + return prompt.FilterHasPrefix(s, in.GetWordBeforeCursor(), true) +} + +func main() { + p := prompt.New( + executor, + completer, + prompt.OptionPrefix(">>> "), + prompt.OptionTitle("sql-prompt for multi width characters"), + ) + p.Run() +} diff --git a/buffer.go b/buffer.go index 929d2fc..6c29b46 100644 --- a/buffer.go +++ b/buffer.go @@ -9,7 +9,7 @@ import ( type Buffer struct { workingLines []string // The working lines. Similar to history workingIndex int - CursorPosition int + cursorPosition int cacheDocument *Document preferredColumn int // Remember the original column for the next up/down movement. } @@ -23,19 +23,25 @@ func (b *Buffer) Text() string { func (b *Buffer) Document() (d *Document) { if b.cacheDocument == nil || b.cacheDocument.Text != b.Text() || - b.cacheDocument.CursorPosition != b.CursorPosition { + b.cacheDocument.cursorPosition != b.cursorPosition { b.cacheDocument = &Document{ Text: b.Text(), - CursorPosition: b.CursorPosition, + cursorPosition: b.cursorPosition, } } return b.cacheDocument } +// 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() +} + // InsertText insert string from current line. func (b *Buffer) InsertText(v string, overwrite bool, moveCursor bool) { or := []rune(b.Text()) - oc := b.CursorPosition + oc := b.cursorPosition if overwrite { overwritten := string(or[oc : oc+len(v)]) @@ -49,15 +55,15 @@ func (b *Buffer) InsertText(v string, overwrite bool, moveCursor bool) { } if moveCursor { - b.CursorPosition += len([]rune(v)) + b.cursorPosition += len([]rune(v)) } } -// SetText method to set text and update CursorPosition. +// 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) { - if b.CursorPosition > len([]rune(v)) { + if b.cursorPosition > len([]rune(v)) { log.Print("[ERROR] The length of input value should be shorter than the position of cursor.") } o := b.workingLines[b.workingIndex] @@ -72,11 +78,11 @@ func (b *Buffer) setText(v string) { // Set cursor position. Return whether it changed. func (b *Buffer) setCursorPosition(p int) { - o := b.CursorPosition + o := b.cursorPosition if p > 0 { - b.CursorPosition = p + b.cursorPosition = p } else { - b.CursorPosition = 0 + b.cursorPosition = 0 } if p != o { // Cursor position is changed. @@ -86,21 +92,21 @@ func (b *Buffer) setCursorPosition(p int) { func (b *Buffer) setDocument(d *Document) { b.cacheDocument = d - b.setCursorPosition(d.CursorPosition) // Call before setText because setText check the relation between cursorPosition and line length. + b.setCursorPosition(d.cursorPosition) // Call before setText because setText check the relation between cursorPosition and line length. b.setText(d.Text) } // CursorLeft move to left on the current line. func (b *Buffer) CursorLeft(count int) { l := b.Document().GetCursorLeftPosition(count) - b.CursorPosition += l + b.cursorPosition += l return } // CursorRight move to right on the current line. func (b *Buffer) CursorRight(count int) { l := b.Document().GetCursorRightPosition(count) - b.CursorPosition += l + b.cursorPosition += l return } @@ -111,7 +117,7 @@ func (b *Buffer) CursorUp(count int) { if b.preferredColumn == -1 { // -1 means nil orig = b.Document().CursorPositionCol() } - b.CursorPosition += b.Document().GetCursorUpPosition(count, orig) + b.cursorPosition += b.Document().GetCursorUpPosition(count, orig) // Remember the original column for the next up/down movement. b.preferredColumn = orig @@ -124,7 +130,7 @@ func (b *Buffer) CursorDown(count int) { if b.preferredColumn == -1 { // -1 means nil orig = b.Document().CursorPositionCol() } - b.CursorPosition += b.Document().GetCursorDownPosition(count, orig) + b.cursorPosition += b.Document().GetCursorDownPosition(count, orig) // Remember the original column for the next up/down movement. b.preferredColumn = orig @@ -137,15 +143,15 @@ func (b *Buffer) DeleteBeforeCursor(count int) (deleted string) { } r := []rune(b.Text()) - if b.CursorPosition > 0 { - start := b.CursorPosition - count + if b.cursorPosition > 0 { + start := b.cursorPosition - count if start < 0 { start = 0 } - deleted = string(r[start:b.CursorPosition]) + deleted = string(r[start:b.cursorPosition]) b.setDocument(&Document{ - Text: string(r[:start]) + string(r[b.CursorPosition:]), - CursorPosition: b.CursorPosition - len([]rune(deleted)), + Text: string(r[:start]) + string(r[b.cursorPosition:]), + cursorPosition: b.cursorPosition - len([]rune(deleted)), }) } return @@ -163,9 +169,9 @@ func (b *Buffer) NewLine(copyMargin bool) { // Delete specified number of characters and Return the deleted text. func (b *Buffer) Delete(count int) (deleted string) { r := []rune(b.Text()) - if b.CursorPosition < len(r) { + if b.cursorPosition < len(r) { deleted = b.Document().TextAfterCursor()[:count] - b.setText(string(r[:b.CursorPosition]) + string(r[b.CursorPosition+len(deleted):])) + b.setText(string(r[:b.cursorPosition]) + string(r[b.cursorPosition+len(deleted):])) } return } @@ -173,7 +179,7 @@ func (b *Buffer) Delete(count int) (deleted string) { // JoinNextLine joins the next line to the current one by deleting the line ending after the current line. func (b *Buffer) JoinNextLine(separator string) { if !b.Document().OnLastLine() { - b.CursorPosition += b.Document().GetEndOfLinePosition() + b.cursorPosition += b.Document().GetEndOfLinePosition() b.Delete(1) // Remove spaces b.setText(b.Document().TextBeforeCursor() + separator + strings.TrimLeft(b.Document().TextAfterCursor(), " ")) @@ -182,10 +188,10 @@ func (b *Buffer) JoinNextLine(separator string) { // SwapCharactersBeforeCursor swaps the last two characters before the cursor. func (b *Buffer) SwapCharactersBeforeCursor() { - if b.CursorPosition >= 2 { - x := b.Text()[b.CursorPosition-2 : b.CursorPosition-1] - y := b.Text()[b.CursorPosition-1 : b.CursorPosition] - b.setText(b.Text()[:b.CursorPosition-2] + y + x + b.Text()[b.CursorPosition:]) + if b.cursorPosition >= 2 { + x := b.Text()[b.cursorPosition-2 : b.cursorPosition-1] + y := b.Text()[b.cursorPosition-1 : b.cursorPosition] + b.setText(b.Text()[:b.cursorPosition-2] + y + x + b.Text()[b.cursorPosition:]) } } diff --git a/buffer_test.go b/buffer_test.go index 2dff255..dceb3ce 100644 --- a/buffer_test.go +++ b/buffer_test.go @@ -23,8 +23,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 != len("some_text") { + t.Errorf("cursorPosition should be %#v, got %#v", len("some_text"), b.cursorPosition) } } @@ -39,8 +39,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 != len("some_teA") { + t.Errorf("Text should be %#v, got %#v", len("some_teA"), b.cursorPosition) } // Moving over left character counts. @@ -49,8 +49,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 != len("A") { + t.Errorf("Text should be %#v, got %#v", len("some_teA"), b.cursorPosition) } // TODO: Going right already at right end. @@ -69,43 +69,43 @@ func TestBuffer_CursorUp(t *testing.T) { b := NewBuffer() b.InsertText("long line1\nline2", false, true) b.CursorUp(1) - if b.Document().CursorPosition != 5 { - t.Errorf("Should be %#v, got %#v", 5, b.Document().CursorPosition) + if b.Document().cursorPosition != 5 { + t.Errorf("Should be %#v, got %#v", 5, b.Document().cursorPosition) } // Going up when already at the top. b.CursorUp(1) - if b.Document().CursorPosition != 5 { - t.Errorf("Should be %#v, got %#v", 5, b.Document().CursorPosition) + if b.Document().cursorPosition != 5 { + t.Errorf("Should be %#v, got %#v", 5, b.Document().cursorPosition) } // Going up to a line that's shorter. b.setDocument(&Document{}) b.InsertText("line1\nlong line2", false, true) b.CursorUp(1) - if b.Document().CursorPosition != 5 { - t.Errorf("Should be %#v, got %#v", 5, b.Document().CursorPosition) + if b.Document().cursorPosition != 5 { + t.Errorf("Should be %#v, got %#v", 5, b.Document().cursorPosition) } } func TestBuffer_CursorDown(t *testing.T) { b := NewBuffer() b.InsertText("line1\nline2", false, true) - b.CursorPosition = 3 + b.cursorPosition = 3 // 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 != len("line1\nlin") { + t.Errorf("Should be %#v, got %#v", len("line1\nlin"), b.Document().cursorPosition) } // Going down to a line that's storter. b = NewBuffer() b.InsertText("long line1\na\nb", false, true) - b.CursorPosition = 3 + b.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 != len("long line1\na") { + t.Errorf("Should be %#v, got %#v", len("long line1\na"), b.Document().cursorPosition) } } @@ -121,8 +121,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 != len("some_t") { + t.Errorf("Should be %#v, got %#v", len("some_t"), b.cursorPosition) } // Delete over the characters length before cursor. @@ -176,7 +176,7 @@ func TestBuffer_JoinNextLine(t *testing.T) { // Test when there is no '\n' in the text b = NewBuffer() b.InsertText("line1", false, true) - b.CursorPosition = 0 + b.cursorPosition = 0 b.JoinNextLine(" ") ac = b.Text() ex = "line1" diff --git a/completion.go b/completion.go index 0c0f54b..6054437 100644 --- a/completion.go +++ b/completion.go @@ -3,16 +3,21 @@ package prompt import ( "log" "strings" + + "github.com/mattn/go-runewidth" ) const ( - shortenSuffix = "..." - leftPrefix = " " - leftSuffix = " " - rightPrefix = " " - rightSuffix = " " - leftMargin = len(leftPrefix + leftSuffix) - rightMargin = len(rightPrefix + rightSuffix) + shortenSuffix = "..." + leftPrefix = " " + leftSuffix = " " + rightPrefix = " " + rightSuffix = " " +) + +var ( + leftMargin = runewidth.StringWidth(leftPrefix + leftSuffix) + rightMargin = runewidth.StringWidth(rightPrefix + rightSuffix) completionMargin = leftMargin + rightMargin ) @@ -106,13 +111,14 @@ func formatTexts(o []string, max int, prefix, suffix string) (new []string, widt l := len(o) n := make([]string, l) - lenPrefix := len([]rune(prefix)) - lenSuffix := len([]rune(suffix)) - lenShorten := len(shortenSuffix) + lenPrefix := runewidth.StringWidth(prefix) + lenSuffix := runewidth.StringWidth(suffix) + lenShorten := runewidth.StringWidth(shortenSuffix) min := lenPrefix + lenSuffix + lenShorten for i := 0; i < l; i++ { - if width < len([]rune(o[i])) { - width = len([]rune(o[i])) + w := runewidth.StringWidth(o[i]) + if width < w { + width = w } } @@ -128,13 +134,12 @@ func formatTexts(o []string, max int, prefix, suffix string) (new []string, widt } for i := 0; i < l; i++ { - r := []rune(o[i]) - x := len(r) + x := runewidth.StringWidth(o[i]) if x <= width { spaces := strings.Repeat(" ", width-x) n[i] = prefix + o[i] + spaces + suffix } else if x > width { - n[i] = prefix + string(r[:width-lenShorten]) + shortenSuffix + suffix + n[i] = prefix + runewidth.Truncate(o[i], width, "...") + suffix } } return n, lenPrefix + width + lenSuffix diff --git a/document.go b/document.go index 4ff06eb..10846ca 100644 --- a/document.go +++ b/document.go @@ -4,22 +4,38 @@ import ( "sort" "strings" "unicode/utf8" + + "github.com/mattn/go-runewidth" ) // Document has text displayed in terminal and cursor position. type Document struct { - Text string - CursorPosition int + Text string + // 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 } // NewDocument return the new empty document. func NewDocument() *Document { return &Document{ Text: "", - CursorPosition: 0, + cursorPosition: 0, } } +// 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 +} + // GetCharRelativeToCursor return character relative to cursor position, or empty string func (d *Document) GetCharRelativeToCursor(offset int) (r rune) { s := d.Text @@ -28,7 +44,7 @@ func (d *Document) GetCharRelativeToCursor(offset int) (r rune) { for len(s) > 0 { cnt++ r, size := utf8.DecodeRuneInString(s) - if cnt == d.CursorPosition+offset { + if cnt == d.cursorPosition+offset { return r } s = s[size:] @@ -39,13 +55,13 @@ func (d *Document) GetCharRelativeToCursor(offset int) (r rune) { // TextBeforeCursor returns the text before the cursor. func (d *Document) TextBeforeCursor() string { r := []rune(d.Text) - return string(r[:d.CursorPosition]) + return string(r[:d.cursorPosition]) } // TextAfterCursor returns the text after the cursor. func (d *Document) TextAfterCursor() string { r := []rune(d.Text) - return string(r[d.CursorPosition:]) + return string(r[d.cursorPosition:]) } // GetWordBeforeCursor returns the word before the cursor. @@ -95,7 +111,7 @@ func (d *Document) FindEndOfCurrentWord() int { if i := strings.IndexByte(x, ' '); i != -1 { return i } else { - return len(x) + return len([]rune(x)) } } @@ -189,7 +205,7 @@ func (d *Document) findLineStartIndex(index int) (pos int, lineStartIndex int) { // CursorPositionRow returns the current row. (0-based.) func (d *Document) CursorPositionRow() (row int) { - row, _ = d.findLineStartIndex(d.CursorPosition) + row, _ = d.findLineStartIndex(d.cursorPosition) return } @@ -197,8 +213,8 @@ func (d *Document) CursorPositionRow() (row int) { 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 + _, index := d.findLineStartIndex(d.cursorPosition) + col = d.cursorPosition - index return } @@ -238,7 +254,7 @@ func (d *Document) GetCursorUpPosition(count int, preferredColumn int) int { if row < 0 { row = 0 } - return d.TranslateRowColToIndex(row, col) - d.CursorPosition + return d.TranslateRowColToIndex(row, col) - d.cursorPosition } // GetCursorDownPosition return the relative cursor position (character index) where we would be if the @@ -251,7 +267,7 @@ func (d *Document) GetCursorDownPosition(count int, preferredColumn int) int { col = preferredColumn } row := d.CursorPositionRow() + count - return d.TranslateRowColToIndex(row, col) - d.CursorPosition + return d.TranslateRowColToIndex(row, col) - d.cursorPosition } // Lines returns the array of all the lines. diff --git a/document_test.go b/document_test.go index 7b1d816..5e56fc3 100644 --- a/document_test.go +++ b/document_test.go @@ -6,27 +6,111 @@ import ( "unicode/utf8" ) -func TestDocument_GetCharRelativeToCursor(t *testing.T) { - d := &Document{ - Text: "line 1\nline 2\nline 3\nline 4\n", - CursorPosition: len([]rune("line 1\n" + "lin")), +func TestDocument_DisplayCursorPosition(t *testing.T) { + patterns := []struct { + document *Document + expected int + }{ + { + document: &Document{ + Text: "hello", + cursorPosition: 2, + }, + expected: 2, + }, + { + document: &Document{ + Text: "こんにちは", + cursorPosition: 2, + }, + expected: 4, + }, + { + document: &Document{ + Text: "Добрый день", + cursorPosition: 3, + }, + expected: 3, + }, } - ac := d.GetCharRelativeToCursor(1) - ex, _ := utf8.DecodeRuneInString("e") - if ac != ex { - t.Errorf("Should be %#v, got %#v", ex, ac) + + for _, p := range patterns { + ac := p.document.DisplayCursorPosition() + if ac != p.expected { + t.Errorf("Should be %#v, got %#v", p.expected, ac) + } + } +} + +func TestDocument_GetCharRelativeToCursor(t *testing.T) { + patterns := []struct { + document *Document + expected string + }{ + { + document: &Document{ + Text: "line 1\nline 2\nline 3\nline 4\n", + cursorPosition: len([]rune("line 1\n" + "lin")), + }, + expected: "e", + }, + { + document: &Document{ + Text: "あいうえお\nかきくけこ\nさしすせそ\nたちつてと\n", + cursorPosition: 8, + }, + expected: "く", + }, + { + document: &Document{ + Text: "Добрый\nдень\nДобрый день", + cursorPosition: 9, + }, + expected: "н", + }, + } + + for i, p := range patterns { + ac := p.document.GetCharRelativeToCursor(1) + ex, _ := utf8.DecodeRuneInString(p.expected) + if ac != ex { + t.Errorf("[%d] Should be %s, got %s", i, string(ex), string(ac)) + } } } func TestDocument_TextBeforeCursor(t *testing.T) { - d := &Document{ - Text: "line 1\nline 2\nline 3\nline 4\n", - CursorPosition: len("line 1\n" + "lin"), + patterns := []struct { + document *Document + expected string + }{ + { + document: &Document{ + Text: "line 1\nline 2\nline 3\nline 4\n", + cursorPosition: len("line 1\n" + "lin"), + }, + expected: "line 1\nlin", + }, + { + document: &Document{ + Text: "あいうえお\nかきくけこ\nさしすせそ\nたちつてと\n", + cursorPosition: 8, + }, + expected: "あいうえお\nかき", + }, + { + document: &Document{ + Text: "Добрый\nдень\nДобрый день", + cursorPosition: 9, + }, + expected: "Добрый\nде", + }, } - ac := d.TextBeforeCursor() - ex := "line 1\nlin" - if ac != ex { - t.Errorf("Should be %#v, got %#v", ex, ac) + for i, p := range patterns { + ac := p.document.TextBeforeCursor() + if ac != p.expected { + t.Errorf("[%d] Should be %s, got %s", i, p.expected, ac) + } } } @@ -38,23 +122,37 @@ 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: len("line 1\n" + "lin"), }, expected: "e 2\nline 3\nline 4\n", }, { document: &Document{ Text: "", - CursorPosition: 0, + cursorPosition: 0, }, expected: "", }, + { + document: &Document{ + Text: "あいうえお\nかきくけこ\nさしすせそ\nたちつてと\n", + cursorPosition: 8, + }, + expected: "くけこ\nさしすせそ\nたちつてと\n", + }, + { + document: &Document{ + Text: "Добрый\nдень\nДобрый день", + cursorPosition: 9, + }, + expected: "нь\nДобрый день", + }, } - for _, p := range pattern { + for i, p := range pattern { ac := p.document.TextAfterCursor() if ac != p.expected { - t.Errorf("Should be %#v, got %#v", p.expected, ac) + t.Errorf("[%d] Should be %#v, got %#v", i, p.expected, ac) } } } @@ -67,21 +165,164 @@ func TestDocument_GetWordBeforeCursor(t *testing.T) { { document: &Document{ Text: "apple bana", - CursorPosition: len("apple bana"), + cursorPosition: len("apple bana"), }, expected: "bana", }, { document: &Document{ Text: "apple ", - CursorPosition: len("apple "), + cursorPosition: len("apple "), }, expected: "", }, + { + document: &Document{ + Text: "あいうえお かきくけこ さしすせそ", + cursorPosition: 8, + }, + expected: "かき", + }, + { + document: &Document{ + Text: "Добрый день Добрый день", + cursorPosition: 9, + }, + expected: "де", + }, + } + + for i, p := range pattern { + ac := p.document.GetWordBeforeCursor() + if ac != p.expected { + t.Errorf("[%d] Should be %#v, got %#v", i, p.expected, ac) + } + } +} + +func TestDocument_GetWordBeforeCursorWithSpace(t *testing.T) { + pattern := []struct { + document *Document + expected string + }{ + { + document: &Document{ + Text: "apple bana ", + cursorPosition: len("apple bana "), + }, + expected: "bana ", + }, + { + document: &Document{ + Text: "apple ", + cursorPosition: len("apple "), + }, + expected: "apple ", + }, + { + document: &Document{ + Text: "あいうえお かきくけこ ", + cursorPosition: 12, + }, + expected: "かきくけこ ", + }, + { + document: &Document{ + Text: "Добрый день ", + cursorPosition: 12, + }, + expected: "день ", + }, } for _, p := range pattern { - ac := p.document.GetWordBeforeCursor() + ac := p.document.GetWordBeforeCursorWithSpace() + if ac != p.expected { + t.Errorf("Should be %#v, got %#v", p.expected, ac) + } + } +} + +func TestDocument_FindStartOfPreviousWord(t *testing.T) { + pattern := []struct { + document *Document + expected int + }{ + { + document: &Document{ + Text: "apple bana", + cursorPosition: len("apple bana"), + }, + expected: len("apple "), + }, + { + document: &Document{ + Text: "apple ", + cursorPosition: len("apple "), + }, + expected: len("apple "), + }, + { + document: &Document{ + Text: "あいうえお かきくけこ さしすせそ", + cursorPosition: 8, // between 'き' and 'く' + }, + expected: len("あいうえお "), // this function returns index byte in string + }, + { + document: &Document{ + Text: "Добрый день Добрый день", + cursorPosition: 9, + }, + expected: len("Добрый "), // this function returns index byte in string + }, + } + + for _, p := range pattern { + ac := p.document.FindStartOfPreviousWord() + if ac != p.expected { + t.Errorf("Should be %#v, got %#v", p.expected, ac) + } + } +} + +func TestDocument_FindStartOfPreviousWordWithSpace(t *testing.T) { + pattern := []struct { + document *Document + expected int + }{ + { + document: &Document{ + Text: "apple bana ", + cursorPosition: len("apple bana "), + }, + expected: len("apple "), + }, + { + document: &Document{ + Text: "apple ", + cursorPosition: len("apple "), + }, + expected: len(""), + }, + { + document: &Document{ + Text: "あいうえお かきくけこ ", + cursorPosition: 12, // cursor points to last + }, + expected: len("あいうえお "), // this function returns index byte in string + }, + { + document: &Document{ + Text: "Добрый день ", + cursorPosition: 12, + }, + expected: len("Добрый "), // this function returns index byte in string + }, + } + + for _, p := range pattern { + ac := p.document.FindStartOfPreviousWordWithSpace() if ac != p.expected { t.Errorf("Should be %#v, got %#v", p.expected, ac) } @@ -96,66 +337,51 @@ func TestDocument_GetWordAfterCursor(t *testing.T) { { document: &Document{ Text: "apple bana", - CursorPosition: len("apple bana"), + cursorPosition: len("apple bana"), }, expected: "", }, { document: &Document{ Text: "apple bana", - CursorPosition: len("apple "), + cursorPosition: len("apple "), }, expected: "bana", }, { document: &Document{ Text: "apple bana", - CursorPosition: len("apple"), + cursorPosition: len("apple"), }, expected: "", }, { document: &Document{ Text: "apple bana", - CursorPosition: len("ap"), + cursorPosition: len("ap"), }, expected: "ple", }, + { + document: &Document{ + Text: "あいうえお かきくけこ さしすせそ", + cursorPosition: 8, + }, + expected: "くけこ", + }, + { + document: &Document{ + Text: "Добрый день Добрый день", + cursorPosition: 9, + }, + expected: "нь", + }, } for k, p := range pattern { ac := p.document.GetWordAfterCursor() if ac != p.expected { - t.Errorf("[%d]Should be %#v, got %#v", k, p.expected, ac) - } - } -} - -func TestDocument_GetWordBeforeCursorWithSpace(t *testing.T) { - pattern := []struct { - document *Document - expected string - }{ - { - document: &Document{ - Text: "apple bana ", - CursorPosition: len("apple bana "), - }, - expected: "bana ", - }, - { - document: &Document{ - Text: "apple ", - CursorPosition: len("apple "), - }, - expected: "apple ", - }, - } - - for _, p := range pattern { - ac := p.document.GetWordBeforeCursorWithSpace() - if ac != p.expected { - t.Errorf("Should be %#v, got %#v", p.expected, ac) + t.Errorf("[%d] Should be %#v, got %#v", k, p.expected, ac) } } } @@ -168,31 +394,45 @@ func TestDocument_GetWordAfterCursorWithSpace(t *testing.T) { { document: &Document{ Text: "apple bana", - CursorPosition: len("apple bana"), + cursorPosition: len("apple bana"), }, expected: "", }, { document: &Document{ Text: "apple bana", - CursorPosition: len("apple "), + cursorPosition: len("apple "), }, expected: "bana", }, { document: &Document{ Text: "apple bana", - CursorPosition: len("apple"), + cursorPosition: len("apple"), }, expected: " bana", }, { document: &Document{ Text: "apple bana", - CursorPosition: len("ap"), + cursorPosition: len("ap"), }, expected: "ple", }, + { + document: &Document{ + Text: "あいうえお かきくけこ さしすせそ", + cursorPosition: 5, + }, + expected: " かきくけこ", + }, + { + document: &Document{ + Text: "Добрый день Добрый день", + cursorPosition: 6, + }, + expected: " день", + }, } for k, p := range pattern { @@ -203,35 +443,6 @@ func TestDocument_GetWordAfterCursorWithSpace(t *testing.T) { } } -func TestDocument_FindStartOfPreviousWord(t *testing.T) { - pattern := []struct { - document *Document - expected int - }{ - { - document: &Document{ - Text: "apple bana", - CursorPosition: len("apple bana"), - }, - expected: len("apple "), - }, - { - document: &Document{ - Text: "apple ", - CursorPosition: len("apple "), - }, - expected: len("apple "), - }, - } - - for _, p := range pattern { - ac := p.document.FindStartOfPreviousWord() - if ac != p.expected { - t.Errorf("Should be %#v, got %#v", p.expected, ac) - } - } -} - func TestDocument_FindEndOfCurrentWord(t *testing.T) { pattern := []struct { document *Document @@ -240,66 +451,60 @@ func TestDocument_FindEndOfCurrentWord(t *testing.T) { { document: &Document{ Text: "apple bana", - CursorPosition: len("apple bana"), + cursorPosition: len("apple bana"), }, expected: len(""), }, { document: &Document{ Text: "apple bana", - CursorPosition: len("apple "), + cursorPosition: len("apple "), }, expected: len("bana"), }, { document: &Document{ Text: "apple bana", - CursorPosition: len("apple"), + cursorPosition: len("apple"), }, expected: len(""), }, { document: &Document{ Text: "apple bana", - CursorPosition: len("ap"), + cursorPosition: len("ap"), }, expected: len("ple"), }, + { + // りん(cursor)ご ばなな + document: &Document{ + Text: "りんご ばなな", + cursorPosition: 2, + }, + expected: len("ご"), + }, + { + document: &Document{ + Text: "りんご ばなな", + cursorPosition: 3, + }, + expected: 0, + }, + { + // Доб(cursor)рый день + document: &Document{ + Text: "Добрый день", + cursorPosition: 3, + }, + expected: len("рый"), + }, } for k, p := range pattern { ac := p.document.FindEndOfCurrentWord() if ac != p.expected { - t.Errorf("[%d]Should be %#v, got %#v", k, p.expected, ac) - } - } -} - -func TestDocument_FindStartOfPreviousWordWithSpace(t *testing.T) { - pattern := []struct { - document *Document - expected int - }{ - { - document: &Document{ - Text: "apple bana ", - CursorPosition: len("apple bana "), - }, - expected: len("apple "), - }, - { - document: &Document{ - Text: "apple ", - CursorPosition: len("apple "), - }, - expected: len(""), - }, - } - - for _, p := range pattern { - ac := p.document.FindStartOfPreviousWordWithSpace() - if ac != p.expected { - t.Errorf("Should be %#v, got %#v", p.expected, ac) + t.Errorf("[%d] Should be %#v, got %#v", k, p.expected, ac) } } } @@ -312,37 +517,58 @@ func TestDocument_FindEndOfCurrentWordWithSpace(t *testing.T) { { document: &Document{ Text: "apple bana", - CursorPosition: len("apple bana"), + cursorPosition: len("apple bana"), }, expected: len(""), }, { document: &Document{ Text: "apple bana", - CursorPosition: len("apple "), + cursorPosition: len("apple "), }, expected: len("bana"), }, { document: &Document{ Text: "apple bana", - CursorPosition: len("apple"), + cursorPosition: len("apple"), }, expected: len(" bana"), }, { document: &Document{ Text: "apple bana", - CursorPosition: len("ap"), + cursorPosition: len("ap"), }, expected: len("ple"), }, + { + document: &Document{ + Text: "あいうえお かきくけこ", + cursorPosition: 6, + }, + expected: len("かきくけこ"), + }, + { + document: &Document{ + Text: "あいうえお かきくけこ", + cursorPosition: 5, + }, + expected: len(" かきくけこ"), + }, + { + document: &Document{ + Text: "Добрый день", + cursorPosition: 6, + }, + expected: len(" день"), + }, } for k, p := range pattern { ac := p.document.FindEndOfCurrentWordWithSpace() if ac != p.expected { - t.Errorf("[%d]Should be %#v, got %#v", k, p.expected, ac) + t.Errorf("[%d] Should be %#v, got %#v", k, p.expected, ac) } } } @@ -350,7 +576,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: len("line 1\n" + "lin"), } ac := d.CurrentLineBeforeCursor() ex := "lin" @@ -362,7 +588,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: len("line 1\n" + "lin"), } ac := d.CurrentLineAfterCursor() ex := "e 2" @@ -374,7 +600,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: len("line 1\n" + "lin"), } ac := d.CurrentLine() ex := "line 2" @@ -383,27 +609,23 @@ func TestDocument_CurrentLine(t *testing.T) { } } -// Table Driven Tests for CursorPositionRow and CursorPositionCol -type cursorPositionTest struct { - document *Document - expectedRow int - expectedCol int -} - -var cursorPositionTests = []cursorPositionTest{ - { - document: &Document{Text: "line 1\nline 2\nline 3\n", CursorPosition: len("line 1\n" + "lin")}, - expectedRow: 1, - expectedCol: 3, - }, - { - document: &Document{Text: "", CursorPosition: 0}, - expectedRow: 0, - expectedCol: 0, - }, -} - func TestDocument_CursorPositionRowAndCol(t *testing.T) { + var cursorPositionTests = []struct { + document *Document + expectedRow int + expectedCol int + }{ + { + document: &Document{Text: "line 1\nline 2\nline 3\n", cursorPosition: len("line 1\n" + "lin")}, + expectedRow: 1, + expectedCol: 3, + }, + { + document: &Document{Text: "", cursorPosition: 0}, + expectedRow: 0, + expectedCol: 0, + }, + } for _, test := range cursorPositionTests { ac := test.document.CursorPositionRow() if ac != test.expectedRow { @@ -419,7 +641,7 @@ 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: len("line 1\n" + "line 2\n" + "lin"), } ac := d.GetCursorLeftPosition(2) ex := -2 @@ -436,7 +658,7 @@ 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: len("line 1\n" + "line 2\n" + "lin"), } ac := d.GetCursorUpPosition(2, -1) ex := len("lin") - len("line 1\n"+"line 2\n"+"lin") @@ -454,7 +676,7 @@ 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: len("lin"), } ac := d.GetCursorDownPosition(2, -1) ex := len("line 1\n"+"line 2\n"+"lin") - len("lin") @@ -472,7 +694,7 @@ 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: len("line 1\n" + "line 2\n" + "lin"), } ac := d.GetCursorRightPosition(2) ex := 2 @@ -489,7 +711,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: len("line 1\n" + "lin"), } ac := d.Lines() ex := []string{"line 1", "line 2", "line 3", "line 4", ""} @@ -501,7 +723,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: len("line 1\n" + "lin"), } ac := d.LineCount() ex := 5 @@ -513,7 +735,7 @@ 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: len("line 1\n" + "lin"), } row, col := d.TranslateIndexToPosition(len("line 1\nline 2\nlin")) if row != 2 { @@ -534,7 +756,7 @@ 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: len("line 1\n" + "lin"), } ac := d.TranslateRowColToIndex(2, 3) ex := len("line 1\nline 2\nlin") @@ -551,13 +773,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: len("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 = len("line 1\nline 2\nline") ac = d.OnLastLine() if !ac { t.Errorf("Should be %#v, got %#v", true, ac) @@ -567,7 +789,7 @@ 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: len("line 1\nli"), } ac := d.GetEndOfLinePosition() ex := len("ne 2") diff --git a/emacs_test.go b/emacs_test.go index 081cb51..bfda59e 100644 --- a/emacs_test.go +++ b/emacs_test.go @@ -5,20 +5,20 @@ import "testing" func TestEmacsKeyBindings(t *testing.T) { buf := NewBuffer() buf.InsertText("abcde", false, true) - if buf.CursorPosition != len("abcde") { - t.Errorf("Want %d, but got %d", len("abcde"), buf.CursorPosition) + if buf.cursorPosition != len("abcde") { + t.Errorf("Want %d, but got %d", len("abcde"), buf.cursorPosition) } // Go to the beginning of the line applyEmacsKeyBind(buf, ControlA) - if buf.CursorPosition != 0 { - t.Errorf("Want %d, but got %d", 0, buf.CursorPosition) + if buf.cursorPosition != 0 { + t.Errorf("Want %d, but got %d", 0, buf.cursorPosition) } // Go to the end of the line applyEmacsKeyBind(buf, ControlE) - if buf.CursorPosition != len("abcde") { - t.Errorf("Want %d, but got %d", len("abcde"), buf.CursorPosition) + if buf.cursorPosition != len("abcde") { + t.Errorf("Want %d, but got %d", len("abcde"), buf.cursorPosition) } } diff --git a/render.go b/render.go index 9432f35..35fd963 100644 --- a/render.go +++ b/render.go @@ -2,6 +2,8 @@ package prompt import ( "runtime" + + "github.com/mattn/go-runewidth" ) // Render to render prompt information from state of Buffer. @@ -97,7 +99,7 @@ func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) { prefix := r.getCurrentPrefix() formatted, width := formatSuggestions( suggestions, - int(r.col)-len(prefix)-1, // -1 means a width of scrollbar + int(r.col)-runewidth.StringWidth(prefix)-1, // -1 means a width of scrollbar ) // +1 means a width of scrollbar. width++ @@ -109,7 +111,7 @@ func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) { formatted = formatted[completions.verticalScroll : completions.verticalScroll+windowHeight] r.prepareArea(windowHeight) - cursor := len(prefix) + len(buf.Document().TextBeforeCursor()) + cursor := runewidth.StringWidth(prefix) + runewidth.StringWidth(buf.Document().TextBeforeCursor()) x, _ := r.toPos(cursor) if x+width >= int(r.col) { cursor = r.backward(cursor, x+width-int(r.col)) @@ -178,7 +180,7 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager) { line := buffer.Text() prefix := r.getCurrentPrefix() - cursor := len(prefix) + len(line) + cursor := runewidth.StringWidth(prefix) + runewidth.StringWidth(line) // prepare area _, y := r.toPos(cursor) @@ -201,23 +203,23 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager) { r.out.EraseDown() - cursor = r.backward(cursor, len(line)-buffer.CursorPosition) + cursor = r.backward(cursor, runewidth.StringWidth(line)-buffer.DisplayCursorPosition()) r.renderCompletion(buffer, completion) if suggest, ok := completion.GetSelectedSuggestion(); ok { - cursor = r.backward(cursor, len(buffer.Document().GetWordBeforeCursor())) + cursor = r.backward(cursor, runewidth.StringWidth(buffer.Document().GetWordBeforeCursor())) r.out.SetColor(r.previewSuggestionTextColor, r.previewSuggestionBGColor, false) r.out.WriteStr(suggest.Text) r.out.SetColor(DefaultColor, DefaultColor, false) - cursor += len(suggest.Text) + cursor += runewidth.StringWidth(suggest.Text) rest := buffer.Document().TextAfterCursor() r.out.WriteStr(rest) - cursor += len(rest) + cursor += runewidth.StringWidth(rest) r.lineWrap(cursor) - cursor = r.backward(cursor, len(rest)) + cursor = r.backward(cursor, runewidth.StringWidth(rest)) } r.previousCursor = cursor } @@ -225,7 +227,7 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager) { // BreakLine to break line. func (r *Render) BreakLine(buffer *Buffer) { // Erasing and Render - cursor := len(buffer.Document().TextBeforeCursor()) + len(r.getCurrentPrefix()) + cursor := runewidth.StringWidth(buffer.Document().TextBeforeCursor()) + runewidth.StringWidth(r.getCurrentPrefix()) r.clear(cursor) r.renderPrefix() r.out.SetColor(r.inputTextColor, r.inputBGColor, false)