| // 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" |
| "errors" |
| "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/internal/event" |
| "golang.org/x/tools/internal/lsp/protocol" |
| "golang.org/x/tools/internal/typeparams" |
| ) |
| |
| // HoverContext contains context extracted from the syntax and type information |
| // of a given node, for use in various summaries (hover, autocomplete, |
| // signature help). |
| type HoverContext struct { |
| // signatureSource is the object or node use to derive the hover signature. |
| // |
| // It may also hold a precomputed string. |
| // TODO(rfindley): pre-compute all signatures to avoid this indirection. |
| signatureSource interface{} |
| |
| // comment is the most relevant comment group associated with the hovered object. |
| Comment *ast.CommentGroup |
| } |
| |
| // 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 types.Object.Name for the given symbol. |
| 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"` |
| } |
| |
| func Hover(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (*protocol.Hover, error) { |
| ident, err := Identifier(ctx, snapshot, fh, position) |
| if err != nil { |
| if hover, innerErr := hoverRune(ctx, snapshot, fh, position); innerErr == nil { |
| return hover, nil |
| } |
| return nil, nil |
| } |
| h, err := HoverIdentifier(ctx, ident) |
| if err != nil { |
| return nil, err |
| } |
| rng, err := ident.Range() |
| if err != nil { |
| return nil, err |
| } |
| 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 |
| } |
| |
| func hoverRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (*protocol.Hover, error) { |
| ctx, done := event.Start(ctx, "source.hoverRune") |
| defer done() |
| |
| r, mrng, err := findRune(ctx, snapshot, fh, position) |
| if err != nil { |
| return nil, err |
| } |
| rng, err := mrng.Range() |
| if err != nil { |
| return 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 &protocol.Hover{ |
| Contents: protocol.MarkupContent{ |
| Kind: snapshot.View().Options().PreferredContentFormat, |
| Value: desc, |
| }, |
| Range: rng, |
| }, nil |
| } |
| |
| // ErrNoRuneFound is the error returned when no rune is found at a particular position. |
| var ErrNoRuneFound = errors.New("no rune found") |
| |
| // findRune returns rune information for a position in a file. |
| func findRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (rune, MappedRange, error) { |
| pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage) |
| if err != nil { |
| return 0, MappedRange{}, err |
| } |
| spn, err := pgf.Mapper.PointSpan(position) |
| if err != nil { |
| return 0, MappedRange{}, err |
| } |
| rng, err := spn.Range(pgf.Mapper.Converter) |
| if err != nil { |
| return 0, MappedRange{}, err |
| } |
| pos := rng.Start |
| |
| // Find the basic literal enclosing the given position, if there is one. |
| var lit *ast.BasicLit |
| var found bool |
| ast.Inspect(pgf.File, func(n ast.Node) bool { |
| if found { |
| return false |
| } |
| if n, ok := n.(*ast.BasicLit); ok && pos >= n.Pos() && pos <= n.End() { |
| lit = n |
| found = true |
| } |
| return !found |
| }) |
| if !found { |
| return 0, MappedRange{}, ErrNoRuneFound |
| } |
| |
| var r rune |
| var start, end token.Pos |
| 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 0, MappedRange{}, ErrNoRuneFound |
| } |
| r, _ = utf8.DecodeRuneInString(s) |
| if r == utf8.RuneError { |
| return 0, MappedRange{}, fmt.Errorf("rune error") |
| } |
| start, end = lit.Pos(), lit.End() |
| case token.INT: |
| // It's an integer, scan only if it is a hex litteral 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 0, MappedRange{}, ErrNoRuneFound |
| } |
| v, err := strconv.ParseUint(lit.Value[2:], 16, 32) |
| if err != nil { |
| return 0, MappedRange{}, err |
| } |
| r = rune(v) |
| if r == utf8.RuneError { |
| return 0, MappedRange{}, 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. |
| var found bool |
| litOffset, err := Offset(pgf.Tok, lit.Pos()) |
| if err != nil { |
| return 0, MappedRange{}, err |
| } |
| offset, err := Offset(pgf.Tok, pos) |
| if err != nil { |
| return 0, MappedRange{}, 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 0, MappedRange{}, 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 0, MappedRange{}, ErrNoRuneFound |
| } |
| // 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) |
| found = true |
| break |
| } |
| } |
| if !found { |
| // No escape sequence found |
| return 0, MappedRange{}, ErrNoRuneFound |
| } |
| default: |
| return 0, MappedRange{}, ErrNoRuneFound |
| } |
| |
| mappedRange, err := posToMappedRange(snapshot, pkg, start, end) |
| if err != nil { |
| return 0, MappedRange{}, err |
| } |
| return r, mappedRange, nil |
| } |
| |
| func HoverIdentifier(ctx context.Context, i *IdentifierInfo) (*HoverJSON, error) { |
| ctx, done := event.Start(ctx, "source.Hover") |
| defer done() |
| |
| hoverCtx, err := FindHoverContext(ctx, i.Snapshot, i.pkg, i.Declaration.obj, i.Declaration.node, i.Declaration.fullDecl) |
| if err != nil { |
| return nil, err |
| } |
| |
| h := &HoverJSON{ |
| FullDocumentation: hoverCtx.Comment.Text(), |
| Synopsis: doc.Synopsis(hoverCtx.Comment.Text()), |
| } |
| |
| fset := i.Snapshot.FileSet() |
| // Determine the symbol's signature. |
| switch x := hoverCtx.signatureSource.(type) { |
| case string: |
| h.Signature = x // a pre-computed signature |
| |
| case *ast.TypeSpec: |
| x2 := *x |
| // Don't duplicate comments when formatting type specs. |
| x2.Doc = nil |
| x2.Comment = nil |
| var b strings.Builder |
| b.WriteString("type ") |
| if err := format.Node(&b, fset, &x2); err != nil { |
| return nil, err |
| } |
| h.Signature = b.String() |
| |
| case ast.Node: |
| var b strings.Builder |
| if err := format.Node(&b, fset, x); err != nil { |
| return nil, err |
| } |
| h.Signature = b.String() |
| |
| // Check if the variable is an integer whose value we can present in a more |
| // user-friendly way, i.e. `var hex = 0xe34e` becomes `var hex = 58190` |
| if spec, ok := x.(*ast.ValueSpec); ok && len(spec.Values) > 0 { |
| if lit, ok := spec.Values[0].(*ast.BasicLit); ok && len(spec.Names) > 0 { |
| val := constant.MakeFromLiteral(types.ExprString(lit), lit.Kind, 0) |
| h.Signature = fmt.Sprintf("var %s = %s", spec.Names[0], val) |
| } |
| } |
| |
| case types.Object: |
| // If the variable is implicitly declared in a type switch, we need to |
| // manually generate its object string. |
| if typ := i.Declaration.typeSwitchImplicit; typ != nil { |
| if v, ok := x.(*types.Var); ok { |
| h.Signature = fmt.Sprintf("var %s %s", v.Name(), types.TypeString(typ, i.qf)) |
| break |
| } |
| } |
| h.Signature = objectString(x, i.qf, i.Inferred) |
| } |
| if obj := i.Declaration.obj; obj != nil { |
| h.SingleLine = objectString(obj, i.qf, nil) |
| } |
| obj := i.Declaration.obj |
| if obj == nil { |
| return h, nil |
| } |
| |
| // Check if the identifier is test-only (and is therefore not part of a |
| // package's API). This is true if the request originated in a test package, |
| // and if the declaration is also found in the same test package. |
| if i.pkg != nil && obj.Pkg() != nil && i.pkg.ForTest() != "" { |
| if _, err := i.pkg.File(i.Declaration.MappedRange[0].URI()); err == nil { |
| return h, nil |
| } |
| } |
| |
| h.SymbolName, h.LinkPath, h.LinkAnchor = linkData(obj, i.enclosing) |
| |
| // See golang/go#36998: don't link to modules matching GOPRIVATE. |
| // |
| // The path returned by linkData is an import path. |
| if i.Snapshot.View().IsGoPrivatePath(h.LinkPath) { |
| h.LinkPath = "" |
| } else if mod, version, ok := moduleAtVersion(h.LinkPath, i); ok { |
| h.LinkPath = strings.Replace(h.LinkPath, mod, mod+"@"+version, 1) |
| } |
| |
| return h, nil |
| } |
| |
| // linkData returns the name, import path, and anchor to use in building links |
| // to obj. |
| // |
| // If obj is not visible in documentation, the returned name will be empty. |
| func linkData(obj types.Object, enclosing *types.TypeName) (name, importPath, anchor string) { |
| // Package names simply link to the package. |
| if obj, ok := obj.(*types.PkgName); ok { |
| return obj.Name(), obj.Imported().Path(), "" |
| } |
| |
| // Builtins link to the special builtin package. |
| if obj.Parent() == types.Universe { |
| return obj.Name(), "builtin", obj.Name() |
| } |
| |
| // In all other cases, the object must be exported. |
| if !obj.Exported() { |
| return "", "", "" |
| } |
| |
| var recv types.Object // If non-nil, the field or method receiver base. |
| |
| switch obj := obj.(type) { |
| case *types.Var: |
| // If the object is a field, and we have an associated selector |
| // composite literal, or struct, we can determine the link. |
| if obj.IsField() && enclosing != nil { |
| recv = enclosing |
| } |
| case *types.Func: |
| typ, ok := obj.Type().(*types.Signature) |
| if !ok { |
| // Note: this should never happen. go/types guarantees that the type of |
| // *Funcs are Signatures. |
| // |
| // TODO(rfindley): given a 'debug' mode, we should panic here. |
| return "", "", "" |
| } |
| if r := typ.Recv(); r != nil { |
| if rtyp, _ := Deref(r.Type()).(*types.Named); rtyp != nil { |
| // If we have an unexported type, see if the enclosing type is |
| // exported (we may have an interface or struct we can link |
| // to). If not, don't show any link. |
| if !rtyp.Obj().Exported() { |
| if enclosing != nil { |
| recv = enclosing |
| } else { |
| return "", "", "" |
| } |
| } else { |
| recv = rtyp.Obj() |
| } |
| } |
| } |
| } |
| |
| if recv != nil && !recv.Exported() { |
| return "", "", "" |
| } |
| |
| // Either the object or its receiver must be in the package scope. |
| scopeObj := obj |
| if recv != nil { |
| scopeObj = recv |
| } |
| if scopeObj.Pkg() == nil || scopeObj.Pkg().Scope().Lookup(scopeObj.Name()) != scopeObj { |
| return "", "", "" |
| } |
| |
| // golang/go#52211: somehow we get here with a nil obj.Pkg |
| // TODO: allow using debug.Bug here, to catch this bug. |
| if obj.Pkg() == nil { |
| return "", "", "" |
| } |
| |
| importPath = obj.Pkg().Path() |
| if recv != nil { |
| anchor = fmt.Sprintf("%s.%s", recv.Name(), obj.Name()) |
| name = fmt.Sprintf("(%s.%s).%s", obj.Pkg().Name(), recv.Name(), obj.Name()) |
| } else { |
| // For most cases, the link is "package/path#symbol". |
| anchor = obj.Name() |
| name = fmt.Sprintf("%s.%s", obj.Pkg().Name(), obj.Name()) |
| } |
| return name, importPath, anchor |
| } |
| |
| func moduleAtVersion(path string, i *IdentifierInfo) (string, string, bool) { |
| // TODO(rfindley): moduleAtVersion should not be responsible for deciding |
| // whether or not the link target supports module version links. |
| if strings.ToLower(i.Snapshot.View().Options().LinkTarget) != "pkg.go.dev" { |
| return "", "", false |
| } |
| impPkg, err := i.pkg.GetImport(path) |
| if err != nil { |
| return "", "", false |
| } |
| if impPkg.Version() == nil { |
| return "", "", false |
| } |
| version, modpath := impPkg.Version().Version, impPkg.Version().Path |
| if modpath == "" || version == "" { |
| return "", "", false |
| } |
| return modpath, version, true |
| } |
| |
| // objectString is a wrapper around the types.ObjectString function. |
| // It handles adding more information to the object string. |
| func objectString(obj types.Object, qf types.Qualifier, inferred *types.Signature) string { |
| // If the signature type was inferred, prefer the preferred 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 |
| } |
| |
| // FindHoverContext returns a HoverContext struct for an AST node and its |
| // declaration object. node should be the actual node used in type checking, |
| // while fullNode could be a separate node with more complete syntactic |
| // information. |
| func FindHoverContext(ctx context.Context, s Snapshot, pkg Package, obj types.Object, pkgNode ast.Node, fullDecl ast.Decl) (*HoverContext, error) { |
| var info *HoverContext |
| |
| // Type parameters get their signature from their declaration object. |
| if _, isTypeName := obj.(*types.TypeName); isTypeName { |
| if _, isTypeParam := obj.Type().(*typeparams.TypeParam); isTypeParam { |
| return &HoverContext{signatureSource: obj}, nil |
| } |
| } |
| |
| // This is problematic for a number of reasons. We really need to have a more |
| // general mechanism to validate the coherency of AST with type information, |
| // but absent that we must do our best to ensure that we don't use fullNode |
| // when we actually need the node that was type checked. |
| // |
| // pkgNode may be nil, if it was eliminated from the type-checked syntax. In |
| // that case, use fullDecl if available. |
| node := pkgNode |
| if node == nil && fullDecl != nil { |
| node = fullDecl |
| } |
| |
| switch node := node.(type) { |
| case *ast.Ident: |
| // The package declaration. |
| for _, f := range pkg.GetSyntax() { |
| if f.Name == pkgNode { |
| info = &HoverContext{Comment: f.Doc} |
| } |
| } |
| case *ast.ImportSpec: |
| // Try to find the package documentation for an imported package. |
| pkgPath, err := strconv.Unquote(node.Path.Value) |
| if err != nil { |
| return nil, err |
| } |
| imp, err := pkg.GetImport(pkgPath) |
| if err != nil { |
| return nil, err |
| } |
| // Assume that only one file will contain package documentation, |
| // so pick the first file that has a doc comment. |
| for _, file := range imp.GetSyntax() { |
| if file.Doc != nil { |
| info = &HoverContext{Comment: file.Doc} |
| if file.Name != nil { |
| info.signatureSource = "package " + file.Name.Name |
| } |
| break |
| } |
| } |
| case *ast.GenDecl: |
| switch obj := obj.(type) { |
| case *types.TypeName, *types.Var, *types.Const, *types.Func: |
| // Always use the full declaration here if we have it, because the |
| // dependent code doesn't rely on pointer identity. This is fragile. |
| if d, _ := fullDecl.(*ast.GenDecl); d != nil { |
| node = d |
| } |
| // obj may not have been produced by type checking the AST containing |
| // node, so we need to be careful about using token.Pos. |
| tok := s.FileSet().File(obj.Pos()) |
| offset, err := Offset(tok, obj.Pos()) |
| if err != nil { |
| return nil, err |
| } |
| |
| // fullTok and fullPos are the *token.File and object position in for the |
| // full AST. |
| fullTok := s.FileSet().File(node.Pos()) |
| fullPos, err := Pos(fullTok, offset) |
| if err != nil { |
| return nil, err |
| } |
| |
| var spec ast.Spec |
| for _, s := range node.Specs { |
| // Avoid panics by guarding the calls to token.Offset (golang/go#48249). |
| start, err := Offset(fullTok, s.Pos()) |
| if err != nil { |
| return nil, err |
| } |
| end, err := Offset(fullTok, s.End()) |
| if err != nil { |
| return nil, err |
| } |
| if start <= offset && offset <= end { |
| spec = s |
| break |
| } |
| } |
| |
| info, err = hoverGenDecl(node, spec, fullPos, obj) |
| if err != nil { |
| return nil, err |
| } |
| } |
| case *ast.TypeSpec: |
| if obj.Parent() == types.Universe { |
| if genDecl, ok := fullDecl.(*ast.GenDecl); ok { |
| info = hoverTypeSpec(node, genDecl) |
| } |
| } |
| case *ast.FuncDecl: |
| switch obj.(type) { |
| case *types.Func: |
| info = &HoverContext{signatureSource: obj, Comment: node.Doc} |
| case *types.Builtin: |
| info = &HoverContext{Comment: node.Doc} |
| if sig, err := NewBuiltinSignature(ctx, s, obj.Name()); err == nil { |
| info.signatureSource = "func " + sig.name + sig.Format() |
| } else { |
| // Fall back on the object as a signature source. |
| |
| // TODO(rfindley): refactor so that we can report bugs from the source |
| // package. |
| |
| // debug.Bug(ctx, "invalid builtin hover", "did not find builtin signature: %v", err) |
| info.signatureSource = obj |
| } |
| case *types.Var: |
| // Object is a function param or the field of an anonymous struct |
| // declared with ':='. Skip the first one because only fields |
| // can have docs. |
| if isFunctionParam(obj, node) { |
| break |
| } |
| |
| field, err := s.PosToField(ctx, pkg, obj.Pos()) |
| if err != nil { |
| return nil, err |
| } |
| |
| if field != nil { |
| comment := field.Doc |
| if comment.Text() == "" { |
| comment = field.Comment |
| } |
| info = &HoverContext{signatureSource: obj, Comment: comment} |
| } |
| } |
| } |
| |
| if info == nil { |
| info = &HoverContext{signatureSource: obj} |
| } |
| |
| return info, nil |
| } |
| |
| // isFunctionParam returns true if the passed object is either an incoming |
| // or an outgoing function param |
| func isFunctionParam(obj types.Object, node *ast.FuncDecl) bool { |
| for _, f := range node.Type.Params.List { |
| if f.Pos() == obj.Pos() { |
| return true |
| } |
| } |
| if node.Type.Results != nil { |
| for _, f := range node.Type.Results.List { |
| if f.Pos() == obj.Pos() { |
| return true |
| } |
| } |
| } |
| return false |
| } |
| |
| // hoverGenDecl returns hover information an object declared via spec inside |
| // of the GenDecl node. obj is the type-checked object corresponding to the |
| // declaration, but may have been type-checked using a different AST than the |
| // given nodes; fullPos is the position of obj in node's AST. |
| func hoverGenDecl(node *ast.GenDecl, spec ast.Spec, fullPos token.Pos, obj types.Object) (*HoverContext, error) { |
| if spec == nil { |
| return nil, fmt.Errorf("no spec for node %v at position %v", node, fullPos) |
| } |
| |
| // If we have a field or method. |
| switch obj.(type) { |
| case *types.Var, *types.Const, *types.Func: |
| return hoverVar(spec, fullPos, obj, node), nil |
| } |
| // Handle types. |
| switch spec := spec.(type) { |
| case *ast.TypeSpec: |
| return hoverTypeSpec(spec, node), nil |
| case *ast.ValueSpec: |
| return &HoverContext{signatureSource: spec, Comment: spec.Doc}, nil |
| case *ast.ImportSpec: |
| return &HoverContext{signatureSource: spec, Comment: spec.Doc}, nil |
| } |
| return nil, fmt.Errorf("unable to format spec %v (%T)", spec, spec) |
| } |
| |
| // TODO(rfindley): rename this function. |
| func hoverTypeSpec(spec *ast.TypeSpec, decl *ast.GenDecl) *HoverContext { |
| comment := spec.Doc |
| if comment == nil && decl != nil { |
| comment = decl.Doc |
| } |
| if comment == nil { |
| comment = spec.Comment |
| } |
| return &HoverContext{ |
| signatureSource: spec, |
| Comment: comment, |
| } |
| } |
| |
| func hoverVar(node ast.Spec, fullPos token.Pos, obj types.Object, decl *ast.GenDecl) *HoverContext { |
| var fieldList *ast.FieldList |
| switch spec := node.(type) { |
| case *ast.TypeSpec: |
| switch t := spec.Type.(type) { |
| case *ast.StructType: |
| fieldList = t.Fields |
| case *ast.InterfaceType: |
| fieldList = t.Methods |
| } |
| case *ast.ValueSpec: |
| // Try to extract the field list of an anonymous struct |
| if fieldList = extractFieldList(spec.Type); fieldList != nil { |
| break |
| } |
| |
| comment := spec.Doc |
| if comment == nil { |
| comment = decl.Doc |
| } |
| if comment == nil { |
| comment = spec.Comment |
| } |
| |
| // We need the AST nodes for variable declarations of basic literals with |
| // associated values so that we can augment their hover with more information. |
| if _, ok := obj.(*types.Var); ok && spec.Type == nil && len(spec.Values) > 0 { |
| if _, ok := spec.Values[0].(*ast.BasicLit); ok { |
| return &HoverContext{signatureSource: spec, Comment: comment} |
| } |
| } |
| |
| return &HoverContext{signatureSource: obj, Comment: comment} |
| } |
| |
| if fieldList != nil { |
| comment := findFieldComment(fullPos, fieldList) |
| return &HoverContext{signatureSource: obj, Comment: comment} |
| } |
| return &HoverContext{signatureSource: obj, Comment: decl.Doc} |
| } |
| |
| // 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 |
| } |
| |
| // findFieldComment visits all fields in depth-first order and returns |
| // the comment of a field with passed position. If no comment is found, |
| // nil is returned. |
| func findFieldComment(pos token.Pos, fieldList *ast.FieldList) *ast.CommentGroup { |
| for _, field := range fieldList.List { |
| if field.Pos() == pos { |
| if field.Doc.Text() != "" { |
| return field.Doc |
| } |
| return field.Comment |
| } |
| |
| if nestedFieldList := extractFieldList(field.Type); nestedFieldList != nil { |
| if c := findFieldComment(pos, nestedFieldList); c != nil { |
| return c |
| } |
| } |
| } |
| 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) |
| |
| // Don't write out final newline. |
| if i == len(parts) { |
| continue |
| } |
| // If any elements of the remainder of the list are non-empty, |
| // write a 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 link with the given target, path, and anchor. |
| func BuildLink(target, path, anchor string) string { |
| link := fmt.Sprintf("https://%s/%s", target, path) |
| if target == "pkg.go.dev" { |
| link += "?utm_source=gopls" |
| } |
| 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) |
| } |
| return doc |
| } |
| |
| func anyNonEmpty(x []string) bool { |
| for _, el := range x { |
| if el != "" { |
| return true |
| } |
| } |
| return false |
| } |