Add option to customize insertion points for completions

This commit is contained in:
c-bata 2018-06-24 04:29:58 +09:00
parent c704dcd9d4
commit d3ef529afd
6 changed files with 458 additions and 44 deletions

View File

@ -35,6 +35,7 @@ type CompletionManager struct {
completer Completer
verticalScroll int
wordSeparator string
}
// GetSelectedSuggestion returns the selected item.

View File

@ -92,35 +92,48 @@ func (d *Document) GetWordAfterCursorWithSpace() string {
return x[:d.FindEndOfCurrentWordWithSpace()]
}
// FindStartOfPreviousWord returns an index relative to the cursor position
// pointing to the start of the previous word. Return `None` if nothing was found.
func (d *Document) FindStartOfPreviousWord() int {
// Reverse the text before the cursor, in order to do an efficient backwards search.
// GetWordBeforeCursorUntilSeparator returns the text before the cursor until next separator.
func (d *Document) GetWordBeforeCursorUntilSeparator(sep string) string {
x := d.TextBeforeCursor()
if i := strings.LastIndexByte(x, ' '); i != -1 {
return x[d.FindStartOfPreviousWordUntilSeparator(sep):]
}
// GetWordAfterCursorUntilSeparator returns the text after the cursor until next separator.
func (d *Document) GetWordAfterCursorUntilSeparator(sep string) string {
x := d.TextAfterCursor()
return x[:d.FindEndOfCurrentWordUntilSeparator(sep)]
}
// GetWordBeforeCursorUntilSeparatorIgnoreNextToCursor returns the word before the cursor.
// Unlike GetWordBeforeCursor, it returns string containing space
func (d *Document) GetWordBeforeCursorUntilSeparatorIgnoreNextToCursor(sep string) string {
x := d.TextBeforeCursor()
return x[d.FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor(sep):]
}
// GetWordAfterCursorUntilSeparatorIgnoreNextToCursor returns the word after the cursor.
// Unlike GetWordAfterCursor, it returns string containing space
func (d *Document) GetWordAfterCursorUntilSeparatorIgnoreNextToCursor(sep string) string {
x := d.TextAfterCursor()
return x[:d.FindEndOfCurrentWordUntilSeparatorIgnoreNextToCursor(sep)]
}
// FindStartOfPreviousWord returns an index relative to the cursor position
// pointing to the start of the previous word. Return 0 if nothing was found.
func (d *Document) FindStartOfPreviousWord() int {
x := d.TextBeforeCursor()
i := strings.LastIndexByte(x, ' ')
if i != -1 {
return i + 1
} else {
return 0
}
}
// FindEndOfCurrentWord returns an index relative to the cursor position
// pointing to the end of the current word. Return `None` if nothing was found.
func (d *Document) FindEndOfCurrentWord() int {
x := d.TextAfterCursor()
if i := strings.IndexByte(x, ' '); i != -1 {
return i
} else {
return len([]rune(x))
}
}
// FindStartOfPreviousWordWithSpace is almost the same as FindStartOfPreviousWord.
// The only difference is to ignore contiguous spaces.
func (d *Document) FindStartOfPreviousWordWithSpace() int {
// Reverse the text before the cursor, in order to do an efficient backwards search.
x := d.TextBeforeCursor()
end := lastIndexByteNot(x, ' ')
if end == -1 {
return 0
@ -133,6 +146,53 @@ func (d *Document) FindStartOfPreviousWordWithSpace() int {
return start + 1
}
// FindStartOfPreviousWordUntilSeparator is almost the same as FindStartOfPreviousWord.
// But this can specify Separator. Return 0 if nothing was found.
func (d *Document) FindStartOfPreviousWordUntilSeparator(sep string) int {
if sep == "" {
return d.FindStartOfPreviousWord()
}
x := d.TextBeforeCursor()
i := strings.LastIndexAny(x, sep)
if i != -1 {
return i + 1
} else {
return 0
}
}
// FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor is almost the same as FindStartOfPreviousWordWithSpace.
// But this can specify Separator. Return 0 if nothing was found.
func (d *Document) FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor(sep string) int {
if sep == "" {
return d.FindStartOfPreviousWordWithSpace()
}
x := d.TextBeforeCursor()
end := lastIndexAnyNot(x, sep)
if end == -1 {
return 0
}
start := strings.LastIndexAny(x[:end], sep)
if start == -1 {
return 0
}
return start + 1
}
// FindEndOfCurrentWord returns an index relative to the cursor position.
// pointing to the end of the current word. Return 0 if nothing was found.
func (d *Document) FindEndOfCurrentWord() int {
x := d.TextAfterCursor()
i := strings.IndexByte(x, ' ')
if i != -1 {
return i
} else {
return len(x)
}
}
// FindEndOfCurrentWordWithSpace is almost the same as FindEndOfCurrentWord.
// The only difference is to ignore contiguous spaces.
func (d *Document) FindEndOfCurrentWordWithSpace() int {
@ -151,6 +211,44 @@ func (d *Document) FindEndOfCurrentWordWithSpace() int {
return start + end
}
// FindEndOfCurrentWordUntilSeparator is almost the same as FindEndOfCurrentWord.
// But this can specify Separator. Return 0 if nothing was found.
func (d *Document) FindEndOfCurrentWordUntilSeparator(sep string) int {
if sep == "" {
return d.FindEndOfCurrentWord()
}
x := d.TextAfterCursor()
i := strings.IndexAny(x, sep)
if i != -1 {
return i
} else {
return len(x)
}
}
// FindEndOfCurrentWordUntilSeparatorIgnoreNextToCursor is almost the same as FindEndOfCurrentWordWithSpace.
// But this can specify Separator. Return 0 if nothing was found.
func (d *Document) FindEndOfCurrentWordUntilSeparatorIgnoreNextToCursor(sep string) int {
if sep == "" {
return d.FindEndOfCurrentWordWithSpace()
}
x := d.TextAfterCursor()
start := indexAnyNot(x, sep)
if start == -1 {
return len(x)
}
end := strings.IndexAny(x[start:], sep)
if end == -1 {
return len(x)
}
return start + end
}
// CurrentLineBeforeCursor returns the text from the start of the line until the cursor.
func (d *Document) CurrentLineBeforeCursor() string {
s := strings.Split(d.TextBeforeCursor(), "\n")
@ -369,3 +467,71 @@ func lastIndexByteNot(s string, c byte) int {
}
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

@ -161,6 +161,7 @@ func TestDocument_GetWordBeforeCursor(t *testing.T) {
pattern := []struct {
document *Document
expected string
sep string
}{
{
document: &Document{
@ -169,6 +170,29 @@ func TestDocument_GetWordBeforeCursor(t *testing.T) {
},
expected: "bana",
},
{
document: &Document{
Text: "apply -f ./file/foo.json",
cursorPosition: len("apply -f ./file/foo.json"),
},
expected: "foo.json",
sep: " /",
},
{
document: &Document{
Text: "apple banana orange",
cursorPosition: len("apple ba"),
},
expected: "ba",
},
{
document: &Document{
Text: "apply -f ./file/foo.json",
cursorPosition: len("apply -f ./fi"),
},
expected: "fi",
sep: " /",
},
{
document: &Document{
Text: "apple ",
@ -193,9 +217,20 @@ func TestDocument_GetWordBeforeCursor(t *testing.T) {
}
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)
if p.sep == "" {
ac := p.document.GetWordBeforeCursor()
if ac != p.expected {
t.Errorf("[%d] Should be %#v, got %#v", i, p.expected, ac)
}
ac = p.document.GetWordBeforeCursorUntilSeparator("")
if ac != p.expected {
t.Errorf("[%d] Should be %#v, got %#v", i, p.expected, ac)
}
} else {
ac := p.document.GetWordBeforeCursorUntilSeparator(p.sep)
if ac != p.expected {
t.Errorf("[%d] Should be %#v, got %#v", i, p.expected, ac)
}
}
}
}
@ -204,6 +239,7 @@ func TestDocument_GetWordBeforeCursorWithSpace(t *testing.T) {
pattern := []struct {
document *Document
expected string
sep string
}{
{
document: &Document{
@ -212,6 +248,14 @@ func TestDocument_GetWordBeforeCursorWithSpace(t *testing.T) {
},
expected: "bana ",
},
{
document: &Document{
Text: "apply -f /path/to/file/",
cursorPosition: len("apply -f /path/to/file/"),
},
expected: "file/",
sep: " /",
},
{
document: &Document{
Text: "apple ",
@ -219,6 +263,14 @@ func TestDocument_GetWordBeforeCursorWithSpace(t *testing.T) {
},
expected: "apple ",
},
{
document: &Document{
Text: "path/",
cursorPosition: len("path/"),
},
expected: "path/",
sep: " /",
},
{
document: &Document{
Text: "あいうえお かきくけこ ",
@ -236,9 +288,20 @@ func TestDocument_GetWordBeforeCursorWithSpace(t *testing.T) {
}
for _, p := range pattern {
ac := p.document.GetWordBeforeCursorWithSpace()
if ac != p.expected {
t.Errorf("Should be %#v, got %#v", p.expected, ac)
if p.sep == "" {
ac := p.document.GetWordBeforeCursorWithSpace()
if ac != p.expected {
t.Errorf("Should be %#v, got %#v", p.expected, ac)
}
ac = p.document.GetWordBeforeCursorUntilSeparatorIgnoreNextToCursor("")
if ac != p.expected {
t.Errorf("Should be %#v, got %#v", p.expected, ac)
}
} else {
ac := p.document.GetWordBeforeCursorUntilSeparatorIgnoreNextToCursor(p.sep)
if ac != p.expected {
t.Errorf("Should be %#v, got %#v", p.expected, ac)
}
}
}
}
@ -247,6 +310,7 @@ func TestDocument_FindStartOfPreviousWord(t *testing.T) {
pattern := []struct {
document *Document
expected int
sep string
}{
{
document: &Document{
@ -255,6 +319,14 @@ func TestDocument_FindStartOfPreviousWord(t *testing.T) {
},
expected: len("apple "),
},
{
document: &Document{
Text: "apply -f ./file/foo.json",
cursorPosition: len("apply -f ./file/foo.json"),
},
expected: len("apply -f ./file/"),
sep: " /",
},
{
document: &Document{
Text: "apple ",
@ -262,6 +334,14 @@ func TestDocument_FindStartOfPreviousWord(t *testing.T) {
},
expected: len("apple "),
},
{
document: &Document{
Text: "apply -f ./file/foo.json",
cursorPosition: len("apply -f ./"),
},
expected: len("apply -f ./"),
sep: " /",
},
{
document: &Document{
Text: "あいうえお かきくけこ さしすせそ",
@ -279,9 +359,20 @@ func TestDocument_FindStartOfPreviousWord(t *testing.T) {
}
for _, p := range pattern {
ac := p.document.FindStartOfPreviousWord()
if ac != p.expected {
t.Errorf("Should be %#v, got %#v", p.expected, ac)
if p.sep == "" {
ac := p.document.FindStartOfPreviousWord()
if ac != p.expected {
t.Errorf("Should be %#v, got %#v", p.expected, ac)
}
ac = p.document.FindStartOfPreviousWordUntilSeparator("")
if ac != p.expected {
t.Errorf("Should be %#v, got %#v", p.expected, ac)
}
} else {
ac := p.document.FindStartOfPreviousWordUntilSeparator(p.sep)
if ac != p.expected {
t.Errorf("Should be %#v, got %#v", p.expected, ac)
}
}
}
}
@ -290,6 +381,7 @@ func TestDocument_FindStartOfPreviousWordWithSpace(t *testing.T) {
pattern := []struct {
document *Document
expected int
sep string
}{
{
document: &Document{
@ -298,6 +390,14 @@ func TestDocument_FindStartOfPreviousWordWithSpace(t *testing.T) {
},
expected: len("apple "),
},
{
document: &Document{
Text: "apply -f /file/foo/",
cursorPosition: len("apply -f /file/foo/"),
},
expected: len("apply -f /file/"),
sep: " /",
},
{
document: &Document{
Text: "apple ",
@ -305,6 +405,14 @@ func TestDocument_FindStartOfPreviousWordWithSpace(t *testing.T) {
},
expected: len(""),
},
{
document: &Document{
Text: "file/",
cursorPosition: len("file/"),
},
expected: len(""),
sep: " /",
},
{
document: &Document{
Text: "あいうえお かきくけこ ",
@ -322,9 +430,20 @@ func TestDocument_FindStartOfPreviousWordWithSpace(t *testing.T) {
}
for _, p := range pattern {
ac := p.document.FindStartOfPreviousWordWithSpace()
if ac != p.expected {
t.Errorf("Should be %#v, got %#v", p.expected, ac)
if p.sep == "" {
ac := p.document.FindStartOfPreviousWordWithSpace()
if ac != p.expected {
t.Errorf("Should be %#v, got %#v", p.expected, ac)
}
ac = p.document.FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor("")
if ac != p.expected {
t.Errorf("Should be %#v, got %#v", p.expected, ac)
}
} else {
ac := p.document.FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor(p.sep)
if ac != p.expected {
t.Errorf("Should be %#v, got %#v", p.expected, ac)
}
}
}
}
@ -333,6 +452,7 @@ func TestDocument_GetWordAfterCursor(t *testing.T) {
pattern := []struct {
document *Document
expected string
sep string
}{
{
document: &Document{
@ -341,6 +461,14 @@ func TestDocument_GetWordAfterCursor(t *testing.T) {
},
expected: "",
},
{
document: &Document{
Text: "apply -f ./file/foo.json",
cursorPosition: len("apply -f ./fi"),
},
expected: "le",
sep: " /",
},
{
document: &Document{
Text: "apple bana",
@ -355,6 +483,14 @@ func TestDocument_GetWordAfterCursor(t *testing.T) {
},
expected: "",
},
{
document: &Document{
Text: "apply -f ./file/foo.json",
cursorPosition: len("apply -f ."),
},
expected: "",
sep: " /",
},
{
document: &Document{
Text: "apple bana",
@ -379,9 +515,20 @@ func TestDocument_GetWordAfterCursor(t *testing.T) {
}
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)
if p.sep == "" {
ac := p.document.GetWordAfterCursor()
if ac != p.expected {
t.Errorf("[%d] Should be %#v, got %#v", k, p.expected, ac)
}
ac = p.document.GetWordAfterCursorUntilSeparator("")
if ac != p.expected {
t.Errorf("[%d] Should be %#v, got %#v", k, p.expected, ac)
}
} else {
ac := p.document.GetWordAfterCursorUntilSeparator(p.sep)
if ac != p.expected {
t.Errorf("[%d] Should be %#v, got %#v", k, p.expected, ac)
}
}
}
}
@ -390,6 +537,7 @@ func TestDocument_GetWordAfterCursorWithSpace(t *testing.T) {
pattern := []struct {
document *Document
expected string
sep string
}{
{
document: &Document{
@ -405,6 +553,22 @@ func TestDocument_GetWordAfterCursorWithSpace(t *testing.T) {
},
expected: "bana",
},
{
document: &Document{
Text: "/path/to",
cursorPosition: len("/path/"),
},
expected: "to",
sep: " /",
},
{
document: &Document{
Text: "/path/to/file",
cursorPosition: len("/path/"),
},
expected: "to",
sep: " /",
},
{
document: &Document{
Text: "apple bana",
@ -412,6 +576,14 @@ func TestDocument_GetWordAfterCursorWithSpace(t *testing.T) {
},
expected: " bana",
},
{
document: &Document{
Text: "path/to",
cursorPosition: len("path"),
},
expected: "/to",
sep: " /",
},
{
document: &Document{
Text: "apple bana",
@ -436,9 +608,20 @@ func TestDocument_GetWordAfterCursorWithSpace(t *testing.T) {
}
for k, p := range pattern {
ac := p.document.GetWordAfterCursorWithSpace()
if ac != p.expected {
t.Errorf("[%d]Should be %#v, got %#v", k, p.expected, ac)
if p.sep == "" {
ac := p.document.GetWordAfterCursorWithSpace()
if ac != p.expected {
t.Errorf("[%d] Should be %#v, got %#v", k, p.expected, ac)
}
ac = p.document.GetWordAfterCursorUntilSeparatorIgnoreNextToCursor("")
if ac != p.expected {
t.Errorf("[%d] Should be %#v, got %#v", k, p.expected, ac)
}
} else {
ac := p.document.GetWordAfterCursorUntilSeparatorIgnoreNextToCursor(p.sep)
if ac != p.expected {
t.Errorf("[%d] Should be %#v, got %#v", k, p.expected, ac)
}
}
}
}
@ -447,6 +630,7 @@ func TestDocument_FindEndOfCurrentWord(t *testing.T) {
pattern := []struct {
document *Document
expected int
sep string
}{
{
document: &Document{
@ -462,6 +646,14 @@ func TestDocument_FindEndOfCurrentWord(t *testing.T) {
},
expected: len("bana"),
},
{
document: &Document{
Text: "apply -f ./file/foo.json",
cursorPosition: len("apply -f ./"),
},
expected: len("file"),
sep: " /",
},
{
document: &Document{
Text: "apple bana",
@ -469,6 +661,14 @@ func TestDocument_FindEndOfCurrentWord(t *testing.T) {
},
expected: len(""),
},
{
document: &Document{
Text: "apply -f ./file/foo.json",
cursorPosition: len("apply -f ."),
},
expected: len(""),
sep: " /",
},
{
document: &Document{
Text: "apple bana",
@ -502,9 +702,20 @@ func TestDocument_FindEndOfCurrentWord(t *testing.T) {
}
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)
if p.sep == "" {
ac := p.document.FindEndOfCurrentWord()
if ac != p.expected {
t.Errorf("[%d] Should be %#v, got %#v", k, p.expected, ac)
}
ac = p.document.FindEndOfCurrentWordUntilSeparator("")
if ac != p.expected {
t.Errorf("[%d] Should be %#v, got %#v", k, p.expected, ac)
}
} else {
ac := p.document.FindEndOfCurrentWordUntilSeparator(p.sep)
if ac != p.expected {
t.Errorf("[%d] Should be %#v, got %#v", k, p.expected, ac)
}
}
}
}
@ -513,6 +724,7 @@ func TestDocument_FindEndOfCurrentWordWithSpace(t *testing.T) {
pattern := []struct {
document *Document
expected int
sep string
}{
{
document: &Document{
@ -528,6 +740,14 @@ func TestDocument_FindEndOfCurrentWordWithSpace(t *testing.T) {
},
expected: len("bana"),
},
{
document: &Document{
Text: "apply -f /file/foo.json",
cursorPosition: len("apply -f /"),
},
expected: len("file"),
sep: " /",
},
{
document: &Document{
Text: "apple bana",
@ -535,6 +755,14 @@ func TestDocument_FindEndOfCurrentWordWithSpace(t *testing.T) {
},
expected: len(" bana"),
},
{
document: &Document{
Text: "apply -f /path/to",
cursorPosition: len("apply -f /path"),
},
expected: len("/to"),
sep: " /",
},
{
document: &Document{
Text: "apple bana",
@ -566,9 +794,20 @@ func TestDocument_FindEndOfCurrentWordWithSpace(t *testing.T) {
}
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)
if p.sep == "" {
ac := p.document.FindEndOfCurrentWordWithSpace()
if ac != p.expected {
t.Errorf("[%d] Should be %#v, got %#v", k, p.expected, ac)
}
ac = p.document.FindEndOfCurrentWordUntilSeparatorIgnoreNextToCursor("")
if ac != p.expected {
t.Errorf("[%d] Should be %#v, got %#v", k, p.expected, ac)
}
} else {
ac := p.document.FindEndOfCurrentWordUntilSeparatorIgnoreNextToCursor(p.sep)
if ac != p.expected {
t.Errorf("[%d] Should be %#v, got %#v", k, p.expected, ac)
}
}
}
}

View File

@ -36,6 +36,14 @@ func OptionPrefix(x string) Option {
}
}
// OptionCompletionWordSeparator to set word separators. Enable only ' ' if empty.
func OptionCompletionWordSeparator(x string) Option {
return func(p *Prompt) error {
p.completion.wordSeparator = x
return nil
}
}
// OptionLivePrefix to change the prefix dynamically by callback function
func OptionLivePrefix(f func() (prefix string, useLivePrefix bool)) Option {
return func(p *Prompt) error {

View File

@ -170,7 +170,7 @@ func (p *Prompt) handleCompletionKeyBinding(key Key, completing bool) {
p.completion.Previous()
default:
if s, ok := p.completion.GetSelectedSuggestion(); ok {
w := p.buf.Document().GetWordBeforeCursor()
w := p.buf.Document().GetWordBeforeCursorUntilSeparator(p.completion.wordSeparator)
if w != "" {
p.buf.DeleteBeforeCursor(len([]rune(w)))
}

View File

@ -207,7 +207,7 @@ func (r *Render) Render(buffer *Buffer, completion *CompletionManager) {
r.renderCompletion(buffer, completion)
if suggest, ok := completion.GetSelectedSuggestion(); ok {
cursor = r.backward(cursor, runewidth.StringWidth(buffer.Document().GetWordBeforeCursor()))
cursor = r.backward(cursor, runewidth.StringWidth(buffer.Document().GetWordBeforeCursorUntilSeparator(completion.wordSeparator)))
r.out.SetColor(r.previewSuggestionTextColor, r.previewSuggestionBGColor, false)
r.out.WriteStr(suggest.Text)