diff --git a/internal/action/actions.go b/internal/action/actions.go index 304f0c9a..3a77e264 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -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) } diff --git a/internal/action/infocomplete.go b/internal/action/infocomplete.go index 12f7844b..83892910 100644 --- a/internal/action/infocomplete.go +++ b/internal/action/infocomplete.go @@ -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 diff --git a/internal/buffer/autocomplete.go b/internal/buffer/autocomplete.go index 7b8f3b11..135637a5 100644 --- a/internal/buffer/autocomplete.go +++ b/internal/buffer/autocomplete.go @@ -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 } diff --git a/internal/buffer/buffer.go b/internal/buffer/buffer.go index 91927b3d..9de39d95 100644 --- a/internal/buffer/buffer.go +++ b/internal/buffer/buffer.go @@ -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 diff --git a/internal/display/infowindow.go b/internal/display/infowindow.go index 32662be2..b5c7af10 100644 --- a/internal/display/infowindow.go +++ b/internal/display/infowindow.go @@ -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) } diff --git a/internal/display/statusline.go b/internal/display/statusline.go index 5c5f5512..daec8125 100644 --- a/internal/display/statusline.go +++ b/internal/display/statusline.go @@ -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 {