package ls import ( "context" "fmt" "slices" "strings" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/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) { 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() quickInfo, documentation := getQuickInfoAndDocumentation(c, node) if quickInfo == "" { return lsproto.HoverOrNull{}, nil } return lsproto.HoverOrNull{ Hover: &lsproto.Hover{ Contents: lsproto.MarkupContentOrStringOrMarkedStringWithLanguageOrMarkedStrings{ MarkupContent: &lsproto.MarkupContent{ Kind: lsproto.MarkupKindMarkdown, Value: formatQuickInfo(quickInfo) + documentation, }, }, }, }, nil } func getQuickInfoAndDocumentation(c *checker.Checker, node *ast.Node) (string, string) { return getQuickInfoAndDocumentationForSymbol(c, c.GetSymbolAtLocation(node), getNodeForQuickInfo(node)) } func getQuickInfoAndDocumentationForSymbol(c *checker.Checker, symbol *ast.Symbol, node *ast.Node) (string, string) { quickInfo, declaration := getQuickInfoAndDeclarationAtLocation(c, symbol, node) if quickInfo == "" { return "", "" } var b strings.Builder if declaration != nil { if jsdoc := getJSDocOrTag(declaration); jsdoc != nil && !containsTypedefTag(jsdoc) { writeComments(&b, jsdoc.Comments()) 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*@") b.WriteString(tag.TagName().Text()) b.WriteString("*") switch tag.Kind { case ast.KindJSDocParameterTag, ast.KindJSDocPropertyTag: writeOptionalEntityName(&b, tag.Name()) case ast.KindJSDocAugmentsTag: writeOptionalEntityName(&b, tag.AsJSDocAugmentsTag().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("— ") } } writeComments(&b, comments) } } } } } } return quickInfo, 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) { isAlias := symbol != nil && symbol.Flags&ast.SymbolFlagsAlias != 0 if isAlias { symbol = c.GetAliasedSymbol(symbol) } if symbol == nil || symbol == c.GetUnknownSymbol() { return "", nil } declaration := symbol.ValueDeclaration if symbol.Flags&ast.SymbolFlagsClass != 0 && inConstructorContext(node) { if s := symbol.Members[ast.InternalSymbolNameConstructor]; s != nil { symbol = s declaration = core.Find(symbol.Declarations, func(d *ast.Node) bool { return ast.IsConstructorDeclaration(d) || ast.IsConstructSignatureDeclaration(d) }) } } flags := symbol.Flags if flags&ast.SymbolFlagsType != 0 && (ast.IsPartOfTypeNode(node) || ast.IsTypeDeclarationName(node)) { // If the symbol has a type meaning and we're in a type context, remove value-only meanings flags &^= ast.SymbolFlagsVariable | ast.SymbolFlagsFunction } container := getContainerNode(node) var b strings.Builder 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 ") } } } b.WriteString(c.SymbolToStringEx(symbol, container, ast.SymbolFlagsNone, symbolFormatFlags)) b.WriteString(": ") 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: signatures := getSignaturesAtLocation(c, symbol, checker.SignatureKindCall, node) if len(signatures) == 1 && signatures[0].Declaration() != nil { declaration = signatures[0].Declaration() } prefix := core.IfElse(symbol.Flags&ast.SymbolFlagsMethod != 0, "(method) ", "function ") writeSignatures(&b, c, signatures, container, prefix, symbol) case flags&ast.SymbolFlagsConstructor != 0: signatures := getSignaturesAtLocation(c, symbol.Parent, checker.SignatureKindConstruct, node) if len(signatures) == 1 && signatures[0].Declaration() != nil { declaration = signatures[0].Declaration() } writeSignatures(&b, c, signatures, container, "constructor ", symbol.Parent) case flags&(ast.SymbolFlagsClass|ast.SymbolFlagsInterface) != 0: if node.Kind == ast.KindThisKeyword || ast.IsThisInTypeQuery(node) { b.WriteString("this") } else { b.WriteString(core.IfElse(symbol.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) case flags&ast.SymbolFlagsAlias != 0: b.WriteString("import ") b.WriteString(c.SymbolToStringEx(symbol, container, ast.SymbolFlagsNone, symbolFormatFlags)) default: b.WriteString(c.TypeToStringEx(c.GetTypeOfSymbol(symbol), container, typeFormatFlags)) } return b.String(), declaration } 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 inConstructorContext(node *ast.Node) bool { if node.Kind == ast.KindConstructorKeyword { return true } if ast.IsIdentifier(node) { for ast.IsRightSideOfQualifiedNameOrPropertyAccess(node) { node = node.Parent } if ast.IsNewExpression(node.Parent) { return true } } return false } 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) { 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, prefix string, symbol *ast.Symbol) { for i, sig := range signatures { if i != 0 { b.WriteString("\n") } 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 { 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(node *ast.Node) *ast.Node { if jsdoc := getJSDoc(node); jsdoc != nil { return jsdoc } switch { case ast.IsParameter(node): return getMatchingJSDocTag(node.Parent, node.Name().Text(), isMatchingParameterTag) case ast.IsTypeParameterDeclaration(node): return getMatchingJSDocTag(node.Parent, node.Name().Text(), isMatchingTemplateTag) case ast.IsVariableDeclaration(node) && ast.IsVariableDeclarationList(node.Parent) && core.FirstOrNil(node.Parent.AsVariableDeclarationList().Declarations.Nodes) == node: return getJSDocOrTag(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(node.Parent) } return nil } func getMatchingJSDocTag(node *ast.Node, name string, match func(*ast.Node, string) bool) *ast.Node { if jsdoc := getJSDocOrTag(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 writeComments(b *strings.Builder, comments []*ast.Node) { for _, comment := range comments { switch comment.Kind { case ast.KindJSDocText: b.WriteString(comment.Text()) case ast.KindJSDocLink: name := comment.Name() text := comment.AsJSDocLink().Text() if name != nil { if text == "" { writeEntityName(b, name) } else { writeEntityNameParts(b, name) } } b.WriteString(text) case ast.KindJSDocLinkCode: // !!! TODO: This is a temporary placeholder implementation that needs to be updated later name := comment.Name() text := comment.AsJSDocLinkCode().Text() if name != nil { if text == "" { writeEntityName(b, name) } else { writeEntityNameParts(b, name) } } b.WriteString(text) case ast.KindJSDocLinkPlain: // !!! TODO: This is a temporary placeholder implementation that needs to be updated later name := comment.Name() text := comment.AsJSDocLinkPlain().Text() if name != nil { if text == "" { writeEntityName(b, name) } else { writeEntityNameParts(b, name) } } b.WriteString(text) } } } func writeOptionalEntityName(b *strings.Builder, name *ast.Node) { if name != nil { b.WriteString(" ") writeEntityName(b, name) } } func writeEntityName(b *strings.Builder, name *ast.Node) { b.WriteString("`") writeEntityNameParts(b, name) b.WriteString("`") } 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()) } }