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