diff --git a/internal/ast/ast.go b/internal/ast/ast.go index 831fb1b378..34eaa83185 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -897,6 +897,7 @@ func (n *Node) IsTypeOnly() bool { return false } +// If updating this function, also update `hasComment`. func (n *Node) CommentList() *NodeList { switch n.Kind { case KindJSDoc: @@ -1033,6 +1034,8 @@ func (n *Node) ElementList() *NodeList { return n.AsNamedImports().Elements case KindNamedExports: return n.AsNamedExports().Elements + case KindObjectBindingPattern, KindArrayBindingPattern: + return n.AsBindingPattern().Elements } panic("Unhandled case in Node.ElementList: " + n.Kind.String()) @@ -1060,6 +1063,32 @@ func (n *Node) QuestionDotToken() *Node { panic("Unhandled case in Node.QuestionDotToken: " + n.Kind.String()) } +func (n *Node) TypeExpression() *Node { + switch n.Kind { + case KindJSDocPropertyTag, KindJSDocParameterTag: + return n.AsJSDocParameterOrPropertyTag().TypeExpression + case KindJSDocReturnTag: + return n.AsJSDocReturnTag().TypeExpression + case KindJSDocTypeTag: + return n.AsJSDocTypeTag().TypeExpression + case KindJSDocTypedefTag: + return n.AsJSDocTypedefTag().TypeExpression + case KindJSDocSatisfiesTag: + return n.AsJSDocSatisfiesTag().TypeExpression + } + panic("Unhandled case in Node.TypeExpression: " + n.Kind.String()) +} + +func (n *Node) ClassName() *Node { + switch n.Kind { + case KindJSDocAugmentsTag: + return n.AsJSDocAugmentsTag().ClassName + case KindJSDocImplementsTag: + return n.AsJSDocImplementsTag().ClassName + } + panic("Unhandled case in Node.ClassName: " + n.Kind.String()) +} + // Determines if `n` contains `descendant` by walking up the `Parent` pointers from `descendant`. This method panics if // `descendant` or one of its ancestors is not parented except when that node is a `SourceFile`. func (n *Node) Contains(descendant *Node) bool { @@ -2049,6 +2078,8 @@ type ( NamedExportsNode = Node UnionType = Node LiteralType = Node + JSDocNode = Node + BindingPatternNode = Node ) type ( @@ -9535,6 +9566,10 @@ func (node *JSDocTemplateTag) Clone(f NodeFactoryCoercible) *Node { return cloneNode(f.AsNodeFactory().NewJSDocTemplateTag(node.TagName, node.Constraint, node.TypeParameters, node.Comment), node.AsNode(), f.AsNodeFactory().hooks) } +func IsJSDocTemplateTag(n *Node) bool { + return n.Kind == KindJSDocTemplateTag +} + // JSDocParameterOrPropertyTag type JSDocParameterOrPropertyTag struct { JSDocTagBase @@ -9600,6 +9635,10 @@ func (node *JSDocParameterOrPropertyTag) Clone(f NodeFactoryCoercible) *Node { func (node *JSDocParameterOrPropertyTag) Name() *EntityName { return node.name } +func IsJSDocParameterTag(node *Node) bool { + return node.Kind == KindJSDocParameterTag +} + // JSDocReturnTag type JSDocReturnTag struct { JSDocTagBase @@ -9893,6 +9932,10 @@ func (node *JSDocImplementsTag) Clone(f NodeFactoryCoercible) *Node { return cloneNode(f.AsNodeFactory().NewJSDocImplementsTag(node.TagName, node.ClassName, node.Comment), node.AsNode(), f.AsNodeFactory().hooks) } +func IsJSDocImplementsTag(node *Node) bool { + return node.Kind == KindJSDocImplementsTag +} + // JSDocAugmentsTag type JSDocAugmentsTag struct { JSDocTagBase diff --git a/internal/ast/utilities.go b/internal/ast/utilities.go index b34fa3d51f..3d8dcbf395 100644 --- a/internal/ast/utilities.go +++ b/internal/ast/utilities.go @@ -2103,16 +2103,6 @@ func TryGetTextOfPropertyName(name *Node) (string, bool) { return "", false } -// True if node is of a JSDoc kind that may contain comment text. -func IsJSDocCommentContainingNode(node *Node) bool { - return node.Kind == KindJSDoc || - node.Kind == KindJSDocText || - node.Kind == KindJSDocTypeLiteral || - node.Kind == KindJSDocSignature || - IsJSDocLinkLike(node) || - IsJSDocTag(node) -} - func IsJSDocNode(node *Node) bool { return node.Kind >= KindFirstJSDocNode && node.Kind <= KindLastJSDocNode } @@ -3017,13 +3007,27 @@ func IsTypeKeywordToken(node *Node) bool { return node.Kind == KindTypeKeyword } -// If node is a single comment JSDoc, we do not visit the comment node list. -func IsJSDocSingleCommentNodeList(parent *Node, nodeList *NodeList) bool { - return IsJSDocSingleCommentNode(parent) && nodeList == parent.AsJSDoc().Comment +// See `IsJSDocSingleCommentNode`. +func IsJSDocSingleCommentNodeList(nodeList *NodeList) bool { + if nodeList == nil || len(nodeList.Nodes) == 0 { + return false + } + parent := nodeList.Nodes[0].Parent + return IsJSDocSingleCommentNode(parent) && nodeList == parent.CommentList() +} + +// See `IsJSDocSingleCommentNode`. +func IsJSDocSingleCommentNodeComment(node *Node) bool { + if node == nil || node.Parent == nil { + return false + } + return IsJSDocSingleCommentNode(node.Parent) && node == node.Parent.CommentList().Nodes[0] } +// In Strada, if a JSDoc node has a single comment, that comment is represented as a string property +// as a simplification, and therefore that comment is not visited by `forEachChild`. func IsJSDocSingleCommentNode(node *Node) bool { - return node.Kind == KindJSDoc && node.AsJSDoc().Comment != nil && len(node.AsJSDoc().Comment.Nodes) == 1 + return hasComment(node.Kind) && node.CommentList() != nil && len(node.CommentList().Nodes) == 1 } func IsValidTypeOnlyAliasUseSite(useSite *Node) bool { @@ -3635,3 +3639,18 @@ func GetSemanticJsxChildren(children []*JsxChild) []*JsxChild { } }) } + +// Returns true if the node kind has a comment property. +func hasComment(kind Kind) bool { + switch kind { + case KindJSDoc, KindJSDocTag, KindJSDocAugmentsTag, KindJSDocImplementsTag, + KindJSDocDeprecatedTag, KindJSDocPublicTag, KindJSDocPrivateTag, KindJSDocProtectedTag, + KindJSDocReadonlyTag, KindJSDocOverrideTag, KindJSDocCallbackTag, KindJSDocOverloadTag, + KindJSDocParameterTag, KindJSDocPropertyTag, KindJSDocReturnTag, KindJSDocThisTag, + KindJSDocTypeTag, KindJSDocTemplateTag, KindJSDocTypedefTag, KindJSDocSeeTag, + KindJSDocSatisfiesTag, KindJSDocImportTag: + return true + default: + return false + } +} diff --git a/internal/astnav/tokens.go b/internal/astnav/tokens.go index c4b7f8186b..dffde94e89 100644 --- a/internal/astnav/tokens.go +++ b/internal/astnav/tokens.go @@ -125,10 +125,8 @@ func getTokenAtPosition( return nodeList } - nodeVisitor := getNodeVisitor(visitNode, visitNodeList) - for { - VisitEachChildAndJSDoc(current, sourceFile, nodeVisitor) + VisitEachChildAndJSDoc(current, sourceFile, visitNode, visitNodeList) // If prevSubtree was set on the last iteration, it ends at the target position. // Check if the rightmost token of prevSubtree should be returned based on the // `includePrecedingTokenAtEndPosition` callback. @@ -146,7 +144,7 @@ func getTokenAtPosition( // we can in the AST. We've either found a token, or we need to run the scanner // to construct one that isn't stored in the AST. if next == nil { - if ast.IsTokenKind(current.Kind) || ast.IsJSDocCommentContainingNode(current) { + if ast.IsTokenKind(current.Kind) || shouldSkipChild(current) { return current } scanner := scanner.GetScannerForSourceFile(sourceFile, left) @@ -217,7 +215,13 @@ func findRightmostNode(node *ast.Node) *ast.Node { } } -func VisitEachChildAndJSDoc(node *ast.Node, sourceFile *ast.SourceFile, visitor *ast.NodeVisitor) { +func VisitEachChildAndJSDoc( + node *ast.Node, + sourceFile *ast.SourceFile, + visitNode func(*ast.Node, *ast.NodeVisitor) *ast.Node, + visitNodes func(*ast.NodeList, *ast.NodeVisitor) *ast.NodeList, +) { + visitor := getNodeVisitor(visitNode, visitNodes) if node.Flags&ast.NodeFlagsHasJSDoc != 0 { for _, jsdoc := range node.JSDoc(sourceFile) { if visitor.Hooks.VisitNode != nil { @@ -275,9 +279,6 @@ func FindPrecedingTokenEx(sourceFile *ast.SourceFile, position int, startNode *a } if nodeList != nil && len(nodeList.Nodes) > 0 { nodes := nodeList.Nodes - if ast.IsJSDocSingleCommentNodeList(n, nodeList) { - return nodeList - } index, match := core.BinarySearchUniqueFunc(nodes, func(middle int, _ *ast.Node) int { // synthetic jsdoc nodes should have jsdocNode.End() <= n.Pos() if nodes[middle].Flags&ast.NodeFlagsReparsed != 0 { @@ -308,8 +309,7 @@ func FindPrecedingTokenEx(sourceFile *ast.SourceFile, position int, startNode *a } return nodeList } - nodeVisitor := getNodeVisitor(visitNode, visitNodes) - VisitEachChildAndJSDoc(n, sourceFile, nodeVisitor) + VisitEachChildAndJSDoc(n, sourceFile, visitNode, visitNodes) if foundChild != nil { // Note that the span of a node's tokens is [getStartOfNode(node, ...), node.end). @@ -420,9 +420,6 @@ func findRightmostValidToken(endPos int, sourceFile *ast.SourceFile, containingN } visitNodes := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList { if nodeList != nil && len(nodeList.Nodes) > 0 { - if ast.IsJSDocSingleCommentNodeList(n, nodeList) { - return nodeList - } hasChildren = true index, _ := core.BinarySearchUniqueFunc(nodeList.Nodes, func(middle int, node *ast.Node) int { if node.End() > endPos { @@ -450,8 +447,7 @@ func findRightmostValidToken(endPos int, sourceFile *ast.SourceFile, containingN } return nodeList } - nodeVisitor := getNodeVisitor(visitNode, visitNodes) - VisitEachChildAndJSDoc(n, sourceFile, nodeVisitor) + VisitEachChildAndJSDoc(n, sourceFile, visitNode, visitNodes) // Three cases: // 1. The answer is a token of `rightmostValidNode`. @@ -459,7 +455,7 @@ func findRightmostValidToken(endPos int, sourceFile *ast.SourceFile, containingN // 3. The current node is a childless, token-less node. The answer is the current node. // Case 2: Look at unvisited trailing tokens that occur in between the rightmost visited nodes. - if !ast.IsJSDocCommentContainingNode(n) { // JSDoc nodes don't include trivia tokens as children. + if !shouldSkipChild(n) { // JSDoc nodes don't include trivia tokens as children. var startPos int if rightmostValidNode != nil { startPos = rightmostValidNode.End() @@ -563,8 +559,7 @@ func FindNextToken(previousToken *ast.Node, parent *ast.Node, file *ast.SourceFi } return nodeList } - nodeVisitor := getNodeVisitor(visitNode, visitNodes) - VisitEachChildAndJSDoc(n, file, nodeVisitor) + VisitEachChildAndJSDoc(n, file, visitNode, visitNodes) // Cases: // 1. no answer exists // 2. answer is an unvisited token @@ -597,15 +592,44 @@ func getNodeVisitor( visitNode func(*ast.Node, *ast.NodeVisitor) *ast.Node, visitNodes func(*ast.NodeList, *ast.NodeVisitor) *ast.NodeList, ) *ast.NodeVisitor { + var wrappedVisitNode func(*ast.Node, *ast.NodeVisitor) *ast.Node + var wrappedVisitNodes func(*ast.NodeList, *ast.NodeVisitor) *ast.NodeList + if visitNode != nil { + wrappedVisitNode = func(n *ast.Node, v *ast.NodeVisitor) *ast.Node { + if ast.IsJSDocSingleCommentNodeComment(n) { + return n + } + return visitNode(n, v) + } + } + + if visitNodes != nil { + wrappedVisitNodes = func(n *ast.NodeList, v *ast.NodeVisitor) *ast.NodeList { + if ast.IsJSDocSingleCommentNodeList(n) { + return n + } + return visitNodes(n, v) + } + } + return ast.NewNodeVisitor(core.Identity, nil, ast.NodeVisitorHooks{ - VisitNode: visitNode, - VisitToken: visitNode, - VisitNodes: visitNodes, + VisitNode: wrappedVisitNode, + VisitToken: wrappedVisitNode, + VisitNodes: wrappedVisitNodes, VisitModifiers: func(modifiers *ast.ModifierList, visitor *ast.NodeVisitor) *ast.ModifierList { if modifiers != nil { - visitNodes(&modifiers.NodeList, visitor) + wrappedVisitNodes(&modifiers.NodeList, visitor) } return modifiers }, }) } + +func shouldSkipChild(node *ast.Node) bool { + return node.Kind == ast.KindJSDoc || + node.Kind == ast.KindJSDocText || + node.Kind == ast.KindJSDocTypeLiteral || + node.Kind == ast.KindJSDocSignature || + ast.IsJSDocLinkLike(node) || + ast.IsJSDocTag(node) +} diff --git a/internal/checker/printer.go b/internal/checker/printer.go index 7d466c8e54..8e82a2350f 100644 --- a/internal/checker/printer.go +++ b/internal/checker/printer.go @@ -372,3 +372,8 @@ func (c *Checker) formatUnionTypes(types []*Type) []*Type { } return result } + +func (c *Checker) TypeToTypeNode(t *Type, enclosingDeclaration *ast.Node, flags nodebuilder.Flags) *ast.TypeNode { + nodeBuilder := c.getNodeBuilder() + return nodeBuilder.TypeToTypeNode(t, enclosingDeclaration, flags, nodebuilder.InternalFlagsNone, nil) +} diff --git a/internal/format/api.go b/internal/format/api.go index db223ef543..7e11bd3266 100644 --- a/internal/format/api.go +++ b/internal/format/api.go @@ -80,7 +80,7 @@ func formatNodeLines(ctx context.Context, sourceFile *ast.SourceFile, node *ast. return nil } tokenStart := scanner.GetTokenPosOfNode(node, sourceFile, false) - lineStart := getLineStartPositionForPosition(tokenStart, sourceFile) + lineStart := GetLineStartPositionForPosition(tokenStart, sourceFile) span := core.NewTextRange(lineStart, node.End()) return FormatSpan(ctx, span, sourceFile, requestKind) } @@ -90,7 +90,7 @@ func FormatDocument(ctx context.Context, sourceFile *ast.SourceFile) []core.Text } func FormatSelection(ctx context.Context, sourceFile *ast.SourceFile, start int, end int) []core.TextChange { - return FormatSpan(ctx, core.NewTextRange(getLineStartPositionForPosition(start, sourceFile), end), sourceFile, FormatRequestKindFormatSelection) + return FormatSpan(ctx, core.NewTextRange(GetLineStartPositionForPosition(start, sourceFile), end), sourceFile, FormatRequestKindFormatSelection) } func FormatOnOpeningCurly(ctx context.Context, sourceFile *ast.SourceFile, position int) []core.TextChange { @@ -112,7 +112,7 @@ func FormatOnOpeningCurly(ctx context.Context, sourceFile *ast.SourceFile, posit * ``` * and we wouldn't want to move the closing brace. */ - textRange := core.NewTextRange(getLineStartPositionForPosition(scanner.GetTokenPosOfNode(outermostNode, sourceFile, false), sourceFile), position) + textRange := core.NewTextRange(GetLineStartPositionForPosition(scanner.GetTokenPosOfNode(outermostNode, sourceFile, false), sourceFile), position) return FormatSpan(ctx, textRange, sourceFile, FormatRequestKindFormatOnOpeningCurlyBrace) } diff --git a/internal/format/span.go b/internal/format/span.go index 87fd829daa..7c409c02c4 100644 --- a/internal/format/span.go +++ b/internal/format/span.go @@ -467,7 +467,7 @@ func (w *formatSpanWorker) processChildNodes( // }: {}; indentationOnListStartToken = w.indentationOnLastIndentedLine } else { - startLinePosition := getLineStartPositionForPosition(tokenInfo.token.Loc.Pos(), w.sourceFile) + startLinePosition := GetLineStartPositionForPosition(tokenInfo.token.Loc.Pos(), w.sourceFile) indentationOnListStartToken = findFirstNonWhitespaceColumn(startLinePosition, tokenInfo.token.Loc.Pos(), w.sourceFile, w.formattingContext.Options) } @@ -577,7 +577,7 @@ func (w *formatSpanWorker) tryComputeIndentationForListItem(startPos int, endPos } } else { startLine, _ := scanner.GetLineAndCharacterOfPosition(w.sourceFile, startPos) - startLinePosition := getLineStartPositionForPosition(startPos, w.sourceFile) + startLinePosition := GetLineStartPositionForPosition(startPos, w.sourceFile) column := findFirstNonWhitespaceColumn(startLinePosition, startPos, w.sourceFile, w.formattingContext.Options) if startLine != parentStartLine || startPos == column { // Use the base indent size if it is greater than diff --git a/internal/format/util.go b/internal/format/util.go index a7b08f2899..f6088272c8 100644 --- a/internal/format/util.go +++ b/internal/format/util.go @@ -75,7 +75,7 @@ func getCloseTokenForOpenToken(kind ast.Kind) ast.Kind { return ast.KindUnknown } -func getLineStartPositionForPosition(position int, sourceFile *ast.SourceFile) int { +func GetLineStartPositionForPosition(position int, sourceFile *ast.SourceFile) int { lineStarts := scanner.GetLineStarts(sourceFile) line, _ := scanner.GetLineAndCharacterOfPosition(sourceFile, position) return int(lineStarts[line]) diff --git a/internal/fourslash/_scripts/failingTests.txt b/internal/fourslash/_scripts/failingTests.txt index 873155459d..2bbbb8f568 100644 --- a/internal/fourslash/_scripts/failingTests.txt +++ b/internal/fourslash/_scripts/failingTests.txt @@ -65,10 +65,8 @@ TestCompletionImportModuleSpecifierEndingTsxReact TestCompletionImportModuleSpecifierEndingUnsupportedExtension TestCompletionInFunctionLikeBody_includesPrimitiveTypes TestCompletionInJsDoc -TestCompletionInJsDocQualifiedNames TestCompletionInNamedImportLocation TestCompletionInUncheckedJSFile -TestCompletionJSDocNamePath TestCompletionListBuilderLocations_VariableDeclarations TestCompletionListForDerivedType1 TestCompletionListForTransitivelyExportedMembers04 @@ -131,10 +129,6 @@ TestCompletionsJSDocImportTagAttributesEmptyModuleSpecifier1 TestCompletionsJSDocImportTagAttributesErrorModuleSpecifier1 TestCompletionsJSDocImportTagEmptyModuleSpecifier1 TestCompletionsJSDocNoCrash1 -TestCompletionsJSDocNoCrash2 -TestCompletionsJSDocNoCrash3 -TestCompletionsJsdocParamTypeBeforeName -TestCompletionsJsdocTag TestCompletionsJsdocTypeTagCast TestCompletionsJsxAttribute2 TestCompletionsJsxAttributeInitializer2 @@ -247,7 +241,6 @@ TestJsDocFunctionSignatures12 TestJsDocFunctionSignatures13 TestJsDocFunctionSignatures7 TestJsDocFunctionSignatures8 -TestJsDocFunctionTypeCompletionsNoCrash TestJsDocGenerics2 TestJsDocInheritDoc TestJsDocPropertyDescription1 @@ -266,26 +259,15 @@ TestJsDocTagsWithHyphen TestJsQuickInfoGenerallyAcceptableSize TestJsRequireQuickInfo TestJsdocCallbackTag -TestJsdocExtendsTagCompletion -TestJsdocImplementsTagCompletion -TestJsdocImportTagCompletion1 TestJsdocLink2 TestJsdocLink3 TestJsdocLink6 TestJsdocLink_findAllReferences1 -TestJsdocOverloadTagCompletion -TestJsdocParameterNameCompletion -TestJsdocPropTagCompletion -TestJsdocPropertyTagCompletion -TestJsdocSatisfiesTagCompletion1 -TestJsdocSatisfiesTagCompletion2 TestJsdocTemplatePrototypeCompletions -TestJsdocTemplateTagCompletion TestJsdocThrowsTagCompletion TestJsdocTypedefTag TestJsdocTypedefTag2 TestJsdocTypedefTagNamespace -TestJsdocTypedefTagTypeExpressionCompletion TestJsxFindAllReferencesOnRuntimeImportWithPaths1 TestLetQuickInfoAndCompletionList TestLocalFunction diff --git a/internal/fourslash/tests/basicJSDocCompletions_test.go b/internal/fourslash/tests/basicJSDocCompletions_test.go new file mode 100644 index 0000000000..10dcaf412e --- /dev/null +++ b/internal/fourslash/tests/basicJSDocCompletions_test.go @@ -0,0 +1,123 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + . "github.com/microsoft/typescript-go/internal/fourslash/tests/util" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestBasicJSDocCompletions(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = ` +// @filename: file.js +// @allowJs: true +/** + * @/*1*/ + */ +function foo(x) { + return x + 1; +} + +/** + * /*2*/ + */ +function bar(x, { y }) { + return x + y; +} + +/** + * @param {number} x + * /*3*/ + */ +function baz(x, { y }) { + return x + y; +} + +/** + * @param {number} x + * @param {object} param1 + * @param {n/*4*/} param1.y + */ +function baz(x, { y }) { + return x + y; +} + +/** + * @/*5*/ + */ +function baz(x = 0) { + return x * 2; +} +` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyCompletions(t, "1", &fourslash.CompletionsExpectedList{ + IsIncomplete: false, + ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ + CommitCharacters: &DefaultCommitCharacters, + }, + Items: &fourslash.CompletionsExpectedItems{ + Includes: []fourslash.CompletionsExpectedItem{ + &lsproto.CompletionItem{ + Label: "link", + Kind: PtrTo(lsproto.CompletionItemKindKeyword), + Detail: PtrTo("link"), + }, + "param", + "returns", + "param {*} x ", + }, + }, + }) + f.VerifyCompletions(t, "2", &fourslash.CompletionsExpectedList{ + IsIncomplete: false, + ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ + CommitCharacters: &DefaultCommitCharacters, + }, + Items: &fourslash.CompletionsExpectedItems{ + Includes: []fourslash.CompletionsExpectedItem{ + "@param", + "@param {*} x ", + }, + }, + }) + f.VerifyCompletions(t, "3", &fourslash.CompletionsExpectedList{ + IsIncomplete: false, + ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ + CommitCharacters: &DefaultCommitCharacters, + }, + Items: &fourslash.CompletionsExpectedItems{ + Includes: []fourslash.CompletionsExpectedItem{ + "@param", + "@param {object} param1 \n* @param {*} param1.y ", + }, + }, + }) + f.VerifyCompletions(t, "4", &fourslash.CompletionsExpectedList{ + IsIncomplete: false, + ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ + CommitCharacters: &DefaultCommitCharacters, + EditRange: Ignored, + }, + Items: &fourslash.CompletionsExpectedItems{ + Includes: []fourslash.CompletionsExpectedItem{ + "number", + }, + }, + }) + f.VerifyCompletions(t, "5", &fourslash.CompletionsExpectedList{ + IsIncomplete: false, + ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ + CommitCharacters: &DefaultCommitCharacters, + EditRange: Ignored, + }, + Items: &fourslash.CompletionsExpectedItems{ + Includes: []fourslash.CompletionsExpectedItem{ + "param {number} [x=0] ", + }, + }, + }) +} diff --git a/internal/fourslash/tests/gen/completionInJsDocQualifiedNames_test.go b/internal/fourslash/tests/gen/completionInJsDocQualifiedNames_test.go index 24e35f94b0..1758c02b7c 100644 --- a/internal/fourslash/tests/gen/completionInJsDocQualifiedNames_test.go +++ b/internal/fourslash/tests/gen/completionInJsDocQualifiedNames_test.go @@ -11,7 +11,7 @@ import ( func TestCompletionInJsDocQualifiedNames(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `// @allowJs: true // @Filename: /node_modules/foo/index.d.ts diff --git a/internal/fourslash/tests/gen/completionJSDocNamePath_test.go b/internal/fourslash/tests/gen/completionJSDocNamePath_test.go index 2a93607b6d..6011e23ed3 100644 --- a/internal/fourslash/tests/gen/completionJSDocNamePath_test.go +++ b/internal/fourslash/tests/gen/completionJSDocNamePath_test.go @@ -10,7 +10,7 @@ import ( func TestCompletionJSDocNamePath(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `// @noLib: true /** diff --git a/internal/fourslash/tests/gen/completionsJSDocNoCrash2_test.go b/internal/fourslash/tests/gen/completionsJSDocNoCrash2_test.go index 74cd0e8083..07f43dfa6b 100644 --- a/internal/fourslash/tests/gen/completionsJSDocNoCrash2_test.go +++ b/internal/fourslash/tests/gen/completionsJSDocNoCrash2_test.go @@ -10,7 +10,7 @@ import ( func TestCompletionsJSDocNoCrash2(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `// @strict: true // @filename: index.ts diff --git a/internal/fourslash/tests/gen/completionsJSDocNoCrash3_test.go b/internal/fourslash/tests/gen/completionsJSDocNoCrash3_test.go index b146ed0150..bac7331902 100644 --- a/internal/fourslash/tests/gen/completionsJSDocNoCrash3_test.go +++ b/internal/fourslash/tests/gen/completionsJSDocNoCrash3_test.go @@ -12,7 +12,7 @@ import ( func TestCompletionsJSDocNoCrash3(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `// @strict: true // @filename: index.ts diff --git a/internal/fourslash/tests/gen/completionsJsdocParamTypeBeforeName_test.go b/internal/fourslash/tests/gen/completionsJsdocParamTypeBeforeName_test.go index 348a89d8c8..56d8f40da2 100644 --- a/internal/fourslash/tests/gen/completionsJsdocParamTypeBeforeName_test.go +++ b/internal/fourslash/tests/gen/completionsJsdocParamTypeBeforeName_test.go @@ -10,7 +10,7 @@ import ( func TestCompletionsJsdocParamTypeBeforeName(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `/** @param /*name1*/ {/*type*/} /*name2*/ */ function toString(obj) {}` diff --git a/internal/fourslash/tests/gen/completionsJsdocTag_test.go b/internal/fourslash/tests/gen/completionsJsdocTag_test.go index 645b1de8ab..5b3c8238f7 100644 --- a/internal/fourslash/tests/gen/completionsJsdocTag_test.go +++ b/internal/fourslash/tests/gen/completionsJsdocTag_test.go @@ -11,7 +11,7 @@ import ( func TestCompletionsJsdocTag(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `/** * @typedef {object} T diff --git a/internal/fourslash/tests/gen/jsDocFunctionTypeCompletionsNoCrash_test.go b/internal/fourslash/tests/gen/jsDocFunctionTypeCompletionsNoCrash_test.go index 1204e50be6..7a5d243aec 100644 --- a/internal/fourslash/tests/gen/jsDocFunctionTypeCompletionsNoCrash_test.go +++ b/internal/fourslash/tests/gen/jsDocFunctionTypeCompletionsNoCrash_test.go @@ -10,7 +10,7 @@ import ( func TestJsDocFunctionTypeCompletionsNoCrash(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `/** * @returns {function/**/(): string} diff --git a/internal/fourslash/tests/gen/jsdocExtendsTagCompletion_test.go b/internal/fourslash/tests/gen/jsdocExtendsTagCompletion_test.go index 8c8af65893..8acd316c68 100644 --- a/internal/fourslash/tests/gen/jsdocExtendsTagCompletion_test.go +++ b/internal/fourslash/tests/gen/jsdocExtendsTagCompletion_test.go @@ -10,7 +10,7 @@ import ( func TestJsdocExtendsTagCompletion(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `/** @extends {/**/} */ class A {}` diff --git a/internal/fourslash/tests/gen/jsdocImplementsTagCompletion_test.go b/internal/fourslash/tests/gen/jsdocImplementsTagCompletion_test.go index 7f5f5b8311..c207695616 100644 --- a/internal/fourslash/tests/gen/jsdocImplementsTagCompletion_test.go +++ b/internal/fourslash/tests/gen/jsdocImplementsTagCompletion_test.go @@ -10,7 +10,7 @@ import ( func TestJsdocImplementsTagCompletion(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `/** @implements {/**/} */ class A {}` diff --git a/internal/fourslash/tests/gen/jsdocImportTagCompletion1_test.go b/internal/fourslash/tests/gen/jsdocImportTagCompletion1_test.go index 0fa2c80ff8..340677af9d 100644 --- a/internal/fourslash/tests/gen/jsdocImportTagCompletion1_test.go +++ b/internal/fourslash/tests/gen/jsdocImportTagCompletion1_test.go @@ -10,7 +10,7 @@ import ( func TestJsdocImportTagCompletion1(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `// @allowJS: true // @checkJs: true diff --git a/internal/fourslash/tests/gen/jsdocOverloadTagCompletion_test.go b/internal/fourslash/tests/gen/jsdocOverloadTagCompletion_test.go index 8138cf715a..c87be8afe0 100644 --- a/internal/fourslash/tests/gen/jsdocOverloadTagCompletion_test.go +++ b/internal/fourslash/tests/gen/jsdocOverloadTagCompletion_test.go @@ -10,7 +10,7 @@ import ( func TestJsdocOverloadTagCompletion(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `// @allowJS: true // @checkJs: true diff --git a/internal/fourslash/tests/gen/jsdocParameterNameCompletion_test.go b/internal/fourslash/tests/gen/jsdocParameterNameCompletion_test.go index 3e2f5cf7d8..2a5b1bd389 100644 --- a/internal/fourslash/tests/gen/jsdocParameterNameCompletion_test.go +++ b/internal/fourslash/tests/gen/jsdocParameterNameCompletion_test.go @@ -10,7 +10,7 @@ import ( func TestJsdocParameterNameCompletion(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `/** * @param /*0*/ diff --git a/internal/fourslash/tests/gen/jsdocPropTagCompletion_test.go b/internal/fourslash/tests/gen/jsdocPropTagCompletion_test.go index 5b93e4d3b9..eb004ed071 100644 --- a/internal/fourslash/tests/gen/jsdocPropTagCompletion_test.go +++ b/internal/fourslash/tests/gen/jsdocPropTagCompletion_test.go @@ -10,7 +10,7 @@ import ( func TestJsdocPropTagCompletion(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `/** * @typedef Foo diff --git a/internal/fourslash/tests/gen/jsdocPropertyTagCompletion_test.go b/internal/fourslash/tests/gen/jsdocPropertyTagCompletion_test.go index 82719cfd9c..fc11da5f88 100644 --- a/internal/fourslash/tests/gen/jsdocPropertyTagCompletion_test.go +++ b/internal/fourslash/tests/gen/jsdocPropertyTagCompletion_test.go @@ -10,7 +10,7 @@ import ( func TestJsdocPropertyTagCompletion(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `/** * @typedef {Object} Foo diff --git a/internal/fourslash/tests/gen/jsdocSatisfiesTagCompletion1_test.go b/internal/fourslash/tests/gen/jsdocSatisfiesTagCompletion1_test.go index f2997afbf9..bfd5f12d0b 100644 --- a/internal/fourslash/tests/gen/jsdocSatisfiesTagCompletion1_test.go +++ b/internal/fourslash/tests/gen/jsdocSatisfiesTagCompletion1_test.go @@ -10,7 +10,7 @@ import ( func TestJsdocSatisfiesTagCompletion1(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `// @noEmit: true // @allowJS: true diff --git a/internal/fourslash/tests/gen/jsdocSatisfiesTagCompletion2_test.go b/internal/fourslash/tests/gen/jsdocSatisfiesTagCompletion2_test.go index 27538bc590..ae6e0d4288 100644 --- a/internal/fourslash/tests/gen/jsdocSatisfiesTagCompletion2_test.go +++ b/internal/fourslash/tests/gen/jsdocSatisfiesTagCompletion2_test.go @@ -10,7 +10,7 @@ import ( func TestJsdocSatisfiesTagCompletion2(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `// @noEmit: true // @allowJS: true diff --git a/internal/fourslash/tests/gen/jsdocTemplateTagCompletion_test.go b/internal/fourslash/tests/gen/jsdocTemplateTagCompletion_test.go index 23ff81f66e..52e9626807 100644 --- a/internal/fourslash/tests/gen/jsdocTemplateTagCompletion_test.go +++ b/internal/fourslash/tests/gen/jsdocTemplateTagCompletion_test.go @@ -10,7 +10,7 @@ import ( func TestJsdocTemplateTagCompletion(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `/** * @template {/**/} T diff --git a/internal/fourslash/tests/gen/jsdocTypedefTagTypeExpressionCompletion_test.go b/internal/fourslash/tests/gen/jsdocTypedefTagTypeExpressionCompletion_test.go index d4af6bb12f..059588bca2 100644 --- a/internal/fourslash/tests/gen/jsdocTypedefTagTypeExpressionCompletion_test.go +++ b/internal/fourslash/tests/gen/jsdocTypedefTagTypeExpressionCompletion_test.go @@ -12,7 +12,7 @@ import ( func TestJsdocTypedefTagTypeExpressionCompletion(t *testing.T) { t.Parallel() - t.Skip() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") const content = `interface I { age: number; diff --git a/internal/ls/completions.go b/internal/ls/completions.go index 8a40ca0793..8a72ad4d72 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -18,9 +18,12 @@ import ( "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/format" "github.com/microsoft/typescript-go/internal/jsnum" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/lsutil" + "github.com/microsoft/typescript-go/internal/nodebuilder" + "github.com/microsoft/typescript-go/internal/printer" "github.com/microsoft/typescript-go/internal/scanner" "github.com/microsoft/typescript-go/internal/stringutil" "golang.org/x/text/collate" @@ -70,7 +73,7 @@ func ensureItemData(fileName string, pos int, list *lsproto.CompletionList) *lsp return list } -// *completionDataData | *completionDataKeyword +// *completionDataData | *completionDataKeyword | *completionDataJSDocTagName | *completionDataJSDocTag | *completionDataJSDocParameterName type completionData = any type completionDataData struct { @@ -106,6 +109,14 @@ type completionDataKeyword struct { isNewIdentifierLocation bool } +type completionDataJSDocTagName struct{} + +type completionDataJSDocTag struct{} + +type completionDataJSDocParameterName struct { + tag *ast.JSDocParameterTag +} + type importStatementCompletionInfo struct { // !!! } @@ -383,13 +394,46 @@ func (l *LanguageService) getCompletionsAtPosition( data.isNewIdentifierLocation, optionalReplacementSpan, ) - // !!! jsdoc completion data cases + case *completionDataJSDocTagName: + // If the current position is a jsDoc tag name, only tag names should be provided for completion + items := getJSDocTagNameCompletions() + items = append(items, getJSDocParameterCompletions( + clientOptions, + file, + position, + checker, + compilerOptions, + preferences, + /*tagNameOnly*/ true, + )...) + return l.jsDocCompletionInfo(clientOptions, position, file, items) + case *completionDataJSDocTag: + // If the current position is a jsDoc tag, only tags should be provided for completion + items := getJSDocTagCompletions() + items = append(items, getJSDocParameterCompletions( + clientOptions, + file, + position, + checker, + compilerOptions, + preferences, + /*tagNameOnly*/ false, + )...) + return l.jsDocCompletionInfo(clientOptions, position, file, items) + case *completionDataJSDocParameterName: + return l.jsDocCompletionInfo(clientOptions, position, file, getJSDocParameterNameCompletions(data.tag)) default: panic("getCompletionData() returned unexpected type: " + fmt.Sprintf("%T", data)) } } -func getCompletionData(program *compiler.Program, typeChecker *checker.Checker, file *ast.SourceFile, position int, preferences *UserPreferences) completionData { +func getCompletionData( + program *compiler.Program, + typeChecker *checker.Checker, + file *ast.SourceFile, + position int, + preferences *UserPreferences, +) completionData { inCheckedFile := isCheckedFile(file, program.Options()) currentToken := astnav.GetTokenAtPosition(file, position) @@ -400,8 +444,77 @@ func getCompletionData(program *compiler.Program, typeChecker *checker.Checker, insideJsDocImportTag := false isInSnippetScope := false if insideComment != nil { - // !!! jsdoc - return nil + if hasDocComment(file, position) { + if file.Text()[position] == '@' { + // The current position is next to the '@' sign, when no tag name being provided yet. + // Provide a full list of tag names + return &completionDataJSDocTagName{} + } else { + // When completion is requested without "@", we will have check to make sure that + // there are no comments prefix the request position. We will only allow "*" and space. + // e.g + // /** |c| /* + // + // /** + // |c| + // */ + // + // /** + // * |c| + // */ + // + // /** + // * |c| + // */ + lineStart := format.GetLineStartPositionForPosition(position, file) + noCommentPrefix := true + for _, r := range file.Text()[lineStart:position] { + if !(stringutil.IsWhiteSpaceSingleLine(r) || r == '*' || r == '/' || r == '(' || r == ')' || r == '|') { + noCommentPrefix = false + break + } + } + if noCommentPrefix { + return &completionDataJSDocTag{} + } + } + } + + // Completion should work inside certain JSDoc tags. For example: + // /** @type {number | string} */ + // Completion should work in the brackets + if tag := getJSDocTagAtPosition(currentToken, position); tag != nil { + if tag.TagName().Pos() <= position && position <= tag.TagName().End() { + return &completionDataJSDocTagName{} + } + if ast.IsJSDocImportTag(tag) { + insideJsDocImportTag = true + } else { + if typeExpression := tryGetTypeExpressionFromTag(tag); typeExpression != nil { + currentToken = astnav.GetTokenAtPosition(file, position) + if currentToken == nil || + (!ast.IsDeclarationName(currentToken) && + (currentToken.Parent.Kind != ast.KindJSDocPropertyTag || + currentToken.Parent.Name() != currentToken)) { + // Use as type location if inside tag's type expression + insideJSDocTagTypeExpression = isCurrentlyEditingNode(typeExpression, file, position) + } + } + if !insideJSDocTagTypeExpression && + ast.IsJSDocParameterTag(tag) && + (ast.NodeIsMissing(tag.Name()) || tag.Name().Pos() <= position && position <= tag.Name().End()) { + return &completionDataJSDocParameterName{ + tag: tag.AsJSDocParameterOrPropertyTag(), + } + } + } + } + + if !insideJSDocTagTypeExpression && !insideJsDocImportTag { + // Proceed if the current position is in JSDoc tag expression; otherwise it is a normal + // comment or the plain text part of a JSDoc comment, so no completion should be available + return nil + } } // The decision to provide completion depends on the contextToken, which is determined through the previousToken. @@ -4679,14 +4792,24 @@ func (l *LanguageService) getCompletionItemDetails( ) switch { case symbolCompletion.request != nil: - request := symbolCompletion.request - // !!! JSDoc completions - if core.Some(request.keywordCompletions, func(c *lsproto.CompletionItem) bool { - return c.Label == itemData.Name - }) { + request := *symbolCompletion.request + switch request := request.(type) { + case *completionDataJSDocTagName: + return createSimpleDetails(item, itemData.Name) + case *completionDataJSDocTag: + return createSimpleDetails(item, itemData.Name) + case *completionDataJSDocParameterName: return createSimpleDetails(item, itemData.Name) + case *completionDataKeyword: + if core.Some(request.keywordCompletions, func(c *lsproto.CompletionItem) bool { + return c.Label == itemData.Name + }) { + return createSimpleDetails(item, itemData.Name) + } + return item + default: + panic(fmt.Sprintf("Unexpected completion data type: %T", request)) } - return nil case symbolCompletion.symbol != nil: symbolDetails := symbolCompletion.symbol actions := getCompletionItemActions(symbolDetails.symbol) @@ -4710,13 +4833,13 @@ func (l *LanguageService) getCompletionItemDetails( }) { return createSimpleDetails(item, itemData.Name) } - return nil + return item } } type detailsData struct { symbol *symbolDetails - request *completionDataKeyword + request *completionData literal *literalValue cases *struct{} } @@ -4755,9 +4878,9 @@ func getSymbolCompletionFromItemData( return detailsData{} } - if completionData, ok := completionData.(*completionDataKeyword); ok { + if _, ok := completionData.(*completionDataData); !ok { return detailsData{ - request: completionData, + request: &completionData, } } @@ -4860,3 +4983,625 @@ func createCompletionDetailsForSymbol( func getCompletionItemActions(symbol *ast.Symbol) []codeAction { return nil } + +func hasDocComment(file *ast.SourceFile, position int) bool { + token := astnav.GetTokenAtPosition(file, position) + return ast.FindAncestor(token, (*ast.Node).IsJSDoc) != nil +} + +// Get the corresponding JSDocTag node if the position is in a JSDoc comment +func getJSDocTagAtPosition(node *ast.Node, position int) *ast.JSDocTag { + return ast.FindAncestorOrQuit(node, func(n *ast.Node) ast.FindAncestorResult { + if ast.IsJSDocTag(n) && n.Loc.ContainsInclusive(position) { + return ast.FindAncestorTrue + } + if n.IsJSDoc() { + return ast.FindAncestorQuit + } + return ast.FindAncestorFalse + }) +} + +func tryGetTypeExpressionFromTag(tag *ast.JSDocTag) *ast.Node { + if isTagWithTypeExpression(tag) { + var typeExpression *ast.Node + if ast.IsJSDocTemplateTag(tag) { + typeExpression = tag.AsJSDocTemplateTag().Constraint + } else { + typeExpression = tag.TypeExpression() + } + if typeExpression != nil && typeExpression.Kind == ast.KindJSDocTypeExpression { + return typeExpression + } + } + if ast.IsJSDocAugmentsTag(tag) || ast.IsJSDocImplementsTag(tag) { + return tag.ClassName() + } + return nil +} + +func isTagWithTypeExpression(tag *ast.JSDocTag) bool { + switch tag.Kind { + case ast.KindJSDocParameterTag, ast.KindJSDocPropertyTag, ast.KindJSDocReturnTag, ast.KindJSDocTypeTag, + ast.KindJSDocTypedefTag, ast.KindJSDocSatisfiesTag: + return true + case ast.KindJSDocTemplateTag: + return tag.AsJSDocTemplateTag().Constraint != nil + default: + return false + } +} + +func (l *LanguageService) jsDocCompletionInfo( + clientOptions *lsproto.CompletionClientCapabilities, + position int, + file *ast.SourceFile, + items []*lsproto.CompletionItem, +) *lsproto.CompletionList { + defaultCommitCharacters := getDefaultCommitCharacters(false /*isNewIdentifierLocation*/) + itemDefaults := l.setItemDefaults( + clientOptions, + position, + file, + items, + &defaultCommitCharacters, + nil, /*optionalReplacementSpan*/ + ) + return &lsproto.CompletionList{ + IsIncomplete: false, + ItemDefaults: itemDefaults, + Items: items, + } +} + +var jsDocTagNames = []string{ + "abstract", + "access", + "alias", + "argument", + "async", + "augments", + "author", + "borrows", + "callback", + "class", + "classdesc", + "constant", + "constructor", + "constructs", + "copyright", + "default", + "deprecated", + "description", + "emits", + "enum", + "event", + "example", + "exports", + "extends", + "external", + "field", + "file", + "fileoverview", + "fires", + "function", + "generator", + "global", + "hideconstructor", + "host", + "ignore", + "implements", + "import", + "inheritdoc", + "inner", + "instance", + "interface", + "kind", + "lends", + "license", + "link", + "linkcode", + "linkplain", + "listens", + "member", + "memberof", + "method", + "mixes", + "module", + "name", + "namespace", + "overload", + "override", + "package", + "param", + "private", + "prop", + "property", + "protected", + "public", + "readonly", + "requires", + "returns", + "satisfies", + "see", + "since", + "static", + "summary", + "template", + "this", + "throws", + "todo", + "tutorial", + "type", + "typedef", + "var", + "variation", + "version", + "virtual", + "yields", +} + +var jsDocTagNameCompletionItems = sync.OnceValue(func() []*lsproto.CompletionItem { + items := make([]*lsproto.CompletionItem, 0, len(jsDocTagNames)) + for _, tagName := range jsDocTagNames { + item := &lsproto.CompletionItem{ + Label: tagName, + Kind: ptrTo(lsproto.CompletionItemKindKeyword), + SortText: ptrTo(string(SortTextLocationPriority)), + } + items = append(items, item) + } + return items +}) + +var jsDocTagCompletionItems = sync.OnceValue(func() []*lsproto.CompletionItem { + items := make([]*lsproto.CompletionItem, 0, len(jsDocTagNames)) + for _, tagName := range jsDocTagNames { + item := &lsproto.CompletionItem{ + Label: "@" + tagName, + Kind: ptrTo(lsproto.CompletionItemKindKeyword), + SortText: ptrTo(string(SortTextLocationPriority)), + } + items = append(items, item) + } + return items +}) + +func getJSDocTagNameCompletions() []*lsproto.CompletionItem { + return cloneItems(jsDocTagNameCompletionItems()) +} + +func getJSDocTagCompletions() []*lsproto.CompletionItem { + return cloneItems(jsDocTagCompletionItems()) +} + +func getJSDocParameterCompletions( + clientOptions *lsproto.CompletionClientCapabilities, + file *ast.SourceFile, + position int, + typeChecker *checker.Checker, + options *core.CompilerOptions, + preferences *UserPreferences, + tagNameOnly bool, +) []*lsproto.CompletionItem { + currentToken := astnav.GetTokenAtPosition(file, position) + if !ast.IsJSDocTag(currentToken) && !currentToken.IsJSDoc() { + return nil + } + var jsDoc *ast.JSDocNode + if currentToken.IsJSDoc() { + jsDoc = currentToken + } else { + jsDoc = currentToken.Parent + } + if !jsDoc.IsJSDoc() { + return nil + } + fun := jsDoc.Parent + if !ast.IsFunctionLike(fun) { + return nil + } + + isJS := ast.IsSourceFileJS(file) + // isSnippet := clientSupportsItemSnippet(clientOptions) + isSnippet := false // !!! need snippet printer + paramTagCount := 0 + var tags []*ast.JSDocTag + if jsDoc.AsJSDoc().Tags != nil { + tags = jsDoc.AsJSDoc().Tags.Nodes + } + for _, tag := range tags { + if ast.IsJSDocParameterTag(tag) && + astnav.GetStartOfNode(tag, file, false /*includeJSDoc*/) < position && + ast.IsIdentifier(tag.Name()) { + paramTagCount++ + } + } + paramIndex := -1 + return core.MapNonNil(fun.Parameters(), func(param *ast.ParameterDeclarationNode) *lsproto.CompletionItem { + paramIndex++ + if paramIndex < paramTagCount { + // This parameter is already annotated. + return nil + } + if ast.IsIdentifier(param.Name()) { // Named parameter + tabstopCounter := 1 + paramName := param.Name().Text() + displayText := getJSDocParamAnnotation( + paramName, + param.Initializer(), + param.AsParameterDeclaration().DotDotDotToken, + isJS, + /*isObject*/ false, + /*isSnippet*/ false, + typeChecker, + options, + preferences, + &tabstopCounter, + ) + var snippetText string + if isSnippet { + snippetText = getJSDocParamAnnotation( + paramName, + param.Initializer(), + param.AsParameterDeclaration().DotDotDotToken, + isJS, + /*isObject*/ false, + /*isSnippet*/ true, + typeChecker, + options, + preferences, + &tabstopCounter, + ) + } + if tagNameOnly { // Remove `@` + displayText = displayText[1:] + if snippetText != "" { + snippetText = snippetText[1:] + } + } + + return &lsproto.CompletionItem{ + Label: displayText, + Kind: ptrTo(lsproto.CompletionItemKindVariable), + SortText: ptrTo(string(SortTextLocationPriority)), + InsertText: strPtrTo(snippetText), + InsertTextFormat: core.IfElse(isSnippet, ptrTo(lsproto.InsertTextFormatSnippet), nil), + } + } else if paramIndex == paramTagCount { + // Destructuring parameter; do it positionally + paramPath := fmt.Sprintf("param%d", paramIndex) + displayTextResult := generateJSDocParamTagsForDestructuring( + paramPath, + param.Name(), + param.Initializer(), + param.AsParameterDeclaration().DotDotDotToken, + isJS, + /*isSnippet*/ false, + typeChecker, + options, + preferences, + ) + var snippetText string + if isSnippet { + snippetTextResult := generateJSDocParamTagsForDestructuring( + paramPath, + param.Name(), + param.Initializer(), + param.AsParameterDeclaration().DotDotDotToken, + isJS, + /*isSnippet*/ true, + typeChecker, + options, + preferences, + ) + snippetText = strings.Join(snippetTextResult, options.NewLine.GetNewLineCharacter()+"* ") + } + displayText := strings.Join(displayTextResult, options.NewLine.GetNewLineCharacter()+"* ") + if tagNameOnly { // Remove `@` + displayText = strings.TrimPrefix(displayText, "@") + snippetText = strings.TrimPrefix(snippetText, "@") + } + return &lsproto.CompletionItem{ + Label: displayText, + Kind: ptrTo(lsproto.CompletionItemKindVariable), + SortText: ptrTo(string(SortTextLocationPriority)), + InsertText: strPtrTo(snippetText), + InsertTextFormat: core.IfElse(isSnippet, ptrTo(lsproto.InsertTextFormatSnippet), nil), + } + } + return nil + }) +} + +func getJSDocParamAnnotation( + paramName string, + initializer *ast.Expression, + dotDotDotToken *ast.TokenNode, + isJS bool, + isObject bool, + isSnippet bool, + typeChecker *checker.Checker, + options *core.CompilerOptions, + preferences *UserPreferences, + tabstopCounter *int, +) string { + if isSnippet { + // !!! Debug.assertIsDefined(tabstopCounter); + } + if initializer != nil { + paramName = getJSDocParamNameWithInitializer(paramName, initializer) + } + if isSnippet { + paramName = escapeSnippetText(paramName) + } + if isJS { + t := "*" + if isObject { + // !!! Debug.assert(!dotDotDotToken, `Cannot annotate a rest parameter with type 'object'.`); + t = "object" + } else { + if initializer != nil { + inferredType := typeChecker.GetTypeAtLocation(initializer.Parent) + if inferredType.Flags()&(checker.TypeFlagsAny|checker.TypeFlagsVoid) == 0 { + file := ast.GetSourceFileOfNode(initializer) + quotePreference := getQuotePreference(file, preferences) + builderFlags := core.IfElse( + quotePreference == quotePreferenceSingle, + nodebuilder.FlagsUseSingleQuotesForStringLiteralType, + nodebuilder.FlagsNone, + ) + typeNode := typeChecker.TypeToTypeNode(inferredType, ast.FindAncestor(initializer, ast.IsFunctionLike), builderFlags) + if typeNode != nil { + emitContext := printer.NewEmitContext() + // !!! snippet p + p := printer.NewPrinter(printer.PrinterOptions{ + RemoveComments: true, + // !!! + // Module: options.Module, + // ModuleResolution: options.ModuleResolution, + // Target: options.Target, + }, printer.PrintHandlers{}, emitContext) + emitContext.SetEmitFlags(typeNode, printer.EFSingleLine) + t = p.Emit(typeNode, file) + } + } + } + if isSnippet && t == "*" { + tabstop := *tabstopCounter + *tabstopCounter++ + t = fmt.Sprintf("${%d:%s}", tabstop, t) + } + } + dotDotDot := core.IfElse(!isObject && dotDotDotToken != nil, "...", "") + var description string + if isSnippet { + tabstop := *tabstopCounter + *tabstopCounter++ + description = fmt.Sprintf("${%d}", tabstop) + } + return fmt.Sprintf("@param {%s%s} %s %s", dotDotDot, t, paramName, description) + } else { + var description string + if isSnippet { + tabstop := *tabstopCounter + *tabstopCounter++ + description = fmt.Sprintf("${%d}", tabstop) + } + return fmt.Sprintf("@param %s %s", paramName, description) + } +} + +func getJSDocParamNameWithInitializer(paramName string, initializer *ast.Expression) string { + initializerText := strings.TrimSpace(scanner.GetTextOfNode(initializer)) + if strings.Contains(initializerText, "\n") || len(initializerText) > 80 { + return fmt.Sprintf("[%s]", paramName) + } + return fmt.Sprintf("[%s=%s]", paramName, initializerText) +} + +func generateJSDocParamTagsForDestructuring( + path string, + pattern *ast.BindingPatternNode, + initializer *ast.Expression, + dotDotDotToken *ast.TokenNode, + isJS bool, + isSnippet bool, + typeChecker *checker.Checker, + options *core.CompilerOptions, + preferences *UserPreferences, +) []string { + tabstopCounter := 1 + if !isJS { + return []string{getJSDocParamAnnotation( + path, + initializer, + dotDotDotToken, + isJS, + /*isObject*/ false, + isSnippet, + typeChecker, + options, + preferences, + &tabstopCounter, + )} + } + return jsDocParamPatternWorker( + path, + pattern, + initializer, + dotDotDotToken, + isJS, + isSnippet, + typeChecker, + options, + preferences, + &tabstopCounter, + ) +} + +func jsDocParamPatternWorker( + path string, + pattern *ast.BindingPatternNode, + initializer *ast.Expression, + dotDotDotToken *ast.TokenNode, + isJS bool, + isSnippet bool, + typeChecker *checker.Checker, + options *core.CompilerOptions, + preferences *UserPreferences, + counter *int, +) []string { + if ast.IsObjectBindingPattern(pattern) && dotDotDotToken == nil { + childCounter := *counter + rootParam := getJSDocParamAnnotation( + path, + initializer, + dotDotDotToken, + isJS, + /*isObject*/ true, + isSnippet, + typeChecker, + options, + preferences, + &childCounter, + ) + var childTags []string + for _, element := range pattern.Elements() { + elementTags := jsDocParamElementWorker( + path, + element, + initializer, + dotDotDotToken, + isJS, + isSnippet, + typeChecker, + options, + preferences, + &childCounter, + ) + if len(elementTags) == 0 { + childTags = nil + break + } + childTags = append(childTags, elementTags...) + } + if len(childTags) > 0 { + *counter = childCounter + return append([]string{rootParam}, childTags...) + } + } + return []string{ + getJSDocParamAnnotation( + path, + initializer, + dotDotDotToken, + isJS, + /*isObject*/ false, + isSnippet, + typeChecker, + options, + preferences, + counter, + ), + } +} + +// Assumes binding element is inside object binding pattern. +// We can't deeply annotate an array binding pattern. +func jsDocParamElementWorker( + path string, + element *ast.BindingElementNode, + initializer *ast.Expression, + dotDotDotToken *ast.TokenNode, + isJS bool, + isSnippet bool, + typeChecker *checker.Checker, + options *core.CompilerOptions, + preferences *UserPreferences, + counter *int, +) []string { + if ast.IsIdentifier(element.Name()) { // `{ b }` or `{ b: newB }` + var propertyName string + if element.PropertyName() != nil { + propertyName, _ = ast.TryGetTextOfPropertyName(element.PropertyName()) + } else { + propertyName = element.Name().Text() + } + if propertyName == "" { + return nil + } + paramName := fmt.Sprintf("%s.%s", path, propertyName) + return []string{ + getJSDocParamAnnotation( + paramName, + element.Initializer(), + element.AsBindingElement().DotDotDotToken, + isJS, + /*isObject*/ false, + isSnippet, + typeChecker, + options, + preferences, + counter, + ), + } + } else if element.PropertyName() != nil { // `{ b: {...} }` or `{ b: [...] }` + propertyName, _ := ast.TryGetTextOfPropertyName(element.PropertyName()) + if propertyName == "" { + return nil + } + return jsDocParamPatternWorker( + fmt.Sprintf("%s.%s", path, propertyName), + element.Name(), + element.Initializer(), + element.AsBindingElement().DotDotDotToken, + isJS, + isSnippet, + typeChecker, + options, + preferences, + counter, + ) + } + return nil +} + +func getJSDocParameterNameCompletions(tag *ast.JSDocParameterTag) []*lsproto.CompletionItem { + if !ast.IsIdentifier(tag.Name()) { + return nil + } + nameThusFar := tag.Name().Text() + jsDoc := tag.Parent + fn := jsDoc.Parent + if !ast.IsFunctionLike(fn) { + return nil + } + + var tags []*ast.JSDocTag + if jsDoc.AsJSDoc().Tags != nil { + tags = jsDoc.AsJSDoc().Tags.Nodes + } + + return core.MapNonNil(fn.Parameters(), func(param *ast.ParameterDeclarationNode) *lsproto.CompletionItem { + if !ast.IsIdentifier(param.Name()) { + return nil + } + + name := param.Name().Text() + if core.Some(tags, func(t *ast.JSDocTag) bool { + return t != tag.AsNode() && + ast.IsJSDocParameterTag(t) && + ast.IsIdentifier(t.Name()) && + t.Name().Text() == name + }) || nameThusFar != "" && !strings.HasPrefix(name, nameThusFar) { + return nil + } + + return &lsproto.CompletionItem{ + Label: name, + Kind: ptrTo(lsproto.CompletionItemKindVariable), + SortText: ptrTo(string(SortTextLocationPriority)), + } + }) +} diff --git a/internal/ls/string_completions.go b/internal/ls/string_completions.go index 6af4212224..c81b3a79f2 100644 --- a/internal/ls/string_completions.go +++ b/internal/ls/string_completions.go @@ -669,7 +669,7 @@ func (l *LanguageService) getStringLiteralCompletionDetails( preferences *UserPreferences, ) *lsproto.CompletionItem { if contextToken == nil || !ast.IsStringLiteralLike(contextToken) { - return nil + return item } completions := l.getStringLiteralCompletionEntries( ctx, @@ -680,7 +680,7 @@ func (l *LanguageService) getStringLiteralCompletionDetails( preferences, ) if completions == nil { - return nil + return item } checker, done := program.GetTypeCheckerForFile(ctx, file) defer done() @@ -718,5 +718,5 @@ func stringLiteralCompletionDetails( } } } - return nil + return item } diff --git a/internal/ls/utilities.go b/internal/ls/utilities.go index 3cd9ee217b..ab58122018 100644 --- a/internal/ls/utilities.go +++ b/internal/ls/utilities.go @@ -438,6 +438,9 @@ func probablyUsesSemicolons(file *ast.SourceFile) bool { var visit func(node *ast.Node) bool visit = func(node *ast.Node) bool { + if node.Flags&ast.NodeFlagsReparsed != 0 { + return false + } if lsutil.SyntaxRequiresTrailingSemicolonOrASI(node.Kind) { lastToken := lsutil.GetLastToken(node, file) if lastToken != nil && lastToken.Kind == ast.KindSemicolonToken { @@ -1614,18 +1617,7 @@ func findContainingList(node *ast.Node, file *ast.SourceFile) *ast.NodeList { } return nodes } - nodeVisitor := ast.NewNodeVisitor(core.Identity, nil, ast.NodeVisitorHooks{ - VisitNode: visitNode, - VisitToken: visitNode, - VisitNodes: visitNodes, - VisitModifiers: func(modifiers *ast.ModifierList, visitor *ast.NodeVisitor) *ast.ModifierList { - if modifiers != nil { - visitNodes(&modifiers.NodeList, visitor) - } - return modifiers - }, - }) - astnav.VisitEachChildAndJSDoc(node.Parent, file, nodeVisitor) + astnav.VisitEachChildAndJSDoc(node.Parent, file, visitNode, visitNodes) return list } diff --git a/internal/lsutil/children.go b/internal/lsutil/children.go index 651172d25a..7e6c0d1ed0 100644 --- a/internal/lsutil/children.go +++ b/internal/lsutil/children.go @@ -10,7 +10,7 @@ import ( // Replaces last(node.getChildren(sourceFile)) func GetLastChild(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node { lastChildNode := GetLastVisitedChild(node, sourceFile) - if ast.IsJSDocSingleCommentNode(node) { + if ast.IsJSDocSingleCommentNode(node) && lastChildNode == nil { return nil } var tokenStartPos int @@ -67,7 +67,7 @@ func GetLastVisitedChild(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node { return n } visitNodeList := func(nodeList *ast.NodeList, _ *ast.NodeVisitor) *ast.NodeList { - if nodeList != nil && len(nodeList.Nodes) > 0 && !ast.IsJSDocSingleCommentNodeList(node, nodeList) { + if nodeList != nil && len(nodeList.Nodes) > 0 { for i := len(nodeList.Nodes) - 1; i >= 0; i-- { if nodeList.Nodes[i].Flags&ast.NodeFlagsReparsed == 0 { lastChild = nodeList.Nodes[i] @@ -78,19 +78,7 @@ func GetLastVisitedChild(node *ast.Node, sourceFile *ast.SourceFile) *ast.Node { return nodeList } - nodeVisitor := ast.NewNodeVisitor(core.Identity, nil, ast.NodeVisitorHooks{ - VisitNode: visitNode, - VisitToken: visitNode, - VisitNodes: visitNodeList, - VisitModifiers: func(modifiers *ast.ModifierList, visitor *ast.NodeVisitor) *ast.ModifierList { - if modifiers != nil { - visitNodeList(&modifiers.NodeList, visitor) - } - return modifiers - }, - }) - - astnav.VisitEachChildAndJSDoc(node, sourceFile, nodeVisitor) + astnav.VisitEachChildAndJSDoc(node, sourceFile, visitNode, visitNodeList) return lastChild } diff --git a/testdata/baselines/reference/astnav/GetTokenAtPosition.mapCode.ts.baseline.txt b/testdata/baselines/reference/astnav/GetTokenAtPosition.mapCode.ts.baseline.txt index c33d649c10..06da9dd9a0 100644 --- a/testdata/baselines/reference/astnav/GetTokenAtPosition.mapCode.ts.baseline.txt +++ b/testdata/baselines/reference/astnav/GetTokenAtPosition.mapCode.ts.baseline.txt @@ -51,29 +51,16 @@ 74 │ // which one makes the most sense, and grab the NodeArray from there. Do -〚Positions: [1611, 1730]〛 -【TS: JSDoc [1611, 1733)】 -《Go: JSDocText [1611, 1731)》 -66 │} -67 │ -68 │《【〚/** -69 │ * Tries to parse something into either "top-level" statements, or into blocks -70 │ * of class-context definitions. -71 │ 〛》*/】 -72 │function parse(sourceFile: SourceFile, content: string): NodeArray { -73 │ // We're going to speculatively parse different kinds of contexts to see - - -〚Positions: [1731, 1732]〛 +〚Positions: [1611, 1732]〛 【TS: JSDoc [1611, 1733)】 《Go: JSDoc [1607, 1733)》 64 │ }, 65 │ ); 66 │}《 67 │ -68 │【/** +68 │【〚/** 69 │ * Tries to parse something into either "top-level" statements, or into blocks 70 │ * of class-context definitions. -71 │ 〚*/〛】》 +71 │ */〛】》 72 │function parse(sourceFile: SourceFile, content: string): NodeArray { 73 │ // We're going to speculatively parse different kinds of contexts to see diff --git a/testdata/baselines/reference/astnav/GetTouchingPropertyName.mapCode.ts.baseline.txt b/testdata/baselines/reference/astnav/GetTouchingPropertyName.mapCode.ts.baseline.txt index 451fb23458..f99eb69681 100644 --- a/testdata/baselines/reference/astnav/GetTouchingPropertyName.mapCode.ts.baseline.txt +++ b/testdata/baselines/reference/astnav/GetTouchingPropertyName.mapCode.ts.baseline.txt @@ -58,29 +58,16 @@ 68 │/** -〚Positions: [1611, 1730]〛 -【TS: JSDoc [1611, 1733)】 -《Go: JSDocText [1611, 1731)》 -66 │} -67 │ -68 │《【〚/** -69 │ * Tries to parse something into either "top-level" statements, or into blocks -70 │ * of class-context definitions. -71 │ 〛》*/】 -72 │function parse(sourceFile: SourceFile, content: string): NodeArray { -73 │ // We're going to speculatively parse different kinds of contexts to see - - -〚Positions: [1731, 1732]〛 +〚Positions: [1611, 1732]〛 【TS: JSDoc [1611, 1733)】 《Go: JSDoc [1607, 1733)》 64 │ }, 65 │ ); 66 │}《 67 │ -68 │【/** +68 │【〚/** 69 │ * Tries to parse something into either "top-level" statements, or into blocks 70 │ * of class-context definitions. -71 │ 〚*/〛】》 +71 │ */〛】》 72 │function parse(sourceFile: SourceFile, content: string): NodeArray { 73 │ // We're going to speculatively parse different kinds of contexts to see