diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index def08e81758..25b38277991 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -5831,7 +5831,6 @@ namespace ts { if (!name) { parseErrorAtPosition(pos, 0, Diagnostics.Identifier_expected); - return undefined; } let preName: Identifier, postName: Identifier; diff --git a/src/services/services.ts b/src/services/services.ts index 89052356447..8fda36d1160 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -132,6 +132,46 @@ namespace ts { let scanner: Scanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ true); let emptyArray: any[] = []; + + const jsDocTagNames = [ + "augments", + "author", + "argument", + "borrows", + "class", + "constant", + "constructor", + "constructs", + "default", + "deprecated", + "description", + "event", + "example", + "extends", + "field", + "fileOverview", + "function", + "ignore", + "inner", + "lends", + "link", + "memberOf", + "name", + "namespace", + "param", + "private", + "property", + "public", + "requires", + "returns", + "see", + "since", + "static", + "throws", + "type", + "version" + ]; + let jsDocCompletionEntries: CompletionEntry[]; function createNode(kind: SyntaxKind, pos: number, end: number, flags: NodeFlags, parent?: Node): NodeObject { let node = new (getNodeConstructor(kind))(); @@ -2971,6 +3011,8 @@ namespace ts { let sourceFile = getValidSourceFile(fileName); let isJavaScriptFile = isJavaScript(fileName); + let isJsDocTagName = false; + let start = new Date().getTime(); let currentToken = getTokenAtPosition(sourceFile, position); log("getCompletionData: Get current token: " + (new Date().getTime() - start)); @@ -2981,8 +3023,44 @@ namespace ts { log("getCompletionData: Is inside comment: " + (new Date().getTime() - start)); if (insideComment) { - log("Returning an empty list because completion was inside a comment."); - return undefined; + // The current position is next to the '@' sign, when no tag name being provided yet. + // Provide a full list of tag names + if (hasDocComment(sourceFile, position) && sourceFile.text.charCodeAt(position - 1) === CharacterCodes.at) { + isJsDocTagName = true; + } + + // Completion should work inside certain JsDoc tags. For example: + // /** @type {number | string} */ + // Completion should work in the brackets + let insideJsDocTagExpression = false; + let tag = getJsDocTagAtPosition(sourceFile, position); + if (tag) { + if (tag.tagName.pos <= position && position <= tag.tagName.end) { + isJsDocTagName = true; + } + + switch (tag.kind) { + case SyntaxKind.JSDocTypeTag: + case SyntaxKind.JSDocParameterTag: + case SyntaxKind.JSDocReturnTag: + let tagWithExpression = tag; + if (tagWithExpression.typeExpression) { + insideJsDocTagExpression = tagWithExpression.typeExpression.pos < position && position < tagWithExpression.typeExpression.end; + } + break; + } + } + + if (isJsDocTagName) { + return { symbols: undefined, isMemberCompletion: false, isNewIdentifierLocation: false, location: undefined, isRightOfDot: false, isJsDocTagName }; + } + + if (!insideJsDocTagExpression) { + // 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 + log("Returning an empty list because completion was inside a regular comment or plain text part of a JsDoc comment."); + return undefined; + } } start = new Date().getTime(); @@ -3068,7 +3146,7 @@ namespace ts { log("getCompletionData: Semantic work: " + (new Date().getTime() - semanticStart)); - return { symbols, isMemberCompletion, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag) }; + return { symbols, isMemberCompletion, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), isJsDocTagName }; function getTypeScriptMemberSymbols(): void { // Right of dot member completion list @@ -3708,9 +3786,14 @@ namespace ts { return undefined; } - let { symbols, isMemberCompletion, isNewIdentifierLocation, location, isRightOfDot } = completionData; + let { symbols, isMemberCompletion, isNewIdentifierLocation, location, isRightOfDot, isJsDocTagName } = completionData; let entries: CompletionEntry[]; + if (isJsDocTagName) { + // If the current position is a jsDoc tag name, only tag names should be provided for completion + return { isMemberCompletion: false, isNewIdentifierLocation: false, entries: getAllJsDocCompletionEntries() }; + } + if (isRightOfDot && isJavaScript(fileName)) { entries = getCompletionEntriesFromSymbols(symbols); addRange(entries, getJavaScriptCompletionEntries()); @@ -3724,7 +3807,7 @@ namespace ts { } // Add keywords if this is not a member completion list - if (!isMemberCompletion) { + if (!isMemberCompletion && !isJsDocTagName) { addRange(entries, keywordCompletions); } @@ -3757,6 +3840,17 @@ namespace ts { return entries; } + function getAllJsDocCompletionEntries(): CompletionEntry[] { + return jsDocCompletionEntries || (jsDocCompletionEntries = ts.map(jsDocTagNames, tagName => { + return { + name: tagName, + kind: ScriptElementKind.keyword, + kindModifiers: "", + sortText: "0", + } + })); + } + function createCompletionEntry(symbol: Symbol, location: Node): CompletionEntry { // Try to get a valid display name for this symbol, if we could not find one, then ignore it. // We would like to only show things that can be added after a dot, so for instance numeric properties can diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 04bfd04057c..d5ad93260cd 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -469,6 +469,39 @@ namespace ts { } } + /** + * Get the corresponding JSDocTag node if the position is in a jsDoc comment + */ + export function getJsDocTagAtPosition(sourceFile: SourceFile, position: number): JSDocTag { + let node = ts.getTokenAtPosition(sourceFile, position); + if (isToken(node)) { + switch (node.kind) { + case SyntaxKind.VarKeyword: + case SyntaxKind.LetKeyword: + case SyntaxKind.ConstKeyword: + // if the current token is var, let or const, skip the VariableDeclarationList + node = node.parent === undefined ? undefined : node.parent.parent; + break; + default: + node = node.parent; + break; + } + } + + if (node) { + let jsDocComment = node.jsDocComment; + if (jsDocComment) { + for (let tag of jsDocComment.tags) { + if (tag.pos <= position && position <= tag.end) { + return tag; + } + } + } + } + + return undefined; + } + function nodeHasTokens(n: Node): boolean { // If we have a token or node that has a non-zero width, it must have tokens. // Note, that getWidth() does not take trivia into account. @@ -640,7 +673,6 @@ namespace ts { else if (flags & SymbolFlags.TypeAlias) { return SymbolDisplayPartKind.aliasName; } else if (flags & SymbolFlags.Alias) { return SymbolDisplayPartKind.aliasName; } - return SymbolDisplayPartKind.text; } } diff --git a/tests/cases/fourslash/completionInJsDoc.ts b/tests/cases/fourslash/completionInJsDoc.ts new file mode 100644 index 00000000000..5424c8e79fa --- /dev/null +++ b/tests/cases/fourslash/completionInJsDoc.ts @@ -0,0 +1,62 @@ +/// + +// @allowNonTsExtensions: true +// @Filename: Foo.js +/////** @/*1*/ */ +////var v1; +//// +/////** @p/*2*/ */ +////var v2; +//// +/////** @param /*3*/ */ +////var v3; +//// +/////** @param { n/*4*/ } bar */ +////var v4; +//// +/////** @type { n/*5*/ } */ +////var v5; +//// +////// @/*6*/ +////var v6; +//// +////// @pa/*7*/ +////var v7; +//// +/////** @param { n/*8*/ } */ +////var v8; +//// +/////** @return { n/*9*/ } */ +////var v9; + +goTo.marker('1'); +verify.completionListContains("constructor"); +verify.completionListContains("param"); +verify.completionListContains("type"); + +goTo.marker('2'); +verify.completionListContains("constructor"); +verify.completionListContains("param"); +verify.completionListContains("type"); + +goTo.marker('3'); +verify.completionListIsEmpty(); + +goTo.marker('4'); +verify.completionListContains('number'); + +goTo.marker('5'); +verify.completionListContains('number'); + +goTo.marker('6'); +verify.completionListIsEmpty(); + +goTo.marker('7'); +verify.completionListIsEmpty(); + +goTo.marker('8'); +verify.completionListContains('number'); + +goTo.marker('9'); +verify.completionListContains('number'); +