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

@ -13,27 +13,33 @@
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
version = "v0.0.3" version = "v0.0.3"
[[projects]]
branch = "master"
name = "github.com/mattn/go-runewidth"
packages = ["."]
revision = "ce7b0b5c7b45a81508558cd1dba6bb1e4ddb51bb"
[[projects]] [[projects]]
branch = "master" branch = "master"
name = "github.com/mattn/go-tty" name = "github.com/mattn/go-tty"
packages = ["."] packages = ["."]
revision = "061c12e2dc3ef933c21c2249823e6f42e6935c40" revision = "931426f7535ac39720c8909d70ece5a41a2502a6"
[[projects]] [[projects]]
branch = "master" branch = "master"
name = "github.com/pkg/term" name = "github.com/pkg/term"
packages = ["termios"] packages = ["termios"]
revision = "b1f72af2d63057363398bec5873d16a98b453312" revision = "cda20d4ac917ad418d86e151eff439648b06185b"
[[projects]] [[projects]]
branch = "master" branch = "master"
name = "golang.org/x/sys" name = "golang.org/x/sys"
packages = ["unix"] packages = ["unix"]
revision = "37707fdb30a5b38865cfb95e5aab41707daec7fd" revision = "ad87a3a340fa7f3bed189293fbfa7a9b7e021ae1"
[solve-meta] [solve-meta]
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
inputs-digest = "6c442f55617e93df75aa1ee2fd2b36cfab23003fded4723be56c6b6fd0545c56" inputs-digest = "d6a0ea9e49092cfd8cb3d6077c97a938de7c39195b83828dae2a0befdd207ffd"
solver-name = "gps-cdcl" solver-name = "gps-cdcl"
solver-version = 1 solver-version = 1

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

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

