489 lines
16 KiB
Go
489 lines
16 KiB
Go
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())
|
|
}
|
|
}
|