github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/internal/lsp/source/hover.go (about) 1 // Copyright 2019 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package source 6 7 import ( 8 "context" 9 "encoding/json" 10 "fmt" 11 "go/ast" 12 "go/constant" 13 "go/doc" 14 "go/format" 15 "go/token" 16 "go/types" 17 "strconv" 18 "strings" 19 "time" 20 "unicode/utf8" 21 22 "golang.org/x/text/unicode/runenames" 23 "github.com/powerman/golang-tools/internal/event" 24 "github.com/powerman/golang-tools/internal/lsp/protocol" 25 "github.com/powerman/golang-tools/internal/typeparams" 26 errors "golang.org/x/xerrors" 27 ) 28 29 // HoverContext contains context extracted from the syntax and type information 30 // of a given node, for use in various summaries (hover, autocomplete, 31 // signature help). 32 type HoverContext struct { 33 // signatureSource is the object or node use to derive the hover signature. 34 // 35 // It may also hold a precomputed string. 36 // TODO(rfindley): pre-compute all signatures to avoid this indirection. 37 signatureSource interface{} 38 39 // comment is the most relevant comment group associated with the hovered object. 40 Comment *ast.CommentGroup 41 } 42 43 // HoverJSON contains information used by hover. It is also the JSON returned 44 // for the "structured" hover format 45 type HoverJSON struct { 46 // Synopsis is a single sentence synopsis of the symbol's documentation. 47 Synopsis string `json:"synopsis"` 48 49 // FullDocumentation is the symbol's full documentation. 50 FullDocumentation string `json:"fullDocumentation"` 51 52 // Signature is the symbol's signature. 53 Signature string `json:"signature"` 54 55 // SingleLine is a single line describing the symbol. 56 // This is recommended only for use in clients that show a single line for hover. 57 SingleLine string `json:"singleLine"` 58 59 // SymbolName is the types.Object.Name for the given symbol. 60 SymbolName string `json:"symbolName"` 61 62 // LinkPath is the pkg.go.dev link for the given symbol. 63 // For example, the "go/ast" part of "pkg.go.dev/go/ast#Node". 64 LinkPath string `json:"linkPath"` 65 66 // LinkAnchor is the pkg.go.dev link anchor for the given symbol. 67 // For example, the "Node" part of "pkg.go.dev/go/ast#Node". 68 LinkAnchor string `json:"linkAnchor"` 69 } 70 71 func Hover(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (*protocol.Hover, error) { 72 ident, err := Identifier(ctx, snapshot, fh, position) 73 if err != nil { 74 if hover, innerErr := hoverRune(ctx, snapshot, fh, position); innerErr == nil { 75 return hover, nil 76 } 77 return nil, nil 78 } 79 h, err := HoverIdentifier(ctx, ident) 80 if err != nil { 81 return nil, err 82 } 83 rng, err := ident.Range() 84 if err != nil { 85 return nil, err 86 } 87 hover, err := FormatHover(h, snapshot.View().Options()) 88 if err != nil { 89 return nil, err 90 } 91 return &protocol.Hover{ 92 Contents: protocol.MarkupContent{ 93 Kind: snapshot.View().Options().PreferredContentFormat, 94 Value: hover, 95 }, 96 Range: rng, 97 }, nil 98 } 99 100 func hoverRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (*protocol.Hover, error) { 101 ctx, done := event.Start(ctx, "source.hoverRune") 102 defer done() 103 104 r, mrng, err := findRune(ctx, snapshot, fh, position) 105 if err != nil { 106 return nil, err 107 } 108 rng, err := mrng.Range() 109 if err != nil { 110 return nil, err 111 } 112 113 var desc string 114 runeName := runenames.Name(r) 115 if len(runeName) > 0 && runeName[0] == '<' { 116 // Check if the rune looks like an HTML tag. If so, trim the surrounding <> 117 // characters to work around https://github.com/microsoft/vscode/issues/124042. 118 runeName = strings.TrimRight(runeName[1:], ">") 119 } 120 if strconv.IsPrint(r) { 121 desc = fmt.Sprintf("'%s', U+%04X, %s", string(r), uint32(r), runeName) 122 } else { 123 desc = fmt.Sprintf("U+%04X, %s", uint32(r), runeName) 124 } 125 return &protocol.Hover{ 126 Contents: protocol.MarkupContent{ 127 Kind: snapshot.View().Options().PreferredContentFormat, 128 Value: desc, 129 }, 130 Range: rng, 131 }, nil 132 } 133 134 // ErrNoRuneFound is the error returned when no rune is found at a particular position. 135 var ErrNoRuneFound = errors.New("no rune found") 136 137 // findRune returns rune information for a position in a file. 138 func findRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (rune, MappedRange, error) { 139 pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage) 140 if err != nil { 141 return 0, MappedRange{}, err 142 } 143 spn, err := pgf.Mapper.PointSpan(position) 144 if err != nil { 145 return 0, MappedRange{}, err 146 } 147 rng, err := spn.Range(pgf.Mapper.Converter) 148 if err != nil { 149 return 0, MappedRange{}, err 150 } 151 pos := rng.Start 152 153 // Find the basic literal enclosing the given position, if there is one. 154 var lit *ast.BasicLit 155 var found bool 156 ast.Inspect(pgf.File, func(n ast.Node) bool { 157 if found { 158 return false 159 } 160 if n, ok := n.(*ast.BasicLit); ok && pos >= n.Pos() && pos <= n.End() { 161 lit = n 162 found = true 163 } 164 return !found 165 }) 166 if !found { 167 return 0, MappedRange{}, ErrNoRuneFound 168 } 169 170 var r rune 171 var start, end token.Pos 172 switch lit.Kind { 173 case token.CHAR: 174 s, err := strconv.Unquote(lit.Value) 175 if err != nil { 176 // If the conversion fails, it's because of an invalid syntax, therefore 177 // there is no rune to be found. 178 return 0, MappedRange{}, ErrNoRuneFound 179 } 180 r, _ = utf8.DecodeRuneInString(s) 181 if r == utf8.RuneError { 182 return 0, MappedRange{}, fmt.Errorf("rune error") 183 } 184 start, end = lit.Pos(), lit.End() 185 case token.INT: 186 // It's an integer, scan only if it is a hex litteral whose bitsize in 187 // ranging from 8 to 32. 188 if !(strings.HasPrefix(lit.Value, "0x") && len(lit.Value[2:]) >= 2 && len(lit.Value[2:]) <= 8) { 189 return 0, MappedRange{}, ErrNoRuneFound 190 } 191 v, err := strconv.ParseUint(lit.Value[2:], 16, 32) 192 if err != nil { 193 return 0, MappedRange{}, err 194 } 195 r = rune(v) 196 if r == utf8.RuneError { 197 return 0, MappedRange{}, fmt.Errorf("rune error") 198 } 199 start, end = lit.Pos(), lit.End() 200 case token.STRING: 201 // It's a string, scan only if it contains a unicode escape sequence under or before the 202 // current cursor position. 203 var found bool 204 litOffset, err := Offset(pgf.Tok, lit.Pos()) 205 if err != nil { 206 return 0, MappedRange{}, err 207 } 208 offset, err := Offset(pgf.Tok, pos) 209 if err != nil { 210 return 0, MappedRange{}, err 211 } 212 for i := offset - litOffset; i > 0; i-- { 213 // Start at the cursor position and search backward for the beginning of a rune escape sequence. 214 rr, _ := utf8.DecodeRuneInString(lit.Value[i:]) 215 if rr == utf8.RuneError { 216 return 0, MappedRange{}, fmt.Errorf("rune error") 217 } 218 if rr == '\\' { 219 // Got the beginning, decode it. 220 var tail string 221 r, _, tail, err = strconv.UnquoteChar(lit.Value[i:], '"') 222 if err != nil { 223 // If the conversion fails, it's because of an invalid syntax, therefore is no rune to be found. 224 return 0, MappedRange{}, ErrNoRuneFound 225 } 226 // Only the rune escape sequence part of the string has to be highlighted, recompute the range. 227 runeLen := len(lit.Value) - (int(i) + len(tail)) 228 start = token.Pos(int(lit.Pos()) + int(i)) 229 end = token.Pos(int(start) + runeLen) 230 found = true 231 break 232 } 233 } 234 if !found { 235 // No escape sequence found 236 return 0, MappedRange{}, ErrNoRuneFound 237 } 238 default: 239 return 0, MappedRange{}, ErrNoRuneFound 240 } 241 242 mappedRange, err := posToMappedRange(snapshot, pkg, start, end) 243 if err != nil { 244 return 0, MappedRange{}, err 245 } 246 return r, mappedRange, nil 247 } 248 249 func HoverIdentifier(ctx context.Context, i *IdentifierInfo) (*HoverJSON, error) { 250 ctx, done := event.Start(ctx, "source.Hover") 251 defer done() 252 253 hoverCtx, err := FindHoverContext(ctx, i.Snapshot, i.pkg, i.Declaration.obj, i.Declaration.node, i.Declaration.fullDecl) 254 if err != nil { 255 return nil, err 256 } 257 258 h := &HoverJSON{ 259 FullDocumentation: hoverCtx.Comment.Text(), 260 Synopsis: doc.Synopsis(hoverCtx.Comment.Text()), 261 } 262 263 fset := i.Snapshot.FileSet() 264 // Determine the symbol's signature. 265 switch x := hoverCtx.signatureSource.(type) { 266 case string: 267 h.Signature = x // a pre-computed signature 268 269 case *ast.TypeSpec: 270 x2 := *x 271 // Don't duplicate comments when formatting type specs. 272 x2.Doc = nil 273 x2.Comment = nil 274 var b strings.Builder 275 b.WriteString("type ") 276 if err := format.Node(&b, fset, &x2); err != nil { 277 return nil, err 278 } 279 h.Signature = b.String() 280 281 case ast.Node: 282 var b strings.Builder 283 if err := format.Node(&b, fset, x); err != nil { 284 return nil, err 285 } 286 h.Signature = b.String() 287 288 // Check if the variable is an integer whose value we can present in a more 289 // user-friendly way, i.e. `var hex = 0xe34e` becomes `var hex = 58190` 290 if spec, ok := x.(*ast.ValueSpec); ok && len(spec.Values) > 0 { 291 if lit, ok := spec.Values[0].(*ast.BasicLit); ok && len(spec.Names) > 0 { 292 val := constant.MakeFromLiteral(types.ExprString(lit), lit.Kind, 0) 293 h.Signature = fmt.Sprintf("var %s = %s", spec.Names[0], val) 294 } 295 } 296 297 case types.Object: 298 // If the variable is implicitly declared in a type switch, we need to 299 // manually generate its object string. 300 if typ := i.Declaration.typeSwitchImplicit; typ != nil { 301 if v, ok := x.(*types.Var); ok { 302 h.Signature = fmt.Sprintf("var %s %s", v.Name(), types.TypeString(typ, i.qf)) 303 break 304 } 305 } 306 h.Signature = objectString(x, i.qf, i.Inferred) 307 } 308 if obj := i.Declaration.obj; obj != nil { 309 h.SingleLine = objectString(obj, i.qf, nil) 310 } 311 obj := i.Declaration.obj 312 if obj == nil { 313 return h, nil 314 } 315 316 // Check if the identifier is test-only (and is therefore not part of a 317 // package's API). This is true if the request originated in a test package, 318 // and if the declaration is also found in the same test package. 319 if i.pkg != nil && obj.Pkg() != nil && i.pkg.ForTest() != "" { 320 if _, err := i.pkg.File(i.Declaration.MappedRange[0].URI()); err == nil { 321 return h, nil 322 } 323 } 324 325 h.SymbolName, h.LinkPath, h.LinkAnchor = linkData(obj, i.enclosing) 326 327 // See golang/go#36998: don't link to modules matching GOPRIVATE. 328 // 329 // The path returned by linkData is an import path. 330 if i.Snapshot.View().IsGoPrivatePath(h.LinkPath) { 331 h.LinkPath = "" 332 } else if mod, version, ok := moduleAtVersion(h.LinkPath, i); ok { 333 h.LinkPath = strings.Replace(h.LinkPath, mod, mod+"@"+version, 1) 334 } 335 336 return h, nil 337 } 338 339 // linkData returns the name, import path, and anchor to use in building links 340 // to obj. 341 // 342 // If obj is not visible in documentation, the returned name will be empty. 343 func linkData(obj types.Object, enclosing *types.TypeName) (name, importPath, anchor string) { 344 // Package names simply link to the package. 345 if obj, ok := obj.(*types.PkgName); ok { 346 return obj.Name(), obj.Imported().Path(), "" 347 } 348 349 // Builtins link to the special builtin package. 350 if obj.Parent() == types.Universe { 351 return obj.Name(), "builtin", obj.Name() 352 } 353 354 // In all other cases, the object must be exported. 355 if !obj.Exported() { 356 return "", "", "" 357 } 358 359 var recv types.Object // If non-nil, the field or method receiver base. 360 361 switch obj := obj.(type) { 362 case *types.Var: 363 // If the object is a field, and we have an associated selector 364 // composite literal, or struct, we can determine the link. 365 if obj.IsField() && enclosing != nil { 366 recv = enclosing 367 } 368 case *types.Func: 369 typ, ok := obj.Type().(*types.Signature) 370 if !ok { 371 // Note: this should never happen. go/types guarantees that the type of 372 // *Funcs are Signatures. 373 // 374 // TODO(rfindley): given a 'debug' mode, we should panic here. 375 return "", "", "" 376 } 377 if r := typ.Recv(); r != nil { 378 if rtyp, _ := Deref(r.Type()).(*types.Named); rtyp != nil { 379 // If we have an unexported type, see if the enclosing type is 380 // exported (we may have an interface or struct we can link 381 // to). If not, don't show any link. 382 if !rtyp.Obj().Exported() { 383 if enclosing != nil { 384 recv = enclosing 385 } else { 386 return "", "", "" 387 } 388 } else { 389 recv = rtyp.Obj() 390 } 391 } 392 } 393 } 394 395 if recv != nil && !recv.Exported() { 396 return "", "", "" 397 } 398 399 // Either the object or its receiver must be in the package scope. 400 scopeObj := obj 401 if recv != nil { 402 scopeObj = recv 403 } 404 if scopeObj.Pkg() == nil || scopeObj.Pkg().Scope().Lookup(scopeObj.Name()) != scopeObj { 405 return "", "", "" 406 } 407 408 importPath = obj.Pkg().Path() 409 if recv != nil { 410 anchor = fmt.Sprintf("%s.%s", recv.Name(), obj.Name()) 411 name = fmt.Sprintf("(%s.%s).%s", obj.Pkg().Name(), recv.Name(), obj.Name()) 412 } else { 413 // For most cases, the link is "package/path#symbol". 414 anchor = obj.Name() 415 name = fmt.Sprintf("%s.%s", obj.Pkg().Name(), obj.Name()) 416 } 417 return name, importPath, anchor 418 } 419 420 func moduleAtVersion(path string, i *IdentifierInfo) (string, string, bool) { 421 // TODO(rfindley): moduleAtVersion should not be responsible for deciding 422 // whether or not the link target supports module version links. 423 if strings.ToLower(i.Snapshot.View().Options().LinkTarget) != "pkg.go.dev" { 424 return "", "", false 425 } 426 impPkg, err := i.pkg.GetImport(path) 427 if err != nil { 428 return "", "", false 429 } 430 if impPkg.Version() == nil { 431 return "", "", false 432 } 433 version, modpath := impPkg.Version().Version, impPkg.Version().Path 434 if modpath == "" || version == "" { 435 return "", "", false 436 } 437 return modpath, version, true 438 } 439 440 // objectString is a wrapper around the types.ObjectString function. 441 // It handles adding more information to the object string. 442 func objectString(obj types.Object, qf types.Qualifier, inferred *types.Signature) string { 443 // If the signature type was inferred, prefer the preferred signature with a 444 // comment showing the generic signature. 445 if sig, _ := obj.Type().(*types.Signature); sig != nil && typeparams.ForSignature(sig).Len() > 0 && inferred != nil { 446 obj2 := types.NewFunc(obj.Pos(), obj.Pkg(), obj.Name(), inferred) 447 str := types.ObjectString(obj2, qf) 448 // Try to avoid overly long lines. 449 if len(str) > 60 { 450 str += "\n" 451 } else { 452 str += " " 453 } 454 str += "// " + types.TypeString(sig, qf) 455 return str 456 } 457 str := types.ObjectString(obj, qf) 458 switch obj := obj.(type) { 459 case *types.Const: 460 str = fmt.Sprintf("%s = %s", str, obj.Val()) 461 462 // Try to add a formatted duration as an inline comment 463 typ, ok := obj.Type().(*types.Named) 464 if !ok { 465 break 466 } 467 pkg := typ.Obj().Pkg() 468 if pkg.Path() == "time" && typ.Obj().Name() == "Duration" { 469 if d, ok := constant.Int64Val(obj.Val()); ok { 470 str += " // " + time.Duration(d).String() 471 } 472 } 473 } 474 return str 475 } 476 477 // FindHoverContext returns a HoverContext struct for an AST node and its 478 // declaration object. node should be the actual node used in type checking, 479 // while fullNode could be a separate node with more complete syntactic 480 // information. 481 func FindHoverContext(ctx context.Context, s Snapshot, pkg Package, obj types.Object, pkgNode ast.Node, fullDecl ast.Decl) (*HoverContext, error) { 482 var info *HoverContext 483 484 // Type parameters get their signature from their declaration object. 485 if _, isTypeName := obj.(*types.TypeName); isTypeName { 486 if _, isTypeParam := obj.Type().(*typeparams.TypeParam); isTypeParam { 487 return &HoverContext{signatureSource: obj}, nil 488 } 489 } 490 491 // This is problematic for a number of reasons. We really need to have a more 492 // general mechanism to validate the coherency of AST with type information, 493 // but absent that we must do our best to ensure that we don't use fullNode 494 // when we actually need the node that was type checked. 495 // 496 // pkgNode may be nil, if it was eliminated from the type-checked syntax. In 497 // that case, use fullDecl if available. 498 node := pkgNode 499 if node == nil && fullDecl != nil { 500 node = fullDecl 501 } 502 503 switch node := node.(type) { 504 case *ast.Ident: 505 // The package declaration. 506 for _, f := range pkg.GetSyntax() { 507 if f.Name == pkgNode { 508 info = &HoverContext{Comment: f.Doc} 509 } 510 } 511 case *ast.ImportSpec: 512 // Try to find the package documentation for an imported package. 513 if pkgName, ok := obj.(*types.PkgName); ok { 514 imp, err := pkg.GetImport(pkgName.Imported().Path()) 515 if err != nil { 516 return nil, err 517 } 518 // Assume that only one file will contain package documentation, 519 // so pick the first file that has a doc comment. 520 for _, file := range imp.GetSyntax() { 521 if file.Doc != nil { 522 info = &HoverContext{signatureSource: obj, Comment: file.Doc} 523 break 524 } 525 } 526 } 527 info = &HoverContext{signatureSource: node} 528 case *ast.GenDecl: 529 switch obj := obj.(type) { 530 case *types.TypeName, *types.Var, *types.Const, *types.Func: 531 // Always use the full declaration here if we have it, because the 532 // dependent code doesn't rely on pointer identity. This is fragile. 533 if d, _ := fullDecl.(*ast.GenDecl); d != nil { 534 node = d 535 } 536 // obj may not have been produced by type checking the AST containing 537 // node, so we need to be careful about using token.Pos. 538 tok := s.FileSet().File(obj.Pos()) 539 offset, err := Offset(tok, obj.Pos()) 540 if err != nil { 541 return nil, err 542 } 543 544 // fullTok and fullPos are the *token.File and object position in for the 545 // full AST. 546 fullTok := s.FileSet().File(node.Pos()) 547 fullPos, err := Pos(fullTok, offset) 548 if err != nil { 549 return nil, err 550 } 551 552 var spec ast.Spec 553 for _, s := range node.Specs { 554 // Avoid panics by guarding the calls to token.Offset (golang/go#48249). 555 start, err := Offset(fullTok, s.Pos()) 556 if err != nil { 557 return nil, err 558 } 559 end, err := Offset(fullTok, s.End()) 560 if err != nil { 561 return nil, err 562 } 563 if start <= offset && offset <= end { 564 spec = s 565 break 566 } 567 } 568 569 info, err = hoverGenDecl(node, spec, fullPos, obj) 570 if err != nil { 571 return nil, err 572 } 573 } 574 case *ast.TypeSpec: 575 if obj.Parent() == types.Universe { 576 if genDecl, ok := fullDecl.(*ast.GenDecl); ok { 577 info = hoverTypeSpec(node, genDecl) 578 } 579 } 580 case *ast.FuncDecl: 581 switch obj.(type) { 582 case *types.Func: 583 info = &HoverContext{signatureSource: obj, Comment: node.Doc} 584 case *types.Builtin: 585 info = &HoverContext{Comment: node.Doc} 586 if sig, err := NewBuiltinSignature(ctx, s, obj.Name()); err == nil { 587 info.signatureSource = "func " + sig.name + sig.Format() 588 } else { 589 // Fall back on the object as a signature source. 590 591 // TODO(rfindley): refactor so that we can report bugs from the source 592 // package. 593 594 // debug.Bug(ctx, "invalid builtin hover", "did not find builtin signature: %v", err) 595 info.signatureSource = obj 596 } 597 case *types.Var: 598 // Object is a function param or the field of an anonymous struct 599 // declared with ':='. Skip the first one because only fields 600 // can have docs. 601 if isFunctionParam(obj, node) { 602 break 603 } 604 605 field, err := s.PosToField(ctx, pkg, obj.Pos()) 606 if err != nil { 607 return nil, err 608 } 609 610 if field != nil { 611 comment := field.Doc 612 if comment.Text() == "" { 613 comment = field.Comment 614 } 615 info = &HoverContext{signatureSource: obj, Comment: comment} 616 } 617 } 618 } 619 620 if info == nil { 621 info = &HoverContext{signatureSource: obj} 622 } 623 624 return info, nil 625 } 626 627 // isFunctionParam returns true if the passed object is either an incoming 628 // or an outgoing function param 629 func isFunctionParam(obj types.Object, node *ast.FuncDecl) bool { 630 for _, f := range node.Type.Params.List { 631 if f.Pos() == obj.Pos() { 632 return true 633 } 634 } 635 if node.Type.Results != nil { 636 for _, f := range node.Type.Results.List { 637 if f.Pos() == obj.Pos() { 638 return true 639 } 640 } 641 } 642 return false 643 } 644 645 // hoverGenDecl returns hover information an object declared via spec inside 646 // of the GenDecl node. obj is the type-checked object corresponding to the 647 // declaration, but may have been type-checked using a different AST than the 648 // given nodes; fullPos is the position of obj in node's AST. 649 func hoverGenDecl(node *ast.GenDecl, spec ast.Spec, fullPos token.Pos, obj types.Object) (*HoverContext, error) { 650 if spec == nil { 651 return nil, errors.Errorf("no spec for node %v at position %v", node, fullPos) 652 } 653 654 // If we have a field or method. 655 switch obj.(type) { 656 case *types.Var, *types.Const, *types.Func: 657 return hoverVar(spec, fullPos, obj, node), nil 658 } 659 // Handle types. 660 switch spec := spec.(type) { 661 case *ast.TypeSpec: 662 return hoverTypeSpec(spec, node), nil 663 case *ast.ValueSpec: 664 return &HoverContext{signatureSource: spec, Comment: spec.Doc}, nil 665 case *ast.ImportSpec: 666 return &HoverContext{signatureSource: spec, Comment: spec.Doc}, nil 667 } 668 return nil, errors.Errorf("unable to format spec %v (%T)", spec, spec) 669 } 670 671 // TODO(rfindley): rename this function. 672 func hoverTypeSpec(spec *ast.TypeSpec, decl *ast.GenDecl) *HoverContext { 673 comment := spec.Doc 674 if comment == nil && decl != nil { 675 comment = decl.Doc 676 } 677 if comment == nil { 678 comment = spec.Comment 679 } 680 return &HoverContext{ 681 signatureSource: spec, 682 Comment: comment, 683 } 684 } 685 686 func hoverVar(node ast.Spec, fullPos token.Pos, obj types.Object, decl *ast.GenDecl) *HoverContext { 687 var fieldList *ast.FieldList 688 switch spec := node.(type) { 689 case *ast.TypeSpec: 690 switch t := spec.Type.(type) { 691 case *ast.StructType: 692 fieldList = t.Fields 693 case *ast.InterfaceType: 694 fieldList = t.Methods 695 } 696 case *ast.ValueSpec: 697 // Try to extract the field list of an anonymous struct 698 if fieldList = extractFieldList(spec.Type); fieldList != nil { 699 break 700 } 701 702 comment := spec.Doc 703 if comment == nil { 704 comment = decl.Doc 705 } 706 if comment == nil { 707 comment = spec.Comment 708 } 709 710 // We need the AST nodes for variable declarations of basic literals with 711 // associated values so that we can augment their hover with more information. 712 if _, ok := obj.(*types.Var); ok && spec.Type == nil && len(spec.Values) > 0 { 713 if _, ok := spec.Values[0].(*ast.BasicLit); ok { 714 return &HoverContext{signatureSource: spec, Comment: comment} 715 } 716 } 717 718 return &HoverContext{signatureSource: obj, Comment: comment} 719 } 720 721 if fieldList != nil { 722 comment := findFieldComment(fullPos, fieldList) 723 return &HoverContext{signatureSource: obj, Comment: comment} 724 } 725 return &HoverContext{signatureSource: obj, Comment: decl.Doc} 726 } 727 728 // extractFieldList recursively tries to extract a field list. 729 // If it is not found, nil is returned. 730 func extractFieldList(specType ast.Expr) *ast.FieldList { 731 switch t := specType.(type) { 732 case *ast.StructType: 733 return t.Fields 734 case *ast.InterfaceType: 735 return t.Methods 736 case *ast.ArrayType: 737 return extractFieldList(t.Elt) 738 case *ast.MapType: 739 // Map value has a greater chance to be a struct 740 if fields := extractFieldList(t.Value); fields != nil { 741 return fields 742 } 743 return extractFieldList(t.Key) 744 case *ast.ChanType: 745 return extractFieldList(t.Value) 746 } 747 return nil 748 } 749 750 // findFieldComment visits all fields in depth-first order and returns 751 // the comment of a field with passed position. If no comment is found, 752 // nil is returned. 753 func findFieldComment(pos token.Pos, fieldList *ast.FieldList) *ast.CommentGroup { 754 for _, field := range fieldList.List { 755 if field.Pos() == pos { 756 if field.Doc.Text() != "" { 757 return field.Doc 758 } 759 return field.Comment 760 } 761 762 if nestedFieldList := extractFieldList(field.Type); nestedFieldList != nil { 763 if c := findFieldComment(pos, nestedFieldList); c != nil { 764 return c 765 } 766 } 767 } 768 return nil 769 } 770 771 func FormatHover(h *HoverJSON, options *Options) (string, error) { 772 signature := formatSignature(h, options) 773 774 switch options.HoverKind { 775 case SingleLine: 776 return h.SingleLine, nil 777 case NoDocumentation: 778 return signature, nil 779 case Structured: 780 b, err := json.Marshal(h) 781 if err != nil { 782 return "", err 783 } 784 return string(b), nil 785 } 786 787 link := formatLink(h, options) 788 doc := formatDoc(h, options) 789 790 var b strings.Builder 791 parts := []string{signature, doc, link} 792 for i, el := range parts { 793 if el != "" { 794 b.WriteString(el) 795 796 // Don't write out final newline. 797 if i == len(parts) { 798 continue 799 } 800 // If any elements of the remainder of the list are non-empty, 801 // write a newline. 802 if anyNonEmpty(parts[i+1:]) { 803 if options.PreferredContentFormat == protocol.Markdown { 804 b.WriteString("\n\n") 805 } else { 806 b.WriteRune('\n') 807 } 808 } 809 } 810 } 811 return b.String(), nil 812 } 813 814 func formatSignature(h *HoverJSON, options *Options) string { 815 signature := h.Signature 816 if signature != "" && options.PreferredContentFormat == protocol.Markdown { 817 signature = fmt.Sprintf("```go\n%s\n```", signature) 818 } 819 return signature 820 } 821 822 func formatLink(h *HoverJSON, options *Options) string { 823 if !options.LinksInHover || options.LinkTarget == "" || h.LinkPath == "" { 824 return "" 825 } 826 plainLink := BuildLink(options.LinkTarget, h.LinkPath, h.LinkAnchor) 827 switch options.PreferredContentFormat { 828 case protocol.Markdown: 829 return fmt.Sprintf("[`%s` on %s](%s)", h.SymbolName, options.LinkTarget, plainLink) 830 case protocol.PlainText: 831 return "" 832 default: 833 return plainLink 834 } 835 } 836 837 // BuildLink constructs a link with the given target, path, and anchor. 838 func BuildLink(target, path, anchor string) string { 839 link := fmt.Sprintf("https://%s/%s", target, path) 840 if target == "pkg.go.dev" { 841 link += "?utm_source=gopls" 842 } 843 if anchor == "" { 844 return link 845 } 846 return link + "#" + anchor 847 } 848 849 func formatDoc(h *HoverJSON, options *Options) string { 850 var doc string 851 switch options.HoverKind { 852 case SynopsisDocumentation: 853 doc = h.Synopsis 854 case FullDocumentation: 855 doc = h.FullDocumentation 856 } 857 if options.PreferredContentFormat == protocol.Markdown { 858 return CommentToMarkdown(doc) 859 } 860 return doc 861 } 862 863 func anyNonEmpty(x []string) bool { 864 for _, el := range x { 865 if el != "" { 866 return true 867 } 868 } 869 return false 870 }