diff --git a/internal/lsp/hover.go b/internal/lsp/hover.go index 6391c3cd..7d9ae2c0 100644 --- a/internal/lsp/hover.go +++ b/internal/lsp/hover.go @@ -32,7 +32,7 @@ func (s *Server) hover(ctx context.Context, params *protocol.TextDocumentPositio if err != nil { return nil, err } - decl, doc, err := ident.Hover(ctx, nil, s.enhancedHover) + hover, err := ident.Hover(ctx, nil, s.enhancedHover, s.preferredContentFormat == protocol.Markdown) if err != nil { return nil, err } @@ -45,8 +45,11 @@ func (s *Server) hover(ctx context.Context, params *protocol.TextDocumentPositio return nil, err } return &protocol.Hover{ - Contents: markupContent(decl, doc, s.preferredContentFormat), - Range: &rng, + Contents: protocol.MarkupContent{ + Kind: s.preferredContentFormat, + Value: hover, + }, + Range: &rng, }, nil } diff --git a/internal/lsp/source/hover.go b/internal/lsp/source/hover.go index 9d782241..375b2cbf 100644 --- a/internal/lsp/source/hover.go +++ b/internal/lsp/source/hover.go @@ -14,56 +14,123 @@ import ( "go/types" ) -func (i *IdentifierInfo) Hover(ctx context.Context, q types.Qualifier, enhancedHover bool) (string, string, error) { +// formatter returns the a hover value formatted with its documentation. +type formatter func(interface{}, *ast.CommentGroup) (string, error) + +func (i *IdentifierInfo) Hover(ctx context.Context, qf types.Qualifier, enhancedHover, markdownSupported bool) (string, error) { file := i.File.GetAST(ctx) - if q == nil { + if qf == 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)) + qf = qualifier(file, pkg.GetTypes(), pkg.GetTypesInfo()) } b := bytes.NewBuffer(nil) - if err := format.Node(b, fset, decl.Specs[0]); err != nil { - return "", "", err + f := func(x interface{}, c *ast.CommentGroup) (string, error) { + return writeHover(x, i.File.GetFileSet(ctx), b, c, markdownSupported, qf) } - doc := decl.Doc.Text() - return b.String(), doc, nil - + obj := i.Declaration.Object + // TODO(rstambler): Remove this configuration when hover behavior is stable. + if enhancedHover { + switch node := i.Declaration.Node.(type) { + case *ast.GenDecl: + switch obj := obj.(type) { + case *types.TypeName, *types.Var, *types.Const, *types.Func: + return formatGenDecl(node, obj, obj.Type(), f) + } + case *ast.FuncDecl: + if _, ok := obj.(*types.Func); ok { + return f(obj, node.Doc) + } + } + } + return f(obj, nil) +} + +func formatGenDecl(node *ast.GenDecl, obj types.Object, typ types.Type, f formatter) (string, error) { + if _, ok := typ.(*types.Named); ok { + switch typ.Underlying().(type) { + case *types.Interface, *types.Struct: + return formatGenDecl(node, obj, typ.Underlying(), f) + } + } + var spec ast.Spec + for _, s := range node.Specs { + if s.Pos() <= obj.Pos() && obj.Pos() <= s.End() { + spec = s + break + } + } + if spec == nil { + return "", fmt.Errorf("no spec for node %v at position %v", node, obj.Pos()) + } + // If we have a field or method. + switch obj.(type) { + case *types.Var, *types.Const, *types.Func: + return formatVar(spec, obj, f) + } + // Handle types. + switch spec := spec.(type) { + case *ast.TypeSpec: + // If multiple types are declared in the same block. + if len(node.Specs) > 1 { + return f(spec.Type, spec.Doc) + } else { + return f(spec, node.Doc) + } + case *ast.ValueSpec: + return f(spec, spec.Doc) + case *ast.ImportSpec: + return f(spec, spec.Doc) + } + return "", fmt.Errorf("unable to format spec %v (%T)", spec, spec) +} + +func formatVar(node ast.Spec, obj types.Object, f formatter) (string, error) { + var fieldList *ast.FieldList + if spec, ok := node.(*ast.TypeSpec); ok { + switch t := spec.Type.(type) { + case *ast.StructType: + fieldList = t.Fields + case *ast.InterfaceType: + fieldList = t.Methods + } + } + // If we have a struct or interface declaration, + // we need to match the object to the corresponding field or method. + if fieldList != nil { + for i := 0; i < fieldList.NumFields(); i++ { + field := fieldList.List[i] + if field.Pos() <= obj.Pos() && obj.Pos() <= field.End() { + if field.Doc.Text() != "" { + return f(obj, field.Doc) + } else if field.Comment.Text() != "" { + return f(obj, field.Comment) + } + } + } + } + // If we weren't able to find documentation for the object. + return f(obj, nil) +} + +// writeHover writes the hover for a given node and its documentation. +func writeHover(x interface{}, fset *token.FileSet, b *bytes.Buffer, c *ast.CommentGroup, markdownSupported bool, qf types.Qualifier) (string, error) { + if c != nil { + b.WriteString(c.Text()) + b.WriteRune('\n') + } + if markdownSupported { + b.WriteString("```go\n") + } + switch x := x.(type) { + case ast.Node: + if err := format.Node(b, fset, x); err != nil { + return "", err + } + case types.Object: + b.WriteString(types.ObjectString(x, qf)) + } + if markdownSupported { + b.WriteString("\n```") + } + return b.String(), nil } diff --git a/internal/lsp/source/identifier.go b/internal/lsp/source/identifier.go index da25c296..ff75e056 100644 --- a/internal/lsp/source/identifier.go +++ b/internal/lsp/source/identifier.go @@ -151,15 +151,20 @@ func objToNode(ctx context.Context, v View, obj types.Object, rng span.Range) (a 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 { + for _, node := range path { + switch node := node.(type) { + case *ast.GenDecl: + // Type names, fields, and methods. + switch obj.(type) { + case *types.TypeName, *types.Var, *types.Const, *types.Func: + return node, nil + } + case *ast.FuncDecl: + // Function signatures. + if _, ok := obj.(*types.Func); ok { return node, nil } } } - return nil, nil // didn't find a node, but no error + return nil, nil // didn't find a node, but don't fail }