@ -9,7 +9,7 @@ import (
type Buffer struct { type Buffer struct {
workingLines []string // The working lines. Similar to history workingLines []string // The working lines. Similar to history
workingIndex int workingIndex int
CursorPosition int cursorPosition int
cacheDocument *Document cacheDocument *Document
preferredColumn int // Remember the original column for the next up/down movement. 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) { func (b *Buffer) Document() (d *Document) {
if b.cacheDocument == nil || if b.cacheDocument == nil ||
b.cacheDocument.Text != b.Text() || b.cacheDocument.Text != b.Text() ||
b.cacheDocument.CursorPosition != b.CursorPosition { b.cacheDocument.cursorPosition != b.cursorPosition {
b.cacheDocument = &Document{ b.cacheDocument = &Document{
Text: b.Text(), Text: b.Text(),
CursorPosition: b.CursorPosition, cursorPosition: b.cursorPosition,
} }
} }
return b.cacheDocument 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. // InsertText insert string from current line.
func (b *Buffer) InsertText(v string, overwrite bool, moveCursor bool) { func (b *Buffer) InsertText(v string, overwrite bool, moveCursor bool) {
or := []rune(b.Text()) or := []rune(b.Text())
oc := b.CursorPosition oc := b.cursorPosition
if overwrite { if overwrite {
overwritten := string(or[oc : oc+len(v)]) overwritten := string(or[oc : oc+len(v)])
@ -49,15 +55,15 @@ func (b *Buffer) InsertText(v string, overwrite bool, moveCursor bool) {
} }
if moveCursor { 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. // (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.) // text/cursor_position should be consistent at any time, otherwise set a Document instead.)
func (b *Buffer) setText(v string) { 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.") log.Print("[ERROR] The length of input value should be shorter than the position of cursor.")
} }
o := b.workingLines[b.workingIndex] o := b.workingLines[b.workingIndex]
@ -72,11 +78,11 @@ func (b *Buffer) setText(v string) {
// Set cursor position. Return whether it changed. // Set cursor position. Return whether it changed.
func (b *Buffer) setCursorPosition(p int) { func (b *Buffer) setCursorPosition(p int) {
o := b.CursorPosition o := b.cursorPosition
if p > 0 { if p > 0 {
b.CursorPosition = p b.cursorPosition = p
} else { } else {
b.CursorPosition = 0 b.cursorPosition = 0
} }
if p != o { if p != o {
// Cursor position is changed. // Cursor position is changed.
@ -86,21 +92,21 @@ func (b *Buffer) setCursorPosition(p int) {
func (b *Buffer) setDocument(d *Document) { func (b *Buffer) setDocument(d *Document) {
b.cacheDocument = d 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) b.setText(d.Text)
} }
// CursorLeft move to left on the current line. // CursorLeft move to left on the current line.
func (b *Buffer) CursorLeft(count int) { func (b *Buffer) CursorLeft(count int) {
l := b.Document().GetCursorLeftPosition(count) l := b.Document().GetCursorLeftPosition(count)
b.CursorPosition += l b.cursorPosition += l
return return
} }
// CursorRight move to right on the current line. // CursorRight move to right on the current line.
func (b *Buffer) CursorRight(count int) { func (b *Buffer) CursorRight(count int) {
l := b.Document().GetCursorRightPosition(count) l := b.Document().GetCursorRightPosition(count)
b.CursorPosition += l b.cursorPosition += l
return return
} }
@ -111,7 +117,7 @@ func (b *Buffer) CursorUp(count int) {
if b.preferredColumn == -1 { // -1 means nil if b.preferredColumn == -1 { // -1 means nil
orig = b.Document().CursorPositionCol() 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. // Remember the original column for the next up/down movement.
b.preferredColumn = orig b.preferredColumn = orig
@ -124,7 +130,7 @@ func (b *Buffer) CursorDown(count int) {
if b.preferredColumn == -1 { // -1 means nil if b.preferredColumn == -1 { // -1 means nil
orig = b.Document().CursorPositionCol() 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. // Remember the original column for the next up/down movement.
b.preferredColumn = orig b.preferredColumn = orig
@ -137,15 +143,15 @@ func (b *Buffer) DeleteBeforeCursor(count int) (deleted string) {
} }
r := []rune(b.Text()) r := []rune(b.Text())
if b.CursorPosition > 0 { if b.cursorPosition > 0 {
start := b.CursorPosition - count start := b.cursorPosition - count
if start < 0 { if start < 0 {
start = 0 start = 0
} }
deleted = string(r[start:b.CursorPosition]) deleted = string(r[start:b.cursorPosition])
b.setDocument(&Document{ b.setDocument(&Document{
Text: string(r[:start]) + string(r[b.CursorPosition:]), Text: string(r[:start]) + string(r[b.cursorPosition:]),
CursorPosition: b.CursorPosition - len([]rune(deleted)), cursorPosition: b.cursorPosition - len([]rune(deleted)),
}) })
} }
return return
@ -163,9 +169,9 @@ func (b *Buffer) NewLine(copyMargin bool) {
// Delete specified number of characters and Return the deleted text. // Delete specified number of characters and Return the deleted text.
func (b *Buffer) Delete(count int) (deleted string) { func (b *Buffer) Delete(count int) (deleted string) {
r := []rune(b.Text()) r := []rune(b.Text())
if b.CursorPosition < len(r) { if b.cursorPosition < len(r) {
deleted = b.Document().TextAfterCursor()[:count] 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 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. // JoinNextLine joins the next line to the current one by deleting the line ending after the current line.
func (b *Buffer) JoinNextLine(separator string) { func (b *Buffer) JoinNextLine(separator string) {
if !b.Document().OnLastLine() { if !b.Document().OnLastLine() {
b.CursorPosition += b.Document().GetEndOfLinePosition() b.cursorPosition += b.Document().GetEndOfLinePosition()
b.Delete(1) b.Delete(1)
// Remove spaces // Remove spaces
b.setText(b.Document().TextBeforeCursor() + separator + strings.TrimLeft(b.Document().TextAfterCursor(), " ")) 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. // SwapCharactersBeforeCursor swaps the last two characters before the cursor.
func (b *Buffer) SwapCharactersBeforeCursor() { func (b *Buffer) SwapCharactersBeforeCursor() {
if b.CursorPosition >= 2 { if b.cursorPosition >= 2 {
x := b.Text()[b.CursorPosition-2 : b.CursorPosition-1] x := b.Text()[b.cursorPosition-2 : b.cursorPosition-1]
y := b.Text()[b.CursorPosition-1 : b.CursorPosition] y := b.Text()[b.cursorPosition-1 : b.cursorPosition]
b.setText(b.Text()[:b.CursorPosition-2] + y + x + b.Text()[b.CursorPosition:]) b.setText(b.Text()[:b.cursorPosition-2] + y + x + b.Text()[b.cursorPosition:])
} }
} }

@ -23,8 +23,8 @@ func TestBuffer_InsertText(t *testing.T) {
t.Errorf("Text should be %#v, got %#v", "some_text", b.Text()) t.Errorf("Text should be %#v, got %#v", "some_text", b.Text())
} }
if b.CursorPosition != len("some_text") { if b.cursorPosition != len("some_text") {
t.Errorf("CursorPosition should be %#v, got %#v", len("some_text"), b.CursorPosition) 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" { if b.Text() != "some_teAxt" {
t.Errorf("Text should be %#v, got %#v", "some_teAxt", b.Text()) t.Errorf("Text should be %#v, got %#v", "some_teAxt", b.Text())
} }
if b.CursorPosition != len("some_teA") { if b.cursorPosition != len("some_teA") {
t.Errorf("Text should be %#v, got %#v", len("some_teA"), b.CursorPosition) t.Errorf("Text should be %#v, got %#v", len("some_teA"), b.cursorPosition)
} }
// Moving over left character counts. // Moving over left character counts.
@ -49,8 +49,8 @@ func TestBuffer_CursorMovement(t *testing.T) {
if b.Text() != "Asome_teAxt" { if b.Text() != "Asome_teAxt" {
t.Errorf("Text should be %#v, got %#v", "some_teAxt", b.Text()) t.Errorf("Text should be %#v, got %#v", "some_teAxt", b.Text())
} }
if b.CursorPosition != len("A") { if b.cursorPosition != len("A") {
t.Errorf("Text should be %#v, got %#v", len("some_teA"), b.CursorPosition) t.Errorf("Text should be %#v, got %#v", len("some_teA"), b.cursorPosition)
} }
// TODO: Going right already at right end. // TODO: Going right already at right end.
@ -69,43 +69,43 @@ func TestBuffer_CursorUp(t *testing.T) {
b := NewBuffer() b := NewBuffer()
b.InsertText("long line1\nline2", false, true) b.InsertText("long line1\nline2", false, true)
b.CursorUp(1) b.CursorUp(1)
if b.Document().CursorPosition != 5 { if b.Document().cursorPosition != 5 {
t.Errorf("Should be %#v, got %#v", 5, b.Document().CursorPosition) t.Errorf("Should be %#v, got %#v", 5, b.Document().cursorPosition)
} }
// Going up when already at the top. // Going up when already at the top.
b.CursorUp(1) b.CursorUp(1)
if b.Document().CursorPosition != 5 { if b.Document().cursorPosition != 5 {
t.Errorf("Should be %#v, got %#v", 5, b.Document().CursorPosition) t.Errorf("Should be %#v, got %#v", 5, b.Document().cursorPosition)
} }
// Going up to a line that's shorter. // Going up to a line that's shorter.
b.setDocument(&Document{}) b.setDocument(&Document{})
b.InsertText("line1\nlong line2", false, true) b.InsertText("line1\nlong line2", false, true)
b.CursorUp(1) b.CursorUp(1)
if b.Document().CursorPosition != 5 { if b.Document().cursorPosition != 5 {
t.Errorf("Should be %#v, got %#v", 5, b.Document().CursorPosition) t.Errorf("Should be %#v, got %#v", 5, b.Document().cursorPosition)
} }
} }
func TestBuffer_CursorDown(t *testing.T) { func TestBuffer_CursorDown(t *testing.T) {
b := NewBuffer() b := NewBuffer()
b.InsertText("line1\nline2", false, true) b.InsertText("line1\nline2", false, true)
b.CursorPosition = 3 b.cursorPosition = 3
// Normally going down // Normally going down
b.CursorDown(1) b.CursorDown(1)
if b.Document().CursorPosition != len("line1\nlin") { if b.Document().cursorPosition != len("line1\nlin") {
t.Errorf("Should be %#v, got %#v", len("line1\nlin"), b.Document().CursorPosition) t.Errorf("Should be %#v, got %#v", len("line1\nlin"), b.Document().cursorPosition)
} }
// Going down to a line that's storter. // Going down to a line that's storter.
b = NewBuffer() b = NewBuffer()
b.InsertText("long line1\na\nb", false, true) b.InsertText("long line1\na\nb", false, true)
b.CursorPosition = 3 b.cursorPosition = 3
b.CursorDown(1) b.CursorDown(1)
if b.Document().CursorPosition != len("long line1\na") { if b.Document().cursorPosition != len("long line1\na") {
t.Errorf("Should be %#v, got %#v", len("long line1\na"), b.Document().CursorPosition) 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" { if deleted != "e" {
t.Errorf("Should be %#v, got %#v", deleted, "e") t.Errorf("Should be %#v, got %#v", deleted, "e")
} }
if b.CursorPosition != len("some_t") { if b.cursorPosition != len("some_t") {
t.Errorf("Should be %#v, got %#v", len("some_t"), b.CursorPosition) t.Errorf("Should be %#v, got %#v", len("some_t"), b.cursorPosition)
} }
// Delete over the characters length before cursor. // 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 // Test when there is no '\n' in the text
b = NewBuffer() b = NewBuffer()
b.InsertText("line1", false, true) b.InsertText("line1", false, true)
b.CursorPosition = 0 b.cursorPosition = 0
b.JoinNextLine(" ") b.JoinNextLine(" ")
ac = b.Text() ac = b.Text()
ex = "line1" ex = "line1"

@ -3,16 +3,21 @@ package prompt
import ( import (
"log" "log"
"strings" "strings"
"github.com/mattn/go-runewidth"
) )
const ( const (
shortenSuffix = "..." shortenSuffix = "..."
leftPrefix = " " leftPrefix = " "
leftSuffix = " " leftSuffix = " "
rightPrefix = " " rightPrefix = " "
rightSuffix = " " rightSuffix = " "
leftMargin = len(leftPrefix + leftSuffix) )
rightMargin = len(rightPrefix + rightSuffix)
var (
leftMargin = runewidth.StringWidth(leftPrefix + leftSuffix)
rightMargin = runewidth.StringWidth(rightPrefix + rightSuffix)
completionMargin = leftMargin + rightMargin completionMargin = leftMargin + rightMargin
) )
@ -106,13 +111,14 @@ func formatTexts(o []string, max int, prefix, suffix string) (new []string, widt
l := len(o) l := len(o)
n := make([]string, l) n := make([]string, l)
lenPrefix := len([]rune(prefix)) lenPrefix := runewidth.StringWidth(prefix)
lenSuffix := len([]rune(suffix)) lenSuffix := runewidth.StringWidth(suffix)
lenShorten := len(shortenSuffix) lenShorten := runewidth.StringWidth(shortenSuffix)
min := lenPrefix + lenSuffix + lenShorten min := lenPrefix + lenSuffix + lenShorten
for i := 0; i < l; i++ { for i := 0; i < l; i++ {
if width < len([]rune(o[i])) { w := runewidth.StringWidth(o[i])
width = len([]rune(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++ { for i := 0; i < l; i++ {
r := []rune(o[i]) x := runewidth.StringWidth(o[i])
x := len(r)
if x <= width { if x <= width {
spaces := strings.Repeat(" ", width-x) spaces := strings.Repeat(" ", width-x)
n[i] = prefix + o[i] + spaces + suffix n[i] = prefix + o[i] + spaces + suffix
} else if x > width { } 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 return n, lenPrefix + width + lenSuffix

@ -4,22 +4,38 @@ import (
"sort" "sort"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
"github.com/mattn/go-runewidth"
) )
// Document has text displayed in terminal and cursor position. // Document has text displayed in terminal and cursor position.
type Document struct { type Document struct {
Text string Text string
CursorPosition int // 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. // NewDocument return the new empty document.
func NewDocument() *Document { func NewDocument() *Document {
return &Document{ return &Document{
Text: "", 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 // GetCharRelativeToCursor return character relative to cursor position, or empty string
func (d *Document) GetCharRelativeToCursor(offset int) (r rune) { func (d *Document) GetCharRelativeToCursor(offset int) (r rune) {
s := d.Text s := d.Text
@ -28,7 +44,7 @@ func (d *Document) GetCharRelativeToCursor(offset int) (r rune) {
for len(s) > 0 { for len(s) > 0 {
cnt++ cnt++
r, size := utf8.DecodeRuneInString(s) r, size := utf8.DecodeRuneInString(s)
if cnt == d.CursorPosition+offset { if cnt == d.cursorPosition+offset {
return r return r
} }
s = s[size:] s = s[size:]
@ -39,13 +55,13 @@ func (d *Document) GetCharRelativeToCursor(offset int) (r rune) {
// TextBeforeCursor returns the text before the cursor. // TextBeforeCursor returns the text before the cursor.
func (d *Document) TextBeforeCursor() string { func (d *Document) TextBeforeCursor() string {
r := []rune(d.Text) r := []rune(d.Text)
return string(r[:d.CursorPosition]) return string(r[:d.cursorPosition])
} }
// TextAfterCursor returns the text after the cursor. // TextAfterCursor returns the text after the cursor.
func (d *Document) TextAfterCursor() string { func (d *Document) TextAfterCursor() string {
r := []rune(d.Text) r := []rune(d.Text)
return string(r[d.CursorPosition:]) return string(r[d.cursorPosition:])
} }
// GetWordBeforeCursor returns the word before the cursor. // GetWordBeforeCursor returns the word before the cursor.
@ -95,7 +111,7 @@ func (d *Document) FindEndOfCurrentWord() int {
if i := strings.IndexByte(x, ' '); i != -1 { if i := strings.IndexByte(x, ' '); i != -1 {
return i return i
} else { } 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.) // CursorPositionRow returns the current row. (0-based.)
func (d *Document) CursorPositionRow() (row int) { func (d *Document) CursorPositionRow() (row int) {
row, _ = d.findLineStartIndex(d.CursorPosition) row, _ = d.findLineStartIndex(d.cursorPosition)
return return
} }
@ -197,8 +213,8 @@ func (d *Document) CursorPositionRow() (row int) {
func (d *Document) CursorPositionCol() (col int) { func (d *Document) CursorPositionCol() (col int) {
// Don't use self.text_before_cursor to calculate this. Creating substrings // Don't use self.text_before_cursor to calculate this. Creating substrings
// and splitting is too expensive for getting the cursor position. // and splitting is too expensive for getting the cursor position.
_, index := d.findLineStartIndex(d.CursorPosition) _, index := d.findLineStartIndex(d.cursorPosition)
col = d.CursorPosition - index col = d.cursorPosition - index
return return
} }
@ -238,7 +254,7 @@ func (d *Document) GetCursorUpPosition(count int, preferredColumn int) int {
if row < 0 { if row < 0 {
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 // 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 col = preferredColumn
} }
row := d.CursorPositionRow() + count 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. // Lines returns the array of all the lines.

@ -6,27 +6,111 @@ import (
"unicode/utf8" "unicode/utf8"
) )
func TestDocument_GetCharRelativeToCursor(t *testing.T) { func TestDocument_DisplayCursorPosition(t *testing.T) {
d := &Document{ patterns := []struct {
Text: "line 1\nline 2\nline 3\nline 4\n", document *Document
CursorPosition: len([]rune("line 1\n" + "lin")), 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") for _, p := range patterns {
if ac != ex { ac := p.document.DisplayCursorPosition()
t.Errorf("Should be %#v, got %#v", ex, ac) 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) { func TestDocument_TextBeforeCursor(t *testing.T) {
d := &Document{ patterns := []struct {
Text: "line 1\nline 2\nline 3\nline 4\n", document *Document
CursorPosition: len("line 1\n" + "lin"), 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() for i, p := range patterns {
ex := "line 1\nlin" ac := p.document.TextBeforeCursor()
if ac != ex { if ac != p.expected {
t.Errorf("Should be %#v, got %#v", ex, ac) t.Errorf("[%d] Should be %s, got %s", i, p.expected, ac)
}
} }
} }
@ -38,23 +122,37 @@ func TestDocument_TextAfterCursor(t *testing.T) {
{ {
document: &Document{ document: &Document{
Text: "line 1\nline 2\nline 3\nline 4\n", 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", expected: "e 2\nline 3\nline 4\n",
}, },
{ {
document: &Document{ document: &Document{
Text: "", Text: "",
CursorPosition: 0, cursorPosition: 0,
}, },
expected: "", 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() ac := p.document.TextAfterCursor()
if ac != p.expected { 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{ document: &Document{
Text: "apple bana", Text: "apple bana",
CursorPosition: len("apple bana"), cursorPosition: len("apple bana"),
}, },
expected: "bana", expected: "bana",
}, },
{ {
document: &Document{ document: &Document{
Text: "apple ", Text: "apple ",
CursorPosition: len("apple "), cursorPosition: len("apple "),
}, },
expected: "", 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 { 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 { if ac != p.expected {
t.Errorf("Should be %#v, got %#v", p.expected, ac) t.Errorf("Should be %#v, got %#v", p.expected, ac)
} }
@ -96,66 +337,51 @@ func TestDocument_GetWordAfterCursor(t *testing.T) {
{ {
document: &Document{ document: &Document{
Text: "apple bana", Text: "apple bana",
CursorPosition: len("apple bana"), cursorPosition: len("apple bana"),
}, },
expected: "", expected: "",
}, },
{ {
document: &Document{ document: &Document{
Text: "apple bana", Text: "apple bana",
CursorPosition: len("apple "), cursorPosition: len("apple "),
}, },
expected: "bana", expected: "bana",
}, },
{ {
document: &Document{ document: &Document{
Text: "apple bana", Text: "apple bana",
CursorPosition: len("apple"), cursorPosition: len("apple"),
}, },
expected: "", expected: "",
}, },
{ {
document: &Document{ document: &Document{
Text: "apple bana", Text: "apple bana",
CursorPosition: len("ap"), cursorPosition: len("ap"),
}, },
expected: "ple", expected: "ple",
}, },
{
document: &Document{
Text: "あいうえお かきくけこ さしすせそ",
cursorPosition: 8,
},
expected: "くけこ",
},
{
document: &Document{
Text: "Добрый день Добрый день",
cursorPosition: 9,
},
expected: "нь",
},
} }
for k, p := range pattern { for k, p := range pattern {
ac := p.document.GetWordAfterCursor() ac := p.document.GetWordAfterCursor()
if ac != p.expected { 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)
}
}
}
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)
} }
} }
} }
@ -168,31 +394,45 @@ func TestDocument_GetWordAfterCursorWithSpace(t *testing.T) {
{ {
document: &Document{ document: &Document{
Text: "apple bana", Text: "apple bana",
CursorPosition: len("apple bana"), cursorPosition: len("apple bana"),
}, },
expected: "", expected: "",
}, },
{ {
document: &Document{ document: &Document{
Text: "apple bana", Text: "apple bana",
CursorPosition: len("apple "), cursorPosition: len("apple "),
}, },
expected: "bana", expected: "bana",
}, },
{ {
document: &Document{ document: &Document{
Text: "apple bana", Text: "apple bana",
CursorPosition: len("apple"), cursorPosition: len("apple"),
}, },
expected: " bana", expected: " bana",
}, },
{ {
document: &Document{ document: &Document{
Text: "apple bana", Text: "apple bana",
CursorPosition: len("ap"), cursorPosition: len("ap"),
}, },
expected: "ple", expected: "ple",
}, },
{
document: &Document{
Text: "あいうえお かきくけこ さしすせそ",
cursorPosition: 5,
},
expected: " かきくけこ",
},
{
document: &Document{
Text: "Добрый день Добрый день",
cursorPosition: 6,
},
expected: " день",
},
} }
for k, p := range pattern { 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) { func TestDocument_FindEndOfCurrentWord(t *testing.T) {
pattern := []struct { pattern := []struct {
document *Document document *Document
@ -240,66 +451,60 @@ func TestDocument_FindEndOfCurrentWord(t *testing.T) {
{ {
document: &Document{ document: &Document{
Text: "apple bana", Text: "apple bana",
CursorPosition: len("apple bana"), cursorPosition: len("apple bana"),
}, },
expected: len(""), expected: len(""),
}, },
{ {
document: &Document{ document: &Document{
Text: "apple bana", Text: "apple bana",
CursorPosition: len("apple "), cursorPosition: len("apple "),
}, },
expected: len("bana"), expected: len("bana"),
}, },
{ {
document: &Document{ document: &Document{
Text: "apple bana", Text: "apple bana",
CursorPosition: len("apple"), cursorPosition: len("apple"),
}, },
expected: len(""), expected: len(""),
}, },
{ {
document: &Document{ document: &Document{
Text: "apple bana", Text: "apple bana",
CursorPosition: len("ap"), cursorPosition: len("ap"),
}, },
expected: len("ple"), 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 { for k, p := range pattern {
ac := p.document.FindEndOfCurrentWord() ac := p.document.FindEndOfCurrentWord()
if ac != p.expected { 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)
}
}
}
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)
} }
} }
} }
@ -312,37 +517,58 @@ func TestDocument_FindEndOfCurrentWordWithSpace(t *testing.T) {
{ {
document: &Document{ document: &Document{
Text: "apple bana", Text: "apple bana",
CursorPosition: len("apple bana"), cursorPosition: len("apple bana"),
}, },
expected: len(""), expected: len(""),
}, },
{ {
document: &Document{ document: &Document{
Text: "apple bana", Text: "apple bana",
CursorPosition: len("apple "), cursorPosition: len("apple "),
}, },
expected: len("bana"), expected: len("bana"),
}, },
{ {
document: &Document{ document: &Document{
Text: "apple bana", Text: "apple bana",
CursorPosition: len("apple"), cursorPosition: len("apple"),
}, },
expected: len(" bana"), expected: len(" bana"),
}, },
{ {
document: &Document{ document: &Document{
Text: "apple bana", Text: "apple bana",
CursorPosition: len("ap"), cursorPosition: len("ap"),
}, },
expected: len("ple"), 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 { for k, p := range pattern {
ac := p.document.FindEndOfCurrentWordWithSpace() ac := p.document.FindEndOfCurrentWordWithSpace()
if ac != p.expected { 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) { func TestDocument_CurrentLineBeforeCursor(t *testing.T) {
d := &Document{ d := &Document{
Text: "line 1\nline 2\nline 3\nline 4\n", 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() ac := d.CurrentLineBeforeCursor()
ex := "lin" ex := "lin"
@ -362,7 +588,7 @@ func TestDocument_CurrentLineBeforeCursor(t *testing.T) {
func TestDocument_CurrentLineAfterCursor(t *testing.T) { func TestDocument_CurrentLineAfterCursor(t *testing.T) {
d := &Document{ d := &Document{
Text: "line 1\nline 2\nline 3\nline 4\n", 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() ac := d.CurrentLineAfterCursor()
ex := "e 2" ex := "e 2"
@ -374,7 +600,7 @@ func TestDocument_CurrentLineAfterCursor(t *testing.T) {
func TestDocument_CurrentLine(t *testing.T) { func TestDocument_CurrentLine(t *testing.T) {
d := &Document{ d := &Document{
Text: "line 1\nline 2\nline 3\nline 4\n", 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() ac := d.CurrentLine()
ex := "line 2" 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) { 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 { for _, test := range cursorPositionTests {
ac := test.document.CursorPositionRow() ac := test.document.CursorPositionRow()
if ac != test.expectedRow { if ac != test.expectedRow {
@ -419,7 +641,7 @@ func TestDocument_CursorPositionRowAndCol(t *testing.T) {
func TestDocument_GetCursorLeftPosition(t *testing.T) { func TestDocument_GetCursorLeftPosition(t *testing.T) {
d := &Document{ d := &Document{
Text: "line 1\nline 2\nline 3\nline 4\n", 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) ac := d.GetCursorLeftPosition(2)
ex := -2 ex := -2
@ -436,7 +658,7 @@ func TestDocument_GetCursorLeftPosition(t *testing.T) {
func TestDocument_GetCursorUpPosition(t *testing.T) { func TestDocument_GetCursorUpPosition(t *testing.T) {
d := &Document{ d := &Document{
Text: "line 1\nline 2\nline 3\nline 4\n", 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) ac := d.GetCursorUpPosition(2, -1)
ex := len("lin") - len("line 1\n"+"line 2\n"+"lin") 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) { func TestDocument_GetCursorDownPosition(t *testing.T) {
d := &Document{ d := &Document{
Text: "line 1\nline 2\nline 3\nline 4\n", Text: "line 1\nline 2\nline 3\nline 4\n",
CursorPosition: len("lin"), cursorPosition: len("lin"),
} }
ac := d.GetCursorDownPosition(2, -1) ac := d.GetCursorDownPosition(2, -1)
ex := len("line 1\n"+"line 2\n"+"lin") - len("lin") 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) { func TestDocument_GetCursorRightPosition(t *testing.T) {
d := &Document{ d := &Document{
Text: "line 1\nline 2\nline 3\nline 4\n", 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) ac := d.GetCursorRightPosition(2)
ex := 2 ex := 2
@ -489,7 +711,7 @@ func TestDocument_GetCursorRightPosition(t *testing.T) {
func TestDocument_Lines(t *testing.T) { func TestDocument_Lines(t *testing.T) {
d := &Document{ d := &Document{
Text: "line 1\nline 2\nline 3\nline 4\n", 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() ac := d.Lines()
ex := []string{"line 1", "line 2", "line 3", "line 4", ""} 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) { func TestDocument_LineCount(t *testing.T) {
d := &Document{ d := &Document{
Text: "line 1\nline 2\nline 3\nline 4\n", 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() ac := d.LineCount()
ex := 5 ex := 5
@ -513,7 +735,7 @@ func TestDocument_LineCount(t *testing.T) {
func TestDocument_TranslateIndexToPosition(t *testing.T) { func TestDocument_TranslateIndexToPosition(t *testing.T) {
d := &Document{ d := &Document{
Text: "line 1\nline 2\nline 3\nline 4\n", 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")) row, col := d.TranslateIndexToPosition(len("line 1\nline 2\nlin"))
if row != 2 { if row != 2 {
@ -534,7 +756,7 @@ func TestDocument_TranslateIndexToPosition(t *testing.T) {
func TestDocument_TranslateRowColToIndex(t *testing.T) { func TestDocument_TranslateRowColToIndex(t *testing.T) {
d := &Document{ d := &Document{
Text: "line 1\nline 2\nline 3\nline 4\n", 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) ac := d.TranslateRowColToIndex(2, 3)
ex := len("line 1\nline 2\nlin") ex := len("line 1\nline 2\nlin")
@ -551,13 +773,13 @@ func TestDocument_TranslateRowColToIndex(t *testing.T) {
func TestDocument_OnLastLine(t *testing.T) { func TestDocument_OnLastLine(t *testing.T) {
d := &Document{ d := &Document{
Text: "line 1\nline 2\nline 3", Text: "line 1\nline 2\nline 3",
CursorPosition: len("line 1\nline"), cursorPosition: len("line 1\nline"),
} }
ac := d.OnLastLine() ac := d.OnLastLine()
if ac { if ac {
t.Errorf("Should be %#v, got %#v", false, 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() ac = d.OnLastLine()
if !ac { if !ac {
t.Errorf("Should be %#v, got %#v", true, 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) { func TestDocument_GetEndOfLinePosition(t *testing.T) {
d := &Document{ d := &Document{
Text: "line 1\nline 2\nline 3", Text: "line 1\nline 2\nline 3",
CursorPosition: len("line 1\nli"), cursorPosition: len("line 1\nli"),
} }
ac := d.GetEndOfLinePosition() ac := d.GetEndOfLinePosition()
ex := len("ne 2") ex := len("ne 2")

@ -5,20 +5,20 @@ import "testing"
func TestEmacsKeyBindings(t *testing.T) { func TestEmacsKeyBindings(t *testing.T) {
buf := NewBuffer() buf := NewBuffer()
buf.InsertText("abcde", false, true) buf.InsertText("abcde", false, true)
if buf.CursorPosition != len("abcde") { if buf.cursorPosition != len("abcde") {
t.Errorf("Want %d, but got %d", len("abcde"), buf.CursorPosition) t.Errorf("Want %d, but got %d", len("abcde"), buf.cursorPosition)
} }
// Go to the beginning of the line // Go to the beginning of the line
applyEmacsKeyBind(buf, ControlA) applyEmacsKeyBind(buf, ControlA)
if buf.CursorPosition != 0 { if buf.cursorPosition != 0 {
t.Errorf("Want %d, but got %d", 0, buf.CursorPosition) t.Errorf("Want %d, but got %d", 0, buf.cursorPosition)
} }
// Go to the end of the line // Go to the end of the line
applyEmacsKeyBind(buf, ControlE) applyEmacsKeyBind(buf, ControlE)
if buf.CursorPosition != len("abcde") { if buf.cursorPosition != len("abcde") {
t.Errorf("Want %d, but got %d", len("abcde"), buf.CursorPosition) t.Errorf("Want %d, but got %d", len("abcde"), buf.cursorPosition)
} }
} }

@ -2,6 +2,8 @@ package prompt
import ( import (
"runtime" "runtime"
"github.com/mattn/go-runewidth"
) )
// Render to render prompt information from state of Buffer. // Render to render prompt information from state of Buffer.
@ -97,7 +99,7 @@ func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) {
prefix := r.getCurrentPrefix() prefix := r.getCurrentPrefix()
formatted, width := formatSuggestions( formatted, width := formatSuggestions(
suggestions, 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. // +1 means a width of scrollbar.
width++ width++
@ -109,7 +111,7 @@ func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) {
formatted = formatted[completions.verticalScroll : completions.verticalScroll+windowHeight] formatted = formatted[completions.verticalScroll : completions.verticalScroll+windowHeight]
r.prepareArea(windowHeight) r.prepareArea(windowHeight)
cursor := len(prefix) + len(buf.Document().TextBeforeCursor()) cursor := runewidth.StringWidth(prefix) + runewidth.StringWidth(buf.Document().TextBeforeCursor())
x, _ := r.toPos(cursor) x, _ := r.toPos(cursor)
if x+width >= int(r.col) { if x+width >= int(r.col) {
cursor = r.backward(cursor, 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() line := buffer.Text()
prefix := r.getCurrentPrefix() prefix := r.getCurrentPrefix()
cursor := len(prefix) + len(line) cursor := runewidth.StringWidth(prefix) + runewidth.StringWidth(line)
// prepare area // prepare area
_, y := r.toPos(cursor) _, y := r.toPos(cursor)
@ -201,23 +203,23 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager) {
r.out.EraseDown() r.out.EraseDown()
cursor = r.backward(cursor, len(line)-buffer.CursorPosition) cursor = r.backward(cursor, runewidth.StringWidth(line)-buffer.DisplayCursorPosition())
r.renderCompletion(buffer, completion) r.renderCompletion(buffer, completion)
if suggest, ok := completion.GetSelectedSuggestion(); ok { 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.SetColor(r.previewSuggestionTextColor, r.previewSuggestionBGColor, false)
r.out.WriteStr(suggest.Text) r.out.WriteStr(suggest.Text)
r.out.SetColor(DefaultColor, DefaultColor, false) r.out.SetColor(DefaultColor, DefaultColor, false)
cursor += len(suggest.Text) cursor += runewidth.StringWidth(suggest.Text)
rest := buffer.Document().TextAfterCursor() rest := buffer.Document().TextAfterCursor()
r.out.WriteStr(rest) r.out.WriteStr(rest)
cursor += len(rest) cursor += runewidth.StringWidth(rest)
r.lineWrap(cursor) r.lineWrap(cursor)
cursor = r.backward(cursor, len(rest)) cursor = r.backward(cursor, runewidth.StringWidth(rest))
} }
r.previousCursor = cursor r.previousCursor = cursor
} }
@ -225,7 +227,7 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager) {
// BreakLine to break line. // BreakLine to break line.
func (r *Render) BreakLine(buffer *Buffer) { func (r *Render) BreakLine(buffer *Buffer) {
// Erasing and Render // 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.clear(cursor)
r.renderPrefix() r.renderPrefix()
r.out.SetColor(r.inputTextColor, r.inputBGColor, false) r.out.SetColor(r.inputTextColor, r.inputBGColor, false)