aboutsummaryrefslogtreecommitdiff
path: root/gopls/internal/lsp/source/hover.go
diff options
context:
space:
mode:
Diffstat (limited to 'gopls/internal/lsp/source/hover.go')
-rw-r--r--gopls/internal/lsp/source/hover.go951
1 files changed, 951 insertions, 0 deletions
diff --git a/gopls/internal/lsp/source/hover.go b/gopls/internal/lsp/source/hover.go
new file mode 100644
index 000000000..136a3022b
--- /dev/null
+++ b/gopls/internal/lsp/source/hover.go
@@ -0,0 +1,951 @@
+// Copyright 2019 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 (
+ "context"
+ "encoding/json"
+ "fmt"
+ "go/ast"
+ "go/constant"
+ "go/doc"
+ "go/format"
+ "go/token"
+ "go/types"
+ "strconv"
+ "strings"
+ "time"
+ "unicode/utf8"
+
+ "golang.org/x/text/unicode/runenames"
+ "golang.org/x/tools/go/ast/astutil"
+ "golang.org/x/tools/go/types/typeutil"
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/lsp/safetoken"
+ "golang.org/x/tools/gopls/internal/span"
+ "golang.org/x/tools/internal/bug"
+ "golang.org/x/tools/internal/event"
+ "golang.org/x/tools/internal/typeparams"
+)
+
+// HoverJSON contains information used by hover. It is also the JSON returned
+// for the "structured" hover format
+type HoverJSON struct {
+ // Synopsis is a single sentence synopsis of the symbol's documentation.
+ Synopsis string `json:"synopsis"`
+
+ // FullDocumentation is the symbol's full documentation.
+ FullDocumentation string `json:"fullDocumentation"`
+
+ // Signature is the symbol's signature.
+ Signature string `json:"signature"`
+
+ // SingleLine is a single line describing the symbol.
+ // This is recommended only for use in clients that show a single line for hover.
+ SingleLine string `json:"singleLine"`
+
+ // SymbolName is the human-readable name to use for the symbol in links.
+ SymbolName string `json:"symbolName"`
+
+ // LinkPath is the pkg.go.dev link for the given symbol.
+ // For example, the "go/ast" part of "pkg.go.dev/go/ast#Node".
+ LinkPath string `json:"linkPath"`
+
+ // LinkAnchor is the pkg.go.dev link anchor for the given symbol.
+ // For example, the "Node" part of "pkg.go.dev/go/ast#Node".
+ LinkAnchor string `json:"linkAnchor"`
+}
+
+// Hover implements the "textDocument/hover" RPC for Go files.
+func Hover(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (*protocol.Hover, error) {
+ ctx, done := event.Start(ctx, "source.Hover")
+ defer done()
+
+ rng, h, err := hover(ctx, snapshot, fh, position)
+ if err != nil {
+ return nil, err
+ }
+ if h == nil {
+ return nil, nil
+ }
+ hover, err := formatHover(h, snapshot.View().Options())
+ if err != nil {
+ return nil, err
+ }
+ return &protocol.Hover{
+ Contents: protocol.MarkupContent{
+ Kind: snapshot.View().Options().PreferredContentFormat,
+ Value: hover,
+ },
+ Range: rng,
+ }, nil
+}
+
+// hover computes hover information at the given position. If we do not support
+// hovering at the position, it returns _, nil, nil: an error is only returned
+// if the position is valid but we fail to compute hover information.
+func hover(ctx context.Context, snapshot Snapshot, fh FileHandle, pp protocol.Position) (protocol.Range, *HoverJSON, error) {
+ pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), NarrowestPackage)
+ if err != nil {
+ return protocol.Range{}, nil, err
+ }
+ pos, err := pgf.PositionPos(pp)
+ if err != nil {
+ return protocol.Range{}, nil, err
+ }
+
+ // Handle hovering over import paths, which do not have an associated
+ // identifier.
+ for _, spec := range pgf.File.Imports {
+ // We are inclusive of the end point here to allow hovering when the cursor
+ // is just after the import path.
+ if spec.Path.Pos() <= pos && pos <= spec.Path.End() {
+ return hoverImport(ctx, snapshot, pkg, pgf, spec)
+ }
+ }
+
+ // Handle hovering over the package name, which does not have an associated
+ // object.
+ // As with import paths, we allow hovering just after the package name.
+ if pgf.File.Name != nil && pgf.File.Name.Pos() <= pos && pos <= pgf.File.Name.Pos() {
+ return hoverPackageName(pkg, pgf)
+ }
+
+ // Handle hovering over (non-import-path) literals.
+ if path, _ := astutil.PathEnclosingInterval(pgf.File, pos, pos); len(path) > 0 {
+ if lit, _ := path[0].(*ast.BasicLit); lit != nil {
+ return hoverLit(pgf, lit, pos)
+ }
+ }
+
+ // The general case: compute hover information for the object referenced by
+ // the identifier at pos.
+ ident, obj, selectedType := referencedObject(pkg, pgf, pos)
+ if obj == nil || ident == nil {
+ return protocol.Range{}, nil, nil // no object to hover
+ }
+
+ rng, err := pgf.NodeRange(ident)
+ if err != nil {
+ return protocol.Range{}, nil, err
+ }
+
+ // By convention, we qualify hover information relative to the package
+ // from which the request originated.
+ qf := Qualifier(pgf.File, pkg.GetTypes(), pkg.GetTypesInfo())
+
+ // Handle type switch identifiers as a special case, since they don't have an
+ // object.
+ //
+ // There's not much useful information to provide.
+ if selectedType != nil {
+ fakeObj := types.NewVar(obj.Pos(), obj.Pkg(), obj.Name(), selectedType)
+ signature := objectString(fakeObj, qf, nil)
+ return rng, &HoverJSON{
+ Signature: signature,
+ SingleLine: signature,
+ SymbolName: fakeObj.Name(),
+ }, nil
+ }
+
+ // Handle builtins, which don't have a package or position.
+ if obj.Pkg() == nil {
+ h, err := hoverBuiltin(ctx, snapshot, obj)
+ return rng, h, err
+ }
+
+ // For all other objects, consider the full syntax of their declaration in
+ // order to correctly compute their documentation, signature, and link.
+ declPGF, declPos, err := parseFull(ctx, snapshot, pkg.FileSet(), obj.Pos())
+ if err != nil {
+ return protocol.Range{}, nil, fmt.Errorf("re-parsing declaration of %s: %v", obj.Name(), err)
+ }
+ decl, spec, field := findDeclInfo([]*ast.File{declPGF.File}, declPos)
+ comment := chooseDocComment(decl, spec, field)
+ docText := comment.Text()
+
+ // By default, types.ObjectString provides a reasonable signature.
+ signature := objectString(obj, qf, nil)
+ // TODO(rfindley): we could do much better for inferred signatures.
+ if inferred := inferredSignature(pkg.GetTypesInfo(), ident); inferred != nil {
+ signature = objectString(obj, qf, inferred)
+ }
+
+ // For "objects defined by a type spec", the signature produced by
+ // objectString is insufficient:
+ // (1) large structs are formatted poorly, with no newlines
+ // (2) we lose inline comments
+ //
+ // Furthermore, we include a summary of their method set.
+ //
+ // TODO(rfindley): this should use FormatVarType to get proper qualification
+ // of identifiers, and we should revisit the formatting of method set.
+ _, isTypeName := obj.(*types.TypeName)
+ _, isTypeParam := obj.Type().(*typeparams.TypeParam)
+ if isTypeName && !isTypeParam {
+ spec, ok := spec.(*ast.TypeSpec)
+ if !ok {
+ return protocol.Range{}, nil, bug.Errorf("type name %q without type spec", obj.Name())
+ }
+ spec2 := *spec
+ // Don't duplicate comments when formatting type specs.
+ spec2.Doc = nil
+ spec2.Comment = nil
+ var b strings.Builder
+ b.WriteString("type ")
+ fset := FileSetFor(declPGF.Tok)
+ if err := format.Node(&b, fset, &spec2); err != nil {
+ return protocol.Range{}, nil, err
+ }
+
+ // Display the declared methods accessible from the identifier.
+ //
+ // (The format.Node call above displays any struct fields, public
+ // or private, in syntactic form. We choose not to recursively
+ // enumerate any fields and methods promoted from them.)
+ if !types.IsInterface(obj.Type()) {
+ sep := "\n\n"
+ for _, m := range typeutil.IntuitiveMethodSet(obj.Type(), nil) {
+ // Show direct methods that are either exported, or defined in the
+ // current package.
+ if (m.Obj().Exported() || m.Obj().Pkg() == pkg.GetTypes()) && len(m.Index()) == 1 {
+ b.WriteString(sep)
+ sep = "\n"
+ b.WriteString(objectString(m.Obj(), qf, nil))
+ }
+ }
+ }
+ signature = b.String()
+ }
+
+ // Compute link data (on pkg.go.dev or other documentation host).
+ //
+ // If linkPath is empty, the symbol is not linkable.
+ var (
+ linkName string // => link title, always non-empty
+ linkPath string // => link path
+ anchor string // link anchor
+ linkMeta *Metadata // metadata for the linked package
+ )
+ {
+ linkMeta = findFileInDeps(snapshot, pkg.Metadata(), declPGF.URI)
+ if linkMeta == nil {
+ return protocol.Range{}, nil, bug.Errorf("no metadata for %s", declPGF.URI)
+ }
+
+ // For package names, we simply link to their imported package.
+ if pkgName, ok := obj.(*types.PkgName); ok {
+ linkName = pkgName.Name()
+ linkPath = pkgName.Imported().Path()
+ impID := linkMeta.DepsByPkgPath[PackagePath(pkgName.Imported().Path())]
+ linkMeta = snapshot.Metadata(impID)
+ if linkMeta == nil {
+ return protocol.Range{}, nil, bug.Errorf("no metadata for %s", declPGF.URI)
+ }
+ } else {
+ // For all others, check whether the object is in the package scope, or
+ // an exported field or method of an object in the package scope.
+ //
+ // We try to match pkgsite's heuristics for what is linkable, and what is
+ // not.
+ var recv types.Object
+ switch obj := obj.(type) {
+ case *types.Func:
+ sig := obj.Type().(*types.Signature)
+ if sig.Recv() != nil {
+ tname := typeToObject(sig.Recv().Type())
+ if tname != nil { // beware typed nil
+ recv = tname
+ }
+ }
+ case *types.Var:
+ if obj.IsField() {
+ if spec, ok := spec.(*ast.TypeSpec); ok {
+ typeName := spec.Name
+ scopeObj, _ := obj.Pkg().Scope().Lookup(typeName.Name).(*types.TypeName)
+ if scopeObj != nil {
+ if st, _ := scopeObj.Type().Underlying().(*types.Struct); st != nil {
+ for i := 0; i < st.NumFields(); i++ {
+ if obj == st.Field(i) {
+ recv = scopeObj
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Even if the object is not available in package documentation, it may
+ // be embedded in a documented receiver. Detect this by searching
+ // enclosing selector expressions.
+ //
+ // TODO(rfindley): pkgsite doesn't document fields from embedding, just
+ // methods.
+ if recv == nil || !recv.Exported() {
+ path := pathEnclosingObjNode(pgf.File, pos)
+ if enclosing := searchForEnclosing(pkg.GetTypesInfo(), path); enclosing != nil {
+ recv = enclosing
+ } else {
+ recv = nil // note: just recv = ... could result in a typed nil.
+ }
+ }
+
+ pkg := obj.Pkg()
+ if recv != nil {
+ linkName = fmt.Sprintf("(%s.%s).%s", pkg.Name(), recv.Name(), obj.Name())
+ if obj.Exported() && recv.Exported() && pkg.Scope().Lookup(recv.Name()) == recv {
+ linkPath = pkg.Path()
+ anchor = fmt.Sprintf("%s.%s", recv.Name(), obj.Name())
+ }
+ } else {
+ linkName = fmt.Sprintf("%s.%s", pkg.Name(), obj.Name())
+ if obj.Exported() && pkg.Scope().Lookup(obj.Name()) == obj {
+ linkPath = pkg.Path()
+ anchor = obj.Name()
+ }
+ }
+ }
+ }
+
+ if snapshot.View().IsGoPrivatePath(linkPath) || linkMeta.ForTest != "" {
+ linkPath = ""
+ } else if linkMeta.Module != nil && linkMeta.Module.Version != "" {
+ mod := linkMeta.Module
+ linkPath = strings.Replace(linkPath, mod.Path, mod.Path+"@"+mod.Version, 1)
+ }
+
+ return rng, &HoverJSON{
+ Synopsis: doc.Synopsis(docText),
+ FullDocumentation: docText,
+ SingleLine: objectString(obj, qf, nil),
+ SymbolName: linkName,
+ Signature: signature,
+ LinkPath: linkPath,
+ LinkAnchor: anchor,
+ }, nil
+}
+
+// hoverBuiltin computes hover information when hovering over a builtin
+// identifier.
+func hoverBuiltin(ctx context.Context, snapshot Snapshot, obj types.Object) (*HoverJSON, error) {
+ // TODO(rfindley): link to the correct version of Go documentation.
+ builtin, err := snapshot.BuiltinFile(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ // TODO(rfindley): add a test for jump to definition of error.Error (which is
+ // probably failing, considering it lacks special handling).
+ if obj.Name() == "Error" {
+ signature := obj.String()
+ return &HoverJSON{
+ Signature: signature,
+ SingleLine: signature,
+ // TODO(rfindley): these are better than the current behavior.
+ // SymbolName: "(error).Error",
+ // LinkPath: "builtin",
+ // LinkAnchor: "error.Error",
+ }, nil
+ }
+
+ builtinObj := builtin.File.Scope.Lookup(obj.Name())
+ if builtinObj == nil {
+ // All builtins should have a declaration in the builtin file.
+ return nil, bug.Errorf("no builtin object for %s", obj.Name())
+ }
+ node, _ := builtinObj.Decl.(ast.Node)
+ if node == nil {
+ return nil, bug.Errorf("no declaration for %s", obj.Name())
+ }
+
+ var comment *ast.CommentGroup
+ path, _ := astutil.PathEnclosingInterval(builtin.File, node.Pos(), node.End())
+ for _, n := range path {
+ switch n := n.(type) {
+ case *ast.GenDecl:
+ // Separate documentation and signature.
+ comment = n.Doc
+ node2 := *n
+ node2.Doc = nil
+ node = &node2
+ case *ast.FuncDecl:
+ // Ditto.
+ comment = n.Doc
+ node2 := *n
+ node2.Doc = nil
+ node = &node2
+ }
+ }
+
+ signature := FormatNodeFile(builtin.Tok, node)
+ // Replace fake types with their common equivalent.
+ // TODO(rfindley): we should instead use obj.Type(), which would have the
+ // *actual* types of the builtin call.
+ signature = replacer.Replace(signature)
+
+ docText := comment.Text()
+ return &HoverJSON{
+ Synopsis: doc.Synopsis(docText),
+ FullDocumentation: docText,
+ Signature: signature,
+ SingleLine: obj.String(),
+ SymbolName: obj.Name(),
+ LinkPath: "builtin",
+ LinkAnchor: obj.Name(),
+ }, nil
+}
+
+// hoverImport computes hover information when hovering over the import path of
+// imp in the file pgf of pkg.
+//
+// If we do not have metadata for the hovered import, it returns _
+func hoverImport(ctx context.Context, snapshot Snapshot, pkg Package, pgf *ParsedGoFile, imp *ast.ImportSpec) (protocol.Range, *HoverJSON, error) {
+ rng, err := pgf.NodeRange(imp.Path)
+ if err != nil {
+ return protocol.Range{}, nil, err
+ }
+
+ importPath := UnquoteImportPath(imp)
+ if importPath == "" {
+ return protocol.Range{}, nil, fmt.Errorf("invalid import path")
+ }
+ impID := pkg.Metadata().DepsByImpPath[importPath]
+ if impID == "" {
+ return protocol.Range{}, nil, fmt.Errorf("no package data for import %q", importPath)
+ }
+ impMetadata := snapshot.Metadata(impID)
+ if impMetadata == nil {
+ return protocol.Range{}, nil, bug.Errorf("failed to resolve import ID %q", impID)
+ }
+
+ // Find the first file with a package doc comment.
+ var comment *ast.CommentGroup
+ for _, f := range impMetadata.CompiledGoFiles {
+ fh, err := snapshot.GetFile(ctx, f)
+ if err != nil {
+ if ctx.Err() != nil {
+ return protocol.Range{}, nil, ctx.Err()
+ }
+ continue
+ }
+ pgf, err := snapshot.ParseGo(ctx, fh, ParseHeader)
+ if err != nil {
+ if ctx.Err() != nil {
+ return protocol.Range{}, nil, ctx.Err()
+ }
+ continue
+ }
+ if pgf.File.Doc != nil {
+ comment = pgf.File.Doc
+ break
+ }
+ }
+
+ docText := comment.Text()
+ return rng, &HoverJSON{
+ Synopsis: doc.Synopsis(docText),
+ FullDocumentation: docText,
+ }, nil
+}
+
+// hoverPackageName computes hover information for the package name of the file
+// pgf in pkg.
+func hoverPackageName(pkg Package, pgf *ParsedGoFile) (protocol.Range, *HoverJSON, error) {
+ var comment *ast.CommentGroup
+ for _, pgf := range pkg.CompiledGoFiles() {
+ if pgf.File.Doc != nil {
+ comment = pgf.File.Doc
+ break
+ }
+ }
+ rng, err := pgf.NodeRange(pgf.File.Name)
+ if err != nil {
+ return protocol.Range{}, nil, err
+ }
+ docText := comment.Text()
+ return rng, &HoverJSON{
+ Synopsis: doc.Synopsis(docText),
+ FullDocumentation: docText,
+ // Note: including a signature is redundant, since the cursor is already on the
+ // package name.
+ }, nil
+}
+
+// hoverLit computes hover information when hovering over the basic literal lit
+// in the file pgf. The provided pos must be the exact position of the cursor,
+// as it is used to extract the hovered rune in strings.
+//
+// For example, hovering over "\u2211" in "foo \u2211 bar" yields:
+//
+// '∑', U+2211, N-ARY SUMMATION
+func hoverLit(pgf *ParsedGoFile, lit *ast.BasicLit, pos token.Pos) (protocol.Range, *HoverJSON, error) {
+ var r rune
+ var start, end token.Pos
+ // Extract a rune from the current position.
+ // 'Ω', "...Ω...", or 0x03A9 => 'Ω', U+03A9, GREEK CAPITAL LETTER OMEGA
+ switch lit.Kind {
+ case token.CHAR:
+ s, err := strconv.Unquote(lit.Value)
+ if err != nil {
+ // If the conversion fails, it's because of an invalid syntax, therefore
+ // there is no rune to be found.
+ return protocol.Range{}, nil, nil
+ }
+ r, _ = utf8.DecodeRuneInString(s)
+ if r == utf8.RuneError {
+ return protocol.Range{}, nil, fmt.Errorf("rune error")
+ }
+ start, end = lit.Pos(), lit.End()
+ case token.INT:
+ // TODO(rfindley): add support for hex/octal/binary->int conversion here.
+
+ // It's an integer, scan only if it is a hex literal whose bitsize in
+ // ranging from 8 to 32.
+ if !(strings.HasPrefix(lit.Value, "0x") && len(lit.Value[2:]) >= 2 && len(lit.Value[2:]) <= 8) {
+ return protocol.Range{}, nil, nil
+ }
+ v, err := strconv.ParseUint(lit.Value[2:], 16, 32)
+ if err != nil {
+ return protocol.Range{}, nil, fmt.Errorf("parsing int: %v", err)
+ }
+ r = rune(v)
+ if r == utf8.RuneError {
+ return protocol.Range{}, nil, fmt.Errorf("rune error")
+ }
+ start, end = lit.Pos(), lit.End()
+ case token.STRING:
+ // It's a string, scan only if it contains a unicode escape sequence under or before the
+ // current cursor position.
+ litOffset, err := safetoken.Offset(pgf.Tok, lit.Pos())
+ if err != nil {
+ return protocol.Range{}, nil, err
+ }
+ offset, err := safetoken.Offset(pgf.Tok, pos)
+ if err != nil {
+ return protocol.Range{}, nil, err
+ }
+ for i := offset - litOffset; i > 0; i-- {
+ // Start at the cursor position and search backward for the beginning of a rune escape sequence.
+ rr, _ := utf8.DecodeRuneInString(lit.Value[i:])
+ if rr == utf8.RuneError {
+ return protocol.Range{}, nil, fmt.Errorf("rune error")
+ }
+ if rr == '\\' {
+ // Got the beginning, decode it.
+ var tail string
+ r, _, tail, err = strconv.UnquoteChar(lit.Value[i:], '"')
+ if err != nil {
+ // If the conversion fails, it's because of an invalid syntax,
+ // therefore is no rune to be found.
+ return protocol.Range{}, nil, nil
+ }
+ // Only the rune escape sequence part of the string has to be highlighted, recompute the range.
+ runeLen := len(lit.Value) - (int(i) + len(tail))
+ start = token.Pos(int(lit.Pos()) + int(i))
+ end = token.Pos(int(start) + runeLen)
+ break
+ }
+ }
+ }
+ if r == 0 {
+ return protocol.Range{}, nil, nil
+ }
+ rng, err := pgf.PosRange(start, end)
+ if err != nil {
+ return protocol.Range{}, nil, err
+ }
+
+ var desc string
+ runeName := runenames.Name(r)
+ if len(runeName) > 0 && runeName[0] == '<' {
+ // Check if the rune looks like an HTML tag. If so, trim the surrounding <>
+ // characters to work around https://github.com/microsoft/vscode/issues/124042.
+ runeName = strings.TrimRight(runeName[1:], ">")
+ }
+ if strconv.IsPrint(r) {
+ desc = fmt.Sprintf("'%s', U+%04X, %s", string(r), uint32(r), runeName)
+ } else {
+ desc = fmt.Sprintf("U+%04X, %s", uint32(r), runeName)
+ }
+ return rng, &HoverJSON{
+ Synopsis: desc,
+ FullDocumentation: desc,
+ }, nil
+}
+
+// objectString is a wrapper around the types.ObjectString function.
+// It handles adding more information to the object string.
+//
+// TODO(rfindley): this function does too much. We should lift the special
+// handling to callsites.
+func objectString(obj types.Object, qf types.Qualifier, inferred *types.Signature) string {
+ // If the signature type was inferred, prefer the inferred signature with a
+ // comment showing the generic signature.
+ if sig, _ := obj.Type().(*types.Signature); sig != nil && typeparams.ForSignature(sig).Len() > 0 && inferred != nil {
+ obj2 := types.NewFunc(obj.Pos(), obj.Pkg(), obj.Name(), inferred)
+ str := types.ObjectString(obj2, qf)
+ // Try to avoid overly long lines.
+ if len(str) > 60 {
+ str += "\n"
+ } else {
+ str += " "
+ }
+ str += "// " + types.TypeString(sig, qf)
+ return str
+ }
+ str := types.ObjectString(obj, qf)
+ switch obj := obj.(type) {
+ case *types.Const:
+ str = fmt.Sprintf("%s = %s", str, obj.Val())
+
+ // Try to add a formatted duration as an inline comment
+ typ, ok := obj.Type().(*types.Named)
+ if !ok {
+ break
+ }
+ pkg := typ.Obj().Pkg()
+ if pkg.Path() == "time" && typ.Obj().Name() == "Duration" {
+ if d, ok := constant.Int64Val(obj.Val()); ok {
+ str += " // " + time.Duration(d).String()
+ }
+ }
+ }
+ return str
+}
+
+// HoverDocForObject returns the best doc comment for obj (for which
+// fset provides file/line information).
+//
+// TODO(rfindley): there appears to be zero(!) tests for this functionality.
+func HoverDocForObject(ctx context.Context, snapshot Snapshot, fset *token.FileSet, obj types.Object) (*ast.CommentGroup, error) {
+ if _, isTypeName := obj.(*types.TypeName); isTypeName {
+ if _, isTypeParam := obj.Type().(*typeparams.TypeParam); isTypeParam {
+ return nil, nil
+ }
+ }
+
+ pgf, pos, err := parseFull(ctx, snapshot, fset, obj.Pos())
+ if err != nil {
+ return nil, fmt.Errorf("re-parsing: %v", err)
+ }
+
+ decl, spec, field := findDeclInfo([]*ast.File{pgf.File}, pos)
+ return chooseDocComment(decl, spec, field), nil
+}
+
+func chooseDocComment(decl ast.Decl, spec ast.Spec, field *ast.Field) *ast.CommentGroup {
+ if field != nil {
+ if field.Doc != nil {
+ return field.Doc
+ }
+ if field.Comment != nil {
+ return field.Comment
+ }
+ return nil
+ }
+ switch decl := decl.(type) {
+ case *ast.FuncDecl:
+ return decl.Doc
+ case *ast.GenDecl:
+ switch spec := spec.(type) {
+ case *ast.ValueSpec:
+ if spec.Doc != nil {
+ return spec.Doc
+ }
+ if decl.Doc != nil {
+ return decl.Doc
+ }
+ return spec.Comment
+ case *ast.TypeSpec:
+ if spec.Doc != nil {
+ return spec.Doc
+ }
+ if decl.Doc != nil {
+ return decl.Doc
+ }
+ return spec.Comment
+ }
+ }
+ return nil
+}
+
+// parseFull fully parses the file corresponding to position pos (for
+// which fset provides file/line information).
+//
+// It returns the resulting ParsedGoFile as well as new pos contained in the
+// parsed file.
+func parseFull(ctx context.Context, snapshot Snapshot, fset *token.FileSet, pos token.Pos) (*ParsedGoFile, token.Pos, error) {
+ f := fset.File(pos)
+ if f == nil {
+ return nil, 0, bug.Errorf("internal error: no file for position %d", pos)
+ }
+
+ uri := span.URIFromPath(f.Name())
+ fh, err := snapshot.GetFile(ctx, uri)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ pgf, err := snapshot.ParseGo(ctx, fh, ParseFull)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ offset, err := safetoken.Offset(f, pos)
+ if err != nil {
+ return nil, 0, bug.Errorf("offset out of bounds in %q", uri)
+ }
+
+ fullPos, err := safetoken.Pos(pgf.Tok, offset)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ return pgf, fullPos, nil
+}
+
+// extractFieldList recursively tries to extract a field list.
+// If it is not found, nil is returned.
+func extractFieldList(specType ast.Expr) *ast.FieldList {
+ switch t := specType.(type) {
+ case *ast.StructType:
+ return t.Fields
+ case *ast.InterfaceType:
+ return t.Methods
+ case *ast.ArrayType:
+ return extractFieldList(t.Elt)
+ case *ast.MapType:
+ // Map value has a greater chance to be a struct
+ if fields := extractFieldList(t.Value); fields != nil {
+ return fields
+ }
+ return extractFieldList(t.Key)
+ case *ast.ChanType:
+ return extractFieldList(t.Value)
+ }
+ return nil
+}
+
+func formatHover(h *HoverJSON, options *Options) (string, error) {
+ signature := formatSignature(h, options)
+
+ switch options.HoverKind {
+ case SingleLine:
+ return h.SingleLine, nil
+ case NoDocumentation:
+ return signature, nil
+ case Structured:
+ b, err := json.Marshal(h)
+ if err != nil {
+ return "", err
+ }
+ return string(b), nil
+ }
+
+ link := formatLink(h, options)
+ doc := formatDoc(h, options)
+
+ var b strings.Builder
+ parts := []string{signature, doc, link}
+ for i, el := range parts {
+ if el != "" {
+ b.WriteString(el)
+
+ // If any elements of the remainder of the list are non-empty,
+ // write an extra newline.
+ if anyNonEmpty(parts[i+1:]) {
+ if options.PreferredContentFormat == protocol.Markdown {
+ b.WriteString("\n\n")
+ } else {
+ b.WriteRune('\n')
+ }
+ }
+ }
+ }
+ return b.String(), nil
+}
+
+func formatSignature(h *HoverJSON, options *Options) string {
+ signature := h.Signature
+ if signature != "" && options.PreferredContentFormat == protocol.Markdown {
+ signature = fmt.Sprintf("```go\n%s\n```", signature)
+ }
+ return signature
+}
+
+func formatLink(h *HoverJSON, options *Options) string {
+ if !options.LinksInHover || options.LinkTarget == "" || h.LinkPath == "" {
+ return ""
+ }
+ plainLink := BuildLink(options.LinkTarget, h.LinkPath, h.LinkAnchor)
+ switch options.PreferredContentFormat {
+ case protocol.Markdown:
+ return fmt.Sprintf("[`%s` on %s](%s)", h.SymbolName, options.LinkTarget, plainLink)
+ case protocol.PlainText:
+ return ""
+ default:
+ return plainLink
+ }
+}
+
+// BuildLink constructs a URL with the given target, path, and anchor.
+func BuildLink(target, path, anchor string) string {
+ link := fmt.Sprintf("https://%s/%s", target, path)
+ if anchor == "" {
+ return link
+ }
+ return link + "#" + anchor
+}
+
+func formatDoc(h *HoverJSON, options *Options) string {
+ var doc string
+ switch options.HoverKind {
+ case SynopsisDocumentation:
+ doc = h.Synopsis
+ case FullDocumentation:
+ doc = h.FullDocumentation
+ }
+ if options.PreferredContentFormat == protocol.Markdown {
+ return CommentToMarkdown(doc, options)
+ }
+ return doc
+}
+
+func anyNonEmpty(x []string) bool {
+ for _, el := range x {
+ if el != "" {
+ return true
+ }
+ }
+ return false
+}
+
+// findDeclInfo returns the syntax nodes involved in the declaration of the
+// types.Object with position pos, searching the given list of file syntax
+// trees.
+//
+// Pos may be the position of the name-defining identifier in a FuncDecl,
+// ValueSpec, TypeSpec, Field, or as a special case the position of
+// Ellipsis.Elt in an ellipsis field.
+//
+// If found, the resulting decl, spec, and field will be the inner-most
+// instance of each node type surrounding pos.
+//
+// If field is non-nil, pos is the position of a field Var. If field is nil and
+// spec is non-nil, pos is the position of a Var, Const, or TypeName object. If
+// both field and spec are nil and decl is non-nil, pos is the position of a
+// Func object.
+//
+// It returns a nil decl if no object-defining node is found at pos.
+//
+// TODO(rfindley): this function has tricky semantics, and may be worth unit
+// testing and/or refactoring.
+func findDeclInfo(files []*ast.File, pos token.Pos) (decl ast.Decl, spec ast.Spec, field *ast.Field) {
+ // panic(found{}) breaks off the traversal and
+ // causes the function to return normally.
+ type found struct{}
+ defer func() {
+ switch x := recover().(type) {
+ case nil:
+ case found:
+ default:
+ panic(x)
+ }
+ }()
+
+ // Visit the files in search of the node at pos.
+ stack := make([]ast.Node, 0, 20)
+ // Allocate the closure once, outside the loop.
+ f := func(n ast.Node) bool {
+ if n != nil {
+ stack = append(stack, n) // push
+ } else {
+ stack = stack[:len(stack)-1] // pop
+ return false
+ }
+
+ // Skip subtrees (incl. files) that don't contain the search point.
+ if !(n.Pos() <= pos && pos < n.End()) {
+ return false
+ }
+
+ switch n := n.(type) {
+ case *ast.Field:
+ findEnclosingDeclAndSpec := func() {
+ for i := len(stack) - 1; i >= 0; i-- {
+ switch n := stack[i].(type) {
+ case ast.Spec:
+ spec = n
+ case ast.Decl:
+ decl = n
+ return
+ }
+ }
+ }
+
+ // Check each field name since you can have
+ // multiple names for the same type expression.
+ for _, id := range n.Names {
+ if id.Pos() == pos {
+ field = n
+ findEnclosingDeclAndSpec()
+ panic(found{})
+ }
+ }
+
+ // Check *ast.Field itself. This handles embedded
+ // fields which have no associated *ast.Ident name.
+ if n.Pos() == pos {
+ field = n
+ findEnclosingDeclAndSpec()
+ panic(found{})
+ }
+
+ // Also check "X" in "...X". This makes it easy to format variadic
+ // signature params properly.
+ //
+ // TODO(rfindley): I don't understand this comment. How does finding the
+ // field in this case make it easier to format variadic signature params?
+ if ell, ok := n.Type.(*ast.Ellipsis); ok && ell.Elt != nil && ell.Elt.Pos() == pos {
+ field = n
+ findEnclosingDeclAndSpec()
+ panic(found{})
+ }
+
+ case *ast.FuncDecl:
+ if n.Name.Pos() == pos {
+ decl = n
+ panic(found{})
+ }
+
+ case *ast.GenDecl:
+ for _, s := range n.Specs {
+ switch s := s.(type) {
+ case *ast.TypeSpec:
+ if s.Name.Pos() == pos {
+ decl = n
+ spec = s
+ panic(found{})
+ }
+ case *ast.ValueSpec:
+ for _, id := range s.Names {
+ if id.Pos() == pos {
+ decl = n
+ spec = s
+ panic(found{})
+ }
+ }
+ }
+ }
+ }
+ return true
+ }
+ for _, file := range files {
+ ast.Inspect(file, f)
+ }
+
+ return nil, nil, nil
+}