Skip to content

Commit 62911ab

Browse files
Show nested JSDoc descriptions for destructured params in signature help
Signature help rendered an empty description for object-destructured parameters whose docs live on nested `@param parent.child` tags. Resolve each destructured property's documentation the same way quick info does on hover (parent object type -> property -> getDocumentationComment) and show it alongside the parameter's own doc. Fixes #24746 Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 7964e22 commit 62911ab

2 files changed

Lines changed: 86 additions & 1 deletion

File tree

src/services/signatureHelp.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,14 @@ import {
2828
getInvokedExpression,
2929
getPossibleGenericSignatures,
3030
getPossibleTypeArgumentsInfo,
31+
getTextOfIdentifierOrLiteral,
3132
Identifier,
3233
identity,
3334
InternalSymbolName,
3435
isArrayBindingPattern,
3536
isBinaryExpression,
3637
isBindingElement,
38+
isBindingPattern,
3739
isBlock,
3840
isCallOrNewExpression,
3941
isFunctionTypeNode,
@@ -45,6 +47,7 @@ import {
4547
isMethodDeclaration,
4648
isNoSubstitutionTemplateLiteral,
4749
isObjectBindingPattern,
50+
isOmittedExpression,
4851
isParameter,
4952
isPropertyAccessExpression,
5053
isSourceFile,
@@ -59,6 +62,7 @@ import {
5962
JsxTagNameExpression,
6063
last,
6164
lastOrUndefined,
65+
lineBreakPart,
6266
ListFormat,
6367
map,
6468
mapToDisplayParts,
@@ -85,6 +89,7 @@ import {
8589
SyntaxKind,
8690
TaggedTemplateExpression,
8791
TemplateExpression,
92+
textPart,
8893
TextSpan,
8994
tryCast,
9095
TupleTypeReference,
@@ -795,7 +800,43 @@ function createSignatureHelpParameterForParameter(parameter: Symbol, checker: Ty
795800
});
796801
const isOptional = checker.isOptionalParameter(parameter.valueDeclaration as ParameterDeclaration);
797802
const isRest = isTransientSymbol(parameter) && !!(parameter.links.checkFlags & CheckFlags.RestParameter);
798-
return { name: parameter.name, documentation: parameter.getDocumentationComment(checker), displayParts, isOptional, isRest };
803+
let documentation = parameter.getDocumentationComment(checker);
804+
// A destructured parameter (binding pattern) carries the per-property descriptions on nested
805+
// `@param parent.child` tags, which are not part of the parameter symbol's own documentation.
806+
// Surface those alongside the parameter doc, matching how quick info resolves them on hover.
807+
const destructuredDocumentation = getDestructuredParameterDocumentation(parameter, checker);
808+
if (destructuredDocumentation.length) {
809+
documentation = documentation.length
810+
? [...documentation, lineBreakPart(), ...destructuredDocumentation]
811+
: destructuredDocumentation;
812+
}
813+
return { name: parameter.name, documentation, displayParts, isOptional, isRest };
814+
}
815+
816+
function getDestructuredParameterDocumentation(parameter: Symbol, checker: TypeChecker): SymbolDisplayPart[] {
817+
const declaration = parameter.valueDeclaration;
818+
if (!declaration || !isParameter(declaration) || !isBindingPattern(declaration.name)) {
819+
return emptyArray;
820+
}
821+
const objectType = checker.getTypeAtLocation(declaration.name);
822+
const types = objectType.isUnion() ? objectType.types : [objectType];
823+
const parts: SymbolDisplayPart[] = [];
824+
for (const element of declaration.name.elements) {
825+
if (isOmittedExpression(element)) continue;
826+
const nameNode = element.propertyName || element.name;
827+
if (!isIdentifier(nameNode)) continue;
828+
const propertyName = getTextOfIdentifierOrLiteral(nameNode);
829+
const propertyDocumentation = firstDefined(types, type => {
830+
const property = type.getProperty(propertyName);
831+
const doc = property && property.getDocumentationComment(checker);
832+
return doc && doc.length ? doc : undefined;
833+
});
834+
if (propertyDocumentation) {
835+
if (parts.length) parts.push(lineBreakPart());
836+
parts.push(textPart(propertyName), textPart(": "), ...propertyDocumentation);
837+
}
838+
}
839+
return parts;
799840
}
800841

801842
function createSignatureHelpParameterForTypeParameter(typeParameter: TypeParameter, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile, printer: Printer): SignatureHelpParameter {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @allowJs: true
4+
// @checkJs: true
5+
// @Filename: a.js
6+
7+
/////**
8+
//// * @param {Object} opts The options bag.
9+
//// * @param {number} opts.id The numeric id.
10+
//// * @param {string} opts.label The display label.
11+
//// */
12+
////function withParentDoc({ id, label }) {}
13+
////withParentDoc(/*1*/);
14+
////
15+
/////**
16+
//// * @param {Object} opts
17+
//// * @param {number} opts.id The numeric id.
18+
//// * @param {string} opts.label The display label.
19+
//// */
20+
////function withoutParentDoc({ id, label }) {}
21+
////withoutParentDoc(/*2*/);
22+
23+
verify.signatureHelp(
24+
{
25+
marker: "1",
26+
parameterName: "__0",
27+
parameterDocComment: "The options bag.\nid: The numeric id.\nlabel: The display label.",
28+
tags: [
29+
{ name: "param", text: [{ text: "opts", kind: "parameterName" }, { text: " ", kind: "space" }, { text: "The options bag.", kind: "text" }] },
30+
{ name: "param", text: [{ text: "opts.id", kind: "parameterName" }, { text: " ", kind: "space" }, { text: "The numeric id.", kind: "text" }] },
31+
{ name: "param", text: [{ text: "opts.label", kind: "parameterName" }, { text: " ", kind: "space" }, { text: "The display label.", kind: "text" }] },
32+
],
33+
},
34+
{
35+
marker: "2",
36+
parameterName: "__0",
37+
parameterDocComment: "id: The numeric id.\nlabel: The display label.",
38+
tags: [
39+
{ name: "param", text: [{ text: "opts", kind: "text" }] },
40+
{ name: "param", text: [{ text: "opts.id", kind: "parameterName" }, { text: " ", kind: "space" }, { text: "The numeric id.", kind: "text" }] },
41+
{ name: "param", text: [{ text: "opts.label", kind: "parameterName" }, { text: " ", kind: "space" }, { text: "The display label.", kind: "text" }] },
42+
],
43+
},
44+
);

0 commit comments

Comments
 (0)