Support CJK and Cyrillic characters

This commit is contained in:
c-bata 2018-06-22 01:11:58 +09:00
parent 2b80a3f52c
commit 82330a197a
10 changed files with 548 additions and 256 deletions

14
Gopkg.lock generated
View File

@ -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

View File

@ -32,3 +32,7 @@
[[constraint]]
branch = "master"
name = "github.com/pkg/term"
[[constraint]]
branch = "master"
name = "github.com/mattn/go-runewidth"

View File

@ -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()
}

View File

@ -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:])
}
}

View File

@ -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"

View File

@ -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

View File

@ -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.

View File

@ -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")

View File

@ -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)
}
}

View File

@ -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)