Skip to content

Commit f57bdaa

Browse files
authored
Add label details to completion entry (#48429)
* add label details to completion entry * Use label details for obj literal method completions * add label details support flag * add label details support to fourslash * support both label details and non-label details in object literal method snippets * CR fixes * fixes after rebasing * fix tsserver tests
1 parent e25f04a commit f57bdaa

15 files changed

+200
-10
lines changed

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8753,6 +8753,7 @@ namespace ts {
87538753
readonly includeCompletionsWithInsertText?: boolean;
87548754
readonly includeCompletionsWithClassMemberSnippets?: boolean;
87558755
readonly includeCompletionsWithObjectLiteralMethodSnippets?: boolean;
8756+
readonly useLabelDetailsInCompletionEntries?: boolean;
87568757
readonly allowIncompleteCompletions?: boolean;
87578758
readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative";
87588759
/** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */

src/harness/fourslashImpl.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -983,6 +983,8 @@ namespace FourSlash {
983983
assert.equal<boolean | undefined>(actual.isPackageJsonImport, expected.isPackageJsonImport, `At entry ${actual.name}: Expected 'isPackageJsonImport' properties to match`);
984984
}
985985

986+
assert.equal(actual.labelDetails?.description, expected.labelDetails?.description, `At entry ${actual.name}: Expected 'labelDetails.description' properties to match`);
987+
assert.equal(actual.labelDetails?.detail, expected.labelDetails?.detail, `At entry ${actual.name}: Expected 'labelDetails.detail' properties to match`);
986988
assert.equal(actual.hasAction, expected.hasAction, `At entry ${actual.name}: Expected 'hasAction' properties to match`);
987989
assert.equal(actual.isRecommended, expected.isRecommended, `At entry ${actual.name}: Expected 'isRecommended' properties to match'`);
988990
assert.equal(actual.isSnippet, expected.isSnippet, `At entry ${actual.name}: Expected 'isSnippet' properties to match`);

src/harness/fourslashInterfaceImpl.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1719,10 +1719,16 @@ namespace FourSlashInterface {
17191719
readonly text?: string;
17201720
readonly documentation?: string;
17211721
readonly sourceDisplay?: string;
1722+
readonly labelDetails?: ExpectedCompletionEntryLabelDetails;
17221723
readonly tags?: readonly ts.JSDocTagInfo[];
17231724
readonly sortText?: ts.Completions.SortText;
17241725
}
17251726

1727+
export interface ExpectedCompletionEntryLabelDetails {
1728+
detail?: string;
1729+
description?: string;
1730+
}
1731+
17261732
export type ExpectedExactCompletionsPlus = readonly ExpectedCompletionEntry[] & {
17271733
plusFunctionName: string,
17281734
plusArgument: readonly ExpectedCompletionEntry[]

src/server/protocol.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2290,6 +2290,10 @@ namespace ts.server.protocol {
22902290
* Human-readable description of the `source`.
22912291
*/
22922292
sourceDisplay?: SymbolDisplayPart[];
2293+
/**
2294+
* Additional details for the label.
2295+
*/
2296+
labelDetails?: CompletionEntryLabelDetails;
22932297
/**
22942298
* If true, this completion should be highlighted as recommended. There will only be one of these.
22952299
* This will be set when we know the user should write an expression with a certain type and that type is an enum or constructable class.
@@ -2319,6 +2323,21 @@ namespace ts.server.protocol {
23192323
data?: unknown;
23202324
}
23212325

2326+
export interface CompletionEntryLabelDetails {
2327+
/**
2328+
* An optional string which is rendered less prominently directly after
2329+
* {@link CompletionEntry.name name}, without any spacing. Should be
2330+
* used for function signatures or type annotations.
2331+
*/
2332+
detail?: string;
2333+
/**
2334+
* An optional string which is rendered less prominently after
2335+
* {@link CompletionEntryLabelDetails.detail}. Should be used for fully qualified
2336+
* names or file path.
2337+
*/
2338+
description?: string;
2339+
}
2340+
23222341
/**
23232342
* Additional completion entry details, available on demand
23242343
*/
@@ -3413,6 +3432,11 @@ namespace ts.server.protocol {
34133432
* in addition to `const objectLiteral: T = { foo }`.
34143433
*/
34153434
readonly includeCompletionsWithObjectLiteralMethodSnippets?: boolean;
3435+
/**
3436+
* Indicates whether {@link CompletionEntry.labelDetails completion entry label details} are supported.
3437+
* If not, contents of `labelDetails` may be included in the {@link CompletionEntry.name} property.
3438+
*/
3439+
readonly useLabelDetailsInCompletionEntries?: boolean;
34163440
readonly allowIncompleteCompletions?: boolean;
34173441
readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative";
34183442
/** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */

src/server/session.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1837,10 +1837,41 @@ namespace ts.server {
18371837
const prefix = args.prefix || "";
18381838
const entries = mapDefined<CompletionEntry, protocol.CompletionEntry>(completions.entries, entry => {
18391839
if (completions.isMemberCompletion || startsWith(entry.name.toLowerCase(), prefix.toLowerCase())) {
1840-
const { name, kind, kindModifiers, sortText, insertText, replacementSpan, hasAction, source, sourceDisplay, isSnippet, isRecommended, isPackageJsonImport, isImportStatementCompletion, data } = entry;
1840+
const {
1841+
name,
1842+
kind,
1843+
kindModifiers,
1844+
sortText,
1845+
insertText,
1846+
replacementSpan,
1847+
hasAction,
1848+
source,
1849+
sourceDisplay,
1850+
labelDetails,
1851+
isSnippet,
1852+
isRecommended,
1853+
isPackageJsonImport,
1854+
isImportStatementCompletion,
1855+
data } = entry;
18411856
const convertedSpan = replacementSpan ? toProtocolTextSpan(replacementSpan, scriptInfo) : undefined;
18421857
// Use `hasAction || undefined` to avoid serializing `false`.
1843-
return { name, kind, kindModifiers, sortText, insertText, replacementSpan: convertedSpan, isSnippet, hasAction: hasAction || undefined, source, sourceDisplay, isRecommended, isPackageJsonImport, isImportStatementCompletion, data };
1858+
return {
1859+
name,
1860+
kind,
1861+
kindModifiers,
1862+
sortText,
1863+
insertText,
1864+
replacementSpan: convertedSpan,
1865+
isSnippet,
1866+
hasAction: hasAction || undefined,
1867+
source,
1868+
sourceDisplay,
1869+
labelDetails,
1870+
isRecommended,
1871+
isPackageJsonImport,
1872+
isImportStatementCompletion,
1873+
data
1874+
};
18441875
}
18451876
});
18461877

src/services/completions.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ namespace ts.Completions {
9898
interface SymbolOriginInfoObjectLiteralMethod extends SymbolOriginInfo {
9999
importAdder: codefix.ImportAdder,
100100
insertText: string,
101-
sourceDisplay: SymbolDisplayPart[],
101+
labelDetails: CompletionEntryLabelDetails,
102102
isSnippet?: true,
103103
}
104104

@@ -706,6 +706,7 @@ namespace ts.Completions {
706706
let source = getSourceFromOrigin(origin);
707707
let sourceDisplay;
708708
let hasAction;
709+
let labelDetails;
709710

710711
const typeChecker = program.getTypeChecker();
711712
const insertQuestionDot = origin && originIsNullableMember(origin);
@@ -780,7 +781,11 @@ namespace ts.Completions {
780781

781782
if (origin && originIsObjectLiteralMethod(origin)) {
782783
let importAdder;
783-
({ insertText, isSnippet, importAdder, sourceDisplay } = origin);
784+
({ insertText, isSnippet, importAdder, labelDetails } = origin);
785+
if (!preferences.useLabelDetailsInCompletionEntries) {
786+
name = name + labelDetails.detail;
787+
labelDetails = undefined;
788+
}
784789
source = CompletionSource.ObjectLiteralMethodSnippet;
785790
sortText = SortText.SortBelow(sortText);
786791
if (importAdder.hasFixes()) {
@@ -842,6 +847,7 @@ namespace ts.Completions {
842847
insertText,
843848
replacementSpan,
844849
sourceDisplay,
850+
labelDetails,
845851
isSnippet,
846852
isPackageJsonImport: originIsPackageJsonImport(origin) || undefined,
847853
isImportStatementCompletion: !!importCompletionNode || undefined,
@@ -1066,7 +1072,7 @@ namespace ts.Completions {
10661072
options: CompilerOptions,
10671073
preferences: UserPreferences,
10681074
formatContext: formatting.FormatContext | undefined,
1069-
): { insertText: string, isSnippet?: true, importAdder: codefix.ImportAdder, sourceDisplay: SymbolDisplayPart[] } | undefined {
1075+
): { insertText: string, isSnippet?: true, importAdder: codefix.ImportAdder, labelDetails: CompletionEntryLabelDetails } | undefined {
10701076
const isSnippet = preferences.includeCompletionsWithSnippetText || undefined;
10711077
let insertText: string = name;
10721078

@@ -1092,16 +1098,24 @@ namespace ts.Completions {
10921098
insertText = printer.printSnippetList(ListFormat.CommaDelimited | ListFormat.AllowTrailingComma, factory.createNodeArray([method], /*hasTrailingComma*/ true), sourceFile);
10931099
}
10941100

1101+
const signaturePrinter = createPrinter({
1102+
removeComments: true,
1103+
module: options.module,
1104+
target: options.target,
1105+
omitTrailingSemicolon: true,
1106+
});
1107+
// The `labelDetails.detail` will be displayed right beside the method name,
1108+
// so we drop the name (and modifiers) from the signature.
10951109
const methodSignature = factory.createMethodSignature(
1096-
method.modifiers,
1097-
method.name,
1110+
/*modifiers*/ undefined,
1111+
/*name*/ "",
10981112
method.questionToken,
10991113
method.typeParameters,
11001114
method.parameters,
11011115
method.type);
1102-
const sourceDisplay = nodeToDisplayParts(methodSignature, enclosingDeclaration);
1116+
const labelDetails = { detail: signaturePrinter.printNode(EmitHint.Unspecified, methodSignature, sourceFile) };
11031117

1104-
return { isSnippet, insertText, importAdder, sourceDisplay };
1118+
return { isSnippet, insertText, importAdder, labelDetails };
11051119

11061120
};
11071121

src/services/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1232,6 +1232,7 @@ namespace ts {
12321232
hasAction?: true;
12331233
source?: string;
12341234
sourceDisplay?: SymbolDisplayPart[];
1235+
labelDetails?: CompletionEntryLabelDetails;
12351236
isRecommended?: true;
12361237
isFromUncheckedFile?: true;
12371238
isPackageJsonImport?: true;
@@ -1247,6 +1248,11 @@ namespace ts {
12471248
data?: CompletionEntryData;
12481249
}
12491250

1251+
export interface CompletionEntryLabelDetails {
1252+
detail?: string;
1253+
description?: string;
1254+
}
1255+
12501256
export interface CompletionEntryDetails {
12511257
name: string;
12521258
kind: ScriptElementKind;

src/testRunner/unittests/tsserver/completions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ namespace ts.projectSystem {
4242
source: "/a",
4343
sourceDisplay: undefined,
4444
isSnippet: undefined,
45-
data: { exportName: "foo", fileName: "/a.ts", ambientModuleName: undefined, isPackageJsonImport: undefined }
45+
data: { exportName: "foo", fileName: "/a.ts", ambientModuleName: undefined, isPackageJsonImport: undefined },
46+
labelDetails: undefined,
4647
};
4748

4849
// `data.exportMapKey` contains a SymbolId so should not be mocked up with an expected value here.

src/testRunner/unittests/tsserver/partialSemanticServer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import { something } from "something";
7575
data: undefined,
7676
sourceDisplay: undefined,
7777
isSnippet: undefined,
78+
labelDetails: undefined,
7879
};
7980
}
8081
});

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4100,6 +4100,7 @@ declare namespace ts {
41004100
readonly includeCompletionsWithInsertText?: boolean;
41014101
readonly includeCompletionsWithClassMemberSnippets?: boolean;
41024102
readonly includeCompletionsWithObjectLiteralMethodSnippets?: boolean;
4103+
readonly useLabelDetailsInCompletionEntries?: boolean;
41034104
readonly allowIncompleteCompletions?: boolean;
41044105
readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative";
41054106
/** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */
@@ -6485,6 +6486,7 @@ declare namespace ts {
64856486
hasAction?: true;
64866487
source?: string;
64876488
sourceDisplay?: SymbolDisplayPart[];
6489+
labelDetails?: CompletionEntryLabelDetails;
64886490
isRecommended?: true;
64896491
isFromUncheckedFile?: true;
64906492
isPackageJsonImport?: true;
@@ -6499,6 +6501,10 @@ declare namespace ts {
64996501
*/
65006502
data?: CompletionEntryData;
65016503
}
6504+
interface CompletionEntryLabelDetails {
6505+
detail?: string;
6506+
description?: string;
6507+
}
65026508
interface CompletionEntryDetails {
65036509
name: string;
65046510
kind: ScriptElementKind;
@@ -8708,6 +8714,10 @@ declare namespace ts.server.protocol {
87088714
* Human-readable description of the `source`.
87098715
*/
87108716
sourceDisplay?: SymbolDisplayPart[];
8717+
/**
8718+
* Additional details for the label.
8719+
*/
8720+
labelDetails?: CompletionEntryLabelDetails;
87118721
/**
87128722
* If true, this completion should be highlighted as recommended. There will only be one of these.
87138723
* This will be set when we know the user should write an expression with a certain type and that type is an enum or constructable class.
@@ -8736,6 +8746,20 @@ declare namespace ts.server.protocol {
87368746
*/
87378747
data?: unknown;
87388748
}
8749+
interface CompletionEntryLabelDetails {
8750+
/**
8751+
* An optional string which is rendered less prominently directly after
8752+
* {@link CompletionEntry.name name}, without any spacing. Should be
8753+
* used for function signatures or type annotations.
8754+
*/
8755+
detail?: string;
8756+
/**
8757+
* An optional string which is rendered less prominently after
8758+
* {@link CompletionEntryLabelDetails.detail}. Should be used for fully qualified
8759+
* names or file path.
8760+
*/
8761+
description?: string;
8762+
}
87398763
/**
87408764
* Additional completion entry details, available on demand
87418765
*/
@@ -9636,6 +9660,11 @@ declare namespace ts.server.protocol {
96369660
* in addition to `const objectLiteral: T = { foo }`.
96379661
*/
96389662
readonly includeCompletionsWithObjectLiteralMethodSnippets?: boolean;
9663+
/**
9664+
* Indicates whether {@link CompletionEntry.labelDetails completion entry label details} are supported.
9665+
* If not, contents of `labelDetails` may be included in the {@link CompletionEntry.name} property.
9666+
*/
9667+
readonly useLabelDetailsInCompletionEntries?: boolean;
96399668
readonly allowIncompleteCompletions?: boolean;
96409669
readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative";
96419670
/** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4100,6 +4100,7 @@ declare namespace ts {
41004100
readonly includeCompletionsWithInsertText?: boolean;
41014101
readonly includeCompletionsWithClassMemberSnippets?: boolean;
41024102
readonly includeCompletionsWithObjectLiteralMethodSnippets?: boolean;
4103+
readonly useLabelDetailsInCompletionEntries?: boolean;
41034104
readonly allowIncompleteCompletions?: boolean;
41044105
readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative";
41054106
/** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */
@@ -6485,6 +6486,7 @@ declare namespace ts {
64856486
hasAction?: true;
64866487
source?: string;
64876488
sourceDisplay?: SymbolDisplayPart[];
6489+
labelDetails?: CompletionEntryLabelDetails;
64886490
isRecommended?: true;
64896491
isFromUncheckedFile?: true;
64906492
isPackageJsonImport?: true;
@@ -6499,6 +6501,10 @@ declare namespace ts {
64996501
*/
65006502
data?: CompletionEntryData;
65016503
}
6504+
interface CompletionEntryLabelDetails {
6505+
detail?: string;
6506+
description?: string;
6507+
}
65026508
interface CompletionEntryDetails {
65036509
name: string;
65046510
kind: ScriptElementKind;

0 commit comments

Comments
 (0)