mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-02-07 14:34:35 -06:00
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:
parent
b57830f7f9
commit
abb9681248
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -425,7 +425,7 @@ namespace ts {
|
||||
FirstNode = QualifiedName,
|
||||
FirstJSDocNode = JSDocTypeExpression,
|
||||
LastJSDocNode = JSDocLiteralType,
|
||||
FirstJSDocTagNode = JSDocComment,
|
||||
FirstJSDocTagNode = JSDocTag,
|
||||
LastJSDocTagNode = JSDocLiteralType
|
||||
}
|
||||
|
||||
|
||||
@ -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[]) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -40,6 +40,7 @@
|
||||
"end": 27,
|
||||
"text": "name1"
|
||||
},
|
||||
"isBracketed": false,
|
||||
"comment": "Description"
|
||||
},
|
||||
"length": 1,
|
||||
|
||||
@ -40,6 +40,7 @@
|
||||
"end": 32,
|
||||
"text": "name1"
|
||||
},
|
||||
"isBracketed": false,
|
||||
"comment": "Description"
|
||||
},
|
||||
"length": 1,
|
||||
|
||||
@ -40,6 +40,7 @@
|
||||
"end": 29,
|
||||
"text": "name1"
|
||||
},
|
||||
"isBracketed": false,
|
||||
"comment": ""
|
||||
},
|
||||
"length": 1,
|
||||
|
||||
@ -40,6 +40,7 @@
|
||||
"end": 29,
|
||||
"text": "name1"
|
||||
},
|
||||
"isBracketed": false,
|
||||
"comment": "Description text follows"
|
||||
},
|
||||
"length": 1,
|
||||
|
||||
@ -40,6 +40,7 @@
|
||||
"end": 20,
|
||||
"text": "name1"
|
||||
},
|
||||
"isBracketed": false,
|
||||
"comment": ""
|
||||
},
|
||||
"length": 1,
|
||||
|
||||
@ -40,6 +40,7 @@
|
||||
"end": 20,
|
||||
"text": "name1"
|
||||
},
|
||||
"isBracketed": false,
|
||||
"comment": "Description"
|
||||
},
|
||||
"length": 1,
|
||||
|
||||
@ -30,6 +30,7 @@
|
||||
"end": 18,
|
||||
"text": "foo"
|
||||
},
|
||||
"isBracketed": false,
|
||||
"comment": ""
|
||||
},
|
||||
"length": 1,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@ -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");
|
||||
|
||||
29
tests/cases/fourslash/jsdocParameterNameCompletion.ts
Normal file
29
tests/cases/fourslash/jsdocParameterNameCompletion.ts
Normal 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"]);
|
||||
Loading…
x
Reference in New Issue
Block a user