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
This commit is contained in:
Andy 2017-06-07 12:28:52 -07:00 committed by GitHub
parent b57830f7f9
commit abb9681248
21 changed files with 198 additions and 122 deletions

View File

@ -523,8 +523,8 @@ namespace ts {
return result || array;
}
export function mapDefined<T>(array: ReadonlyArray<T>, mapFn: (x: T, i: number) => T | undefined): ReadonlyArray<T> {
const result: T[] = [];
export function mapDefined<T, U>(array: ReadonlyArray<T>, 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);

View File

@ -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 <Identifier>createMissingNode(SyntaxKind.Identifier, /*reportAtCurrentPosition*/ true, Diagnostics.Identifier_expected);
}
else {
parseErrorAtCurrentToken(Diagnostics.Identifier_expected);
return undefined;
}
}
const pos = scanner.getTokenPos();

View File

@ -425,7 +425,7 @@ namespace ts {
FirstNode = QualifiedName,
FirstJSDocNode = JSDocTypeExpression,
LastJSDocNode = JSDocLiteralType,
FirstJSDocTagNode = JSDocComment,
FirstJSDocTagNode = JSDocTag,
LastJSDocTagNode = JSDocLiteralType
}

View File

@ -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<T>(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[]) {

View File

@ -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;
}

View File

@ -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 = <JSDocTypeTag | JSDocParameterTag | JSDocReturnTag>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;
}
}
}

View File

@ -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.

View File

@ -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);
}

View File

@ -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.

View File

@ -40,6 +40,7 @@
"end": 27,
"text": "name1"
},
"isBracketed": false,
"comment": "Description"
},
"length": 1,

View File

@ -40,6 +40,7 @@
"end": 32,
"text": "name1"
},
"isBracketed": false,
"comment": "Description"
},
"length": 1,

View File

@ -40,6 +40,7 @@
"end": 29,
"text": "name1"
},
"isBracketed": false,
"comment": ""
},
"length": 1,

View File

@ -40,6 +40,7 @@
"end": 29,
"text": "name1"
},
"isBracketed": false,
"comment": "Description text follows"
},
"length": 1,

View File

@ -40,6 +40,7 @@
"end": 20,
"text": "name1"
},
"isBracketed": false,
"comment": ""
},
"length": 1,

View File

@ -40,6 +40,7 @@
"end": 20,
"text": "name1"
},
"isBracketed": false,
"comment": "Description"
},
"length": 1,

View File

@ -30,6 +30,7 @@
"end": 18,
"text": "foo"
},
"isBracketed": false,
"comment": ""
},
"length": 1,

View File

@ -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,

View File

@ -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,

View File

@ -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
}
]
},

View File

@ -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");

View File

@ -0,0 +1,29 @@
///<reference path="fourslash.ts" />
/////**
//// * @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"]);