internal/ls/hover.go (567 lines of code) (raw):

package ls import ( "context" "fmt" "slices" "strings" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/astnav" "github.com/microsoft/typescript-go/internal/checker" "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/lsp/lsproto" ) const ( symbolFormatFlags = checker.SymbolFormatFlagsWriteTypeParametersOrArguments | checker.SymbolFormatFlagsUseOnlyExternalAliasing | checker.SymbolFormatFlagsAllowAnyNodeKind | checker.SymbolFormatFlagsUseAliasDefinedOutsideCurrentScope typeFormatFlags = checker.TypeFormatFlagsUseAliasDefinedOutsideCurrentScope ) func (l *LanguageService) ProvideHover(ctx context.Context, documentURI lsproto.DocumentUri, position lsproto.Position) (lsproto.HoverResponse, error) { caps := lsproto.GetClientCapabilities(ctx) contentFormat := lsproto.PreferredMarkupKind(caps.TextDocument.Hover.ContentFormat) program, file := l.getProgramAndFile(documentURI) node := astnav.GetTouchingPropertyName(file, int(l.converters.LineAndCharacterToPosition(file, position))) if node.Kind == ast.KindSourceFile { // Avoid giving quickInfo for the sourceFile as a whole. return lsproto.HoverOrNull{}, nil } c, done := program.GetTypeCheckerForFile(ctx, file) defer done() rangeNode := getNodeForQuickInfo(node) symbol := getSymbolAtLocationForQuickInfo(c, node) quickInfo, documentation := l.getQuickInfoAndDocumentationForSymbol(c, symbol, rangeNode, contentFormat) if quickInfo == "" { return lsproto.HoverOrNull{}, nil } hoverRange := l.getLspRangeOfNode(rangeNode, nil, nil) var content string if contentFormat == lsproto.MarkupKindMarkdown { content = formatQuickInfo(quickInfo) + documentation } else { content = quickInfo + documentation } return lsproto.HoverOrNull{ Hover: &lsproto.Hover{ Contents: lsproto.MarkupContentOrStringOrMarkedStringWithLanguageOrMarkedStrings{ MarkupContent: &lsproto.MarkupContent{ Kind: contentFormat, Value: content, }, }, Range: hoverRange, }, }, nil } func (l *LanguageService) getQuickInfoAndDocumentationForSymbol(c *checker.Checker, symbol *ast.Symbol, node *ast.Node, contentFormat lsproto.MarkupKind) (string, string) { if symbol == nil { return "", "" } quickInfo, declaration := getQuickInfoAndDeclarationAtLocation(c, symbol, node) if quickInfo == "" { return "", "" } return quickInfo, l.getDocumentationFromDeclaration(c, declaration, contentFormat) } func (l *LanguageService) getDocumentationFromDeclaration(c *checker.Checker, declaration *ast.Node, contentFormat lsproto.MarkupKind) string { if declaration == nil { return "" } isMarkdown := contentFormat == lsproto.MarkupKindMarkdown var b strings.Builder if jsdoc := getJSDocOrTag(c, declaration); jsdoc != nil && !containsTypedefTag(jsdoc) { l.writeComments(&b, c, jsdoc.Comments(), isMarkdown) if jsdoc.Kind == ast.KindJSDoc { if tags := jsdoc.AsJSDoc().Tags; tags != nil { for _, tag := range tags.Nodes { if tag.Kind == ast.KindJSDocTypeTag { continue } b.WriteString("\n\n") if isMarkdown { b.WriteString("*@") b.WriteString(tag.TagName().Text()) b.WriteString("*") } else { b.WriteString("@") b.WriteString(tag.TagName().Text()) } switch tag.Kind { case ast.KindJSDocParameterTag, ast.KindJSDocPropertyTag: writeOptionalEntityName(&b, tag.Name()) case ast.KindJSDocAugmentsTag: writeOptionalEntityName(&b, tag.ClassName()) case ast.KindJSDocSeeTag: writeOptionalEntityName(&b, tag.AsJSDocSeeTag().NameExpression) case ast.KindJSDocTemplateTag: for i, tp := range tag.TypeParameters() { if i != 0 { b.WriteString(",") } writeOptionalEntityName(&b, tp.Name()) } } comments := tag.Comments() if len(comments) != 0 { if commentHasPrefix(comments, "```") { b.WriteString("\n") } else { b.WriteString(" ") if !commentHasPrefix(comments, "-") { b.WriteString("— ") } } l.writeComments(&b, c, comments, isMarkdown) } } } } } return b.String() } func formatQuickInfo(quickInfo string) string { var b strings.Builder b.Grow(32) writeCode(&b, "tsx", quickInfo) return b.String() } func getQuickInfoAndDeclarationAtLocation(c *checker.Checker, symbol *ast.Symbol, node *ast.Node) (string, *ast.Node) { var b strings.Builder var visitedAliases collections.Set[*ast.Symbol] container := getContainerNode(node) if node.Kind == ast.KindThisKeyword && ast.IsInExpressionContext(node) { return c.TypeToStringEx(c.GetTypeAtLocation(node), container, typeFormatFlags), nil } writeSymbolMeaning := func(symbol *ast.Symbol, meaning ast.SymbolFlags, isAlias bool) *ast.Node { flags := symbol.Flags & meaning if flags == 0 { return nil } declaration := symbol.ValueDeclaration if flags&ast.SymbolFlagsProperty != 0 && declaration != nil && ast.IsMethodDeclaration(declaration) { flags = ast.SymbolFlagsMethod } if b.Len() != 0 { b.WriteString("\n") } if isAlias { b.WriteString("(alias) ") } switch { case flags&(ast.SymbolFlagsVariable|ast.SymbolFlagsProperty|ast.SymbolFlagsAccessor) != 0: switch { case flags&ast.SymbolFlagsProperty != 0: b.WriteString("(property) ") case flags&ast.SymbolFlagsAccessor != 0: b.WriteString("(accessor) ") default: decl := symbol.ValueDeclaration if decl != nil { switch { case ast.IsParameter(decl): b.WriteString("(parameter) ") case ast.IsVarLet(decl): b.WriteString("let ") case ast.IsVarConst(decl): b.WriteString("const ") case ast.IsVarUsing(decl): b.WriteString("using ") case ast.IsVarAwaitUsing(decl): b.WriteString("await using ") default: b.WriteString("var ") } } } if symbol.Name == ast.InternalSymbolNameExportEquals && symbol.Parent != nil && symbol.Parent.Flags&ast.SymbolFlagsModule != 0 { b.WriteString("exports") } else { b.WriteString(c.SymbolToStringEx(symbol, container, ast.SymbolFlagsNone, symbolFormatFlags)) } b.WriteString(": ") if callNode := getCallOrNewExpression(node); callNode != nil { b.WriteString(c.SignatureToStringEx(c.GetResolvedSignature(callNode), container, typeFormatFlags|checker.TypeFormatFlagsWriteCallStyleSignature|checker.TypeFormatFlagsWriteTypeArgumentsOfSignature|checker.TypeFormatFlagsWriteArrowStyleSignature)) } else { b.WriteString(c.TypeToStringEx(c.GetTypeOfSymbolAtLocation(symbol, node), container, typeFormatFlags)) } case flags&ast.SymbolFlagsEnumMember != 0: b.WriteString("(enum member) ") t := c.GetTypeOfSymbol(symbol) b.WriteString(c.TypeToStringEx(t, container, typeFormatFlags)) if t.Flags()&checker.TypeFlagsLiteral != 0 { b.WriteString(" = ") b.WriteString(t.AsLiteralType().String()) } case flags&(ast.SymbolFlagsFunction|ast.SymbolFlagsMethod) != 0: prefix := core.IfElse(flags&ast.SymbolFlagsMethod != 0, "(method) ", "function ") if ast.IsIdentifier(node) && ast.IsFunctionLikeDeclaration(node.Parent) && node.Parent.Name() == node { declaration = node.Parent signatures := []*checker.Signature{c.GetSignatureFromDeclaration(declaration)} writeSignatures(&b, c, signatures, container, isAlias, prefix, symbol) } else { signatures := getSignaturesAtLocation(c, symbol, checker.SignatureKindCall, node) if len(signatures) == 1 { if d := signatures[0].Declaration(); d != nil && d.Flags&ast.NodeFlagsJSDoc == 0 { declaration = d } } writeSignatures(&b, c, signatures, container, isAlias, prefix, symbol) } case flags&(ast.SymbolFlagsClass|ast.SymbolFlagsInterface) != 0: if node.Kind == ast.KindThisKeyword || ast.IsThisInTypeQuery(node) { b.WriteString("this") } else if node.Kind == ast.KindConstructorKeyword && (ast.IsConstructorDeclaration(node.Parent) || ast.IsConstructSignatureDeclaration(node.Parent)) { declaration = node.Parent signatures := []*checker.Signature{c.GetSignatureFromDeclaration(declaration)} writeSignatures(&b, c, signatures, container, isAlias, "constructor ", symbol) } else { var signatures []*checker.Signature if flags&ast.SymbolFlagsClass != 0 && getCallOrNewExpression(node) != nil { signatures = getSignaturesAtLocation(c, symbol, checker.SignatureKindConstruct, node) } if len(signatures) == 1 { if d := signatures[0].Declaration(); d != nil && d.Flags&ast.NodeFlagsJSDoc == 0 { declaration = d } writeSignatures(&b, c, signatures, container, isAlias, "constructor ", symbol) } else { b.WriteString(core.IfElse(flags&ast.SymbolFlagsClass != 0, "class ", "interface ")) b.WriteString(c.SymbolToStringEx(symbol, container, ast.SymbolFlagsNone, symbolFormatFlags)) params := c.GetDeclaredTypeOfSymbol(symbol).AsInterfaceType().LocalTypeParameters() writeTypeParams(&b, c, params) } } if flags&ast.SymbolFlagsInterface != 0 { declaration = core.Find(symbol.Declarations, ast.IsInterfaceDeclaration) } case flags&ast.SymbolFlagsEnum != 0: b.WriteString("enum ") b.WriteString(c.SymbolToStringEx(symbol, container, ast.SymbolFlagsNone, symbolFormatFlags)) case flags&ast.SymbolFlagsModule != 0: b.WriteString(core.IfElse(symbol.ValueDeclaration != nil && ast.IsSourceFile(symbol.ValueDeclaration), "module ", "namespace ")) b.WriteString(c.SymbolToStringEx(symbol, container, ast.SymbolFlagsNone, symbolFormatFlags)) case flags&ast.SymbolFlagsTypeParameter != 0: b.WriteString("(type parameter) ") tp := c.GetDeclaredTypeOfSymbol(symbol) b.WriteString(c.SymbolToStringEx(symbol, container, ast.SymbolFlagsNone, symbolFormatFlags)) cons := c.GetConstraintOfTypeParameter(tp) if cons != nil { b.WriteString(" extends ") b.WriteString(c.TypeToStringEx(cons, container, typeFormatFlags)) } declaration = core.Find(symbol.Declarations, ast.IsTypeParameterDeclaration) case flags&ast.SymbolFlagsTypeAlias != 0: b.WriteString("type ") b.WriteString(c.SymbolToStringEx(symbol, container, ast.SymbolFlagsNone, symbolFormatFlags)) writeTypeParams(&b, c, c.GetTypeAliasTypeParameters(symbol)) if len(symbol.Declarations) != 0 { b.WriteString(" = ") b.WriteString(c.TypeToStringEx(c.GetDeclaredTypeOfSymbol(symbol), container, typeFormatFlags|checker.TypeFormatFlagsInTypeAlias)) } declaration = core.Find(symbol.Declarations, ast.IsTypeAliasDeclaration) default: b.WriteString(c.TypeToStringEx(c.GetTypeOfSymbol(symbol), container, typeFormatFlags)) } return declaration } var writeSymbol func(*ast.Symbol, bool) *ast.Node writeSymbol = func(symbol *ast.Symbol, isAlias bool) *ast.Node { var declaration *ast.Node // Recursively write all meanings of alias if symbol.Flags&ast.SymbolFlagsAlias != 0 && visitedAliases.AddIfAbsent(symbol) { if aliasedSymbol := c.GetAliasedSymbol(symbol); aliasedSymbol != c.GetUnknownSymbol() { declaration = writeSymbol(aliasedSymbol, true /*isAlias*/) } } // Write the value meaning, if any declaration = core.OrElse(declaration, writeSymbolMeaning(symbol, ast.SymbolFlagsValue|ast.SymbolFlagsSignature, isAlias)) // Write the type meaning, if any declaration = core.OrElse(declaration, writeSymbolMeaning(symbol, ast.SymbolFlagsType&^ast.SymbolFlagsValue, isAlias)) // Write the namespace meaning, if any declaration = core.OrElse(declaration, writeSymbolMeaning(symbol, ast.SymbolFlagsNamespace&^ast.SymbolFlagsValue, isAlias)) // Return the first declaration return declaration } firstDeclaration := writeSymbol(symbol, false /*isAlias*/) return b.String(), firstDeclaration } func getNodeForQuickInfo(node *ast.Node) *ast.Node { if node.Parent == nil { return node } if ast.IsNewExpression(node.Parent) && node.Pos() == node.Parent.Pos() { return node.Parent.Expression() } if ast.IsNamedTupleMember(node.Parent) && node.Pos() == node.Parent.Pos() { return node.Parent } if ast.IsImportMeta(node.Parent) && node.Parent.Name() == node { return node.Parent } if ast.IsJsxNamespacedName(node.Parent) { return node.Parent } return node } func getSymbolAtLocationForQuickInfo(c *checker.Checker, node *ast.Node) *ast.Symbol { if objectElement := getContainingObjectLiteralElement(node); objectElement != nil { if contextualType := c.GetContextualType(objectElement.Parent, checker.ContextFlagsNone); contextualType != nil { if properties := c.GetPropertySymbolsFromContextualType(objectElement, contextualType, false /*unionSymbolOk*/); len(properties) == 1 { return properties[0] } } } return c.GetSymbolAtLocation(node) } func getSignaturesAtLocation(c *checker.Checker, symbol *ast.Symbol, kind checker.SignatureKind, node *ast.Node) []*checker.Signature { signatures := c.GetSignaturesOfType(c.GetTypeOfSymbol(symbol), kind) if len(signatures) > 1 || len(signatures) == 1 && len(signatures[0].TypeParameters()) != 0 { if callNode := getCallOrNewExpression(node); callNode != nil { signature := c.GetResolvedSignature(callNode) // If we have a resolved signature, make sure it isn't a synthetic signature if signature != nil && (slices.Contains(signatures, signature) || signature.Target() != nil && slices.Contains(signatures, signature.Target())) { return []*checker.Signature{signature} } } } return signatures } func getCallOrNewExpression(node *ast.Node) *ast.Node { if ast.IsSourceFile(node) { return nil } if ast.IsPropertyAccessExpression(node.Parent) && node.Parent.Name() == node { node = node.Parent } if (ast.IsCallExpression(node.Parent) || ast.IsNewExpression(node.Parent)) && node.Parent.Expression() == node { return node.Parent } return nil } func writeTypeParams(b *strings.Builder, c *checker.Checker, params []*checker.Type) { if len(params) > 0 { b.WriteString("<") for i, tp := range params { if i != 0 { b.WriteString(", ") } symbol := tp.Symbol() b.WriteString(c.SymbolToStringEx(symbol, nil, ast.SymbolFlagsNone, symbolFormatFlags)) cons := c.GetConstraintOfTypeParameter(tp) if cons != nil { b.WriteString(" extends ") b.WriteString(c.TypeToStringEx(cons, nil, typeFormatFlags)) } } b.WriteString(">") } } func writeSignatures(b *strings.Builder, c *checker.Checker, signatures []*checker.Signature, container *ast.Node, isAlias bool, prefix string, symbol *ast.Symbol) { for i, sig := range signatures { if i != 0 { b.WriteString("\n") if isAlias { b.WriteString("(alias) ") } } if i == 3 && len(signatures) >= 5 { b.WriteString(fmt.Sprintf("// +%v more overloads", len(signatures)-3)) break } b.WriteString(prefix) b.WriteString(c.SymbolToStringEx(symbol, container, ast.SymbolFlagsNone, symbolFormatFlags)) b.WriteString(c.SignatureToStringEx(sig, container, typeFormatFlags|checker.TypeFormatFlagsWriteCallStyleSignature|checker.TypeFormatFlagsWriteTypeArgumentsOfSignature)) } } func containsTypedefTag(jsdoc *ast.Node) bool { if jsdoc.Kind == ast.KindJSDoc { if tags := jsdoc.AsJSDoc().Tags; tags != nil { for _, tag := range tags.Nodes { if tag.Kind == ast.KindJSDocTypedefTag || tag.Kind == ast.KindJSDocCallbackTag { return true } } } } return false } func commentHasPrefix(comments []*ast.Node, prefix string) bool { return comments[0].Kind == ast.KindJSDocText && strings.HasPrefix(comments[0].Text(), prefix) } func getJSDoc(node *ast.Node) *ast.Node { return core.LastOrNil(node.JSDoc(nil)) } func getJSDocOrTag(c *checker.Checker, node *ast.Node) *ast.Node { if jsdoc := getJSDoc(node); jsdoc != nil { return jsdoc } switch { case ast.IsParameter(node): return getMatchingJSDocTag(c, node.Parent, node.Name().Text(), isMatchingParameterTag) case ast.IsTypeParameterDeclaration(node): return getMatchingJSDocTag(c, node.Parent, node.Name().Text(), isMatchingTemplateTag) case ast.IsVariableDeclaration(node) && ast.IsVariableDeclarationList(node.Parent) && core.FirstOrNil(node.Parent.AsVariableDeclarationList().Declarations.Nodes) == node: return getJSDocOrTag(c, node.Parent.Parent) case (ast.IsFunctionExpressionOrArrowFunction(node) || ast.IsClassExpression(node)) && (ast.IsVariableDeclaration(node.Parent) || ast.IsPropertyDeclaration(node.Parent) || ast.IsPropertyAssignment(node.Parent)) && node.Parent.Initializer() == node: return getJSDocOrTag(c, node.Parent) } if symbol := node.Symbol(); symbol != nil && node.Parent != nil && ast.IsClassOrInterfaceLike(node.Parent) { isStatic := ast.HasStaticModifier(node) for _, baseType := range c.GetBaseTypes(c.GetDeclaredTypeOfSymbol(node.Parent.Symbol())) { t := baseType if isStatic { t = c.GetTypeOfSymbol(baseType.Symbol()) } if prop := c.GetPropertyOfType(t, symbol.Name); prop != nil && prop.ValueDeclaration != nil { if jsDoc := getJSDocOrTag(c, prop.ValueDeclaration); jsDoc != nil { return jsDoc } } } } return nil } func getMatchingJSDocTag(c *checker.Checker, node *ast.Node, name string, match func(*ast.Node, string) bool) *ast.Node { if jsdoc := getJSDocOrTag(c, node); jsdoc != nil && jsdoc.Kind == ast.KindJSDoc { if tags := jsdoc.AsJSDoc().Tags; tags != nil { for _, tag := range tags.Nodes { if match(tag, name) { return tag } } } } return nil } func isMatchingParameterTag(tag *ast.Node, name string) bool { return tag.Kind == ast.KindJSDocParameterTag && isNodeWithName(tag, name) } func isMatchingTemplateTag(tag *ast.Node, name string) bool { return tag.Kind == ast.KindJSDocTemplateTag && core.Some(tag.TypeParameters(), func(tp *ast.Node) bool { return isNodeWithName(tp, name) }) } func isNodeWithName(node *ast.Node, name string) bool { nodeName := node.Name() return ast.IsIdentifier(nodeName) && nodeName.Text() == name } func writeCode(b *strings.Builder, lang string, code string) { if code == "" { return } ticks := 3 for strings.Contains(code, strings.Repeat("`", ticks)) { ticks++ } for range ticks { b.WriteByte('`') } b.WriteString(lang) b.WriteByte('\n') b.WriteString(code) b.WriteByte('\n') for range ticks { b.WriteByte('`') } b.WriteByte('\n') } func (l *LanguageService) writeComments(b *strings.Builder, c *checker.Checker, comments []*ast.Node, isMarkdown bool) { for _, comment := range comments { switch comment.Kind { case ast.KindJSDocText: b.WriteString(comment.Text()) case ast.KindJSDocLink, ast.KindJSDocLinkPlain: l.writeJSDocLink(b, c, comment, false /*quote*/, isMarkdown) case ast.KindJSDocLinkCode: l.writeJSDocLink(b, c, comment, true /*quote*/, isMarkdown) } } } func (l *LanguageService) writeJSDocLink(b *strings.Builder, c *checker.Checker, link *ast.Node, quote bool, isMarkdown bool) { name := link.Name() text := strings.Trim(link.Text(), " ") if name == nil { writeQuotedString(b, text, quote && isMarkdown) return } if ast.IsIdentifier(name) && (name.Text() == "http" || name.Text() == "https") && strings.HasPrefix(text, "://") { linkText := name.Text() + text linkUri := linkText if commentPos := strings.IndexFunc(linkText, func(ch rune) bool { return ch == ' ' || ch == '|' }); commentPos >= 0 { linkUri = linkText[:commentPos] linkText = trimCommentPrefix(linkText[commentPos:]) if linkText == "" { linkText = linkUri } } if isMarkdown { writeMarkdownLink(b, linkText, linkUri, quote) } else { writeQuotedString(b, linkText, false) if linkText != linkUri { b.WriteString(" (") b.WriteString(linkUri) b.WriteString(")") } } return } declarations := getDeclarationsFromLocation(c, name) if len(declarations) != 0 { declaration := declarations[0] file := ast.GetSourceFileOfNode(declaration) node := core.OrElse(ast.GetNameOfDeclaration(declaration), declaration) loc := l.getMappedLocation(file.FileName(), createRangeFromNode(node, file)) prefixLen := core.IfElse(strings.HasPrefix(text, "()"), 2, 0) linkText := trimCommentPrefix(text[prefixLen:]) if linkText == "" { linkText = getEntityNameString(name) + text[:prefixLen] } if isMarkdown { linkUri := fmt.Sprintf("%s#%d,%d-%d,%d", loc.Uri, loc.Range.Start.Line+1, loc.Range.Start.Character+1, loc.Range.End.Line+1, loc.Range.End.Character+1) writeMarkdownLink(b, linkText, linkUri, quote) } else { writeQuotedString(b, linkText, false) } return } writeQuotedString(b, getEntityNameString(name)+" "+text, quote && isMarkdown) } func trimCommentPrefix(text string) string { return strings.TrimLeft(strings.TrimPrefix(strings.TrimLeft(text, " "), "|"), " ") } func writeMarkdownLink(b *strings.Builder, text string, uri string, quote bool) { b.WriteString("[") writeQuotedString(b, text, quote) b.WriteString("](") b.WriteString(uri) b.WriteString(")") } func writeOptionalEntityName(b *strings.Builder, name *ast.Node) { if name != nil { b.WriteString(" ") writeQuotedString(b, getEntityNameString(name), true /*quote*/) } } func writeQuotedString(b *strings.Builder, str string, quote bool) { if quote && !strings.Contains(str, "`") { b.WriteString("`") b.WriteString(str) b.WriteString("`") } else { b.WriteString(str) } } func getEntityNameString(name *ast.Node) string { var b strings.Builder writeEntityNameParts(&b, name) return b.String() } func writeEntityNameParts(b *strings.Builder, node *ast.Node) { switch node.Kind { case ast.KindIdentifier: b.WriteString(node.Text()) case ast.KindQualifiedName: writeEntityNameParts(b, node.AsQualifiedName().Left) b.WriteByte('.') writeEntityNameParts(b, node.AsQualifiedName().Right) case ast.KindPropertyAccessExpression: writeEntityNameParts(b, node.Expression()) b.WriteByte('.') writeEntityNameParts(b, node.Name()) case ast.KindParenthesizedExpression, ast.KindExpressionWithTypeArguments: writeEntityNameParts(b, node.Expression()) case ast.KindJSDocNameReference: writeEntityNameParts(b, node.Name()) } }