1
0
mirror of https://github.com/golang/go synced 2024-10-01 01:38:33 -06:00

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>
This commit is contained in:
Rebecca Stambler 2019-07-02 17:31:31 -04:00
parent e491332ed8
commit 4f9eeaf1bf
7 changed files with 104 additions and 62 deletions

View File

@ -31,34 +31,15 @@ func (s *Server) completion(ctx context.Context, params *protocol.CompletionPara
return nil, err return nil, err
} }
candidates, surrounding, err := source.Completion(ctx, view, f, rng.Start, source.CompletionOptions{ candidates, surrounding, err := source.Completion(ctx, view, f, rng.Start, source.CompletionOptions{
DeepComplete: s.useDeepCompletions, DeepComplete: s.useDeepCompletions,
WantDocumentaton: s.wantCompletionDocumentation,
}) })
if err != nil { 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) 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{ return &protocol.CompletionList{
IsIncomplete: false, IsIncomplete: false,
Items: toProtocolCompletionItems(candidates, prefix, insertionRng, s.insertTextFormat, s.usePlaceholders, s.useDeepCompletions), Items: s.toProtocolCompletionItems(ctx, view, m, candidates, params.Position, surrounding),
}, nil }, nil
} }
@ -66,41 +47,54 @@ func (s *Server) completion(ctx context.Context, params *protocol.CompletionPara
// to be useful. // to be useful.
const maxDeepCompletions = 3 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 the candidates by score, since that is not supported by LSP yet.
sort.SliceStable(candidates, func(i, j int) bool { sort.SliceStable(candidates, func(i, j int) bool {
return candidates[i].Score > candidates[j].Score 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. var numDeepCompletionsSeen int
prefix = strings.ToLower(prefix)
var ( items := make([]protocol.CompletionItem, 0, len(candidates))
items = make([]protocol.CompletionItem, 0, len(candidates))
numDeepCompletionsSeen int
)
for i, candidate := range candidates { for i, candidate := range candidates {
// Match against the label (case-insensitive). // Match against the label (case-insensitive).
if !strings.HasPrefix(strings.ToLower(candidate.Label), prefix) { if !strings.HasPrefix(strings.ToLower(candidate.Label), prefix) {
continue continue
} }
// Limit the number of deep completions to not overwhelm the user in cases // Limit the number of deep completions to not overwhelm the user in cases
// with dozens of deep completion matches. // with dozens of deep completion matches.
if candidate.Depth > 0 { if candidate.Depth > 0 {
if !useDeepCompletions { if !s.useDeepCompletions {
continue continue
} }
if numDeepCompletionsSeen >= maxDeepCompletions { if numDeepCompletionsSeen >= maxDeepCompletions {
continue continue
} }
numDeepCompletionsSeen++ numDeepCompletionsSeen++
} }
insertText := candidate.InsertText insertText := candidate.InsertText
if insertTextFormat == protocol.SnippetTextFormat { if s.insertTextFormat == protocol.SnippetTextFormat {
insertText = candidate.Snippet(usePlaceholders) insertText = candidate.Snippet(s.usePlaceholders)
} }
item := protocol.CompletionItem{ item := protocol.CompletionItem{
Label: candidate.Label, Label: candidate.Label,
@ -108,15 +102,16 @@ func toProtocolCompletionItems(candidates []source.CompletionItem, prefix string
Kind: toProtocolCompletionItemKind(candidate.Kind), Kind: toProtocolCompletionItemKind(candidate.Kind),
TextEdit: &protocol.TextEdit{ TextEdit: &protocol.TextEdit{
NewText: insertText, 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 // 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 // according to their score. This can be removed upon the resolution of
// https://github.com/Microsoft/language-server-protocol/issues/348. // https://github.com/Microsoft/language-server-protocol/issues/348.
SortText: fmt.Sprintf("%05d", i), SortText: fmt.Sprintf("%05d", i),
FilterText: candidate.InsertText, FilterText: candidate.InsertText,
Preselect: i == 0, Preselect: i == 0,
Documentation: candidate.Documentation,
} }
// Trigger signature help for any function or method completion. // Trigger signature help for any function or method completion.
// This is helpful even if a function does not have parameters, // This is helpful even if a function does not have parameters,

View File

@ -64,7 +64,6 @@ func (s *Server) initialize(ctx context.Context, params *protocol.InitializePara
return nil, err return nil, err
} }
} }
return &protocol.InitializeResult{ return &protocol.InitializeResult{
Capabilities: protocol.ServerCapabilities{ Capabilities: protocol.ServerCapabilities{
CodeActionProvider: true, CodeActionProvider: true,
@ -192,6 +191,10 @@ func (s *Server) processConfig(ctx context.Context, view source.View, config int
} }
view.SetBuildFlags(flags) 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. // Check if placeholders are enabled.
if usePlaceholders, ok := c["usePlaceholders"].(bool); ok { if usePlaceholders, ok := c["usePlaceholders"].(bool); ok {
s.usePlaceholders = usePlaceholders s.usePlaceholders = usePlaceholders

View File

@ -73,6 +73,7 @@ type Server struct {
usePlaceholders bool usePlaceholders bool
hoverKind source.HoverKind hoverKind source.HoverKind
useDeepCompletions bool useDeepCompletions bool
wantCompletionDocumentation bool
insertTextFormat protocol.InsertTextFormat insertTextFormat protocol.InsertTextFormat
configurationSupported bool configurationSupported bool
dynamicConfigurationSupported bool dynamicConfigurationSupported bool
@ -164,8 +165,8 @@ func (s *Server) Completion(ctx context.Context, params *protocol.CompletionPara
return s.completion(ctx, params) return s.completion(ctx, params)
} }
func (s *Server) CompletionResolve(context.Context, *protocol.CompletionItem) (*protocol.CompletionItem, error) { func (s *Server) Resolve(ctx context.Context, item *protocol.CompletionItem) (*protocol.CompletionItem, error) {
return nil, notImplemented("CompletionResolve") return nil, notImplemented("completionItem/resolve")
} }
func (s *Server) Hover(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.Hover, error) { func (s *Server) Hover(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.Hover, error) {
@ -260,10 +261,6 @@ func (s *Server) PrepareRename(context.Context, *protocol.TextDocumentPositionPa
return nil, notImplemented("PrepareRename") 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 { func (s *Server) SetTraceNotification(context.Context, *protocol.SetTraceParams) error {
return notImplemented("SetTraceNotification") return notImplemented("SetTraceNotification")
} }

View File

@ -65,6 +65,9 @@ type CompletionItem struct {
// foo(${1:a int}, ${2: b int}, ${3: c int}) // foo(${1:a int}, ${2: b int}, ${3: c int})
// //
placeholderSnippet *snippet.Builder 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 // Snippet is a convenience function that determines the snippet that should be
@ -115,6 +118,7 @@ type completer struct {
types *types.Package types *types.Package
info *types.Info info *types.Info
qf types.Qualifier qf types.Qualifier
opts CompletionOptions
// view is the View associated with this completion request. // view is the View associated with this completion request.
view View view View
@ -206,9 +210,9 @@ func (c *completer) setSurrounding(ident *ast.Ident) {
// found adds a candidate completion. We will also search through the object's // found adds a candidate completion. We will also search through the object's
// members for more candidates. // 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() { 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() { if c.inDeepCompletion() {
@ -217,13 +221,13 @@ func (c *completer) found(obj types.Object, score float64) {
// "bar.Baz" even though "Baz" is represented the same types.Object in both. // "bar.Baz" even though "Baz" is represented the same types.Object in both.
for _, seenObj := range c.deepState.chain { for _, seenObj := range c.deepState.chain {
if seenObj == obj { if seenObj == obj {
return return nil
} }
} }
} else { } else {
// At the top level, dedupe by object. // At the top level, dedupe by object.
if c.seen[obj] { if c.seen[obj] {
return return nil
} }
c.seen[obj] = true c.seen[obj] = true
} }
@ -239,10 +243,14 @@ func (c *completer) found(obj types.Object, score float64) {
// Favor shallow matches by lowering weight according to depth. // Favor shallow matches by lowering weight according to depth.
cand.score -= stdScore * float64(len(c.deepState.chain)) cand.score -= stdScore * float64(len(c.deepState.chain))
item, err := c.item(cand)
c.items = append(c.items, c.item(cand)) if err != nil {
return err
}
c.items = append(c.items, item)
c.deepSearch(obj) c.deepSearch(obj)
return nil
} }
// candidate represents a completion candidate. // candidate represents a completion candidate.
@ -259,7 +267,8 @@ type candidate struct {
} }
type CompletionOptions struct { type CompletionOptions struct {
DeepComplete bool DeepComplete bool
WantDocumentaton bool
} }
// Completion returns a list of possible candidates for completion, given a // Completion returns a list of possible candidates for completion, given a
@ -310,6 +319,7 @@ func Completion(ctx context.Context, view View, f GoFile, pos token.Pos, opts Co
seen: make(map[types.Object]bool), seen: make(map[types.Object]bool),
enclosingFunction: enclosingFunction(path, pos, pkg.GetTypesInfo()), enclosingFunction: enclosingFunction(path, pos, pkg.GetTypesInfo()),
enclosingCompositeLiteral: clInfo, enclosingCompositeLiteral: clInfo,
opts: opts,
} }
c.deepState.enabled = opts.DeepComplete c.deepState.enabled = opts.DeepComplete

View File

@ -14,10 +14,11 @@ import (
"strings" "strings"
"golang.org/x/tools/internal/lsp/snippet" "golang.org/x/tools/internal/lsp/snippet"
"golang.org/x/tools/internal/span"
) )
// formatCompletion creates a completion item for a given candidate. // 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 obj := cand.obj
// Handle builtin types separately. // Handle builtin types separately.
@ -83,8 +84,7 @@ func (c *completer) item(cand candidate) CompletionItem {
} }
detail = strings.TrimPrefix(detail, "untyped ") detail = strings.TrimPrefix(detail, "untyped ")
item := CompletionItem{
return CompletionItem{
Label: label, Label: label,
InsertText: insert, InsertText: insert,
Detail: detail, Detail: detail,
@ -94,6 +94,35 @@ func (c *completer) item(cand candidate) CompletionItem {
plainSnippet: plainSnippet, plainSnippet: plainSnippet,
placeholderSnippet: placeholderSnippet, 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 // isParameter returns true if the given *types.Var is a parameter
@ -110,7 +139,7 @@ func (c *completer) isParameter(v *types.Var) bool {
return false return false
} }
func (c *completer) formatBuiltin(cand candidate) CompletionItem { func (c *completer) formatBuiltin(cand candidate) (CompletionItem, error) {
obj := cand.obj obj := cand.obj
item := CompletionItem{ item := CompletionItem{
Label: obj.Name(), Label: obj.Name(),
@ -140,7 +169,7 @@ func (c *completer) formatBuiltin(cand candidate) CompletionItem {
case *types.Nil: case *types.Nil:
item.Kind = VariableCompletionItem item.Kind = VariableCompletionItem
} }
return item return item, nil
} }
var replacer = strings.NewReplacer( var replacer = strings.NewReplacer(

View File

@ -40,7 +40,7 @@ func (i *IdentifierInfo) Hover(ctx context.Context, markdownSupported bool, hove
return "", err return "", err
} }
var b strings.Builder var b strings.Builder
if comment := formatDocumentation(hoverKind, h.comment); comment != "" { if comment := formatDocumentation(h.comment, hoverKind); comment != "" {
b.WriteString(comment) b.WriteString(comment)
b.WriteRune('\n') b.WriteRune('\n')
} }
@ -61,7 +61,7 @@ func (i *IdentifierInfo) Hover(ctx context.Context, markdownSupported bool, hove
return b.String(), nil return b.String(), nil
} }
func formatDocumentation(hoverKind HoverKind, c *ast.CommentGroup) string { func formatDocumentation(c *ast.CommentGroup, hoverKind HoverKind) string {
switch hoverKind { switch hoverKind {
case SynopsisDocumentation: case SynopsisDocumentation:
return doc.Synopsis((c.Text())) return doc.Synopsis((c.Text()))
@ -71,6 +71,14 @@ func formatDocumentation(hoverKind HoverKind, c *ast.CommentGroup) string {
return "" 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) { func (d declaration) hover(ctx context.Context) (*documentation, error) {
ctx, ts := trace.StartSpan(ctx, "source.hover") ctx, ts := trace.StartSpan(ctx, "source.hover")
defer ts.End() defer ts.End()

View File

@ -156,7 +156,7 @@ func signatureInformation(name string, comment *ast.CommentGroup, params, result
return &SignatureInformation{ return &SignatureInformation{
Label: label, Label: label,
// TODO: Should we have the HoverKind apply to signature information as well? // TODO: Should we have the HoverKind apply to signature information as well?
Documentation: formatDocumentation(SynopsisDocumentation, comment), Documentation: formatDocumentation(comment, SynopsisDocumentation),
Parameters: paramInfo, Parameters: paramInfo,
ActiveParameter: activeParam, ActiveParameter: activeParam,
} }