mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-11-18 17:21:48 +00:00
Provide snippet completions for @param in JSDoc (#53260)
This commit is contained in:
committed by
GitHub
parent
a280cafbf8
commit
e83d61398e
@@ -45703,6 +45703,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
|
||||
return symbol ? getDeclaredTypeOfSymbol(symbol) : errorType;
|
||||
}
|
||||
|
||||
if (isBindingElement(node)) {
|
||||
return getTypeForVariableLikeDeclaration(node, /*includeOptionality*/ true, CheckMode.Normal) || errorType;
|
||||
}
|
||||
|
||||
if (isDeclaration(node)) {
|
||||
// In this case, we call getSymbolOfNode instead of getSymbolAtLocation because it is a declaration
|
||||
const symbol = getSymbolOfDeclaration(node);
|
||||
|
||||
+323
-3
@@ -3,6 +3,8 @@ import {
|
||||
addToSeen,
|
||||
append,
|
||||
BinaryExpression,
|
||||
BindingElement,
|
||||
BindingPattern,
|
||||
BreakOrContinueStatement,
|
||||
CancellationToken,
|
||||
canUsePropertyAccess,
|
||||
@@ -32,6 +34,7 @@ import {
|
||||
concatenate,
|
||||
ConstructorDeclaration,
|
||||
ContextFlags,
|
||||
countWhere,
|
||||
createModuleSpecifierResolutionHost,
|
||||
createPackageJsonImportFilter,
|
||||
createPrinter,
|
||||
@@ -44,6 +47,8 @@ import {
|
||||
Diagnostics,
|
||||
diagnosticToString,
|
||||
displayPart,
|
||||
DotDotDotToken,
|
||||
EmitFlags,
|
||||
EmitHint,
|
||||
EmitTextWriter,
|
||||
EntityName,
|
||||
@@ -79,6 +84,7 @@ import {
|
||||
getEscapedTextOfIdentifierOrLiteral,
|
||||
getExportInfoMap,
|
||||
getFormatCodeSettingsForWriting,
|
||||
getJSDocParameterTags,
|
||||
getLanguageVariant,
|
||||
getLeftmostAccessExpression,
|
||||
getLineAndCharacterOfPosition,
|
||||
@@ -309,6 +315,7 @@ import {
|
||||
ScriptElementKindModifier,
|
||||
ScriptTarget,
|
||||
SemanticMeaning,
|
||||
setEmitFlags,
|
||||
setSnippetElement,
|
||||
shouldUseUriStyleNodeCoreModules,
|
||||
SignatureHelp,
|
||||
@@ -344,6 +351,7 @@ import {
|
||||
tokenToString,
|
||||
tryCast,
|
||||
tryGetImportFromModuleSpecifier,
|
||||
tryGetTextOfPropertyName,
|
||||
Type,
|
||||
TypeChecker,
|
||||
TypeElement,
|
||||
@@ -669,9 +677,10 @@ export function getCompletionsAtPosition(
|
||||
|
||||
}
|
||||
|
||||
const compilerOptions = program.getCompilerOptions();
|
||||
const checker = program.getTypeChecker();
|
||||
// If the request is a continuation of an earlier `isIncomplete` response,
|
||||
// we can continue it from the cached previous response.
|
||||
const compilerOptions = program.getCompilerOptions();
|
||||
const incompleteCompletionsCache = preferences.allowIncompleteCompletions ? host.getIncompleteCompletionsCache?.() : undefined;
|
||||
if (incompleteCompletionsCache && completionKind === CompletionTriggerKind.TriggerForIncompleteCompletions && previousToken && isIdentifier(previousToken)) {
|
||||
const incompleteContinuation = continuePreviousIncompleteResponse(incompleteCompletionsCache, sourceFile, previousToken, program, host, preferences, cancellationToken, position);
|
||||
@@ -707,10 +716,26 @@ export function getCompletionsAtPosition(
|
||||
return response;
|
||||
case CompletionDataKind.JsDocTagName:
|
||||
// If the current position is a jsDoc tag name, only tag names should be provided for completion
|
||||
return jsdocCompletionInfo(JsDoc.getJSDocTagNameCompletions());
|
||||
return jsdocCompletionInfo([
|
||||
...JsDoc.getJSDocTagNameCompletions(),
|
||||
...getJSDocParameterCompletions(
|
||||
sourceFile,
|
||||
position,
|
||||
checker,
|
||||
compilerOptions,
|
||||
preferences,
|
||||
/*tagNameOnly*/ true)]);
|
||||
case CompletionDataKind.JsDocTag:
|
||||
// If the current position is a jsDoc tag, only tags should be provided for completion
|
||||
return jsdocCompletionInfo(JsDoc.getJSDocTagCompletions());
|
||||
return jsdocCompletionInfo([
|
||||
...JsDoc.getJSDocTagCompletions(),
|
||||
...getJSDocParameterCompletions(
|
||||
sourceFile,
|
||||
position,
|
||||
checker,
|
||||
compilerOptions,
|
||||
preferences,
|
||||
/*tagNameOnly*/ false)]);
|
||||
case CompletionDataKind.JsDocParameterName:
|
||||
return jsdocCompletionInfo(JsDoc.getJSDocParameterNameCompletions(completionData.tag));
|
||||
case CompletionDataKind.Keywords:
|
||||
@@ -827,6 +852,301 @@ function jsdocCompletionInfo(entries: CompletionEntry[]): CompletionInfo {
|
||||
return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries };
|
||||
}
|
||||
|
||||
function getJSDocParameterCompletions(
|
||||
sourceFile: SourceFile,
|
||||
position: number,
|
||||
checker: TypeChecker,
|
||||
options: CompilerOptions,
|
||||
preferences: UserPreferences,
|
||||
tagNameOnly: boolean): CompletionEntry[] {
|
||||
const currentToken = getTokenAtPosition(sourceFile, position);
|
||||
if (!isJSDocTag(currentToken) && !isJSDoc(currentToken)) {
|
||||
return [];
|
||||
}
|
||||
const jsDoc = isJSDoc(currentToken) ? currentToken : currentToken.parent;
|
||||
if (!isJSDoc(jsDoc)) {
|
||||
return [];
|
||||
}
|
||||
const func = jsDoc.parent;
|
||||
if (!isFunctionLike(func)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const isJs = isSourceFileJS(sourceFile);
|
||||
const isSnippet = preferences.includeCompletionsWithSnippetText || undefined;
|
||||
const paramTagCount = countWhere(jsDoc.tags, tag => isJSDocParameterTag(tag) && tag.getEnd() <= position);
|
||||
return mapDefined(func.parameters, param => {
|
||||
if (getJSDocParameterTags(param).length) {
|
||||
return undefined; // Parameter is already annotated.
|
||||
}
|
||||
if (isIdentifier(param.name)) { // Named parameter
|
||||
const tabstopCounter = { tabstop: 1 };
|
||||
const paramName = param.name.text;
|
||||
let displayText =
|
||||
getJSDocParamAnnotation(
|
||||
paramName,
|
||||
param.initializer,
|
||||
param.dotDotDotToken,
|
||||
isJs,
|
||||
/*isObject*/ false,
|
||||
/*isSnippet*/ false,
|
||||
checker,
|
||||
options,
|
||||
preferences);
|
||||
let snippetText = isSnippet
|
||||
? getJSDocParamAnnotation(
|
||||
paramName,
|
||||
param.initializer,
|
||||
param.dotDotDotToken,
|
||||
isJs,
|
||||
/*isObject*/ false,
|
||||
/*isSnippet*/ true,
|
||||
checker,
|
||||
options,
|
||||
preferences,
|
||||
tabstopCounter)
|
||||
: undefined;
|
||||
if (tagNameOnly) { // Remove `@`
|
||||
displayText = displayText.slice(1);
|
||||
if (snippetText) snippetText = snippetText.slice(1);
|
||||
}
|
||||
return {
|
||||
name: displayText,
|
||||
kind: ScriptElementKind.parameterElement,
|
||||
sortText: SortText.LocationPriority,
|
||||
insertText: isSnippet ? snippetText : undefined,
|
||||
isSnippet,
|
||||
};
|
||||
}
|
||||
else if (param.parent.parameters.indexOf(param) === paramTagCount) { // Destructuring parameter; do it positionally
|
||||
const paramPath = `param${paramTagCount}`;
|
||||
const displayTextResult =
|
||||
generateJSDocParamTagsForDestructuring(
|
||||
paramPath,
|
||||
param.name,
|
||||
param.initializer,
|
||||
param.dotDotDotToken,
|
||||
isJs,
|
||||
/*isSnippet*/ false,
|
||||
checker,
|
||||
options,
|
||||
preferences,);
|
||||
const snippetTextResult = isSnippet
|
||||
? generateJSDocParamTagsForDestructuring(
|
||||
paramPath,
|
||||
param.name,
|
||||
param.initializer,
|
||||
param.dotDotDotToken,
|
||||
isJs,
|
||||
/*isSnippet*/ true,
|
||||
checker,
|
||||
options,
|
||||
preferences,)
|
||||
: undefined;
|
||||
let displayText = displayTextResult.join(getNewLineCharacter(options) + "* ");
|
||||
let snippetText = snippetTextResult?.join(getNewLineCharacter(options) + "* ");
|
||||
if (tagNameOnly) { // Remove `@`
|
||||
displayText = displayText.slice(1);
|
||||
if (snippetText) snippetText = snippetText.slice(1);
|
||||
}
|
||||
return {
|
||||
name: displayText,
|
||||
kind: ScriptElementKind.parameterElement,
|
||||
sortText: SortText.LocationPriority,
|
||||
insertText: isSnippet ? snippetText : undefined,
|
||||
isSnippet,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function generateJSDocParamTagsForDestructuring(
|
||||
path: string,
|
||||
pattern: BindingPattern,
|
||||
initializer: Expression | undefined,
|
||||
dotDotDotToken: DotDotDotToken | undefined,
|
||||
isJs: boolean,
|
||||
isSnippet: boolean,
|
||||
checker: TypeChecker,
|
||||
options: CompilerOptions,
|
||||
preferences: UserPreferences): string[] {
|
||||
if (!isJs) {
|
||||
return [
|
||||
getJSDocParamAnnotation(
|
||||
path,
|
||||
initializer,
|
||||
dotDotDotToken,
|
||||
isJs,
|
||||
/*isObject*/ false,
|
||||
isSnippet,
|
||||
checker,
|
||||
options,
|
||||
preferences,
|
||||
{ tabstop: 1 })
|
||||
];
|
||||
}
|
||||
return patternWorker(path, pattern, initializer, dotDotDotToken, { tabstop: 1 });
|
||||
|
||||
function patternWorker(
|
||||
path: string,
|
||||
pattern: BindingPattern,
|
||||
initializer: Expression | undefined,
|
||||
dotDotDotToken: DotDotDotToken | undefined,
|
||||
counter: TabStopCounter): string[] {
|
||||
if (isObjectBindingPattern(pattern) && !dotDotDotToken) {
|
||||
const oldTabstop = counter.tabstop;
|
||||
const childCounter = { tabstop: oldTabstop };
|
||||
const rootParam =
|
||||
getJSDocParamAnnotation(
|
||||
path,
|
||||
initializer,
|
||||
dotDotDotToken,
|
||||
isJs,
|
||||
/*isObject*/ true,
|
||||
isSnippet,
|
||||
checker,
|
||||
options,
|
||||
preferences,
|
||||
childCounter);
|
||||
let childTags: string[] | undefined = [];
|
||||
for (const element of pattern.elements) {
|
||||
const elementTags = elementWorker(path, element, childCounter);
|
||||
if (!elementTags) {
|
||||
childTags = undefined;
|
||||
break;
|
||||
}
|
||||
else {
|
||||
childTags.push(...elementTags);
|
||||
}
|
||||
}
|
||||
if (childTags) {
|
||||
counter.tabstop = childCounter.tabstop;
|
||||
return [rootParam, ...childTags];
|
||||
}
|
||||
}
|
||||
return [
|
||||
getJSDocParamAnnotation(
|
||||
path,
|
||||
initializer,
|
||||
dotDotDotToken,
|
||||
isJs,
|
||||
/*isObject*/ false,
|
||||
isSnippet,
|
||||
checker,
|
||||
options,
|
||||
preferences,
|
||||
counter)
|
||||
];
|
||||
}
|
||||
|
||||
// Assumes binding element is inside object binding pattern.
|
||||
// We can't really deeply annotate an array binding pattern.
|
||||
function elementWorker(path: string, element: BindingElement, counter: TabStopCounter): string[] | undefined {
|
||||
if ((!element.propertyName && isIdentifier(element.name)) || isIdentifier(element.name)) { // `{ b }` or `{ b: newB }`
|
||||
const propertyName = element.propertyName ? tryGetTextOfPropertyName(element.propertyName) : element.name.text;
|
||||
if (!propertyName) {
|
||||
return undefined;
|
||||
}
|
||||
const paramName = `${path}.${propertyName}`;
|
||||
return [
|
||||
getJSDocParamAnnotation(
|
||||
paramName,
|
||||
element.initializer,
|
||||
element.dotDotDotToken,
|
||||
isJs,
|
||||
/*isObject*/ false,
|
||||
isSnippet,
|
||||
checker,
|
||||
options,
|
||||
preferences,
|
||||
counter)];
|
||||
}
|
||||
else if (element.propertyName) { // `{ b: {...} }` or `{ b: [...] }`
|
||||
const propertyName = tryGetTextOfPropertyName(element.propertyName);
|
||||
return propertyName
|
||||
&& patternWorker(`${path}.${propertyName}`, element.name, element.initializer, element.dotDotDotToken, counter);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
interface TabStopCounter {
|
||||
tabstop: number;
|
||||
}
|
||||
|
||||
function getJSDocParamAnnotation(
|
||||
paramName: string,
|
||||
initializer: Expression | undefined,
|
||||
dotDotDotToken: DotDotDotToken | undefined,
|
||||
isJs: boolean,
|
||||
isObject: boolean,
|
||||
isSnippet: boolean,
|
||||
checker: TypeChecker,
|
||||
options: CompilerOptions,
|
||||
preferences: UserPreferences,
|
||||
tabstopCounter?: TabStopCounter) {
|
||||
if (isSnippet) {
|
||||
Debug.assertIsDefined(tabstopCounter);
|
||||
}
|
||||
if (initializer) {
|
||||
paramName = getJSDocParamNameWithInitializer(paramName, initializer);
|
||||
}
|
||||
if (isSnippet) {
|
||||
paramName = escapeSnippetText(paramName);
|
||||
}
|
||||
if (isJs) {
|
||||
let type = "*";
|
||||
if (isObject) {
|
||||
Debug.assert(!dotDotDotToken, `Cannot annotate a rest parameter with type 'Object'.`);
|
||||
type = "Object";
|
||||
}
|
||||
else {
|
||||
if (initializer) {
|
||||
const inferredType = checker.getTypeAtLocation(initializer.parent);
|
||||
if (!(inferredType.flags & (TypeFlags.Any | TypeFlags.Void))) {
|
||||
const sourceFile = initializer.getSourceFile();
|
||||
const quotePreference = getQuotePreference(sourceFile, preferences);
|
||||
const builderFlags = (quotePreference === QuotePreference.Single ? NodeBuilderFlags.UseSingleQuotesForStringLiteralType : NodeBuilderFlags.None);
|
||||
const typeNode = checker.typeToTypeNode(inferredType, findAncestor(initializer, isFunctionLike), builderFlags);
|
||||
if (typeNode) {
|
||||
const printer = isSnippet
|
||||
? createSnippetPrinter({
|
||||
removeComments: true,
|
||||
module: options.module,
|
||||
target: options.target,
|
||||
})
|
||||
: createPrinter({
|
||||
removeComments: true,
|
||||
module: options.module,
|
||||
target: options.target
|
||||
});
|
||||
setEmitFlags(typeNode, EmitFlags.SingleLine);
|
||||
type = printer.printNode(EmitHint.Unspecified, typeNode, sourceFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isSnippet && type === "*") {
|
||||
type = `\${${tabstopCounter!.tabstop++}:${type}}`;
|
||||
}
|
||||
}
|
||||
const dotDotDot = !isObject && dotDotDotToken ? "..." : "";
|
||||
const description = isSnippet ? `\${${tabstopCounter!.tabstop++}}` : "";
|
||||
return `@param {${dotDotDot}${type}} ${paramName} ${description}`;
|
||||
}
|
||||
else {
|
||||
const description = isSnippet ? `\${${tabstopCounter!.tabstop++}}` : "";
|
||||
return `@param ${paramName} ${description}`;
|
||||
}
|
||||
}
|
||||
|
||||
function getJSDocParamNameWithInitializer(paramName: string, initializer: Expression): string {
|
||||
const initializerText = initializer.getText().trim();
|
||||
if (initializerText.includes("\n") || initializerText.length > 80) {
|
||||
return `[${paramName}]`;
|
||||
}
|
||||
return `[${paramName}=${initializerText}]`;
|
||||
}
|
||||
|
||||
function keywordToCompletionEntry(keyword: TokenSyntaxKind) {
|
||||
return {
|
||||
name: tokenToString(keyword)!,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,127 @@
|
||||
///<reference path="fourslash.ts" />
|
||||
|
||||
// @allowJs: true
|
||||
|
||||
// @Filename: a.ts
|
||||
//// /**
|
||||
//// * @para/*0*/
|
||||
//// */
|
||||
//// function printValue(value, maximumFractionDigits) {}
|
||||
////
|
||||
//// /**
|
||||
//// * @p/*a*/
|
||||
//// */
|
||||
//// function aa({ a = 1 }, b: string) {
|
||||
//// a;
|
||||
//// }
|
||||
////
|
||||
//// /**
|
||||
//// * /*b*/
|
||||
//// */
|
||||
//// function bb(b: string) {}
|
||||
////
|
||||
//// /**
|
||||
//// *
|
||||
//// * @p/*c*/
|
||||
//// */
|
||||
//// function cc({ b: { a, c } = { a: 1, c: 3 } }) {
|
||||
////
|
||||
//// }
|
||||
////
|
||||
//// /**
|
||||
//// *
|
||||
//// * @p/*d*/
|
||||
//// */
|
||||
//// function dd({ a: { b, c }, d: [e, f] }: { a: { b: number, c: number }, d: [string, string] }) {
|
||||
////
|
||||
//// }
|
||||
|
||||
// @Filename: b.js
|
||||
//// /**
|
||||
//// * @p/*ja*/
|
||||
//// */
|
||||
//// function aa({ a = 1 }, b) {
|
||||
//// a;
|
||||
//// }
|
||||
////
|
||||
//// /**
|
||||
//// * /*jb*/
|
||||
//// */
|
||||
//// function bb(b) {}
|
||||
////
|
||||
//// /**
|
||||
//// *
|
||||
//// * @p/*jc*/
|
||||
//// */
|
||||
//// function cc({ b: { a, c } = { a: 1, c: 3 } }) {
|
||||
////
|
||||
//// }
|
||||
////
|
||||
//// /**
|
||||
//// *
|
||||
//// * @p/*jd*/
|
||||
//// */
|
||||
//// function dd({ a: { b, c }, d: [e, f] }) {
|
||||
////
|
||||
//// }
|
||||
////
|
||||
//// const someconst = "aa";
|
||||
//// /**
|
||||
//// *
|
||||
//// * @p/*je*/
|
||||
//// */
|
||||
//// function ee({ [someconst]: b }) {
|
||||
////
|
||||
//// }
|
||||
////
|
||||
//// /**
|
||||
//// *
|
||||
//// * @p/*jf*/
|
||||
//// */
|
||||
//// function ff({ "a": b }) {
|
||||
////
|
||||
//// }
|
||||
////
|
||||
//// /**
|
||||
//// *
|
||||
//// * @p/*jg*/
|
||||
//// */
|
||||
//// function gg(a, { b }) {
|
||||
////
|
||||
//// }
|
||||
////
|
||||
//// /**
|
||||
//// *
|
||||
//// * @param {boolean} a a's description
|
||||
//// * @p/*jh*/
|
||||
//// */
|
||||
//// function hh(a, { b }) {
|
||||
////
|
||||
//// }
|
||||
//// /**
|
||||
//// *
|
||||
//// * @p/*ji*/
|
||||
//// */
|
||||
//// function ii({ b, ...c }, ...a) {}
|
||||
////
|
||||
//// /**
|
||||
//// *
|
||||
//// * @p/*jj*/
|
||||
//// */
|
||||
//// function jj(...{ length }) {}
|
||||
////
|
||||
//// /**
|
||||
//// *
|
||||
//// * @p/*jk*/
|
||||
//// */
|
||||
//// function kk(...a) {}
|
||||
////
|
||||
//// function reallylongfunctionnameabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl(a) {}
|
||||
//// /**
|
||||
//// *
|
||||
//// * @p/*jl*/
|
||||
//// */
|
||||
//// function ll(a = reallylongfunctionnameabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl("")) {}
|
||||
////}
|
||||
|
||||
verify.baselineCompletions();
|
||||
@@ -0,0 +1,39 @@
|
||||
///<reference path="fourslash.ts" />
|
||||
|
||||
// @allowJs: true
|
||||
|
||||
// @Filename: a.ts
|
||||
//// /**
|
||||
//// * /*b*/
|
||||
//// */
|
||||
//// function bb(b: string) {}
|
||||
|
||||
// @Filename: b.js
|
||||
//// /**
|
||||
//// * /*jb*/
|
||||
//// */
|
||||
//// function bb(b) {}
|
||||
////
|
||||
//// /**
|
||||
//// *
|
||||
//// * @p/*jc*/
|
||||
//// */
|
||||
//// function cc({ b: { a, c } = { a: 1, c: 3 } }) {
|
||||
////
|
||||
//// }
|
||||
////
|
||||
//// /**
|
||||
//// *
|
||||
//// * @p/*jd*/
|
||||
//// */
|
||||
//// function dd(...a) {}
|
||||
////
|
||||
//// /**
|
||||
//// * @p/*z*/
|
||||
//// */
|
||||
//// function zz(a = 3) {}
|
||||
|
||||
|
||||
verify.baselineCompletions({
|
||||
includeCompletionsWithSnippetText: true,
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
///<reference path="fourslash.ts" />
|
||||
|
||||
// Infer types from initializer
|
||||
|
||||
// @allowJs: true
|
||||
|
||||
// @Filename: a.js
|
||||
//// /**
|
||||
//// * @p/*z*/
|
||||
//// */
|
||||
//// function zz(a = 3) {}
|
||||
|
||||
//// /**
|
||||
//// * @p/*y*/
|
||||
//// */
|
||||
//// function yy({ a = 3 }) {}
|
||||
|
||||
//// /**
|
||||
//// * @p/*x*/
|
||||
//// */
|
||||
//// function xx({ a, o: { b, c: [d, e = 1] }}) {}
|
||||
|
||||
//// /**
|
||||
//// * @p/*w*/
|
||||
//// */
|
||||
//// function ww({ a, o: { b, c: [d, e] = [1, true] }}) {}
|
||||
|
||||
//// /**
|
||||
//// * @p/*v*/
|
||||
//// */
|
||||
//// function vv({ a = [1, true] }) {}
|
||||
|
||||
//// function random(a) { return a }
|
||||
//// /**
|
||||
//// * @p/*u*/
|
||||
//// */
|
||||
//// function uu({ a = random() }) {}
|
||||
|
||||
verify.baselineCompletions();
|
||||
Reference in New Issue
Block a user