Use text edits for autocompletion

This commit is contained in:
Zachary Yedidia
2020-08-15 18:05:29 -04:00
parent 3821a7a075
commit 68270773dd
6 changed files with 118 additions and 92 deletions

View File

@@ -666,6 +666,13 @@ func (h *BufPane) Autocomplete() bool {
return false
}
// if there is an existing completion, always cycle it
if b.HasSuggestions {
b.CycleAutocomplete(true)
return true
}
// don't start a new completion unless the correct conditions are met
if h.Cursor.X == 0 {
return false
}
@@ -675,11 +682,6 @@ func (h *BufPane) Autocomplete() bool {
// don't autocomplete if cursor is on alpha numeric character (middle of a word)
return false
}
if b.HasSuggestions {
b.CycleAutocomplete(true)
return true
}
return b.Autocomplete(buffer.LSPComplete)
}

View File

@@ -15,7 +15,7 @@ import (
// for example with `vsplit filename`.
// CommandComplete autocompletes commands
func CommandComplete(b *buffer.Buffer) ([]string, []string) {
func CommandComplete(b *buffer.Buffer) []buffer.Completion {
c := b.GetActiveCursor()
input, argstart := buffer.GetArg(b)
@@ -32,11 +32,11 @@ func CommandComplete(b *buffer.Buffer) ([]string, []string) {
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
}
return completions, suggestions
return buffer.ConvertCompletions(completions, suggestions, c)
}
// HelpComplete autocompletes help topics
func HelpComplete(b *buffer.Buffer) ([]string, []string) {
func HelpComplete(b *buffer.Buffer) []buffer.Completion {
c := b.GetActiveCursor()
input, argstart := buffer.GetArg(b)
@@ -54,7 +54,7 @@ func HelpComplete(b *buffer.Buffer) ([]string, []string) {
for i := range suggestions {
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
}
return completions, suggestions
return buffer.ConvertCompletions(completions, suggestions, c)
}
// colorschemeComplete tab-completes names of colorschemes.
@@ -87,7 +87,7 @@ func contains(s []string, e string) bool {
}
// OptionComplete autocompletes options
func OptionComplete(b *buffer.Buffer) ([]string, []string) {
func OptionComplete(b *buffer.Buffer) []buffer.Completion {
c := b.GetActiveCursor()
input, argstart := buffer.GetArg(b)
@@ -97,22 +97,17 @@ func OptionComplete(b *buffer.Buffer) ([]string, []string) {
suggestions = append(suggestions, option)
}
}
// for option := range localSettings {
// if strings.HasPrefix(option, input) && !contains(suggestions, option) {
// suggestions = append(suggestions, option)
// }
// }
sort.Strings(suggestions)
completions := make([]string, len(suggestions))
for i := range suggestions {
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
}
return completions, suggestions
return buffer.ConvertCompletions(completions, suggestions, c)
}
// OptionValueComplete completes values for various options
func OptionValueComplete(b *buffer.Buffer) ([]string, []string) {
func OptionValueComplete(b *buffer.Buffer) []buffer.Completion {
c := b.GetActiveCursor()
l := b.LineBytes(c.Y)
l = util.SliceStart(l, c.X)
@@ -128,12 +123,6 @@ func OptionValueComplete(b *buffer.Buffer) ([]string, []string) {
break
}
}
// for option := range localSettings {
// if option == string(args[len(args)-2]) {
// completeValue = true
// break
// }
// }
}
if !completeValue {
return OptionComplete(b)
@@ -150,11 +139,6 @@ func OptionValueComplete(b *buffer.Buffer) ([]string, []string) {
optionVal = option
}
}
// for k, option := range localSettings {
// if k == inputOpt {
// optionVal = option
// }
// }
switch optionVal.(type) {
case bool:
@@ -204,11 +188,11 @@ func OptionValueComplete(b *buffer.Buffer) ([]string, []string) {
for i := range suggestions {
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
}
return completions, suggestions
return buffer.ConvertCompletions(completions, suggestions, c)
}
// PluginCmdComplete autocompletes the plugin command
func PluginCmdComplete(b *buffer.Buffer) ([]string, []string) {
func PluginCmdComplete(b *buffer.Buffer) []buffer.Completion {
c := b.GetActiveCursor()
input, argstart := buffer.GetArg(b)
@@ -224,11 +208,11 @@ func PluginCmdComplete(b *buffer.Buffer) ([]string, []string) {
for i := range suggestions {
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
}
return completions, suggestions
return buffer.ConvertCompletions(completions, suggestions, c)
}
// PluginComplete completes values for the plugin command
func PluginComplete(b *buffer.Buffer) ([]string, []string) {
func PluginComplete(b *buffer.Buffer) []buffer.Completion {
c := b.GetActiveCursor()
l := b.LineBytes(c.Y)
l = util.SliceStart(l, c.X)
@@ -260,7 +244,7 @@ func PluginComplete(b *buffer.Buffer) ([]string, []string) {
for i := range suggestions {
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
}
return completions, suggestions
return buffer.ConvertCompletions(completions, suggestions, c)
}
// PluginNameComplete completes with the names of loaded plugins

View File

@@ -9,6 +9,7 @@ import (
"github.com/zyedidia/micro/v2/internal/lsp"
"github.com/zyedidia/micro/v2/internal/util"
"go.lsp.dev/protocol"
)
// A Completer is a function that takes a buffer and returns info
@@ -18,49 +19,56 @@ import (
// the current cursor location if selected as well as a list of
// suggestion names which can be displayed in an autocomplete box or
// other UI element
type Completer func(*Buffer) ([]string, []string)
func (b *Buffer) GetSuggestions() {
type Completer func(*Buffer) []Completion
type Completion struct {
Edits []protocol.TextEdit
Label string
CommitChars []rune
Kind int
Filter string
Detail string
Doc string
}
// 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 {
b.Completions = c(b)
if len(b.Completions) == 0 {
return false
}
b.CurSuggestion = -1
b.CurCompletion = -1
b.CycleAutocomplete(true)
return true
}
// CycleAutocomplete moves to the next suggestion
func (b *Buffer) CycleAutocomplete(forward bool) {
prevSuggestion := b.CurSuggestion
prevCompletion := b.CurCompletion
if forward {
b.CurSuggestion++
b.CurCompletion++
} else {
b.CurSuggestion--
b.CurCompletion--
}
if b.CurSuggestion >= len(b.Suggestions) {
b.CurSuggestion = 0
} else if b.CurSuggestion < 0 {
b.CurSuggestion = len(b.Suggestions) - 1
if b.CurCompletion >= len(b.Completions) {
b.CurCompletion = 0
} else if b.CurCompletion < 0 {
b.CurCompletion = len(b.Completions) - 1
}
c := b.GetActiveCursor()
start := c.Loc
end := c.Loc
if prevSuggestion < len(b.Suggestions) && prevSuggestion >= 0 {
start = end.Move(-util.CharacterCountInString(b.Completions[prevSuggestion]), b)
} else {
// end = start.Move(1, b)
// undo prev completion
if prevCompletion != -1 {
prev := b.Completions[prevCompletion]
for i := 0; i < len(prev.Edits); i++ {
b.UndoOneEvent()
}
}
b.Replace(start, end, b.Completions[b.CurSuggestion])
if len(b.Suggestions) > 1 {
// apply current completion
comp := b.Completions[b.CurCompletion]
b.ApplyEdits(comp.Edits)
if len(b.Completions) > 1 {
b.HasSuggestions = true
}
}
@@ -105,7 +113,7 @@ func GetArg(b *Buffer) (string, int) {
}
// FileComplete autocompletes filenames
func FileComplete(b *Buffer) ([]string, []string) {
func FileComplete(b *Buffer) []Completion {
c := b.GetActiveCursor()
input, argstart := GetArg(b)
@@ -124,7 +132,7 @@ func FileComplete(b *Buffer) ([]string, []string) {
}
if err != nil {
return nil, nil
return nil
}
var suggestions []string
@@ -150,16 +158,16 @@ func FileComplete(b *Buffer) ([]string, []string) {
completions[i] = util.SliceEndStr(complete, c.X-argstart)
}
return completions, suggestions
return ConvertCompletions(completions, suggestions, c)
}
// BufferComplete autocompletes based on previous words in the buffer
func BufferComplete(b *Buffer) ([]string, []string) {
func BufferComplete(b *Buffer) []Completion {
c := b.GetActiveCursor()
input, argstart := GetWord(b)
if argstart == -1 {
return []string{}, []string{}
return nil
}
inputLen := util.CharacterCount(input)
@@ -202,36 +210,69 @@ func BufferComplete(b *Buffer) ([]string, []string) {
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
}
return completions, suggestions
return ConvertCompletions(completions, suggestions, c)
}
func LSPComplete(b *Buffer) ([]string, []string) {
c := b.GetActiveCursor()
_, argstart := GetWord(b)
if argstart == -1 {
return []string{}, []string{}
func LSPComplete(b *Buffer) []Completion {
if !b.HasLSP() {
return nil
}
c := b.GetActiveCursor()
pos := lsp.Position(c.X, c.Y)
items, err := b.Server.Completion(b.AbsPath, pos)
if err != nil {
return []string{}, []string{}
return nil
}
suggestions := make([]string, len(items))
completions := make([]string, len(items))
completions := make([]Completion, len(items))
for i, item := range items {
suggestions[i] = item.Label
completions[i] = Completion{
Label: item.Label,
Detail: item.Detail,
}
if item.TextEdit != nil && len(item.TextEdit.NewText) > 0 {
completions[i] = util.SliceEndStr(item.TextEdit.NewText, c.X-argstart)
} else if len(item.InsertText) > 0 {
completions[i] = util.SliceEndStr(item.InsertText, c.X-argstart)
completions[i].Edits = []protocol.TextEdit{*item.TextEdit}
} else {
completions[i] = util.SliceEndStr(item.Label, c.X-argstart)
var t string
if len(item.InsertText) > 0 {
t = item.InsertText
} else {
t = item.Label
}
_, argstart := GetWord(b)
str := util.SliceEndStr(t, c.X-argstart)
completions[i].Edits = []protocol.TextEdit{protocol.TextEdit{
NewText: str,
Range: protocol.Range{
Start: lsp.Position(c.X, c.Y),
End: lsp.Position(c.X, c.Y),
},
}}
}
}
return completions, suggestions
return completions
}
// ConvertCompletions converts a list of insert text with suggestion labels
// to an array of completion objects ready for autocompletion
func ConvertCompletions(completions, suggestions []string, c *Cursor) []Completion {
comp := make([]Completion, len(completions))
for i := 0; i < len(completions); i++ {
comp[i] = Completion{
Label: suggestions[i],
}
comp[i].Edits = []protocol.TextEdit{protocol.TextEdit{
NewText: completions[i],
Range: protocol.Range{
Start: lsp.Position(c.X, c.Y),
End: lsp.Position(c.X, c.Y),
},
}}
}
return comp
}

View File

@@ -93,9 +93,8 @@ type SharedBuffer struct {
// Settings customized by the user
Settings map[string]interface{}
Suggestions []string
Completions []string
CurSuggestion int
Completions []Completion
CurCompletion int
Messages []*Message

View File

@@ -179,8 +179,8 @@ func (i *InfoWindow) displayKeyMenu() {
func (i *InfoWindow) totalSize() int {
sum := 0
for _, n := range i.Suggestions {
sum += runewidth.StringWidth(n) + 1
for _, n := range i.Completions {
sum += runewidth.StringWidth(n.Label) + 1
}
return sum
}
@@ -189,9 +189,9 @@ func (i *InfoWindow) scrollToSuggestion() {
x := 0
s := i.totalSize()
for j, n := range i.Suggestions {
c := util.CharacterCountInString(n)
if j == i.CurSuggestion {
for j, n := range i.Completions {
c := util.CharacterCountInString(n.Label)
if j == i.CurCompletion {
if x+c >= i.hscroll+i.Width {
i.hscroll = util.Clamp(x+c+1-i.Width, 0, s-i.Width)
} else if x < i.hscroll {
@@ -236,7 +236,7 @@ func (i *InfoWindow) Display() {
}
}
if i.HasSuggestions && len(i.Suggestions) > 1 {
if i.HasSuggestions && len(i.Completions) > 1 {
i.scrollToSuggestion()
x := -i.hscroll
@@ -273,12 +273,12 @@ func (i *InfoWindow) Display() {
}
}
for j, s := range i.Suggestions {
for j, s := range i.Completions {
style := statusLineStyle
if i.CurSuggestion == j {
if i.CurCompletion == j {
style = style.Reverse(true)
}
for _, r := range s {
for _, r := range s.Label {
draw(r, style)
// screen.SetContent(x, i.Y-keymenuOffset-1, r, nil, style)
}

View File

@@ -100,7 +100,7 @@ func (s *StatusLine) Display() {
b := s.win.Buf
// autocomplete suggestions (for the buffer, not for the infowindow)
if b.HasSuggestions && len(b.Suggestions) > 1 {
if b.HasSuggestions && len(b.Completions) > 1 {
statusLineStyle := config.DefStyle.Reverse(true)
if style, ok := config.Colorscheme["statusline"]; ok {
statusLineStyle = style
@@ -110,12 +110,12 @@ func (s *StatusLine) Display() {
keymenuOffset = len(keydisplay)
}
x := 0
for j, sug := range b.Suggestions {
for j, sug := range b.Completions {
style := statusLineStyle
if b.CurSuggestion == j {
if b.CurCompletion == j {
style = style.Reverse(true)
}
for _, r := range sug {
for _, r := range sug.Label {
screen.SetContent(x, y-keymenuOffset, r, nil, style)
x++
if x >= s.win.Width {