internal/lsp: add documentation to completion items
This change adds documentation to the completion items. This normally
should be done in completionItem/resolve, since it takes more time to
compute documentation. However, I am not sure if that latency incurred
by pre-computing documentation is actually significantly more than the
latency incurred by an extra call to 'completionItem/resolve'. This
needs to be investigated, so we begin by just precomputing all of the
documentation for each item.
Updates golang/go#29151
Change-Id: I148664d271cf3f1d089c1a871901e3ee404ffbe8
Reviewed-on: https://go-review.googlesource.com/c/tools/+/184721
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
diff --git a/internal/lsp/completion.go b/internal/lsp/completion.go
index ecd3968..d1103ff 100644
--- a/internal/lsp/completion.go
+++ b/internal/lsp/completion.go
@@ -31,34 +31,15 @@
return nil, err
}
candidates, surrounding, err := source.Completion(ctx, view, f, rng.Start, source.CompletionOptions{
- DeepComplete: s.useDeepCompletions,
+ DeepComplete: s.useDeepCompletions,
+ WantDocumentaton: s.wantCompletionDocumentation,
})
if err != nil {
s.session.Logger().Infof(ctx, "no completions found for %s:%v:%v: %v", uri, int(params.Position.Line), int(params.Position.Character), err)
}
- // We might need to adjust the position to account for the prefix.
- insertionRng := protocol.Range{
- Start: params.Position,
- End: params.Position,
- }
- var prefix string
- if surrounding != nil {
- prefix = surrounding.Prefix()
- spn, err := surrounding.Range.Span()
- if err != nil {
- s.session.Logger().Infof(ctx, "failed to get span for surrounding position: %s:%v:%v: %v", uri, int(params.Position.Line), int(params.Position.Character), err)
- } else {
- rng, err := m.Range(spn)
- if err != nil {
- s.session.Logger().Infof(ctx, "failed to convert surrounding position: %s:%v:%v: %v", uri, int(params.Position.Line), int(params.Position.Character), err)
- } else {
- insertionRng = rng
- }
- }
- }
return &protocol.CompletionList{
IsIncomplete: false,
- Items: toProtocolCompletionItems(candidates, prefix, insertionRng, s.insertTextFormat, s.usePlaceholders, s.useDeepCompletions),
+ Items: s.toProtocolCompletionItems(ctx, view, m, candidates, params.Position, surrounding),
}, nil
}
@@ -66,41 +47,54 @@
// to be useful.
const maxDeepCompletions = 3
-func toProtocolCompletionItems(candidates []source.CompletionItem, prefix string, rng protocol.Range, insertTextFormat protocol.InsertTextFormat, usePlaceholders bool, useDeepCompletions bool) []protocol.CompletionItem {
+func (s *Server) toProtocolCompletionItems(ctx context.Context, view source.View, m *protocol.ColumnMapper, candidates []source.CompletionItem, pos protocol.Position, surrounding *source.Selection) []protocol.CompletionItem {
// Sort the candidates by score, since that is not supported by LSP yet.
sort.SliceStable(candidates, func(i, j int) bool {
return candidates[i].Score > candidates[j].Score
})
+ // We might need to adjust the position to account for the prefix.
+ insertionRange := protocol.Range{
+ Start: pos,
+ End: pos,
+ }
+ var prefix string
+ if surrounding != nil {
+ prefix = strings.ToLower(surrounding.Prefix())
+ spn, err := surrounding.Range.Span()
+ if err != nil {
+ s.session.Logger().Infof(ctx, "failed to get span for surrounding position: %s:%v:%v: %v", m.URI, int(pos.Line), int(pos.Character), err)
+ } else {
+ rng, err := m.Range(spn)
+ if err != nil {
+ s.session.Logger().Infof(ctx, "failed to convert surrounding position: %s:%v:%v: %v", m.URI, int(pos.Line), int(pos.Character), err)
+ } else {
+ insertionRange = rng
+ }
+ }
+ }
- // Matching against the prefix should be case insensitive.
- prefix = strings.ToLower(prefix)
+ var numDeepCompletionsSeen int
- var (
- items = make([]protocol.CompletionItem, 0, len(candidates))
- numDeepCompletionsSeen int
- )
+ items := make([]protocol.CompletionItem, 0, len(candidates))
for i, candidate := range candidates {
// Match against the label (case-insensitive).
if !strings.HasPrefix(strings.ToLower(candidate.Label), prefix) {
continue
}
-
// Limit the number of deep completions to not overwhelm the user in cases
// with dozens of deep completion matches.
if candidate.Depth > 0 {
- if !useDeepCompletions {
+ if !s.useDeepCompletions {
continue
}
-
if numDeepCompletionsSeen >= maxDeepCompletions {
continue
}
numDeepCompletionsSeen++
}
-
insertText := candidate.InsertText
- if insertTextFormat == protocol.SnippetTextFormat {
- insertText = candidate.Snippet(usePlaceholders)
+ if s.insertTextFormat == protocol.SnippetTextFormat {
+ insertText = candidate.Snippet(s.usePlaceholders)
}
item := protocol.CompletionItem{
Label: candidate.Label,
@@ -108,15 +102,16 @@
Kind: toProtocolCompletionItemKind(candidate.Kind),
TextEdit: &protocol.TextEdit{
NewText: insertText,
- Range: rng,
+ Range: insertionRange,
},
- InsertTextFormat: insertTextFormat,
+ InsertTextFormat: s.insertTextFormat,
// 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),
- FilterText: candidate.InsertText,
- Preselect: i == 0,
+ SortText: fmt.Sprintf("%05d", i),
+ FilterText: candidate.InsertText,
+ Preselect: i == 0,
+ Documentation: candidate.Documentation,
}
// Trigger signature help for any function or method completion.
// This is helpful even if a function does not have parameters,
diff --git a/internal/lsp/general.go b/internal/lsp/general.go
index eb28063..65ba6ea 100644
--- a/internal/lsp/general.go
+++ b/internal/lsp/general.go
@@ -64,7 +64,6 @@
return nil, err
}
}
-
return &protocol.InitializeResult{
Capabilities: protocol.ServerCapabilities{
CodeActionProvider: true,
@@ -192,6 +191,10 @@
}
view.SetBuildFlags(flags)
}
+ // Check if the user wants documentation in completion items.
+ if wantCompletionDocumentation, ok := c["wantCompletionDocumentation"].(bool); ok {
+ s.wantCompletionDocumentation = wantCompletionDocumentation
+ }
// Check if placeholders are enabled.
if usePlaceholders, ok := c["usePlaceholders"].(bool); ok {
s.usePlaceholders = usePlaceholders
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index 2478d73..1e31cbf 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -73,6 +73,7 @@
usePlaceholders bool
hoverKind source.HoverKind
useDeepCompletions bool
+ wantCompletionDocumentation bool
insertTextFormat protocol.InsertTextFormat
configurationSupported bool
dynamicConfigurationSupported bool
@@ -164,8 +165,8 @@
return s.completion(ctx, params)
}
-func (s *Server) CompletionResolve(context.Context, *protocol.CompletionItem) (*protocol.CompletionItem, error) {
- return nil, notImplemented("CompletionResolve")
+func (s *Server) Resolve(ctx context.Context, item *protocol.CompletionItem) (*protocol.CompletionItem, error) {
+ return nil, notImplemented("completionItem/resolve")
}
func (s *Server) Hover(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.Hover, error) {
@@ -260,10 +261,6 @@
return nil, notImplemented("PrepareRename")
}
-func (s *Server) Resolve(context.Context, *protocol.CompletionItem) (*protocol.CompletionItem, error) {
- return nil, notImplemented("Resolve")
-}
-
func (s *Server) SetTraceNotification(context.Context, *protocol.SetTraceParams) error {
return notImplemented("SetTraceNotification")
}
diff --git a/internal/lsp/source/completion.go b/internal/lsp/source/completion.go
index 33980eb..e5e7d6a 100644
--- a/internal/lsp/source/completion.go
+++ b/internal/lsp/source/completion.go
@@ -65,6 +65,9 @@
// foo(${1:a int}, ${2: b int}, ${3: c int})
//
placeholderSnippet *snippet.Builder
+
+ // Documentation is the documentation for the completion item.
+ Documentation string
}
// Snippet is a convenience function that determines the snippet that should be
@@ -115,6 +118,7 @@
types *types.Package
info *types.Info
qf types.Qualifier
+ opts CompletionOptions
// view is the View associated with this completion request.
view View
@@ -206,9 +210,9 @@
// found adds a candidate completion. We will also search through the object's
// members for more candidates.
-func (c *completer) found(obj types.Object, score float64) {
+func (c *completer) found(obj types.Object, score float64) error {
if obj.Pkg() != nil && obj.Pkg() != c.types && !obj.Exported() {
- return // inaccessible
+ return fmt.Errorf("%s is inaccessible from %s", obj.Name(), c.types.Path())
}
if c.inDeepCompletion() {
@@ -217,13 +221,13 @@
// "bar.Baz" even though "Baz" is represented the same types.Object in both.
for _, seenObj := range c.deepState.chain {
if seenObj == obj {
- return
+ return nil
}
}
} else {
// At the top level, dedupe by object.
if c.seen[obj] {
- return
+ return nil
}
c.seen[obj] = true
}
@@ -239,10 +243,14 @@
// Favor shallow matches by lowering weight according to depth.
cand.score -= stdScore * float64(len(c.deepState.chain))
-
- c.items = append(c.items, c.item(cand))
+ item, err := c.item(cand)
+ if err != nil {
+ return err
+ }
+ c.items = append(c.items, item)
c.deepSearch(obj)
+ return nil
}
// candidate represents a completion candidate.
@@ -259,7 +267,8 @@
}
type CompletionOptions struct {
- DeepComplete bool
+ DeepComplete bool
+ WantDocumentaton bool
}
// Completion returns a list of possible candidates for completion, given a
@@ -310,6 +319,7 @@
seen: make(map[types.Object]bool),
enclosingFunction: enclosingFunction(path, pos, pkg.GetTypesInfo()),
enclosingCompositeLiteral: clInfo,
+ opts: opts,
}
c.deepState.enabled = opts.DeepComplete
diff --git a/internal/lsp/source/completion_format.go b/internal/lsp/source/completion_format.go
index b9718f7..1b77142 100644
--- a/internal/lsp/source/completion_format.go
+++ b/internal/lsp/source/completion_format.go
@@ -14,10 +14,11 @@
"strings"
"golang.org/x/tools/internal/lsp/snippet"
+ "golang.org/x/tools/internal/span"
)
// formatCompletion creates a completion item for a given candidate.
-func (c *completer) item(cand candidate) CompletionItem {
+func (c *completer) item(cand candidate) (CompletionItem, error) {
obj := cand.obj
// Handle builtin types separately.
@@ -83,8 +84,7 @@
}
detail = strings.TrimPrefix(detail, "untyped ")
-
- return CompletionItem{
+ item := CompletionItem{
Label: label,
InsertText: insert,
Detail: detail,
@@ -94,6 +94,35 @@
plainSnippet: plainSnippet,
placeholderSnippet: placeholderSnippet,
}
+ if c.opts.WantDocumentaton {
+ declRange, err := objToRange(c.ctx, c.view.Session().Cache().FileSet(), obj)
+ if err != nil {
+ return CompletionItem{}, err
+ }
+ pos := declRange.FileSet.Position(declRange.Start)
+ if !pos.IsValid() {
+ return CompletionItem{}, fmt.Errorf("invalid declaration position for %v", item.Label)
+ }
+ uri := span.FileURI(pos.Filename)
+ f, err := c.view.GetFile(c.ctx, uri)
+ if err != nil {
+ return CompletionItem{}, err
+ }
+ gof, ok := f.(GoFile)
+ if !ok {
+ return CompletionItem{}, fmt.Errorf("declaration for %s not in a Go file: %s", item.Label, uri)
+ }
+ ident, err := Identifier(c.ctx, c.view, gof, declRange.Start)
+ if err != nil {
+ return CompletionItem{}, err
+ }
+ documentation, err := ident.Documentation(c.ctx, SynopsisDocumentation)
+ if err != nil {
+ return CompletionItem{}, err
+ }
+ item.Documentation = documentation
+ }
+ return item, nil
}
// isParameter returns true if the given *types.Var is a parameter
@@ -110,7 +139,7 @@
return false
}
-func (c *completer) formatBuiltin(cand candidate) CompletionItem {
+func (c *completer) formatBuiltin(cand candidate) (CompletionItem, error) {
obj := cand.obj
item := CompletionItem{
Label: obj.Name(),
@@ -140,7 +169,7 @@
case *types.Nil:
item.Kind = VariableCompletionItem
}
- return item
+ return item, nil
}
var replacer = strings.NewReplacer(
diff --git a/internal/lsp/source/hover.go b/internal/lsp/source/hover.go
index db412ef..9fdfe1b 100644
--- a/internal/lsp/source/hover.go
+++ b/internal/lsp/source/hover.go
@@ -40,7 +40,7 @@
return "", err
}
var b strings.Builder
- if comment := formatDocumentation(hoverKind, h.comment); comment != "" {
+ if comment := formatDocumentation(h.comment, hoverKind); comment != "" {
b.WriteString(comment)
b.WriteRune('\n')
}
@@ -61,7 +61,7 @@
return b.String(), nil
}
-func formatDocumentation(hoverKind HoverKind, c *ast.CommentGroup) string {
+func formatDocumentation(c *ast.CommentGroup, hoverKind HoverKind) string {
switch hoverKind {
case SynopsisDocumentation:
return doc.Synopsis((c.Text()))
@@ -71,6 +71,14 @@
return ""
}
+func (i *IdentifierInfo) Documentation(ctx context.Context, hoverKind HoverKind) (string, error) {
+ h, err := i.decl.hover(ctx)
+ if err != nil {
+ return "", err
+ }
+ return formatDocumentation(h.comment, hoverKind), nil
+}
+
func (d declaration) hover(ctx context.Context) (*documentation, error) {
ctx, ts := trace.StartSpan(ctx, "source.hover")
defer ts.End()
diff --git a/internal/lsp/source/signature_help.go b/internal/lsp/source/signature_help.go
index c17b8f5..2d68b7c 100644
--- a/internal/lsp/source/signature_help.go
+++ b/internal/lsp/source/signature_help.go
@@ -156,7 +156,7 @@
return &SignatureInformation{
Label: label,
// TODO: Should we have the HoverKind apply to signature information as well?
- Documentation: formatDocumentation(SynopsisDocumentation, comment),
+ Documentation: formatDocumentation(comment, SynopsisDocumentation),
Parameters: paramInfo,
ActiveParameter: activeParam,
}