Merge pull request #113 from c-bata/refactor-documents

Refactor a Document component
This commit is contained in:
Masashi SHIBATA 2018-12-09 21:17:49 +09:00 committed by GitHub
commit fe8fa91b20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 448 additions and 122 deletions

View File

@ -21,8 +21,12 @@ lint: ## Run golint and go vet.
@go vet .
.PHONY: test
test: ## Run the tests with race condition checking.
@go test -race .
test: ## Run tests with race condition checking.
@go test -race ./...
.PHONY: bench
bench: ## Run benchmarks.
@go test -bench=. -run=- -benchmem ./...
.PHONY: coverage
cover: ## Run the tests.

View File

@ -1,10 +1,11 @@
package prompt
import (
"sort"
"strings"
"unicode/utf8"
"github.com/c-bata/go-prompt/internal/bisect"
istrings "github.com/c-bata/go-prompt/internal/strings"
runewidth "github.com/mattn/go-runewidth"
)
@ -133,7 +134,7 @@ func (d *Document) FindStartOfPreviousWord() int {
// The only difference is to ignore contiguous spaces.
func (d *Document) FindStartOfPreviousWordWithSpace() int {
x := d.TextBeforeCursor()
end := lastIndexByteNot(x, ' ')
end := istrings.LastIndexNotByte(x, ' ')
if end == -1 {
return 0
}
@ -168,7 +169,7 @@ func (d *Document) FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor(sep s
}
x := d.TextBeforeCursor()
end := lastIndexAnyNot(x, sep)
end := istrings.LastIndexNotAny(x, sep)
if end == -1 {
return 0
}
@ -195,7 +196,7 @@ func (d *Document) FindEndOfCurrentWord() int {
func (d *Document) FindEndOfCurrentWordWithSpace() int {
x := d.TextAfterCursor()
start := indexByteNot(x, ' ')
start := istrings.IndexNotByte(x, ' ')
if start == -1 {
return len(x)
}
@ -232,7 +233,7 @@ func (d *Document) FindEndOfCurrentWordUntilSeparatorIgnoreNextToCursor(sep stri
x := d.TextAfterCursor()
start := indexAnyNot(x, sep)
start := istrings.IndexNotAny(x, sep)
if start == -1 {
return len(x)
}
@ -292,7 +293,7 @@ func (d *Document) lineStartIndexes() []int {
// the first character on that line.
func (d *Document) findLineStartIndex(index int) (pos int, lineStartIndex int) {
indexes := d.lineStartIndexes()
pos = bisectRight(indexes, index) - 1
pos = bisect.Right(indexes, index) - 1
lineStartIndex = indexes[pos]
return
}
@ -432,102 +433,3 @@ func (d *Document) leadingWhitespaceInCurrentLine() (margin string) {
margin = d.CurrentLine()[:len(d.CurrentLine())-len(trimmed)]
return
}
// bisectRight to Locate the insertion point for v in a to maintain sorted order.
func bisectRight(a []int, v int) int {
return bisectRightRange(a, v, 0, len(a))
}
func bisectRightRange(a []int, v int, lo, hi int) int {
s := a[lo:hi]
return sort.Search(len(s), func(i int) bool {
return s[i] > v
})
}
func indexByteNot(s string, c byte) int {
n := len(s)
for i := 0; i < n; i++ {
if s[i] != c {
return i
}
}
return -1
}
func lastIndexByteNot(s string, c byte) int {
for i := len(s) - 1; i >= 0; i-- {
if s[i] != c {
return i
}
}
return -1
}
type asciiSet [8]uint32
func (as *asciiSet) notContains(c byte) bool {
return (as[c>>5] & (1 << uint(c&31))) == 0
}
func makeASCIISet(chars string) (as asciiSet, ok bool) {
for i := 0; i < len(chars); i++ {
c := chars[i]
if c >= utf8.RuneSelf {
return as, false
}
as[c>>5] |= 1 << uint(c&31)
}
return as, true
}
func indexAnyNot(s, chars string) int {
if len(chars) > 0 {
if len(s) > 8 {
if as, isASCII := makeASCIISet(chars); isASCII {
for i := 0; i < len(s); i++ {
if as.notContains(s[i]) {
return i
}
}
return -1
}
}
for i := 0; i < len(s); {
// I don't know why strings.IndexAny doesn't add rune count here.
r, size := utf8.DecodeRuneInString(s[i:])
i += size
for _, c := range chars {
if r != c {
return i
}
}
}
}
return -1
}
func lastIndexAnyNot(s, chars string) int {
if len(chars) > 0 {
if len(s) > 8 {
if as, isASCII := makeASCIISet(chars); isASCII {
for i := len(s) - 1; i >= 0; i-- {
if as.notContains(s[i]) {
return i
}
}
return -1
}
}
for i := len(s); i > 0; {
r, size := utf8.DecodeLastRuneInString(s[:i])
i -= size
for _, c := range chars {
if r != c {
return i
}
}
}
}
return -1
}

View File

@ -1,11 +1,224 @@
package prompt
import (
"fmt"
"reflect"
"testing"
"unicode/utf8"
)
func ExampleDocument_CurrentLine() {
d := &Document{
Text: `Hello! my name is c-bata.
This is a example of Document component.
This component has texts displayed in terminal and cursor position.
`,
cursorPosition: len(`Hello! my name is c-bata.
This is a exam`),
}
fmt.Println(d.CurrentLine())
// Output:
// This is a example of Document component.
}
func ExampleDocument_DisplayCursorPosition() {
d := &Document{
Text: `Hello! my name is c-bata.`,
cursorPosition: len(`Hello`),
}
fmt.Println("DisplayCursorPosition", d.DisplayCursorPosition())
// Output:
// DisplayCursorPosition 5
}
func ExampleDocument_CursorPositionRow() {
d := &Document{
Text: `Hello! my name is c-bata.
This is a example of Document component.
This component has texts displayed in terminal and cursor position.
`,
cursorPosition: len(`Hello! my name is c-bata.
This is a exam`),
}
fmt.Println("CursorPositionRow", d.CursorPositionRow())
// Output:
// CursorPositionRow 1
}
func ExampleDocument_CursorPositionCol() {
d := &Document{
Text: `Hello! my name is c-bata.
This is a example of Document component.
This component has texts displayed in terminal and cursor position.
`,
cursorPosition: len(`Hello! my name is c-bata.
This is a exam`),
}
fmt.Println("CursorPositionCol", d.CursorPositionCol())
// Output:
// CursorPositionCol 14
}
func ExampleDocument_TextBeforeCursor() {
d := &Document{
Text: `Hello! my name is c-bata.
This is a example of Document component.
This component has texts displayed in terminal and cursor position.
`,
cursorPosition: len(`Hello! my name is c-bata.
This is a exam`),
}
fmt.Println(d.TextBeforeCursor())
// Output:
// Hello! my name is c-bata.
// This is a exam
}
func ExampleDocument_TextAfterCursor() {
d := &Document{
Text: `Hello! my name is c-bata.
This is a example of Document component.
This component has texts displayed in terminal and cursor position.
`,
cursorPosition: len(`Hello! my name is c-bata.
This is a exam`),
}
fmt.Println(d.TextAfterCursor())
// Output:
// ple of Document component.
// This component has texts displayed in terminal and cursor position.
}
func ExampleDocument_DisplayCursorPosition_withJapanese() {
d := &Document{
Text: `こんにちは、芝田 将です。`,
cursorPosition: 3,
}
fmt.Println("DisplayCursorPosition", d.DisplayCursorPosition())
// Output:
// DisplayCursorPosition 6
}
func ExampleDocument_CurrentLineBeforeCursor() {
d := &Document{
Text: `Hello! my name is c-bata.
This is a example of Document component.
This component has texts displayed in terminal and cursor position.
`,
cursorPosition: len(`Hello! my name is c-bata.
This is a exam`),
}
fmt.Println(d.CurrentLineBeforeCursor())
// Output:
// This is a exam
}
func ExampleDocument_CurrentLineAfterCursor() {
d := &Document{
Text: `Hello! my name is c-bata.
This is a example of Document component.
This component has texts displayed in terminal and cursor position.
`,
cursorPosition: len(`Hello! my name is c-bata.
This is a exam`),
}
fmt.Println(d.CurrentLineAfterCursor())
// Output:
// ple of Document component.
}
func ExampleDocument_GetWordBeforeCursor() {
d := &Document{
Text: `Hello! my name is c-bata.
This is a example of Document component.
`,
cursorPosition: len(`Hello! my name is c-bata.
This is a exam`),
}
fmt.Println(d.GetWordBeforeCursor())
// Output:
// exam
}
func ExampleDocument_GetWordAfterCursor() {
d := &Document{
Text: `Hello! my name is c-bata.
This is a example of Document component.
`,
cursorPosition: len(`Hello! my name is c-bata.
This is a exam`),
}
fmt.Println(d.GetWordAfterCursor())
// Output:
// ple
}
func ExampleDocument_GetWordBeforeCursorWithSpace() {
d := &Document{
Text: `Hello! my name is c-bata.
This is a example of Document component.
`,
cursorPosition: len(`Hello! my name is c-bata.
This is a example `),
}
fmt.Println(d.GetWordBeforeCursorWithSpace())
// Output:
// example
}
func ExampleDocument_GetWordAfterCursorWithSpace() {
d := &Document{
Text: `Hello! my name is c-bata.
This is a example of Document component.
`,
cursorPosition: len(`Hello! my name is c-bata.
This is a`),
}
fmt.Println(d.GetWordAfterCursorWithSpace())
// Output:
// example
}
func ExampleDocument_GetWordBeforeCursorUntilSeparator() {
d := &Document{
Text: `hello,i am c-bata`,
cursorPosition: len(`hello,i am c`),
}
fmt.Println(d.GetWordBeforeCursorUntilSeparator(","))
// Output:
// i am c
}
func ExampleDocument_GetWordAfterCursorUntilSeparator() {
d := &Document{
Text: `hello,i am c-bata,thank you for using go-prompt`,
cursorPosition: len(`hello,i a`),
}
fmt.Println(d.GetWordAfterCursorUntilSeparator(","))
// Output:
// m c-bata
}
func ExampleDocument_GetWordBeforeCursorUntilSeparatorIgnoreNextToCursor() {
d := &Document{
Text: `hello,i am c-bata,thank you for using go-prompt`,
cursorPosition: len(`hello,i am c-bata,`),
}
fmt.Println(d.GetWordBeforeCursorUntilSeparatorIgnoreNextToCursor(","))
// Output:
// i am c-bata,
}
func ExampleDocument_GetWordAfterCursorUntilSeparatorIgnoreNextToCursor() {
d := &Document{
Text: `hello,i am c-bata,thank you for using go-prompt`,
cursorPosition: len(`hello`),
}
fmt.Println(d.GetWordAfterCursorUntilSeparatorIgnoreNextToCursor(","))
// Output:
// ,i am c-bata
}
func TestDocument_DisplayCursorPosition(t *testing.T) {
patterns := []struct {
document *Document
@ -1040,18 +1253,3 @@ func TestDocument_GetEndOfLinePosition(t *testing.T) {
t.Errorf("Should be %#v, got %#v", ex, ac)
}
}
func TestBisectRight(t *testing.T) {
// Thanks!! https://play.golang.org/p/y9NRj_XVIW
in := []int{1, 2, 3, 3, 3, 6, 7}
r := bisectRight(in, 0)
if r != 0 {
t.Errorf("number 0 should inserted at 0 position, but got %d", r)
}
r = bisectRight(in, 4)
if r != 5 {
t.Errorf("number 4 should inserted at 5 position, but got %d", r)
}
}

15
internal/bisect/bisect.go Normal file
View File

@ -0,0 +1,15 @@
package bisect
import "sort"
// Right to locate the insertion point for v in a to maintain sorted order.
func Right(a []int, v int) int {
return bisectRightRange(a, v, 0, len(a))
}
func bisectRightRange(a []int, v int, lo, hi int) int {
s := a[lo:hi]
return sort.Search(len(s), func(i int) bool {
return s[i] > v
})
}

View File

@ -0,0 +1,50 @@
package bisect_test
import (
"fmt"
"math/rand"
"testing"
"github.com/c-bata/go-prompt/internal/bisect"
)
func Example() {
in := []int{1, 2, 3, 3, 3, 6, 7}
fmt.Println("Insertion position for 0 in the slice is", bisect.Right(in, 0))
fmt.Println("Insertion position for 4 in the slice is", bisect.Right(in, 4))
// Output:
// Insertion position for 0 in the slice is 0
// Insertion position for 4 in the slice is 5
}
func TestBisectRight(t *testing.T) {
// Thanks!! https://play.golang.org/p/y9NRj_XVIW
in := []int{1, 2, 3, 3, 3, 6, 7}
r := bisect.Right(in, 0)
if r != 0 {
t.Errorf("number 0 should inserted at 0 position, but got %d", r)
}
r = bisect.Right(in, 4)
if r != 5 {
t.Errorf("number 4 should inserted at 5 position, but got %d", r)
}
}
func BenchmarkRight(b *testing.B) {
rand.Seed(0)
for _, l := range []int{10, 1e2, 1e3, 1e4} {
x := rand.Perm(l)
insertion := rand.Int()
b.Run(fmt.Sprintf("arrayLength=%d", l), func(b *testing.B) {
b.ResetTimer()
for n := 0; n < b.N; n++ {
bisect.Right(x, insertion)
}
})
}
}

110
internal/strings/strings.go Normal file
View File

@ -0,0 +1,110 @@
package strings
import "unicode/utf8"
// IndexNotByte is similar with strings.IndexByte but returns
// the index of the first instance of character except c in s.
// or -1 if s only contains c.
func IndexNotByte(s string, c byte) int {
n := len(s)
for i := 0; i < n; i++ {
if s[i] != c {
return i
}
}
return -1
}
// LastIndexByte is similar with strings.IndexByte but returns
// the index of the last instance of character except c in s,
// or -1 if s only contains c.
func LastIndexNotByte(s string, c byte) int {
for i := len(s) - 1; i >= 0; i-- {
if s[i] != c {
return i
}
}
return -1
}
type asciiSet [8]uint32
func (as *asciiSet) notContains(c byte) bool {
return (as[c>>5] & (1 << uint(c&31))) == 0
}
func makeASCIISet(chars string) (as asciiSet, ok bool) {
for i := 0; i < len(chars); i++ {
c := chars[i]
if c >= utf8.RuneSelf {
return as, false
}
as[c>>5] |= 1 << uint(c&31)
}
return as, true
}
// IndexNotAny is similar with strings.IndexAny but returns
// the index of the first instance of any Unicode code point from chars in s,
// or -1 if no Unicode code point from chars is present in s.
func IndexNotAny(s, chars string) int {
if len(chars) > 0 {
if len(s) > 8 {
if as, isASCII := makeASCIISet(chars); isASCII {
for i := 0; i < len(s); i++ {
if as.notContains(s[i]) {
return i
}
}
return -1
}
}
LabelFirstLoop:
for i, c := range s {
for j, m := range chars {
if c != m && j == len(chars)-1 {
return i
} else if c != m {
continue
} else {
continue LabelFirstLoop
}
}
}
}
return -1
}
// LastIndexAny returns the index of the last instance of any Unicode code
// point from chars in s, or -1 if no Unicode code point from chars is
// present in s.
func LastIndexNotAny(s, chars string) int {
if len(chars) > 0 {
if len(s) > 8 {
if as, isASCII := makeASCIISet(chars); isASCII {
for i := len(s) - 1; i >= 0; i-- {
if as.notContains(s[i]) {
return i
}
}
return -1
}
}
LabelFirstLoop:
for i := len(s); i > 0; {
r, size := utf8.DecodeLastRuneInString(s[:i])
i -= size
for j, m := range chars {
if r != m && j == len(chars)-1 {
return i
} else if r != m {
continue
} else {
continue LabelFirstLoop
}
}
}
}
return -1
}

View File

@ -0,0 +1,47 @@
package strings_test
import (
"fmt"
"github.com/c-bata/go-prompt/internal/strings"
)
func ExampleIndexNotByte() {
fmt.Println(strings.IndexNotByte("golang", 'g'))
fmt.Println(strings.IndexNotByte("golang", 'x'))
fmt.Println(strings.IndexNotByte("gggggg", 'g'))
// Output:
// 1
// 0
// -1
}
func ExampleLastIndexNotByte() {
fmt.Println(strings.LastIndexNotByte("golang", 'g'))
fmt.Println(strings.LastIndexNotByte("golang", 'x'))
fmt.Println(strings.LastIndexNotByte("gggggg", 'g'))
// Output:
// 4
// 5
// -1
}
func ExampleIndexNotAny() {
fmt.Println(strings.IndexNotAny("golang", "glo"))
fmt.Println(strings.IndexNotAny("golang", "gl"))
fmt.Println(strings.IndexNotAny("golang", "golang"))
// Output:
// 3
// 1
// -1
}
func ExampleLastIndexNotAny() {
fmt.Println(strings.LastIndexNotAny("golang", "agn"))
fmt.Println(strings.LastIndexNotAny("golang", "an"))
fmt.Println(strings.LastIndexNotAny("golang", "golang"))
// Output:
// 2
// 5
// -1
}