From 24941a650310c5b732646b7e8fdf5b258006139d Mon Sep 17 00:00:00 2001 From: c-bata Date: Sun, 9 Dec 2018 16:24:01 +0900 Subject: [PATCH 1/3] Move bisectRight implementation to internal/bisect --- Makefile | 8 ++++-- document.go | 16 ++--------- document_test.go | 15 ---------- internal/bisect/bisect.go | 15 ++++++++++ internal/bisect/bisect_test.go | 50 ++++++++++++++++++++++++++++++++++ 5 files changed, 73 insertions(+), 31 deletions(-) create mode 100644 internal/bisect/bisect.go create mode 100644 internal/bisect/bisect_test.go diff --git a/Makefile b/Makefile index 64570ab..33a1b33 100644 --- a/Makefile +++ b/Makefile @@ -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. diff --git a/document.go b/document.go index 5d2cb8e..a3aaee1 100644 --- a/document.go +++ b/document.go @@ -1,10 +1,10 @@ package prompt import ( - "sort" "strings" "unicode/utf8" + "github.com/c-bata/go-prompt/internal/bisect" runewidth "github.com/mattn/go-runewidth" ) @@ -292,7 +292,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 } @@ -433,18 +433,6 @@ func (d *Document) leadingWhitespaceInCurrentLine() (margin string) { 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++ { diff --git a/document_test.go b/document_test.go index 975a8bd..017e2a1 100644 --- a/document_test.go +++ b/document_test.go @@ -1040,18 +1040,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) - } -} diff --git a/internal/bisect/bisect.go b/internal/bisect/bisect.go new file mode 100644 index 0000000..efe162f --- /dev/null +++ b/internal/bisect/bisect.go @@ -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 + }) +} diff --git a/internal/bisect/bisect_test.go b/internal/bisect/bisect_test.go new file mode 100644 index 0000000..9d52a29 --- /dev/null +++ b/internal/bisect/bisect_test.go @@ -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) + } + }) + } +} From 6916c5f66d59123e4578ddeedc39adaece6f1c54 Mon Sep 17 00:00:00 2001 From: c-bata Date: Sun, 9 Dec 2018 16:35:08 +0900 Subject: [PATCH 2/3] Move some strings utilities to internal/strings --- document.go | 96 ++------------------------- internal/strings/strings.go | 110 +++++++++++++++++++++++++++++++ internal/strings/strings_test.go | 47 +++++++++++++ 3 files changed, 162 insertions(+), 91 deletions(-) create mode 100644 internal/strings/strings.go create mode 100644 internal/strings/strings_test.go diff --git a/document.go b/document.go index a3aaee1..54b7a37 100644 --- a/document.go +++ b/document.go @@ -5,6 +5,7 @@ import ( "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) } @@ -432,90 +433,3 @@ func (d *Document) leadingWhitespaceInCurrentLine() (margin string) { margin = d.CurrentLine()[:len(d.CurrentLine())-len(trimmed)] return } - -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 -} diff --git a/internal/strings/strings.go b/internal/strings/strings.go new file mode 100644 index 0000000..c537876 --- /dev/null +++ b/internal/strings/strings.go @@ -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 +} diff --git a/internal/strings/strings_test.go b/internal/strings/strings_test.go new file mode 100644 index 0000000..d9d9bc9 --- /dev/null +++ b/internal/strings/strings_test.go @@ -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 +} From 80e24cac1c239da4d5cd3d09a693d0536816901e Mon Sep 17 00:00:00 2001 From: c-bata Date: Sun, 9 Dec 2018 21:05:15 +0900 Subject: [PATCH 3/3] Add examples for Document component --- document_test.go | 213 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) diff --git a/document_test.go b/document_test.go index 017e2a1..9f273b7 100644 --- a/document_test.go +++ b/document_test.go @@ -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