Skip to content

Commit

Permalink
prompt: allow control of auto-complete word delimiters (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
jedib0t authored Sep 30, 2023
1 parent 2cb1018 commit e7015a9
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 78 deletions.
65 changes: 35 additions & 30 deletions prompt/buffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ func (b *buffer) DeleteWordBackward() {
foundWord := false
line := b.getCurrentLine()
for idx := b.cursor.Column - 1; idx >= 0; idx-- {
isPartOfWord := b.isPartOfWord(line[idx])
isPartOfWord := isPartOfWord(line[idx])
if !isPartOfWord && foundWord {
b.lines[b.cursor.Line] = line[:idx] + line[b.cursor.Column:]
b.cursor.Column = idx
Expand Down Expand Up @@ -184,7 +184,7 @@ func (b *buffer) DeleteWordForward() {
// delete till beginning of previous word
foundWord, foundNonWord := false, false
for idx := b.cursor.Column; idx < len(line); idx++ {
isPartOfWord := b.isPartOfWord(line[idx])
isPartOfWord := isPartOfWord(line[idx])
if !isPartOfWord {
foundNonWord = true
}
Expand Down Expand Up @@ -511,7 +511,7 @@ func (b *buffer) MoveWordLeft(locked ...bool) {
line := b.lines[lineIdx]
for colIdx := b.cursor.Column - 1; colIdx >= 0; colIdx-- {
b.cursor.Column = colIdx
isPartOfWord := b.isPartOfWord(line[colIdx])
isPartOfWord := isPartOfWord(line[colIdx])
if foundWord && (!isPartOfWord || colIdx == 0) {
if !isPartOfWord {
b.cursor.Column++
Expand Down Expand Up @@ -559,7 +559,7 @@ func (b *buffer) MoveWordRight(locked ...bool) {
line := b.lines[lineIdx]
for colIdx := b.cursor.Column; colIdx < len(line); colIdx++ {
b.cursor.Column = colIdx
isPartOfWord := b.isPartOfWord(line[b.cursor.Column])
isPartOfWord := isPartOfWord(line[b.cursor.Column])
if isPartOfWord && foundBreak {
return
}
Expand Down Expand Up @@ -616,19 +616,19 @@ func (b *buffer) getCurrentLine() string {
}

func (b *buffer) getCurrentWord(line string) (string, int, int) {
if len(line) == 0 || b.cursor.Column >= len(line) || !b.isPartOfWord(line[b.cursor.Column]) {
if len(line) == 0 || b.cursor.Column >= len(line) || !isPartOfWord(line[b.cursor.Column]) {
return "", -1, -1
}

idxWordStart, idxWordEnd := -1, -1
for idx := b.cursor.Column; idx >= 0; idx-- {
if !b.isPartOfWord(line[idx]) {
if !isPartOfWord(line[idx]) {
break
}
idxWordStart = idx
}
for idx := b.cursor.Column; idx < len(line); idx++ {
if !b.isPartOfWord(line[idx]) {
if !isPartOfWord(line[idx]) {
break
}
idxWordEnd = idx
Expand All @@ -641,12 +641,17 @@ func (b *buffer) getLine(n int) string {
return b.lines[n]
}

func (b *buffer) getWordAtCursor() (string, int) {
func (b *buffer) getWordAtCursor(wordDelimiters map[byte]bool) (string, int) {
line := b.getCurrentLine()
if b.cursor.Column == len(line) || (b.cursor.Column < len(line) && line[b.cursor.Column] == ' ') {
idxWordStart := -1
for idx := b.cursor.Column - 1; idx >= 0; idx-- {
if !b.isPartOfWord(line[idx]) {
r := line[idx]
if wordDelimiters != nil {
if wordDelimiters[r] {
break
}
} else if !isPartOfWord(r) {
break
}
idxWordStart = idx
Expand All @@ -658,27 +663,6 @@ func (b *buffer) getWordAtCursor() (string, int) {
return "", -1
}

var (
reNonWordRunes = map[byte]bool{
' ': true,
'(': true,
')': true,
',': true,
'.': true,
';': true,
'[': true,
'\n': true,
'\t': true,
']': true,
'{': true,
'}': true,
}
)

func (b *buffer) isPartOfWord(r byte) bool {
return !reNonWordRunes[r]
}

type linesChangedMap map[int]bool

func (lc linesChangedMap) Clear() {
Expand Down Expand Up @@ -719,3 +703,24 @@ func (lc linesChangedMap) String() string {
sort.Ints(lines)
return fmt.Sprintf("%v", lines)
}

var (
nonWordRunes = map[byte]bool{
' ': true,
'(': true,
')': true,
',': true,
'.': true,
';': true,
'[': true,
'\n': true,
'\t': true,
']': true,
'{': true,
'}': true,
}
)

func isPartOfWord(r byte) bool {
return !nonWordRunes[r]
}
16 changes: 12 additions & 4 deletions prompt/buffer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -794,24 +794,32 @@ func TestBuffer_getWordAtCursor(t *testing.T) {
b.InsertString("foo bar baz foo")

b.cursor.Column = 11
word, idx := b.getWordAtCursor()
word, idx := b.getWordAtCursor(nil)
assert.Equal(t, "baz", word)
assert.Equal(t, 8, idx)

b.cursor.Column = 7
word, idx = b.getWordAtCursor()
word, idx = b.getWordAtCursor(nil)
assert.Equal(t, "bar", word)
assert.Equal(t, 4, idx)

b.cursor.Column = 3
word, idx = b.getWordAtCursor()
word, idx = b.getWordAtCursor(nil)
assert.Equal(t, "foo", word)
assert.Equal(t, 0, idx)

b.cursor.Column = 2
word, idx = b.getWordAtCursor()
word, idx = b.getWordAtCursor(nil)
assert.Equal(t, "", word)
assert.Equal(t, -1, idx)

b.Set("foo.")
word, idx = b.getWordAtCursor(nil)
assert.Equal(t, "", word)
assert.Equal(t, -1, idx)
word, idx = b.getWordAtCursor(StyleAutoCompleteDefault.WordDelimiters)
assert.Equal(t, "foo.", word)
assert.Equal(t, 0, idx)
}

func TestLinesChangedMap(t *testing.T) {
Expand Down
7 changes: 4 additions & 3 deletions prompt/prompt_autocomplete.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func (p *prompt) autoComplete(lines []string, cursorPos CursorLocation, startIdx

// get the line styling
linePrefix, prefixWidth, _, numLen, _, _ := p.calculateLineStyling(lines)
word, _ := p.buffer.getWordAtCursor()
word, _ := p.buffer.getWordAtCursor(p.style.AutoComplete.WordDelimiters)
wordLen := len(word)

// get the suggestions printed to super-impose on the displayed lines
Expand Down Expand Up @@ -72,14 +72,15 @@ func (p *prompt) updateSuggestionsInternal(lastLine string, lastWord string, las
p.buffer.mutex.Lock()
line := p.buffer.getCurrentLine()
location := uint(p.buffer.cursor.Column)
word, idx := p.buffer.getWordAtCursor()
word, idx := p.buffer.getWordAtCursor(p.style.AutoComplete.WordDelimiters)
p.buffer.mutex.Unlock()
minChars := p.style.AutoComplete.MinChars

// if there is no word currently, clear drop-down
forced := false
if p.forcedAutoComplete() {
forced = true
} else if word == "" || idx < 0 {
} else if word == "" || idx < 0 || (minChars > 0 && len(word) < minChars) {
p.setSuggestions(make([]Suggestion, 0))
p.clearDebugData("ac.")
return line, word, idx
Expand Down
2 changes: 1 addition & 1 deletion prompt/prompt_handle.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ var autoCompleteActionHandlerMap = map[Action]actionHandler{
return nil
},
AutoCompleteSelect: func(p *prompt, output *termenv.Output, key tea.KeyMsg) error {
word, _ := p.buffer.getWordAtCursor()
word, _ := p.buffer.getWordAtCursor(p.style.AutoComplete.WordDelimiters)
suggestions, suggestionsIdx := p.getSuggestionsAndIdx()
if suggestionsIdx < len(suggestions) {
suggestion := suggestions[suggestionsIdx].Value
Expand Down
41 changes: 39 additions & 2 deletions prompt/prompt_model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ func compareModelLines(t *testing.T, expected, actual []string, msg ...any) {
}

func generateTestPrompt(t *testing.T, ctx context.Context) *prompt {
out := strings.Builder{}

p := &prompt{}
err := p.SetKeyMap(KeyMapDefault)
if err != nil {
Expand All @@ -42,7 +44,7 @@ func generateTestPrompt(t *testing.T, ctx context.Context) *prompt {
p.SetHistoryExecPrefix("!")
p.SetHistoryListPrefix("!!")
p.SetInput(os.Stdin)
p.SetOutput(os.Stdout)
p.SetOutput(&out)
p.SetPrefixer(PrefixText("[" + t.Name() + "] "))
p.SetRefreshInterval(DefaultRefreshInterval)
p.SetStyle(StyleDefault)
Expand Down Expand Up @@ -88,7 +90,42 @@ func TestPrompt_updateModel(t *testing.T) {
p.buffer.InsertString(`select` + ` * from dual`)
p.updateModel(true)
expectedLines := []string{
"[TestPrompt_updateModel/simple_one-liner_with_line-numbers] \x1b[38;5;240;48;5;236m 1 \x1b[0m \x1b[38;5;81mselect\x1b[0m\x1b[38;5;231m \x1b[0m\x1b[38;5;197m*\x1b[0m\x1b[38;5;231m \x1b[0m\x1b[38;5;81mfrom\x1b[0m\x1b[38;5;231m \x1b[0m\x1b[38;5;231mdual\x1b[0m\x1b[38;5;232;48;5;6m \x1b[0m",
"[TestPrompt_updateModel/simple_one-liner_with_line-numbers] \x1b[38;5;239;48;5;235m 1 \x1b[0m \x1b[38;5;81mselect\x1b[0m\x1b[38;5;231m \x1b[0m\x1b[38;5;197m*\x1b[0m\x1b[38;5;231m \x1b[0m\x1b[38;5;81mfrom\x1b[0m\x1b[38;5;231m \x1b[0m\x1b[38;5;231mdual\x1b[0m\x1b[38;5;232;48;5;6m \x1b[0m",
}
compareModelLines(t, expectedLines, p.linesToRender)
})

t.Run("multi-liner with line-numbers and scroll-bar", func(t *testing.T) {
p := generateTestPrompt(t, ctx)
p.SetAutoCompleter(AutoCompleteSQLKeywords())
p.SetSyntaxHighlighter(syntaxHighlighter)
p.Style().LineNumbers = StyleLineNumbersEnabled
p.Style().LineNumbers.ZeroPrefixed = true
p.Style().Dimensions.HeightMin = 5
p.Style().Dimensions.HeightMax = 5
p.init(ctx)

testInput := "foo\nbar\nbaz\n"
p.buffer.InsertString(testInput)
p.updateModel(true)
expectedLines := []string{
"[TestPrompt_updateModel/multi-liner_with_line-numbers_and_scroll-bar] \x1b[38;5;239;48;5;235m 1 \x1b[0m \x1b[38;5;231mfoo\x1b[0m\x1b[38;5;231m",
"[TestPrompt_updateModel/multi-liner_with_line-numbers_and_scroll-bar] \x1b[38;5;239;48;5;235m 2 \x1b[0m \x1b[0m\x1b[38;5;231mbar\x1b[0m\x1b[38;5;231m",
"[TestPrompt_updateModel/multi-liner_with_line-numbers_and_scroll-bar] \x1b[38;5;239;48;5;235m 3 \x1b[0m \x1b[0m\x1b[38;5;231mbaz\x1b[0m\x1b[38;5;231m",
"[TestPrompt_updateModel/multi-liner_with_line-numbers_and_scroll-bar] \x1b[38;5;239;48;5;235m 4 \x1b[0m \x1b[0m\x1b[38;5;232;48;5;6m \x1b[0m",
"[TestPrompt_updateModel/multi-liner_with_line-numbers_and_scroll-bar] \x1b[38;5;239;48;5;235m \x1b[0m",
}
compareModelLines(t, expectedLines, p.linesToRender)

p.buffer.InsertString(testInput)
p.buffer.InsertString(testInput)
p.updateModel(true)
expectedLines = []string{
"[TestPrompt_updateModel/multi-liner_with_line-numbers_and_scroll-bar] \x1b[38;5;239;48;5;235m 06 \x1b[0m \x1b[0m\x1b[38;5;231mbaz\x1b[0m\x1b[38;5;231m \x1b[38;5;237;48;5;233m░\x1b[0m",
"[TestPrompt_updateModel/multi-liner_with_line-numbers_and_scroll-bar] \x1b[38;5;239;48;5;235m 07 \x1b[0m \x1b[0m\x1b[38;5;231mfoo\x1b[0m\x1b[38;5;231m \x1b[38;5;237;48;5;233m░\x1b[0m",
"[TestPrompt_updateModel/multi-liner_with_line-numbers_and_scroll-bar] \x1b[38;5;239;48;5;235m 08 \x1b[0m \x1b[0m\x1b[38;5;231mbar\x1b[0m\x1b[38;5;231m \x1b[38;5;237;48;5;233m░\x1b[0m",
"[TestPrompt_updateModel/multi-liner_with_line-numbers_and_scroll-bar] \x1b[38;5;239;48;5;235m 09 \x1b[0m \x1b[0m\x1b[38;5;231mbaz\x1b[0m\x1b[38;5;231m \x1b[38;5;237;48;5;233m░\x1b[0m",
"[TestPrompt_updateModel/multi-liner_with_line-numbers_and_scroll-bar] \x1b[38;5;239;48;5;235m 10 \x1b[0m \x1b[0m\x1b[38;5;232;48;5;6m \x1b[0m \x1b[38;5;237;48;5;233m█\x1b[0m",
}
compareModelLines(t, expectedLines, p.linesToRender)
})
Expand Down
Loading

0 comments on commit e7015a9

Please sign in to comment.