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