diff --git a/src/services/services.ts b/src/services/services.ts index dd2f0025c58..4cb9e6b994b 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -132,7 +132,7 @@ namespace ts { let emptyArray: any[] = []; - const JsDocTagNames: string[] = [ + const jsDocTagNames: string[] = [ "augments", "author", "argument", @@ -170,6 +170,7 @@ namespace ts { "type", "version" ]; + let jsDocCompletionEntries: CompletionEntry[]; function createNode(kind: SyntaxKind, pos: number, end: number, flags: NodeFlags, parent?: Node): NodeObject { let node = new (getNodeConstructor(kind))(); @@ -2957,6 +2958,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)); @@ -2967,12 +2970,22 @@ namespace ts { log("getCompletionData: Is inside comment: " + (new Date().getTime() - start)); if (insideComment) { + // 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: @@ -2983,7 +2996,14 @@ namespace ts { 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; } @@ -3072,7 +3092,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 @@ -3710,16 +3730,17 @@ namespace ts { let completionData = getCompletionData(fileName, position); if (!completionData) { - let entries = getJsDocCompletionEntries(fileName, position); - if (entries) { - return { isMemberCompletion: false, isNewIdentifierLocation: false, entries }; - } 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()); @@ -3733,7 +3754,7 @@ namespace ts { } // Add keywords if this is not a member completion list - if (!isMemberCompletion) { + if (!isMemberCompletion && !isJsDocTagName) { addRange(entries, keywordCompletions); } @@ -3766,37 +3787,15 @@ namespace ts { return entries; } - function getJsDocCompletionEntries(fileName: string, position: number): CompletionEntry[] { - let sourceFile = getValidSourceFile(fileName); - let hasJsDocComment = ts.hasDocComment(sourceFile, position); - if (!hasJsDocComment) { - return undefined; - } - - // Return a list of JsDoc tag names for completion if the current position is right after an '@' sign - if (sourceFile.text.charCodeAt(position - 1) === CharacterCodes.at) { - return getAllJsDocCompletionEntries(); - } - - // Also return tag names if the current position is inside a tag name, e.g. (at the location of '^') - // /** @par^ */ - let tag = getJsDocTagAtPosition(sourceFile, position); - if (tag) { - if (tag.atToken.end <= position && position <= tag.tagName.end) { - return getAllJsDocCompletionEntries(); - } - } - } - function getAllJsDocCompletionEntries(): CompletionEntry[] { - return ts.map(JsDocTagNames, tagName => { + return jsDocCompletionEntries || (jsDocCompletionEntries = ts.map(jsDocTagNames, tagName => { return { name: tagName, kind: ScriptElementKind.keyword, kindModifiers: "", sortText: "0", } - }); + })); } function createCompletionEntry(symbol: Symbol, location: Node): CompletionEntry { diff --git a/src/services/utilities.ts b/src/services/utilities.ts index e48eefd92ad..67d82f5e425 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -475,18 +475,21 @@ namespace ts { export function getJsDocTagAtPosition(sourceFile: SourceFile, position: number): JSDocTag { let node = ts.getTokenAtPosition(sourceFile, position); let jsDocComment: JSDocComment; - while (true) { - if (node === sourceFile || - node.kind === SyntaxKind.VariableStatement || - node.kind === SyntaxKind.FunctionDeclaration || - node.kind === SyntaxKind.Parameter || - node.jsDocComment) { - jsDocComment = node.jsDocComment; - break; - } - node = node.parent; + if (node.jsDocComment) { + jsDocComment = node.jsDocComment; } - + else { + while (node) { + if (node.kind === SyntaxKind.VariableStatement || + node.kind === SyntaxKind.FunctionDeclaration || + node.kind === SyntaxKind.Parameter) { + jsDocComment = node.jsDocComment; + break; + } + node = node.parent; + } + } + if (jsDocComment) { for (let tag of jsDocComment.tags) { if (tag.pos <= position && position <= tag.end) { @@ -494,6 +497,7 @@ namespace ts { } } } + return undefined; } diff --git a/tests/cases/fourslash/completionInJsDoc.ts b/tests/cases/fourslash/completionInJsDoc.ts index 45631c79ab1..493e63ec1b2 100644 --- a/tests/cases/fourslash/completionInJsDoc.ts +++ b/tests/cases/fourslash/completionInJsDoc.ts @@ -16,6 +16,12 @@ //// /////** @type { n/*5*/ } */ ////var v5; +//// +////// @/*6*/ +////var v6; +//// +////// @pa/*7*/ +////var v7; goTo.marker('1'); verify.completionListContains("constructor"); @@ -36,4 +42,9 @@ verify.completionListContains('number'); goTo.marker('5'); verify.completionListContains('number'); +goTo.marker('6'); +verify.completionListIsEmpty(); + +goTo.marker('7'); +verify.completionListIsEmpty();