From 36a8f0a386c7af6d9788f28ab9595dcf7150fded Mon Sep 17 00:00:00 2001 From: Rebecca Stambler Date: Wed, 21 Nov 2018 23:51:01 -0500 Subject: [PATCH] internal/lsp: sort completions according to rank The LSP specification doesn't have a Score field, so we must provide sortText to the protocol in order to maintain the correct order. Change-Id: I075849f520c21a0465dfb2060c598d8bae5f876b Reviewed-on: https://go-review.googlesource.com/c/151237 Run-TryBot: Rebecca Stambler TryBot-Result: Gobot Gobot Reviewed-by: Ian Cottrell --- internal/lsp/completion.go | 29 +++++++++++++++++++++++++---- internal/lsp/lsp_test.go | 20 +++++++++++++++++++- internal/lsp/server.go | 4 ++-- internal/lsp/source/completion.go | 31 +++++++++++++++---------------- 4 files changed, 61 insertions(+), 23 deletions(-) diff --git a/internal/lsp/completion.go b/internal/lsp/completion.go index f894142c58..d4012f3b8e 100644 --- a/internal/lsp/completion.go +++ b/internal/lsp/completion.go @@ -13,7 +13,7 @@ import ( "golang.org/x/tools/internal/lsp/source" ) -func toProtocolCompletionItems(items []source.CompletionItem, snippetsSupported, signatureHelpEnabled bool) []protocol.CompletionItem { +func toProtocolCompletionItems(items []source.CompletionItem, prefix string, pos protocol.Position, snippetsSupported, signatureHelpEnabled bool) []protocol.CompletionItem { var results []protocol.CompletionItem sort.Slice(items, func(i, j int) bool { return items[i].Score > items[j].Score @@ -22,14 +22,33 @@ func toProtocolCompletionItems(items []source.CompletionItem, snippetsSupported, if snippetsSupported { insertTextFormat = protocol.SnippetTextFormat } - for _, item := range items { + for i, item := range items { + // Matching against the label. + if !strings.HasPrefix(item.Label, prefix) { + continue + } insertText, triggerSignatureHelp := labelToProtocolSnippets(item.Label, item.Kind, insertTextFormat, signatureHelpEnabled) + if prefix != "" { + insertText = insertText[len(prefix):] + } i := protocol.CompletionItem{ Label: item.Label, - InsertText: insertText, Detail: item.Detail, Kind: float64(toProtocolCompletionItemKind(item.Kind)), InsertTextFormat: insertTextFormat, + TextEdit: &protocol.TextEdit{ + NewText: insertText, + Range: protocol.Range{ + Start: pos, + End: pos, + }, + }, + // InsertText is deprecated in favor of TextEdit. + InsertText: insertText, + // This is a hack so that the client sorts completion results in the order + // according to their score. This can be removed upon the resolution of + // https://github.com/Microsoft/language-server-protocol/issues/348. + SortText: fmt.Sprintf("%05d", i), } // If we are completing a function, we should trigger signature help if possible. if triggerSignatureHelp && signatureHelpEnabled { @@ -72,7 +91,9 @@ func labelToProtocolSnippets(label string, kind source.CompletionItemKind, inser case source.ConstantCompletionItem: // The label for constants is of the format " = ". // We should now insert the " = " part of the label. - return label[:strings.Index(label, " =")], false + if i := strings.Index(label, " ="); i >= 0 { + return label[:i], false + } case source.FunctionCompletionItem, source.MethodCompletionItem: trimmed := label[:strings.Index(label, "(")] params := strings.Trim(label[strings.Index(label, "("):], "()") diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go index a4b310f365..03c862f36d 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -201,10 +201,28 @@ func (c completions) test(t *testing.T, exported *packagestest.Exported, s *serv }, }, }) + var got []protocol.CompletionItem + for _, item := range list.Items { + // Skip all types with no details (builtin types). + if item.Detail == "" && item.Kind == float64(protocol.TypeParameterCompletion) { + continue + } + // Skip remaining builtin types. + trimmed := item.Label + if i := strings.Index(trimmed, "("); i >= 0 { + trimmed = trimmed[:i] + } + switch trimmed { + case "append", "cap", "close", "complex", "copy", "delete", + "error", "false", "imag", "iota", "len", "make", "new", + "nil", "panic", "print", "println", "real", "recover", "true": + continue + } + got = append(got, item) + } if err != nil { t.Fatalf("completion failed for %s:%v:%v: %v", filepath.Base(src.Filename), src.Line, src.Column, err) } - got := list.Items if diff := diffC(src, want, got); diff != "" { t.Errorf(diff) } diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 691cdab0b1..f5bdf0d24b 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -169,13 +169,13 @@ func (s *server) Completion(ctx context.Context, params *protocol.CompletionPara return nil, err } pos := fromProtocolPosition(tok, params.Position) - items, err := source.Completion(ctx, f, pos) + items, prefix, err := source.Completion(ctx, f, pos) if err != nil { return nil, err } return &protocol.CompletionList{ IsIncomplete: false, - Items: toProtocolCompletionItems(items, s.snippetsSupported, s.signatureHelpEnabled), + Items: toProtocolCompletionItems(items, prefix, params.Position, s.snippetsSupported, s.signatureHelpEnabled), }, nil } diff --git a/internal/lsp/source/completion.go b/internal/lsp/source/completion.go index 258594b907..5f8e3d8066 100644 --- a/internal/lsp/source/completion.go +++ b/internal/lsp/source/completion.go @@ -34,17 +34,16 @@ const ( PackageCompletionItem ) -func Completion(ctx context.Context, f *File, pos token.Pos) ([]CompletionItem, error) { +func Completion(ctx context.Context, f *File, pos token.Pos) ([]CompletionItem, string, error) { file, err := f.GetAST() if err != nil { - return nil, err + return nil, "", err } pkg, err := f.GetPackage() if err != nil { - return nil, err + return nil, "", err } - items, _, err := completions(file, pos, pkg.Fset, pkg.Types, pkg.TypesInfo) - return items, err + return completions(file, pos, pkg.Fset, pkg.Types, pkg.TypesInfo) } const stdScore float64 = 1.0 @@ -93,9 +92,6 @@ func completions(file *ast.File, pos token.Pos, fset *token.FileSet, pkg *types. if typ != nil && matchingTypes(typ, obj.Type()) { weight *= 10.0 } - if !strings.HasPrefix(obj.Name(), prefix) { - return items - } item := formatCompletion(obj, pkgStringer, weight, func(v *types.Var) bool { return isParameter(sig, v) }) @@ -115,7 +111,8 @@ func completions(file *ast.File, pos token.Pos, fset *token.FileSet, pkg *types. // Is this the Sel part of a selector? if sel, ok := path[1].(*ast.SelectorExpr); ok && sel.Sel == n { - return selector(sel, info, found) + items, err = selector(sel, pos, info, found) + return items, prefix, err } // reject defining identifiers if obj, ok := info.Defs[n]; ok { @@ -137,10 +134,12 @@ func completions(file *ast.File, pos token.Pos, fset *token.FileSet, pkg *types. // recv.‸(arg) case *ast.TypeAssertExpr: // Create a fake selector expression. - return selector(&ast.SelectorExpr{X: n.X}, info, found) + items, err = selector(&ast.SelectorExpr{X: n.X}, pos, info, found) + return items, prefix, err case *ast.SelectorExpr: - return selector(n, info, found) + items, err = selector(n, pos, info, found) + return items, prefix, err default: // fallback to lexical completions @@ -153,7 +152,7 @@ func completions(file *ast.File, pos token.Pos, fset *token.FileSet, pkg *types. // selector finds completions for // the specified selector expression. // TODO(rstambler): Set the prefix filter correctly for selectors. -func selector(sel *ast.SelectorExpr, info *types.Info, found finder) (items []CompletionItem, prefix string, err error) { +func selector(sel *ast.SelectorExpr, pos token.Pos, info *types.Info, found finder) (items []CompletionItem, err error) { // Is sel a qualified identifier? if id, ok := sel.X.(*ast.Ident); ok { if pkgname, ok := info.Uses[id].(*types.PkgName); ok { @@ -164,14 +163,14 @@ func selector(sel *ast.SelectorExpr, info *types.Info, found finder) (items []Co for _, name := range scope.Names() { items = found(scope.Lookup(name), stdScore, items) } - return items, prefix, nil + return items, nil } } // Inv: sel is a true selector. tv, ok := info.Types[sel.X] if !ok { - return nil, "", fmt.Errorf("cannot resolve %s", sel.X) + return nil, fmt.Errorf("cannot resolve %s", sel.X) } // methods of T @@ -193,7 +192,7 @@ func selector(sel *ast.SelectorExpr, info *types.Info, found finder) (items []Co items = found(f, stdScore, items) } - return items, prefix, nil + return items, nil } // lexical finds completions in the lexical environment. @@ -208,7 +207,7 @@ func lexical(path []ast.Node, pos token.Pos, pkg *types.Package, info *types.Inf } scopes = append(scopes, info.Scopes[n]) } - scopes = append(scopes, pkg.Scope()) + scopes = append(scopes, pkg.Scope(), types.Universe) // Process scopes innermost first. for i, scope := range scopes {