Add autocomplete

This commit is contained in:
Zachary Yedidia
2019-06-16 15:56:39 -04:00
parent a5cf06026a
commit 8d85cae4c0
6 changed files with 156 additions and 8 deletions

View File

@@ -560,10 +560,22 @@ func (h *BufPane) OutdentSelection() bool {
// InsertTab inserts a tab or spaces
func (h *BufPane) InsertTab() bool {
indent := h.Buf.IndentString(util.IntOpt(h.Buf.Settings["tabsize"]))
tabBytes := len(indent)
bytesUntilIndent := tabBytes - (h.Cursor.GetVisualX() % tabBytes)
h.Buf.Insert(h.Cursor.Loc, indent[:bytesUntilIndent])
b := h.Buf
if b.HasSuggestions {
b.CycleAutocomplete(true)
return true
}
l := b.LineBytes(h.Cursor.Y)
l = util.SliceStart(l, h.Cursor.X)
hasComplete := b.Autocomplete(buffer.BufferComplete)
if !hasComplete {
indent := b.IndentString(util.IntOpt(b.Settings["tabsize"]))
tabBytes := len(indent)
bytesUntilIndent := tabBytes - (h.Cursor.GetVisualX() % tabBytes)
b.Insert(h.Cursor.Loc, indent[:bytesUntilIndent])
return true
}
return true
}

View File

@@ -227,6 +227,9 @@ func (h *BufPane) HandleEvent(event tcell.Event) {
func (h *BufPane) DoKeyEvent(e Event) bool {
if action, ok := BufKeyBindings[e]; ok {
estr := BufKeyStrings[e]
if estr != "InsertTab" {
h.Buf.HasSuggestions = false
}
for _, s := range MultiActions {
if s == estr {
cursors := h.Buf.GetCursors()

View File

@@ -157,12 +157,17 @@ var InfoOverrides = map[string]InfoKeyAction{
"QuitAll": (*InfoPane).QuitAll,
}
// CursorUp cycles history up
func (h *InfoPane) CursorUp() {
h.UpHistory(h.History[h.PromptType])
}
// CursorDown cycles history down
func (h *InfoPane) CursorDown() {
h.DownHistory(h.History[h.PromptType])
}
// InsertTab begins autocompletion
func (h *InfoPane) InsertTab() {
b := h.Buf
if b.HasSuggestions {
@@ -187,22 +192,32 @@ func (h *InfoPane) InsertTab() {
}
}
}
// CycleBack cycles back in the autocomplete suggestion list
func (h *InfoPane) CycleBack() {
if h.Buf.HasSuggestions {
h.Buf.CycleAutocomplete(false)
}
}
// InsertNewline completes the prompt
func (h *InfoPane) InsertNewline() {
if !h.HasYN {
h.DonePrompt(false)
}
}
// Quit cancels the prompt
func (h *InfoPane) Quit() {
h.DonePrompt(true)
}
// QuitAll cancels the prompt
func (h *InfoPane) QuitAll() {
h.DonePrompt(true)
}
// Escape cancels the prompt
func (h *InfoPane) Escape() {
h.DonePrompt(true)
}

View File

@@ -16,7 +16,7 @@ import (
// cursor location
// It returns a list of string suggestions which will be inserted at
// the current cursor location if selected as well as a list of
// suggestion names which can be displayed in a autocomplete box or
// suggestion names which can be displayed in an autocomplete box or
// other UI element
type Completer func(*Buffer) ([]string, []string)
@@ -24,15 +24,18 @@ func (b *Buffer) GetSuggestions() {
}
func (b *Buffer) Autocomplete(c Completer) {
// Autocomplete starts the autocomplete process
func (b *Buffer) Autocomplete(c Completer) bool {
b.Completions, b.Suggestions = c(b)
if len(b.Completions) != len(b.Suggestions) || len(b.Completions) == 0 {
return
return false
}
b.CurSuggestion = -1
b.CycleAutocomplete(true)
return true
}
// CycleAutocomplete moves to the next suggestion
func (b *Buffer) CycleAutocomplete(forward bool) {
prevSuggestion := b.CurSuggestion
@@ -53,7 +56,7 @@ func (b *Buffer) CycleAutocomplete(forward bool) {
if prevSuggestion < len(b.Suggestions) && prevSuggestion >= 0 {
start = end.Move(-utf8.RuneCountInString(b.Completions[prevSuggestion]), b)
} else {
end = start.Move(1, b)
// end = start.Move(1, b)
}
b.Replace(start, end, b.Completions[b.CurSuggestion])
@@ -62,6 +65,27 @@ func (b *Buffer) CycleAutocomplete(forward bool) {
}
}
// GetWord gets the most recent word separated by any separator
// (whitespace, punctuation, any non alphanumeric character)
func GetWord(b *Buffer) ([]byte, int) {
c := b.GetActiveCursor()
l := b.LineBytes(c.Y)
l = util.SliceStart(l, c.X)
if c.X == 0 || util.IsWhitespace(b.RuneAt(c.Loc)) {
return []byte{}, -1
}
if util.IsNonAlphaNumeric(b.RuneAt(c.Loc)) {
return []byte{}, c.X
}
args := bytes.FieldsFunc(l, util.IsNonAlphaNumeric)
input := args[len(args)-1]
return input, c.X - utf8.RuneCount(input)
}
// GetArg gets the most recent word (separated by ' ' only)
func GetArg(b *Buffer) (string, int) {
c := b.GetActiveCursor()
l := b.LineBytes(c.Y)
@@ -128,3 +152,55 @@ func FileComplete(b *Buffer) ([]string, []string) {
return completions, suggestions
}
// BufferComplete autocompletes based on previous words in the buffer
func BufferComplete(b *Buffer) ([]string, []string) {
c := b.GetActiveCursor()
input, argstart := GetWord(b)
if argstart == -1 {
return []string{}, []string{}
}
inputLen := utf8.RuneCount(input)
suggestionsSet := make(map[string]struct{})
var suggestions []string
for i := c.Y; i >= 0; i-- {
l := b.LineBytes(i)
words := bytes.FieldsFunc(l, util.IsNonAlphaNumeric)
for _, w := range words {
if bytes.HasPrefix(w, input) && utf8.RuneCount(w) > inputLen {
strw := string(w)
if _, ok := suggestionsSet[strw]; !ok {
suggestionsSet[strw] = struct{}{}
suggestions = append(suggestions, strw)
}
}
}
}
for i := c.Y + 1; i < b.LinesNum(); i++ {
l := b.LineBytes(i)
words := bytes.FieldsFunc(l, util.IsNonAlphaNumeric)
for _, w := range words {
if bytes.HasPrefix(w, input) && utf8.RuneCount(w) > inputLen {
strw := string(w)
if _, ok := suggestionsSet[strw]; !ok {
suggestionsSet[strw] = struct{}{}
suggestions = append(suggestions, strw)
}
}
}
}
if len(suggestions) > 1 {
suggestions = append(suggestions, string(input))
}
completions := make([]string, len(suggestions))
for i := range suggestions {
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
}
return completions, suggestions
}

View File

@@ -69,6 +69,43 @@ func (s *StatusLine) Display() {
// We'll draw the line at the lowest line in the window
y := s.win.Height + s.win.Y - 1
b := s.win.Buf
if b.HasSuggestions && len(b.Suggestions) > 1 {
statusLineStyle := config.DefStyle.Reverse(true)
if style, ok := config.Colorscheme["statusline"]; ok {
statusLineStyle = style
}
keymenuOffset := 0
if config.GetGlobalOption("keymenu").(bool) {
keymenuOffset = len(keydisplay)
}
x := 0
for j, sug := range b.Suggestions {
style := statusLineStyle
if b.CurSuggestion == j {
style = style.Reverse(true)
}
for _, r := range sug {
screen.Screen.SetContent(x, y-keymenuOffset, r, nil, style)
x++
if x >= s.win.Width {
return
}
}
screen.Screen.SetContent(x, y-keymenuOffset, ' ', nil, statusLineStyle)
x++
if x >= s.win.Width {
return
}
}
for x < s.win.Width {
screen.Screen.SetContent(x, y-keymenuOffset, ' ', nil, statusLineStyle)
x++
}
return
}
formatter := func(match []byte) []byte {
name := match[2 : len(match)-1]
if bytes.HasPrefix(name, []byte("opt")) {

View File

@@ -10,6 +10,7 @@ import (
"strconv"
"strings"
"time"
"unicode"
"unicode/utf8"
"github.com/blang/semver"
@@ -403,3 +404,7 @@ func Clamp(val, min, max int) int {
}
return val
}
func IsNonAlphaNumeric(c rune) bool {
return !unicode.IsLetter(c) && !unicode.IsNumber(c)
}