Skip to content

Commit 9731010

Browse files
authored
Interactable parameter inlay hints (#54734)
1 parent b211fe9 commit 9731010

File tree

60 files changed

+1330
-171
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+1330
-171
lines changed

src/compiler/checker.ts

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1540,7 +1540,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
15401540
return node ? getTypeFromTypeNode(node) : errorType;
15411541
},
15421542
getParameterType: getTypeAtPosition,
1543-
getParameterIdentifierNameAtPosition,
1543+
getParameterIdentifierInfoAtPosition,
15441544
getPromisedTypeOfPromise,
15451545
getAwaitedType: type => getAwaitedType(type),
15461546
getReturnTypeOfSignature,
@@ -34835,18 +34835,24 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3483534835
return restParameter.escapedName;
3483634836
}
3483734837

34838-
function getParameterIdentifierNameAtPosition(signature: Signature, pos: number): [parameterName: __String, isRestParameter: boolean] | undefined {
34838+
function getParameterIdentifierInfoAtPosition(signature: Signature, pos: number): { parameter: Identifier, parameterName: __String, isRestParameter: boolean } | undefined {
3483934839
if (signature.declaration?.kind === SyntaxKind.JSDocFunctionType) {
3484034840
return undefined;
3484134841
}
3484234842
const paramCount = signature.parameters.length - (signatureHasRestParameter(signature) ? 1 : 0);
3484334843
if (pos < paramCount) {
3484434844
const param = signature.parameters[pos];
34845-
return isParameterDeclarationWithIdentifierName(param) ? [param.escapedName, false] : undefined;
34845+
const paramIdent = getParameterDeclarationIdentifier(param);
34846+
return paramIdent ? {
34847+
parameter: paramIdent,
34848+
parameterName: param.escapedName,
34849+
isRestParameter: false
34850+
} : undefined;
3484634851
}
3484734852

3484834853
const restParameter = signature.parameters[paramCount] || unknownSymbol;
34849-
if (!isParameterDeclarationWithIdentifierName(restParameter)) {
34854+
const restIdent = getParameterDeclarationIdentifier(restParameter);
34855+
if (!restIdent) {
3485034856
return undefined;
3485134857
}
3485234858

@@ -34856,20 +34862,23 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3485634862
const index = pos - paramCount;
3485734863
const associatedName = associatedNames?.[index];
3485834864
const isRestTupleElement = !!associatedName?.dotDotDotToken;
34859-
return associatedName ? [
34860-
getTupleElementLabel(associatedName),
34861-
isRestTupleElement
34862-
] : undefined;
34865+
34866+
if (associatedName) {
34867+
Debug.assert(isIdentifier(associatedName.name));
34868+
return { parameter: associatedName.name, parameterName: associatedName.name.escapedText, isRestParameter: isRestTupleElement };
34869+
}
34870+
34871+
return undefined;
3486334872
}
3486434873

3486534874
if (pos === paramCount) {
34866-
return [restParameter.escapedName, true];
34875+
return { parameter: restIdent, parameterName: restParameter.escapedName, isRestParameter: true };
3486734876
}
3486834877
return undefined;
3486934878
}
3487034879

34871-
function isParameterDeclarationWithIdentifierName(symbol: Symbol) {
34872-
return symbol.valueDeclaration && isParameter(symbol.valueDeclaration) && isIdentifier(symbol.valueDeclaration.name);
34880+
function getParameterDeclarationIdentifier(symbol: Symbol) {
34881+
return symbol.valueDeclaration && isParameter(symbol.valueDeclaration) && isIdentifier(symbol.valueDeclaration.name) && symbol.valueDeclaration.name;
3487334882
}
3487434883
function isValidDeclarationForTupleLabel(d: Declaration): d is NamedTupleMember | (ParameterDeclaration & { name: Identifier }) {
3487534884
return d.kind === SyntaxKind.NamedTupleMember || (isParameter(d) && d.name && isIdentifier(d.name));

src/compiler/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4992,7 +4992,7 @@ export interface TypeChecker {
49924992
* @internal
49934993
*/
49944994
getParameterType(signature: Signature, parameterIndex: number): Type;
4995-
/** @internal */ getParameterIdentifierNameAtPosition(signature: Signature, parameterIndex: number): [parameterName: __String, isRestParameter: boolean] | undefined;
4995+
/** @internal */ getParameterIdentifierInfoAtPosition(signature: Signature, parameterIndex: number): { parameter: Identifier, parameterName: __String, isRestParameter: boolean } | undefined;
49964996
getNullableType(type: Type, flags: TypeFlags): Type;
49974997
getNonNullableType(type: Type): Type;
49984998
/** @internal */ getNonOptionalType(type: Type): Type;
@@ -9986,6 +9986,7 @@ export interface UserPreferences {
99869986
readonly includeInlayPropertyDeclarationTypeHints?: boolean;
99879987
readonly includeInlayFunctionLikeReturnTypeHints?: boolean;
99889988
readonly includeInlayEnumMemberValueHints?: boolean;
9989+
readonly interactiveInlayHints?: boolean;
99899990
readonly allowRenameOfImportPath?: boolean;
99909991
readonly autoImportFileExcludePatterns?: string[];
99919992
readonly organizeImportsIgnoreCase?: "auto" | boolean;

src/harness/client.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -759,11 +759,24 @@ export class SessionClient implements LanguageService {
759759
const request = this.processRequest<protocol.InlayHintsRequest>(protocol.CommandTypes.ProvideInlayHints, args);
760760
const response = this.processResponse<protocol.InlayHintsResponse>(request);
761761

762-
return response.body!.map(item => ({ // TODO: GH#18217
763-
...item,
764-
kind: item.kind as InlayHintKind,
765-
position: this.lineOffsetToPosition(file, item.position),
766-
}));
762+
return response.body!.map(item => {
763+
const { text, position } = item;
764+
const hint = typeof text === "string" ? text : text.map(({ text, span }) => ({
765+
text,
766+
span: span && {
767+
start: this.lineOffsetToPosition(span.file, span.start),
768+
length: this.lineOffsetToPosition(span.file, span.end) - this.lineOffsetToPosition(span.file, span.start),
769+
},
770+
file: span && span.file
771+
}));
772+
773+
return ({
774+
...item,
775+
position: this.lineOffsetToPosition(file, position),
776+
text: hint,
777+
kind: item.kind as InlayHintKind
778+
});
779+
});
767780
}
768781

769782
private createFileLocationOrRangeRequestArgs(positionOrRange: number | TextRange, fileName: string): protocol.FileLocationOrRangeRequestArgs {

src/server/protocol.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2675,13 +2675,18 @@ export interface InlayHintsRequest extends Request {
26752675
}
26762676

26772677
export interface InlayHintItem {
2678-
text: string;
2678+
text: string | InlayHintItemDisplayPart[];
26792679
position: Location;
26802680
kind: InlayHintKind;
26812681
whitespaceBefore?: boolean;
26822682
whitespaceAfter?: boolean;
26832683
}
26842684

2685+
export interface InlayHintItemDisplayPart {
2686+
text: string;
2687+
span?: FileSpan;
2688+
}
2689+
26852690
export interface InlayHintsResponse extends Response {
26862691
body?: InlayHintItem[];
26872692
}
@@ -3536,6 +3541,8 @@ export interface UserPreferences {
35363541
readonly includeInlayPropertyDeclarationTypeHints?: boolean;
35373542
readonly includeInlayFunctionLikeReturnTypeHints?: boolean;
35383543
readonly includeInlayEnumMemberValueHints?: boolean;
3544+
readonly interactiveInlayHints?: boolean;
3545+
35393546
readonly autoImportFileExcludePatterns?: string[];
35403547

35413548
/**

src/server/session.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1844,10 +1844,23 @@ export class Session<TMessage = string> implements EventSender {
18441844
const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!;
18451845
const hints = project.getLanguageService().provideInlayHints(file, args, this.getPreferences(file));
18461846

1847-
return hints.map(hint => ({
1848-
...hint,
1849-
position: scriptInfo.positionToLineOffset(hint.position),
1850-
}));
1847+
return hints.map(hint => {
1848+
const { text, position } = hint;
1849+
const hintText = typeof text === "string" ? text : text.map(({ text, span, file }) => ({
1850+
text,
1851+
span: span && {
1852+
start: scriptInfo.positionToLineOffset(span.start),
1853+
end: scriptInfo.positionToLineOffset(span.start + span.length),
1854+
file: file!
1855+
}
1856+
}));
1857+
1858+
return {
1859+
...hint,
1860+
position: scriptInfo.positionToLineOffset(position),
1861+
text: hintText
1862+
};
1863+
});
18511864
}
18521865

18531866
private setCompilerOptionsForInferredProjects(args: protocol.SetCompilerOptionsForInferredProjectsArgs): void {

src/services/inlayHints.ts

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
ArrowFunction,
44
CallExpression,
55
createPrinterWithRemoveComments,
6+
createTextSpanFromNode,
67
Debug,
78
ElementFlags,
89
EmitHint,
@@ -23,6 +24,7 @@ import {
2324
hasContextSensitiveParameters,
2425
Identifier,
2526
InlayHint,
27+
InlayHintDisplayPart,
2628
InlayHintKind,
2729
InlayHintsContext,
2830
isArrowFunction,
@@ -60,6 +62,7 @@ import {
6062
Signature,
6163
skipParentheses,
6264
some,
65+
SourceFile,
6366
Symbol,
6467
SymbolFlags,
6568
SyntaxKind,
@@ -73,7 +76,7 @@ import {
7376
VariableDeclaration,
7477
} from "./_namespaces/ts";
7578

76-
const maxHintsLength = 30;
79+
const maxTypeHintLength = 30;
7780

7881
const leadingParameterNameCommentRegexFactory = (name: string) => {
7982
return new RegExp(`^\\s?/\\*\\*?\\s?${name}\\s?\\*\\/\\s?$`);
@@ -87,6 +90,10 @@ function shouldShowLiteralParameterNameHintsOnly(preferences: UserPreferences) {
8790
return preferences.includeInlayParameterNameHints === "literals";
8891
}
8992

93+
function shouldUseInteractiveInlayHints(preferences: UserPreferences) {
94+
return preferences.interactiveInlayHints === true;
95+
}
96+
9097
/** @internal */
9198
export function provideInlayHints(context: InlayHintsContext): InlayHint[] {
9299
const { file, program, span, cancellationToken, preferences } = context;
@@ -151,9 +158,17 @@ export function provideInlayHints(context: InlayHintsContext): InlayHint[] {
151158
return isArrowFunction(node) || isFunctionExpression(node) || isFunctionDeclaration(node) || isMethodDeclaration(node) || isGetAccessorDeclaration(node);
152159
}
153160

154-
function addParameterHints(text: string, position: number, isFirstVariadicArgument: boolean) {
161+
function addParameterHints(text: string, parameter: Identifier, position: number, isFirstVariadicArgument: boolean, sourceFile: SourceFile | undefined) {
162+
let hintText: string | InlayHintDisplayPart[] = `${isFirstVariadicArgument ? "..." : ""}${text}`;
163+
if (shouldUseInteractiveInlayHints(preferences)) {
164+
hintText = [getNodeDisplayPart(hintText, parameter, sourceFile!), { text: ":" }];
165+
}
166+
else {
167+
hintText += ":";
168+
}
169+
155170
result.push({
156-
text: `${isFirstVariadicArgument ? "..." : ""}${truncation(text, maxHintsLength)}:`,
171+
text: hintText,
157172
position,
158173
kind: InlayHintKind.Parameter,
159174
whitespaceAfter: true,
@@ -162,7 +177,7 @@ export function provideInlayHints(context: InlayHintsContext): InlayHint[] {
162177

163178
function addTypeHints(text: string, position: number) {
164179
result.push({
165-
text: `: ${truncation(text, maxHintsLength)}`,
180+
text: `: ${text.length > maxTypeHintLength ? text.substr(0, maxTypeHintLength - "...".length) + "..." : text}`,
166181
position,
167182
kind: InlayHintKind.Type,
168183
whitespaceBefore: true,
@@ -171,7 +186,7 @@ export function provideInlayHints(context: InlayHintsContext): InlayHint[] {
171186

172187
function addEnumMemberValueHints(text: string, position: number) {
173188
result.push({
174-
text: `= ${truncation(text, maxHintsLength)}`,
189+
text: `= ${text}`,
175190
position,
176191
kind: InlayHintKind.Enum,
177192
whitespaceBefore: true,
@@ -231,6 +246,7 @@ export function provideInlayHints(context: InlayHintsContext): InlayHint[] {
231246
}
232247

233248
let signatureParamPos = 0;
249+
const sourceFile = shouldUseInteractiveInlayHints(preferences) ? expr.getSourceFile() : undefined;
234250
for (const originalArg of args) {
235251
const arg = skipParentheses(originalArg);
236252
if (shouldShowLiteralParameterNameHintsOnly(preferences) && !isHintableLiteral(arg)) {
@@ -253,10 +269,10 @@ export function provideInlayHints(context: InlayHintsContext): InlayHint[] {
253269
}
254270
}
255271

256-
const identifierNameInfo = checker.getParameterIdentifierNameAtPosition(signature, signatureParamPos);
272+
const identifierInfo = checker.getParameterIdentifierInfoAtPosition(signature, signatureParamPos);
257273
signatureParamPos = signatureParamPos + (spreadArgs || 1);
258-
if (identifierNameInfo) {
259-
const [parameterName, isFirstVariadicArgument] = identifierNameInfo;
274+
if (identifierInfo) {
275+
const { parameter, parameterName, isRestParameter: isFirstVariadicArgument } = identifierInfo;
260276
const isParameterNameNotSameAsArgument = preferences.includeInlayParameterNameHintsWhenArgumentMatchesName || !identifierOrAccessExpressionPostfixMatchesParameterName(arg, parameterName);
261277
if (!isParameterNameNotSameAsArgument && !isFirstVariadicArgument) {
262278
continue;
@@ -267,7 +283,7 @@ export function provideInlayHints(context: InlayHintsContext): InlayHint[] {
267283
continue;
268284
}
269285

270-
addParameterHints(name, originalArg.getStart(), isFirstVariadicArgument);
286+
addParameterHints(name, parameter, originalArg.getStart(), isFirstVariadicArgument, sourceFile);
271287
}
272288
}
273289
}
@@ -394,13 +410,6 @@ export function provideInlayHints(context: InlayHintsContext): InlayHint[] {
394410
return printTypeInSingleLine(signatureParamType);
395411
}
396412

397-
function truncation(text: string, maxLength: number) {
398-
if (text.length > maxLength) {
399-
return text.substr(0, maxLength - "...".length) + "...";
400-
}
401-
return text;
402-
}
403-
404413
function printTypeInSingleLine(type: Type) {
405414
const flags = NodeBuilderFlags.IgnoreErrors | TypeFormatFlags.AllowUniqueESSymbolType | TypeFormatFlags.UseAliasDefinedOutsideCurrentScope;
406415
const printer = createPrinterWithRemoveComments();
@@ -423,4 +432,12 @@ export function provideInlayHints(context: InlayHintsContext): InlayHint[] {
423432
}
424433
return true;
425434
}
435+
436+
function getNodeDisplayPart(text: string, node: Node, sourceFile: SourceFile): InlayHintDisplayPart {
437+
return {
438+
text,
439+
span: createTextSpanFromNode(node, sourceFile),
440+
file: sourceFile.fileName
441+
};
442+
}
426443
}

src/services/types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -866,13 +866,19 @@ export const enum InlayHintKind {
866866
}
867867

868868
export interface InlayHint {
869-
text: string;
869+
text: string | InlayHintDisplayPart[];
870870
position: number;
871871
kind: InlayHintKind;
872872
whitespaceBefore?: boolean;
873873
whitespaceAfter?: boolean;
874874
}
875875

876+
export interface InlayHintDisplayPart {
877+
text: string;
878+
span?: TextSpan;
879+
file?: string;
880+
}
881+
876882
export interface TodoCommentDescriptor {
877883
text: string;
878884
priority: number;

tests/baselines/reference/api/tsserverlibrary.d.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2124,12 +2124,16 @@ declare namespace ts {
21242124
arguments: InlayHintsRequestArgs;
21252125
}
21262126
interface InlayHintItem {
2127-
text: string;
2127+
text: string | InlayHintItemDisplayPart[];
21282128
position: Location;
21292129
kind: InlayHintKind;
21302130
whitespaceBefore?: boolean;
21312131
whitespaceAfter?: boolean;
21322132
}
2133+
interface InlayHintItemDisplayPart {
2134+
text: string;
2135+
span?: FileSpan;
2136+
}
21332137
interface InlayHintsResponse extends Response {
21342138
body?: InlayHintItem[];
21352139
}
@@ -2832,6 +2836,7 @@ declare namespace ts {
28322836
readonly includeInlayPropertyDeclarationTypeHints?: boolean;
28332837
readonly includeInlayFunctionLikeReturnTypeHints?: boolean;
28342838
readonly includeInlayEnumMemberValueHints?: boolean;
2839+
readonly interactiveInlayHints?: boolean;
28352840
readonly autoImportFileExcludePatterns?: string[];
28362841
/**
28372842
* Indicates whether imports should be organized in a case-insensitive manner.
@@ -8400,6 +8405,7 @@ declare namespace ts {
84008405
readonly includeInlayPropertyDeclarationTypeHints?: boolean;
84018406
readonly includeInlayFunctionLikeReturnTypeHints?: boolean;
84028407
readonly includeInlayEnumMemberValueHints?: boolean;
8408+
readonly interactiveInlayHints?: boolean;
84038409
readonly allowRenameOfImportPath?: boolean;
84048410
readonly autoImportFileExcludePatterns?: string[];
84058411
readonly organizeImportsIgnoreCase?: "auto" | boolean;
@@ -10382,12 +10388,17 @@ declare namespace ts {
1038210388
Enum = "Enum"
1038310389
}
1038410390
interface InlayHint {
10385-
text: string;
10391+
text: string | InlayHintDisplayPart[];
1038610392
position: number;
1038710393
kind: InlayHintKind;
1038810394
whitespaceBefore?: boolean;
1038910395
whitespaceAfter?: boolean;
1039010396
}
10397+
interface InlayHintDisplayPart {
10398+
text: string;
10399+
span?: TextSpan;
10400+
file?: string;
10401+
}
1039110402
interface TodoCommentDescriptor {
1039210403
text: string;
1039310404
priority: number;

0 commit comments

Comments
 (0)