From 4796d4bd3df0a291c397154cd7d68f1290cf7deb Mon Sep 17 00:00:00 2001 From: Rebecca Stambler Date: Wed, 17 Apr 2019 18:21:47 -0400 Subject: [PATCH] internal/lsp: use ast.Nodes for hover information This change associates an ast.Node for some object declarations. In this case, we only handle type declarations, but future changes will support other objects as well. This is the first step in adding documentation on hover. Updates golang/go#29151 Change-Id: I39ddccf4130ee3b106725286375cd74bc51bcd38 Reviewed-on: https://go-review.googlesource.com/c/tools/+/172661 Run-TryBot: Rebecca Stambler TryBot-Result: Gobot Gobot Reviewed-by: Ian Cottrell --- internal/lsp/cache/file.go | 5 ++ internal/lsp/general.go | 4 ++ internal/lsp/hover.go | 14 ++-- internal/lsp/server.go | 1 + internal/lsp/source/hover.go | 69 +++++++++++++++++++ .../source/{definition.go => identifier.go} | 40 ++++++++--- internal/lsp/source/view.go | 1 + 7 files changed, 120 insertions(+), 14 deletions(-) create mode 100644 internal/lsp/source/hover.go rename internal/lsp/source/{definition.go => identifier.go} (80%) diff --git a/internal/lsp/cache/file.go b/internal/lsp/cache/file.go index 9efcccb7..0c9b0b2c 100644 --- a/internal/lsp/cache/file.go +++ b/internal/lsp/cache/file.go @@ -40,6 +40,11 @@ func (f *File) URI() span.URI { return f.uris[0] } +// View returns the view associated with the file. +func (f *File) View() source.View { + return f.view +} + // GetContent returns the contents of the file, reading it from file system if needed. func (f *File) GetContent(ctx context.Context) []byte { f.view.mu.Lock() diff --git a/internal/lsp/general.go b/internal/lsp/general.go index b97d46da..b3b48408 100644 --- a/internal/lsp/general.go +++ b/internal/lsp/general.go @@ -164,6 +164,10 @@ func (s *Server) processConfig(view *cache.View, config interface{}) error { if usePlaceholders, ok := c["usePlaceholders"].(bool); ok { s.usePlaceholders = usePlaceholders } + // Check if enhancedHover is enabled. + if enhancedHover, ok := c["enhancedHover"].(bool); ok { + s.enhancedHover = enhancedHover + } return nil } diff --git a/internal/lsp/hover.go b/internal/lsp/hover.go index 8a8e2181..6391c3cd 100644 --- a/internal/lsp/hover.go +++ b/internal/lsp/hover.go @@ -6,6 +6,7 @@ package lsp import ( "context" + "fmt" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" @@ -31,7 +32,7 @@ func (s *Server) hover(ctx context.Context, params *protocol.TextDocumentPositio if err != nil { return nil, err } - content, err := ident.Hover(ctx, nil) + decl, doc, err := ident.Hover(ctx, nil, s.enhancedHover) if err != nil { return nil, err } @@ -44,20 +45,23 @@ func (s *Server) hover(ctx context.Context, params *protocol.TextDocumentPositio return nil, err } return &protocol.Hover{ - Contents: markupContent(content, s.preferredContentFormat), + Contents: markupContent(decl, doc, s.preferredContentFormat), Range: &rng, }, nil } -func markupContent(content string, kind protocol.MarkupKind) protocol.MarkupContent { +func markupContent(decl, doc string, kind protocol.MarkupKind) protocol.MarkupContent { result := protocol.MarkupContent{ Kind: kind, } switch kind { case protocol.PlainText: - result.Value = content + result.Value = decl case protocol.Markdown: - result.Value = "```go\n" + content + "\n```" + result.Value = "```go\n" + decl + "\n```" + } + if doc != "" { + result.Value = fmt.Sprintf("%s\n%s", doc, result.Value) } return result } diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 54bfd1ac..bd466e1f 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -74,6 +74,7 @@ type Server struct { // Configurations. // TODO(rstambler): Separate these into their own struct? usePlaceholders bool + enhancedHover bool insertTextFormat protocol.InsertTextFormat configurationSupported bool dynamicConfigurationSupported bool diff --git a/internal/lsp/source/hover.go b/internal/lsp/source/hover.go new file mode 100644 index 00000000..9d782241 --- /dev/null +++ b/internal/lsp/source/hover.go @@ -0,0 +1,69 @@ +// Copyright 201p The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package source + +import ( + "bytes" + "context" + "fmt" + "go/ast" + "go/format" + "go/token" + "go/types" +) + +func (i *IdentifierInfo) Hover(ctx context.Context, q types.Qualifier, enhancedHover bool) (string, string, error) { + file := i.File.GetAST(ctx) + if q == nil { + pkg := i.File.GetPackage(ctx) + q = qualifier(file, pkg.GetTypes(), pkg.GetTypesInfo()) + } + // TODO(rstambler): Remove this configuration when hover behavior is stable. + if enhancedHover { + switch obj := i.Declaration.Object.(type) { + case *types.TypeName: + if node, ok := i.Declaration.Node.(*ast.GenDecl); ok { + if decl, doc, err := formatTypeName(i.File.GetFileSet(ctx), node, obj, q); err == nil { + return decl, doc, nil + } else { + // Swallow errors so we can return a best-effort response using types.TypeString. + i.File.View().Logger().Errorf(ctx, "no hover for TypeName %v: %v", obj.Name(), err) + } + } + return types.TypeString(obj.Type(), q), "", nil + default: + return types.ObjectString(obj, q), "", nil + } + } + return types.ObjectString(i.Declaration.Object, q), "", nil +} + +func formatTypeName(fset *token.FileSet, decl *ast.GenDecl, obj *types.TypeName, q types.Qualifier) (string, string, error) { + if types.IsInterface(obj.Type()) { + return "", "", fmt.Errorf("no support for interfaces yet") + } + switch t := obj.Type().(type) { + case *types.Struct: + return formatStructType(fset, decl, t) + case *types.Named: + if under, ok := t.Underlying().(*types.Struct); ok { + return formatStructType(fset, decl, under) + } + } + return "", "", fmt.Errorf("no supported for %v, which is of type %T", obj.Name(), obj.Type()) +} + +func formatStructType(fset *token.FileSet, decl *ast.GenDecl, typ *types.Struct) (string, string, error) { + if len(decl.Specs) != 1 { + return "", "", fmt.Errorf("expected 1 TypeSpec got %v", len(decl.Specs)) + } + b := bytes.NewBuffer(nil) + if err := format.Node(b, fset, decl.Specs[0]); err != nil { + return "", "", err + } + doc := decl.Doc.Text() + return b.String(), doc, nil + +} diff --git a/internal/lsp/source/definition.go b/internal/lsp/source/identifier.go similarity index 80% rename from internal/lsp/source/definition.go rename to internal/lsp/source/identifier.go index 2ee7592c..5671f62f 100644 --- a/internal/lsp/source/definition.go +++ b/internal/lsp/source/identifier.go @@ -26,6 +26,7 @@ type IdentifierInfo struct { } Declaration struct { Range span.Range + Node ast.Decl Object types.Object } @@ -49,15 +50,6 @@ func Identifier(ctx context.Context, v View, f File, pos token.Pos) (*Identifier return result, err } -func (i *IdentifierInfo) Hover(ctx context.Context, q types.Qualifier) (string, error) { - if q == nil { - fAST := i.File.GetAST(ctx) - pkg := i.File.GetPackage(ctx) - q = qualifier(fAST, pkg.GetTypes(), pkg.GetTypesInfo()) - } - return types.ObjectString(i.Declaration.Object, q), nil -} - // identifier checks a single position for a potential identifier. func identifier(ctx context.Context, v View, f File, pos token.Pos) (*IdentifierInfo, error) { fAST := f.GetAST(ctx) @@ -105,6 +97,9 @@ func identifier(ctx context.Context, v View, f File, pos token.Pos) (*Identifier if result.Declaration.Range, err = objToRange(ctx, v, result.Declaration.Object); err != nil { return nil, err } + if result.Declaration.Node, err = objToNode(ctx, v, result.Declaration.Object, result.Declaration.Range); err != nil { + return nil, err + } typ := pkg.GetTypesInfo().TypeOf(result.ident) if typ == nil { return nil, fmt.Errorf("no type for %s", result.Name) @@ -140,3 +135,30 @@ func objToRange(ctx context.Context, v View, obj types.Object) (span.Range, erro } return span.NewRange(v.FileSet(), p, p+token.Pos(len(obj.Name()))), nil } + +func objToNode(ctx context.Context, v View, obj types.Object, rng span.Range) (ast.Decl, error) { + s, err := rng.Span() + if err != nil { + return nil, err + } + declFile, err := v.GetFile(ctx, s.URI()) + if err != nil { + return nil, err + } + declAST := declFile.GetAST(ctx) + path, _ := astutil.PathEnclosingInterval(declAST, rng.Start, rng.End) + if path == nil { + return nil, fmt.Errorf("no path for range %v", rng) + } + // TODO(rstambler): Support other node types. + // For now, we only associate an ast.Node for type declarations. + switch obj.Type().(type) { + case *types.Named, *types.Struct, *types.Interface: + for _, node := range path { + if node, ok := node.(*ast.GenDecl); ok && node.Tok == token.TYPE { + return node, nil + } + } + } + return nil, nil // didn't find a node, but no error +} diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go index 896ac8e0..5975f222 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -34,6 +34,7 @@ type View interface { // the loading of packages into their own caching systems. type File interface { URI() span.URI + View() View GetAST(ctx context.Context) *ast.File GetFileSet(ctx context.Context) *token.FileSet GetPackage(ctx context.Context) Package