From abb9681248becf71eb035064e778c5ffb4ba6955 Mon Sep 17 00:00:00 2001 From: Andy Date: Wed, 7 Jun 2017 12:28:52 -0700 Subject: [PATCH] Support completions for JSDoc @param tag names (#16299) * Support completions for JSDoc @param tag names * Undo change to finishNode * Don't include trailing whitespace in @param range; instead, specialize getJsDocTagAtPosition --- src/compiler/core.ts | 4 +- src/compiler/parser.ts | 34 +++--- src/compiler/types.ts | 2 +- src/harness/fourslash.ts | 21 ++-- src/services/classifier.ts | 2 +- src/services/completions.ts | 101 +++++++++++++----- src/services/jsDoc.ts | 18 ++++ src/services/services.ts | 6 +- src/services/utilities.ts | 59 +++------- ...parsesCorrectly.argSynonymForParamTag.json | 1 + ...sCorrectly.argumentSynonymForParamTag.json | 1 + ...cComments.parsesCorrectly.oneParamTag.json | 1 + ...DocComments.parsesCorrectly.paramTag1.json | 1 + ...parsesCorrectly.paramTagNameThenType1.json | 1 + ...parsesCorrectly.paramTagNameThenType2.json | 1 + ...ents.parsesCorrectly.paramWithoutType.json | 1 + ...Comments.parsesCorrectly.twoParamTag2.json | 2 + ...parsesCorrectly.twoParamTagOnSameLine.json | 2 + ...sCorrectly.typedefTagWithChildrenTags.json | 6 +- .../cases/fourslash/commentsCommentParsing.ts | 27 ++--- .../fourslash/jsdocParameterNameCompletion.ts | 29 +++++ 21 files changed, 198 insertions(+), 122 deletions(-) create mode 100644 tests/cases/fourslash/jsdocParameterNameCompletion.ts diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 14da7dfa8f5..6ca99c2d086 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -523,8 +523,8 @@ namespace ts { return result || array; } - export function mapDefined(array: ReadonlyArray, mapFn: (x: T, i: number) => T | undefined): ReadonlyArray { - const result: T[] = []; + export function mapDefined(array: ReadonlyArray, mapFn: (x: T, i: number) => U | undefined): U[] { + const result: U[] = []; for (let i = 0; i < array.length; i++) { const item = array[i]; const mapped = mapFn(item, i); diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index b398ee66e0a..8002709f3f6 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -6663,14 +6663,12 @@ namespace ts { }); } - function parseBracketNameInPropertyAndParamTag() { - let name: Identifier; - let isBracketed: boolean; + function parseBracketNameInPropertyAndParamTag(): { name: Identifier, isBracketed: boolean } { // Looking for something like '[foo]' or 'foo' - if (parseOptionalToken(SyntaxKind.OpenBracketToken)) { - name = parseJSDocIdentifierName(); + const isBracketed = parseOptional(SyntaxKind.OpenBracketToken); + const name = parseJSDocIdentifierName(/*createIfMissing*/ true); + if (isBracketed) { skipWhitespace(); - isBracketed = true; // May have an optional default, e.g. '[foo = 42]' if (parseOptionalToken(SyntaxKind.EqualsToken)) { @@ -6679,9 +6677,7 @@ namespace ts { parseExpected(SyntaxKind.CloseBracketToken); } - else if (tokenIsIdentifierOrKeyword(token())) { - name = parseJSDocIdentifierName(); - } + return { name, isBracketed }; } @@ -6692,11 +6688,6 @@ namespace ts { const { name, isBracketed } = parseBracketNameInPropertyAndParamTag(); skipWhitespace(); - if (!name) { - parseErrorAtPosition(scanner.getStartPos(), 0, Diagnostics.Identifier_expected); - return undefined; - } - let preName: Identifier, postName: Identifier; if (typeExpression) { postName = name; @@ -6947,14 +6938,19 @@ namespace ts { return currentToken = scanner.scanJSDocToken(); } - function parseJSDocIdentifierName(): Identifier { - return createJSDocIdentifier(tokenIsIdentifierOrKeyword(token())); + function parseJSDocIdentifierName(createIfMissing = false): Identifier { + return createJSDocIdentifier(tokenIsIdentifierOrKeyword(token()), createIfMissing); } - function createJSDocIdentifier(isIdentifier: boolean): Identifier { + function createJSDocIdentifier(isIdentifier: boolean, createIfMissing: boolean): Identifier { if (!isIdentifier) { - parseErrorAtCurrentToken(Diagnostics.Identifier_expected); - return undefined; + if (createIfMissing) { + return createMissingNode(SyntaxKind.Identifier, /*reportAtCurrentPosition*/ true, Diagnostics.Identifier_expected); + } + else { + parseErrorAtCurrentToken(Diagnostics.Identifier_expected); + return undefined; + } } const pos = scanner.getTokenPos(); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 5efa10ef437..b9ecc52cee6 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -425,7 +425,7 @@ namespace ts { FirstNode = QualifiedName, FirstJSDocNode = JSDocTypeExpression, LastJSDocNode = JSDocLiteralType, - FirstJSDocTagNode = JSDocComment, + FirstJSDocTagNode = JSDocTag, LastJSDocTagNode = JSDocLiteralType } diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 2d2cb7f8453..f40d63709cb 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -1602,16 +1602,19 @@ namespace FourSlash { } private printMembersOrCompletions(info: ts.CompletionInfo) { + if (info === undefined) { return "No completion info."; } + const { entries } = info; + function pad(s: string, length: number) { return s + new Array(length - s.length + 1).join(" "); } function max(arr: T[], selector: (x: T) => number): number { return arr.reduce((prev, x) => Math.max(prev, selector(x)), 0); } - const longestNameLength = max(info.entries, m => m.name.length); - const longestKindLength = max(info.entries, m => m.kind.length); - info.entries.sort((m, n) => m.sortText > n.sortText ? 1 : m.sortText < n.sortText ? -1 : m.name > n.name ? 1 : m.name < n.name ? -1 : 0); - const membersString = info.entries.map(m => `${pad(m.name, longestNameLength)} ${pad(m.kind, longestKindLength)} ${m.kindModifiers}`).join("\n"); + const longestNameLength = max(entries, m => m.name.length); + const longestKindLength = max(entries, m => m.kind.length); + entries.sort((m, n) => m.sortText > n.sortText ? 1 : m.sortText < n.sortText ? -1 : m.name > n.name ? 1 : m.name < n.name ? -1 : 0); + const membersString = entries.map(m => `${pad(m.name, longestNameLength)} ${pad(m.kind, longestKindLength)} ${m.kindModifiers}`).join("\n"); Harness.IO.log(membersString); } @@ -2163,7 +2166,7 @@ namespace FourSlash { Harness.IO.log(this.spanInfoToString(this.getNameOrDottedNameSpan(pos), "**")); } - private verifyClassifications(expected: { classificationType: string; text: string; textSpan?: TextSpan }[], actual: ts.ClassifiedSpan[]) { + private verifyClassifications(expected: { classificationType: string; text: string; textSpan?: TextSpan }[], actual: ts.ClassifiedSpan[], sourceFileText: string) { if (actual.length !== expected.length) { this.raiseError("verifyClassifications failed - expected total classifications to be " + expected.length + ", but was " + actual.length + @@ -2203,9 +2206,11 @@ namespace FourSlash { }); function jsonMismatchString() { + const showActual = actual.map(({ classificationType, textSpan }) => + ({ classificationType, text: sourceFileText.slice(textSpan.start, textSpan.start + textSpan.length) })); return Harness.IO.newLine() + "expected: '" + Harness.IO.newLine() + stringify(expected) + "'" + Harness.IO.newLine() + - "actual: '" + Harness.IO.newLine() + stringify(actual) + "'"; + "actual: '" + Harness.IO.newLine() + stringify(showActual) + "'"; } } @@ -2228,14 +2233,14 @@ namespace FourSlash { const actual = this.languageService.getSemanticClassifications(this.activeFile.fileName, ts.createTextSpan(0, this.activeFile.content.length)); - this.verifyClassifications(expected, actual); + this.verifyClassifications(expected, actual, this.activeFile.content); } public verifySyntacticClassifications(expected: { classificationType: string; text: string }[]) { const actual = this.languageService.getSyntacticClassifications(this.activeFile.fileName, ts.createTextSpan(0, this.activeFile.content.length)); - this.verifyClassifications(expected, actual); + this.verifyClassifications(expected, actual, this.activeFile.content); } public verifyOutliningSpans(spans: TextSpan[]) { diff --git a/src/services/classifier.ts b/src/services/classifier.ts index beeddda434e..ca8cf52a09b 100644 --- a/src/services/classifier.ts +++ b/src/services/classifier.ts @@ -814,7 +814,7 @@ namespace ts { * False will mean that node is not classified and traverse routine should recurse into node contents. */ function tryClassifyNode(node: Node): boolean { - if (isJSDocTag(node)) { + if (isJSDocNode(node)) { return true; } diff --git a/src/services/completions.ts b/src/services/completions.ts index 71fe4ce1c78..99560ffb4d8 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -18,7 +18,7 @@ namespace ts.Completions { return undefined; } - const { symbols, isGlobalCompletion, isMemberCompletion, isNewIdentifierLocation, location, requestJsDocTagName, requestJsDocTag, hasFilteredClassMemberKeywords } = completionData; + const { symbols, isGlobalCompletion, isMemberCompletion, isNewIdentifierLocation, location, request, hasFilteredClassMemberKeywords } = completionData; if (sourceFile.languageVariant === LanguageVariant.JSX && location && location.parent && location.parent.kind === SyntaxKind.JsxClosingElement) { @@ -36,14 +36,15 @@ namespace ts.Completions { }]}; } - if (requestJsDocTagName) { - // If the current position is a jsDoc tag name, only tag names should be provided for completion - return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries: JsDoc.getJSDocTagNameCompletions() }; - } - - if (requestJsDocTag) { - // If the current position is a jsDoc tag, only tags should be provided for completion - return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries: JsDoc.getJSDocTagCompletions() }; + if (request) { + const entries = request.kind === "JsDocTagName" + // If the current position is a jsDoc tag name, only tag names should be provided for completion + ? JsDoc.getJSDocTagNameCompletions() + : request.kind === "JsDocTag" + // If the current position is a jsDoc tag, only tags should be provided for completion + ? JsDoc.getJSDocTagCompletions() + : JsDoc.getJSDocParameterNameCompletions(request.tag); + return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries }; } const entries: CompletionEntry[] = []; @@ -66,7 +67,7 @@ namespace ts.Completions { addRange(entries, classMemberKeywordCompletions); } // Add keywords if this is not a member completion list - else if (!isMemberCompletion && !requestJsDocTag && !requestJsDocTagName) { + else if (!isMemberCompletion) { addRange(entries, keywordCompletions); } @@ -347,16 +348,27 @@ namespace ts.Completions { return undefined; } - function getCompletionData(typeChecker: TypeChecker, log: (message: string) => void, sourceFile: SourceFile, position: number) { + interface CompletionData { + symbols: Symbol[]; + isGlobalCompletion: boolean; + isMemberCompletion: boolean; + isNewIdentifierLocation: boolean; + location: Node; + isRightOfDot: boolean; + request?: Request; + hasFilteredClassMemberKeywords: boolean; + } + type Request = { kind: "JsDocTagName" } | { kind: "JsDocTag" } | { kind: "JsDocParameterName", tag: JSDocParameterTag }; + + function getCompletionData(typeChecker: TypeChecker, log: (message: string) => void, sourceFile: SourceFile, position: number): CompletionData { const isJavaScriptFile = isSourceFileJavaScript(sourceFile); - // JsDoc tag-name is just the name of the JSDoc tagname (exclude "@") - let requestJsDocTagName = false; - // JsDoc tag includes both "@" and tag-name - let requestJsDocTag = false; + let request: Request | undefined; let start = timestamp(); - const currentToken = getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ false); // TODO: GH#15853 + const currentToken = getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ false); + // We will check for jsdoc comments with insideComment and getJsDocTagAtPosition. (TODO: that seems rather inefficient to check the same thing so many times.) + log("getCompletionData: Get current token: " + (timestamp() - start)); start = timestamp(); @@ -366,10 +378,10 @@ namespace ts.Completions { if (insideComment) { if (hasDocComment(sourceFile, position)) { - // The current position is next to the '@' sign, when no tag name being provided yet. - // Provide a full list of tag names if (sourceFile.text.charCodeAt(position - 1) === CharacterCodes.at) { - requestJsDocTagName = true; + // The current position is next to the '@' sign, when no tag name being provided yet. + // Provide a full list of tag names + request = { kind: "JsDocTagName" }; } else { // When completion is requested without "@", we will have check to make sure that @@ -389,7 +401,9 @@ namespace ts.Completions { // * |c| // */ const lineStart = getLineStartPositionForPosition(position, sourceFile); - requestJsDocTag = !(sourceFile.text.substring(lineStart, position).match(/[^\*|\s|(/\*\*)]/)); + if (!(sourceFile.text.substring(lineStart, position).match(/[^\*|\s|(/\*\*)]/))) { + request = { kind: "JsDocTag" }; + } } } @@ -397,10 +411,10 @@ namespace ts.Completions { // /** @type {number | string} */ // Completion should work in the brackets let insideJsDocTagExpression = false; - const tag = getJsDocTagAtPosition(sourceFile, position); + const tag = getJsDocTagAtPosition(currentToken, position); if (tag) { if (tag.tagName.pos <= position && position <= tag.tagName.end) { - requestJsDocTagName = true; + request = { kind: "JsDocTagName" }; } switch (tag.kind) { @@ -408,15 +422,18 @@ namespace ts.Completions { case SyntaxKind.JSDocParameterTag: case SyntaxKind.JSDocReturnTag: const tagWithExpression = tag; - if (tagWithExpression.typeExpression) { - insideJsDocTagExpression = tagWithExpression.typeExpression.pos < position && position < tagWithExpression.typeExpression.end; + if (tagWithExpression.typeExpression && tagWithExpression.typeExpression.pos < position && position < tagWithExpression.typeExpression.end) { + insideJsDocTagExpression = true; + } + else if (isJSDocParameterTag(tag) && (nodeIsMissing(tag.name) || tag.name.pos <= position && position <= tag.name.end)) { + request = { kind: "JsDocParameterName", tag }; } break; } } - if (requestJsDocTagName || requestJsDocTag) { - return { symbols: undefined, isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, location: undefined, isRightOfDot: false, requestJsDocTagName, requestJsDocTag, hasFilteredClassMemberKeywords: false }; + if (request) { + return { symbols: undefined, isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, location: undefined, isRightOfDot: false, request, hasFilteredClassMemberKeywords: false }; } if (!insideJsDocTagExpression) { @@ -553,7 +570,7 @@ namespace ts.Completions { log("getCompletionData: Semantic work: " + (timestamp() - semanticStart)); - return { symbols, isGlobalCompletion, isMemberCompletion, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), requestJsDocTagName, requestJsDocTag, hasFilteredClassMemberKeywords }; + return { symbols, isGlobalCompletion, isMemberCompletion, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), request, hasFilteredClassMemberKeywords }; function getTypeScriptMemberSymbols(): void { // Right of dot member completion list @@ -1518,4 +1535,34 @@ namespace ts.Completions { kind === SyntaxKind.EqualsEqualsEqualsToken || kind === SyntaxKind.ExclamationEqualsEqualsToken; } + + /** Get the corresponding JSDocTag node if the position is in a jsDoc comment */ + function getJsDocTagAtPosition(node: Node, position: number): JSDocTag | undefined { + const { jsDoc } = getJsDocHavingNode(node); + if (!jsDoc) return undefined; + + for (const { pos, end, tags } of jsDoc) { + if (!tags || position < pos || position > end) continue; + for (let i = tags.length - 1; i >= 0; i--) { + const tag = tags[i]; + if (position >= tag.pos) { + return tag; + } + } + } + } + + function getJsDocHavingNode(node: Node): Node { + if (!isToken(node)) return 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 + return node.parent.parent; + default: + return node.parent; + } + } } diff --git a/src/services/jsDoc.ts b/src/services/jsDoc.ts index 8ca55029603..baf9ae44b5f 100644 --- a/src/services/jsDoc.ts +++ b/src/services/jsDoc.ts @@ -132,6 +132,24 @@ namespace ts.JsDoc { })); } + export function getJSDocParameterNameCompletions(tag: JSDocParameterTag): CompletionEntry[] { + const nameThusFar = tag.name.text; + const jsdoc = tag.parent; + const fn = jsdoc.parent; + if (!ts.isFunctionLike(fn)) return []; + + return mapDefined(fn.parameters, param => { + if (!isIdentifier(param.name)) return undefined; + + const name = param.name.text; + if (jsdoc.tags.some(t => t !== tag && isJSDocParameterTag(t) && t.name.text === name) + || nameThusFar !== undefined && !startsWith(name, nameThusFar)) + return undefined; + + return { name, kind: ScriptElementKind.parameterElement, kindModifiers: "", sortText: "0" }; + }); + } + /** * Checks if position points to a valid position to add JSDoc comments, and if so, * returns the appropriate template. Otherwise returns an empty string. diff --git a/src/services/services.ts b/src/services/services.ts index 853803a2474..22fa67dd1c5 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -136,7 +136,7 @@ namespace ts { } private createChildren(sourceFile?: SourceFileLike) { - if (isJSDocTag(this)) { + if (this.kind === SyntaxKind.JSDocComment || isJSDocTag(this)) { /** Don't add trivia for "tokens" since this is in a comment. */ const children: Node[] = []; this.forEachChild(child => { children.push(child); }); @@ -146,9 +146,9 @@ namespace ts { const children: Node[] = []; scanner.setText((sourceFile || this.getSourceFile()).text); let pos = this.pos; - const useJSDocScanner = this.kind >= SyntaxKind.FirstJSDocTagNode && this.kind <= SyntaxKind.LastJSDocTagNode; + const useJSDocScanner = isJSDocNode(this); const processNode = (node: Node) => { - const isJSDocTagNode = isJSDocTag(node); + const isJSDocTagNode = isJSDocNode(node); if (!isJSDocTagNode && pos < node.pos) { pos = this.addSyntheticNodes(children, pos, node.pos, useJSDocScanner); } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 7a87c0939c7..ab958099c11 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -615,18 +615,21 @@ namespace ts { return getTouchingToken(sourceFile, position, includeJsDocComment, n => isPropertyName(n.kind)); } - /** Returns the token if position is in [start, end) or if position === end and includeItemAtEndPosition(token) === true */ - export function getTouchingToken(sourceFile: SourceFile, position: number, includeJsDocComment: boolean, includeItemAtEndPosition?: (n: Node) => boolean): Node { - return getTokenAtPositionWorker(sourceFile, position, /*allowPositionInLeadingTrivia*/ false, includeItemAtEndPosition, includeJsDocComment); + /** + * Returns the token if position is in [start, end). + * If position === end, returns the preceding token if includeItemAtEndPosition(previousToken) === true + */ + export function getTouchingToken(sourceFile: SourceFile, position: number, includeJsDocComment: boolean, includePrecedingTokenAtEndPosition?: (n: Node) => boolean): Node { + return getTokenAtPositionWorker(sourceFile, position, /*allowPositionInLeadingTrivia*/ false, includePrecedingTokenAtEndPosition, /*includeEndPosition*/ false, includeJsDocComment); } /** Returns a token if position is in [start-of-leading-trivia, end) */ - export function getTokenAtPosition(sourceFile: SourceFile, position: number, includeJsDocComment: boolean): Node { - return getTokenAtPositionWorker(sourceFile, position, /*allowPositionInLeadingTrivia*/ true, /*includeItemAtEndPosition*/ undefined, includeJsDocComment); + export function getTokenAtPosition(sourceFile: SourceFile, position: number, includeJsDocComment: boolean, includeEndPosition?: boolean): Node { + return getTokenAtPositionWorker(sourceFile, position, /*allowPositionInLeadingTrivia*/ true, /*includePrecedingTokenAtEndPosition*/ undefined, includeEndPosition, includeJsDocComment); } /** Get the token whose text contains the position */ - function getTokenAtPositionWorker(sourceFile: SourceFile, position: number, allowPositionInLeadingTrivia: boolean, includeItemAtEndPosition: (n: Node) => boolean, includeJsDocComment: boolean): Node { + function getTokenAtPositionWorker(sourceFile: SourceFile, position: number, allowPositionInLeadingTrivia: boolean, includePrecedingTokenAtEndPosition: (n: Node) => boolean, includeEndPosition: boolean, includeJsDocComment: boolean): Node { let current: Node = sourceFile; outer: while (true) { if (isToken(current)) { @@ -636,7 +639,7 @@ namespace ts { // find the child that contains 'position' for (const child of current.getChildren()) { - if (isJSDocNode(child) && !includeJsDocComment) { + if (!includeJsDocComment && isJSDocNode(child)) { continue; } @@ -646,13 +649,13 @@ namespace ts { } const end = child.getEnd(); - if (position < end || (position === end && child.kind === SyntaxKind.EndOfFileToken)) { + if (position < end || (position === end && (child.kind === SyntaxKind.EndOfFileToken || includeEndPosition))) { current = child; continue outer; } - else if (includeItemAtEndPosition && end === position) { + else if (includePrecedingTokenAtEndPosition && end === position) { const previousToken = findPrecedingToken(position, sourceFile, child); - if (previousToken && includeItemAtEndPosition(previousToken)) { + if (previousToken && includePrecedingTokenAtEndPosition(previousToken)) { return previousToken; } } @@ -901,42 +904,6 @@ 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, /*includeJsDocComment*/ false); - 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) { - if (node.jsDoc) { - for (const jsDoc of node.jsDoc) { - if (jsDoc.tags) { - for (const tag of jsDoc.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. diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argSynonymForParamTag.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argSynonymForParamTag.json index 92b9cb450e6..b47a07e8487 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argSynonymForParamTag.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argSynonymForParamTag.json @@ -40,6 +40,7 @@ "end": 27, "text": "name1" }, + "isBracketed": false, "comment": "Description" }, "length": 1, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argumentSynonymForParamTag.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argumentSynonymForParamTag.json index f398fb3af41..cf86fda872e 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argumentSynonymForParamTag.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.argumentSynonymForParamTag.json @@ -40,6 +40,7 @@ "end": 32, "text": "name1" }, + "isBracketed": false, "comment": "Description" }, "length": 1, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.oneParamTag.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.oneParamTag.json index e70fc95367f..506487232e0 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.oneParamTag.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.oneParamTag.json @@ -40,6 +40,7 @@ "end": 29, "text": "name1" }, + "isBracketed": false, "comment": "" }, "length": 1, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTag1.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTag1.json index 0dbdd83d2ba..abc116d7452 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTag1.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTag1.json @@ -40,6 +40,7 @@ "end": 29, "text": "name1" }, + "isBracketed": false, "comment": "Description text follows" }, "length": 1, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType1.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType1.json index 204d94779b3..9d66ca3dd55 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType1.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType1.json @@ -40,6 +40,7 @@ "end": 20, "text": "name1" }, + "isBracketed": false, "comment": "" }, "length": 1, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType2.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType2.json index 7c79459768b..01d08a103c1 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType2.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramTagNameThenType2.json @@ -40,6 +40,7 @@ "end": 20, "text": "name1" }, + "isBracketed": false, "comment": "Description" }, "length": 1, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramWithoutType.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramWithoutType.json index 2ff182483d9..ab15d24f618 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramWithoutType.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.paramWithoutType.json @@ -30,6 +30,7 @@ "end": 18, "text": "foo" }, + "isBracketed": false, "comment": "" }, "length": 1, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTag2.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTag2.json index b51ab3598e6..5ba60030f1b 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTag2.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTag2.json @@ -40,6 +40,7 @@ "end": 29, "text": "name1" }, + "isBracketed": false, "comment": "" }, "1": { @@ -79,6 +80,7 @@ "end": 55, "text": "name2" }, + "isBracketed": false, "comment": "" }, "length": 2, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTagOnSameLine.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTagOnSameLine.json index 3c20d5edcbc..7d26097bb34 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTagOnSameLine.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.twoParamTagOnSameLine.json @@ -40,6 +40,7 @@ "end": 29, "text": "name1" }, + "isBracketed": false, "comment": "" }, "1": { @@ -79,6 +80,7 @@ "end": 51, "text": "name2" }, + "isBracketed": false, "comment": "" }, "length": 2, diff --git a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.typedefTagWithChildrenTags.json b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.typedefTagWithChildrenTags.json index 7a8f9c4bcc9..13826b63d9c 100644 --- a/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.typedefTagWithChildrenTags.json +++ b/tests/baselines/reference/JSDocParsing/DocComments.parsesCorrectly.typedefTagWithChildrenTags.json @@ -103,7 +103,8 @@ "pos": 66, "end": 69, "text": "age" - } + }, + "isBracketed": false }, { "kind": "JSDocPropertyTag", @@ -141,7 +142,8 @@ "pos": 93, "end": 97, "text": "name" - } + }, + "isBracketed": false } ] }, diff --git a/tests/cases/fourslash/commentsCommentParsing.ts b/tests/cases/fourslash/commentsCommentParsing.ts index ce80fd519dd..f26d2f569f8 100644 --- a/tests/cases/fourslash/commentsCommentParsing.ts +++ b/tests/cases/fourslash/commentsCommentParsing.ts @@ -160,8 +160,8 @@ ////fo/*37q*/oBar(/*37*/"foo",/*38*/"bar"); /////** This is a comment */ ////var x; -/////** -//// * This is a comment +/////** +//// * This is a comment //// */ ////var y; /////** this is jsdoc style function with param tag as well as inline parameter help @@ -173,7 +173,7 @@ ////} /////*44*/jsD/*40q*/ocParamTest(/*40*/30, /*41*/40, /*42*/50, /*43*/60); /////** This is function comment -//// * And properly aligned comment +//// * And properly aligned comment //// */ ////function jsDocCommentAlignmentTest1() { ////} @@ -334,39 +334,40 @@ goTo.marker('27'); verify.completionListContains("multiply", "function multiply(a: number, b: number, c?: number, d?: any, e?: any): void", "This is multiplication function"); verify.completionListContains("f1", "function f1(a: number): any (+1 overload)", "fn f1 with number"); +const subtractDoc = "This is subtract function"; goTo.marker('28'); -verify.currentSignatureHelpDocCommentIs("This is subtract function{ () => string; } } f this is optional param f"); +verify.currentSignatureHelpDocCommentIs(subtractDoc); verify.currentParameterHelpArgumentDocCommentIs(""); verify.quickInfos({ "28q": [ "function subtract(a: number, b: number, c?: () => string, d?: () => string, e?: () => string, f?: () => string): void", - "This is subtract function{ () => string; } } f this is optional param f" + subtractDoc, ], "28aq": "(parameter) a: number" }); goTo.marker('29'); -verify.currentSignatureHelpDocCommentIs("This is subtract function{ () => string; } } f this is optional param f"); +verify.currentSignatureHelpDocCommentIs(subtractDoc); verify.currentParameterHelpArgumentDocCommentIs("this is about b"); verify.quickInfoAt("29aq", "(parameter) b: number", "this is about b"); goTo.marker('30'); -verify.currentSignatureHelpDocCommentIs("This is subtract function{ () => string; } } f this is optional param f"); +verify.currentSignatureHelpDocCommentIs(subtractDoc); verify.currentParameterHelpArgumentDocCommentIs("this is optional param c"); verify.quickInfoAt("30aq", "(parameter) c: () => string", "this is optional param c"); goTo.marker('31'); -verify.currentSignatureHelpDocCommentIs("This is subtract function{ () => string; } } f this is optional param f"); +verify.currentSignatureHelpDocCommentIs(subtractDoc); verify.currentParameterHelpArgumentDocCommentIs("this is optional param d"); verify.quickInfoAt("31aq", "(parameter) d: () => string", "this is optional param d"); goTo.marker('32'); -verify.currentSignatureHelpDocCommentIs("This is subtract function{ () => string; } } f this is optional param f"); +verify.currentSignatureHelpDocCommentIs(subtractDoc); verify.currentParameterHelpArgumentDocCommentIs("this is optional param e"); verify.quickInfoAt("32aq", "(parameter) e: () => string", "this is optional param e"); goTo.marker('33'); -verify.currentSignatureHelpDocCommentIs("This is subtract function{ () => string; } } f this is optional param f"); +verify.currentSignatureHelpDocCommentIs(subtractDoc); verify.currentParameterHelpArgumentDocCommentIs(""); verify.quickInfoAt("33aq", "(parameter) f: () => string"); @@ -454,11 +455,11 @@ verify.quickInfoAt("43aq", "(parameter) d: number"); goTo.marker('44'); verify.completionListContains("jsDocParamTest", "function jsDocParamTest(a: number, b: number, c: number, d: number): number", "this is jsdoc style function with param tag as well as inline parameter help"); verify.completionListContains("x", "var x: any", "This is a comment "); -verify.completionListContains("y", "var y: any", "This is a comment "); +verify.completionListContains("y", "var y: any", "This is a comment"); goTo.marker('45'); -verify.currentSignatureHelpDocCommentIs("This is function comment\nAnd properly aligned comment "); -verify.quickInfoAt("45q", "function jsDocCommentAlignmentTest1(): void", "This is function comment\nAnd properly aligned comment "); +verify.currentSignatureHelpDocCommentIs("This is function comment\nAnd properly aligned comment"); +verify.quickInfoAt("45q", "function jsDocCommentAlignmentTest1(): void", "This is function comment\nAnd properly aligned comment"); goTo.marker('46'); verify.currentSignatureHelpDocCommentIs("This is function comment\n And aligned with 4 space char margin"); diff --git a/tests/cases/fourslash/jsdocParameterNameCompletion.ts b/tests/cases/fourslash/jsdocParameterNameCompletion.ts new file mode 100644 index 00000000000..b379cb77135 --- /dev/null +++ b/tests/cases/fourslash/jsdocParameterNameCompletion.ts @@ -0,0 +1,29 @@ +/// + +/////** +//// * @param /*0*/ +//// */ +////function f(foo, bar) {} + +/////** +//// * @param foo +//// * @param /*1*/ +//// */ +////function g(foo, bar) {} + +/////** +//// * @param can/*2*/ +//// * @param cantaloupe +//// */ +////function h(cat, canary, canoodle, cantaloupe, zebra) {} + +/////** +//// * @param /*3*/ {string} /*4*/ +//// */ +////function i(foo, bar) {} + +verify.completionsAt("0", ["foo", "bar"]); +verify.completionsAt("1", ["bar"]); +verify.completionsAt("2", ["canary", "canoodle"]); +verify.completionsAt("3", ["foo", "bar"]); +verify.completionsAt("4", ["foo", "bar"]);