diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 1a9030bb0f55c..788853371b042 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -8753,6 +8753,7 @@ namespace ts { readonly includeCompletionsWithInsertText?: boolean; readonly includeCompletionsWithClassMemberSnippets?: boolean; readonly includeCompletionsWithObjectLiteralMethodSnippets?: boolean; + readonly useLabelDetailsInCompletionEntries?: boolean; readonly allowIncompleteCompletions?: boolean; readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative"; /** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */ diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index a24e1870beba2..71886f8ff5f26 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -983,6 +983,8 @@ namespace FourSlash { assert.equal(actual.isPackageJsonImport, expected.isPackageJsonImport, `At entry ${actual.name}: Expected 'isPackageJsonImport' properties to match`); } + assert.equal(actual.labelDetails?.description, expected.labelDetails?.description, `At entry ${actual.name}: Expected 'labelDetails.description' properties to match`); + assert.equal(actual.labelDetails?.detail, expected.labelDetails?.detail, `At entry ${actual.name}: Expected 'labelDetails.detail' properties to match`); assert.equal(actual.hasAction, expected.hasAction, `At entry ${actual.name}: Expected 'hasAction' properties to match`); assert.equal(actual.isRecommended, expected.isRecommended, `At entry ${actual.name}: Expected 'isRecommended' properties to match'`); assert.equal(actual.isSnippet, expected.isSnippet, `At entry ${actual.name}: Expected 'isSnippet' properties to match`); diff --git a/src/harness/fourslashInterfaceImpl.ts b/src/harness/fourslashInterfaceImpl.ts index 976fdf9220ba3..dfe3808c8e5d8 100644 --- a/src/harness/fourslashInterfaceImpl.ts +++ b/src/harness/fourslashInterfaceImpl.ts @@ -1719,10 +1719,16 @@ namespace FourSlashInterface { readonly text?: string; readonly documentation?: string; readonly sourceDisplay?: string; + readonly labelDetails?: ExpectedCompletionEntryLabelDetails; readonly tags?: readonly ts.JSDocTagInfo[]; readonly sortText?: ts.Completions.SortText; } + export interface ExpectedCompletionEntryLabelDetails { + detail?: string; + description?: string; + } + export type ExpectedExactCompletionsPlus = readonly ExpectedCompletionEntry[] & { plusFunctionName: string, plusArgument: readonly ExpectedCompletionEntry[] diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 35e941cabe253..426eccb2b1325 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -2290,6 +2290,10 @@ namespace ts.server.protocol { * Human-readable description of the `source`. */ sourceDisplay?: SymbolDisplayPart[]; + /** + * Additional details for the label. + */ + labelDetails?: CompletionEntryLabelDetails; /** * If true, this completion should be highlighted as recommended. There will only be one of these. * 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 { data?: unknown; } + export interface CompletionEntryLabelDetails { + /** + * An optional string which is rendered less prominently directly after + * {@link CompletionEntry.name name}, without any spacing. Should be + * used for function signatures or type annotations. + */ + detail?: string; + /** + * An optional string which is rendered less prominently after + * {@link CompletionEntryLabelDetails.detail}. Should be used for fully qualified + * names or file path. + */ + description?: string; + } + /** * Additional completion entry details, available on demand */ @@ -3413,6 +3432,11 @@ namespace ts.server.protocol { * in addition to `const objectLiteral: T = { foo }`. */ readonly includeCompletionsWithObjectLiteralMethodSnippets?: boolean; + /** + * Indicates whether {@link CompletionEntry.labelDetails completion entry label details} are supported. + * If not, contents of `labelDetails` may be included in the {@link CompletionEntry.name} property. + */ + readonly useLabelDetailsInCompletionEntries?: boolean; readonly allowIncompleteCompletions?: boolean; readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative"; /** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */ diff --git a/src/server/session.ts b/src/server/session.ts index 51585b02f169f..5123867f4334c 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1837,10 +1837,41 @@ namespace ts.server { const prefix = args.prefix || ""; const entries = mapDefined(completions.entries, entry => { if (completions.isMemberCompletion || startsWith(entry.name.toLowerCase(), prefix.toLowerCase())) { - const { name, kind, kindModifiers, sortText, insertText, replacementSpan, hasAction, source, sourceDisplay, isSnippet, isRecommended, isPackageJsonImport, isImportStatementCompletion, data } = entry; + const { + name, + kind, + kindModifiers, + sortText, + insertText, + replacementSpan, + hasAction, + source, + sourceDisplay, + labelDetails, + isSnippet, + isRecommended, + isPackageJsonImport, + isImportStatementCompletion, + data } = entry; const convertedSpan = replacementSpan ? toProtocolTextSpan(replacementSpan, scriptInfo) : undefined; // Use `hasAction || undefined` to avoid serializing `false`. - return { name, kind, kindModifiers, sortText, insertText, replacementSpan: convertedSpan, isSnippet, hasAction: hasAction || undefined, source, sourceDisplay, isRecommended, isPackageJsonImport, isImportStatementCompletion, data }; + return { + name, + kind, + kindModifiers, + sortText, + insertText, + replacementSpan: convertedSpan, + isSnippet, + hasAction: hasAction || undefined, + source, + sourceDisplay, + labelDetails, + isRecommended, + isPackageJsonImport, + isImportStatementCompletion, + data + }; } }); diff --git a/src/services/completions.ts b/src/services/completions.ts index f2f7e83d5fbb5..0e936a774d580 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -98,7 +98,7 @@ namespace ts.Completions { interface SymbolOriginInfoObjectLiteralMethod extends SymbolOriginInfo { importAdder: codefix.ImportAdder, insertText: string, - sourceDisplay: SymbolDisplayPart[], + labelDetails: CompletionEntryLabelDetails, isSnippet?: true, } @@ -706,6 +706,7 @@ namespace ts.Completions { let source = getSourceFromOrigin(origin); let sourceDisplay; let hasAction; + let labelDetails; const typeChecker = program.getTypeChecker(); const insertQuestionDot = origin && originIsNullableMember(origin); @@ -780,7 +781,11 @@ namespace ts.Completions { if (origin && originIsObjectLiteralMethod(origin)) { let importAdder; - ({ insertText, isSnippet, importAdder, sourceDisplay } = origin); + ({ insertText, isSnippet, importAdder, labelDetails } = origin); + if (!preferences.useLabelDetailsInCompletionEntries) { + name = name + labelDetails.detail; + labelDetails = undefined; + } source = CompletionSource.ObjectLiteralMethodSnippet; sortText = SortText.SortBelow(sortText); if (importAdder.hasFixes()) { @@ -842,6 +847,7 @@ namespace ts.Completions { insertText, replacementSpan, sourceDisplay, + labelDetails, isSnippet, isPackageJsonImport: originIsPackageJsonImport(origin) || undefined, isImportStatementCompletion: !!importCompletionNode || undefined, @@ -1066,7 +1072,7 @@ namespace ts.Completions { options: CompilerOptions, preferences: UserPreferences, formatContext: formatting.FormatContext | undefined, - ): { insertText: string, isSnippet?: true, importAdder: codefix.ImportAdder, sourceDisplay: SymbolDisplayPart[] } | undefined { + ): { insertText: string, isSnippet?: true, importAdder: codefix.ImportAdder, labelDetails: CompletionEntryLabelDetails } | undefined { const isSnippet = preferences.includeCompletionsWithSnippetText || undefined; let insertText: string = name; @@ -1092,16 +1098,24 @@ namespace ts.Completions { insertText = printer.printSnippetList(ListFormat.CommaDelimited | ListFormat.AllowTrailingComma, factory.createNodeArray([method], /*hasTrailingComma*/ true), sourceFile); } + const signaturePrinter = createPrinter({ + removeComments: true, + module: options.module, + target: options.target, + omitTrailingSemicolon: true, + }); + // The `labelDetails.detail` will be displayed right beside the method name, + // so we drop the name (and modifiers) from the signature. const methodSignature = factory.createMethodSignature( - method.modifiers, - method.name, + /*modifiers*/ undefined, + /*name*/ "", method.questionToken, method.typeParameters, method.parameters, method.type); - const sourceDisplay = nodeToDisplayParts(methodSignature, enclosingDeclaration); + const labelDetails = { detail: signaturePrinter.printNode(EmitHint.Unspecified, methodSignature, sourceFile) }; - return { isSnippet, insertText, importAdder, sourceDisplay }; + return { isSnippet, insertText, importAdder, labelDetails }; }; diff --git a/src/services/types.ts b/src/services/types.ts index a32fa6599fa22..19749416467ab 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -1232,6 +1232,7 @@ namespace ts { hasAction?: true; source?: string; sourceDisplay?: SymbolDisplayPart[]; + labelDetails?: CompletionEntryLabelDetails; isRecommended?: true; isFromUncheckedFile?: true; isPackageJsonImport?: true; @@ -1247,6 +1248,11 @@ namespace ts { data?: CompletionEntryData; } + export interface CompletionEntryLabelDetails { + detail?: string; + description?: string; + } + export interface CompletionEntryDetails { name: string; kind: ScriptElementKind; diff --git a/src/testRunner/unittests/tsserver/completions.ts b/src/testRunner/unittests/tsserver/completions.ts index e3f5652461445..ab84366efc933 100644 --- a/src/testRunner/unittests/tsserver/completions.ts +++ b/src/testRunner/unittests/tsserver/completions.ts @@ -42,7 +42,8 @@ namespace ts.projectSystem { source: "/a", sourceDisplay: undefined, isSnippet: undefined, - data: { exportName: "foo", fileName: "/a.ts", ambientModuleName: undefined, isPackageJsonImport: undefined } + data: { exportName: "foo", fileName: "/a.ts", ambientModuleName: undefined, isPackageJsonImport: undefined }, + labelDetails: undefined, }; // `data.exportMapKey` contains a SymbolId so should not be mocked up with an expected value here. diff --git a/src/testRunner/unittests/tsserver/partialSemanticServer.ts b/src/testRunner/unittests/tsserver/partialSemanticServer.ts index 290cd1cade215..2aee331883dcf 100644 --- a/src/testRunner/unittests/tsserver/partialSemanticServer.ts +++ b/src/testRunner/unittests/tsserver/partialSemanticServer.ts @@ -75,6 +75,7 @@ import { something } from "something"; data: undefined, sourceDisplay: undefined, isSnippet: undefined, + labelDetails: undefined, }; } }); diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 32bbdc4c79a39..e21f42f5bc214 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -4100,6 +4100,7 @@ declare namespace ts { readonly includeCompletionsWithInsertText?: boolean; readonly includeCompletionsWithClassMemberSnippets?: boolean; readonly includeCompletionsWithObjectLiteralMethodSnippets?: boolean; + readonly useLabelDetailsInCompletionEntries?: boolean; readonly allowIncompleteCompletions?: boolean; readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative"; /** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */ @@ -6485,6 +6486,7 @@ declare namespace ts { hasAction?: true; source?: string; sourceDisplay?: SymbolDisplayPart[]; + labelDetails?: CompletionEntryLabelDetails; isRecommended?: true; isFromUncheckedFile?: true; isPackageJsonImport?: true; @@ -6499,6 +6501,10 @@ declare namespace ts { */ data?: CompletionEntryData; } + interface CompletionEntryLabelDetails { + detail?: string; + description?: string; + } interface CompletionEntryDetails { name: string; kind: ScriptElementKind; @@ -8708,6 +8714,10 @@ declare namespace ts.server.protocol { * Human-readable description of the `source`. */ sourceDisplay?: SymbolDisplayPart[]; + /** + * Additional details for the label. + */ + labelDetails?: CompletionEntryLabelDetails; /** * If true, this completion should be highlighted as recommended. There will only be one of these. * 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 { */ data?: unknown; } + interface CompletionEntryLabelDetails { + /** + * An optional string which is rendered less prominently directly after + * {@link CompletionEntry.name name}, without any spacing. Should be + * used for function signatures or type annotations. + */ + detail?: string; + /** + * An optional string which is rendered less prominently after + * {@link CompletionEntryLabelDetails.detail}. Should be used for fully qualified + * names or file path. + */ + description?: string; + } /** * Additional completion entry details, available on demand */ @@ -9636,6 +9660,11 @@ declare namespace ts.server.protocol { * in addition to `const objectLiteral: T = { foo }`. */ readonly includeCompletionsWithObjectLiteralMethodSnippets?: boolean; + /** + * Indicates whether {@link CompletionEntry.labelDetails completion entry label details} are supported. + * If not, contents of `labelDetails` may be included in the {@link CompletionEntry.name} property. + */ + readonly useLabelDetailsInCompletionEntries?: boolean; readonly allowIncompleteCompletions?: boolean; readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative"; /** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */ diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 9f9f42ebdfa62..cb79446a1da5e 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -4100,6 +4100,7 @@ declare namespace ts { readonly includeCompletionsWithInsertText?: boolean; readonly includeCompletionsWithClassMemberSnippets?: boolean; readonly includeCompletionsWithObjectLiteralMethodSnippets?: boolean; + readonly useLabelDetailsInCompletionEntries?: boolean; readonly allowIncompleteCompletions?: boolean; readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative"; /** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */ @@ -6485,6 +6486,7 @@ declare namespace ts { hasAction?: true; source?: string; sourceDisplay?: SymbolDisplayPart[]; + labelDetails?: CompletionEntryLabelDetails; isRecommended?: true; isFromUncheckedFile?: true; isPackageJsonImport?: true; @@ -6499,6 +6501,10 @@ declare namespace ts { */ data?: CompletionEntryData; } + interface CompletionEntryLabelDetails { + detail?: string; + description?: string; + } interface CompletionEntryDetails { name: string; kind: ScriptElementKind; diff --git a/tests/cases/fourslash/completionsObjectLiteralMethod1.ts b/tests/cases/fourslash/completionsObjectLiteralMethod1.ts index e6fef55cfa4ff..2802c9b06a341 100644 --- a/tests/cases/fourslash/completionsObjectLiteralMethod1.ts +++ b/tests/cases/fourslash/completionsObjectLiteralMethod1.ts @@ -38,6 +38,7 @@ verify.completions({ includeCompletionsWithInsertText: true, includeCompletionsWithSnippetText: false, includeCompletionsWithObjectLiteralMethodSnippets: true, + useLabelDetailsInCompletionEntries: true, }, includes: [ { @@ -51,6 +52,9 @@ verify.completions({ completion.SortText.ObjectLiteralProperty(completion.SortText.LocationPriority, "bar")), source: completion.CompletionSource.ObjectLiteralMethodSnippet, insertText: "bar(x: number): void {\n},", + labelDetails: { + detail: "(x: number): void", + }, }, ], }); @@ -60,6 +64,7 @@ verify.completions({ includeCompletionsWithInsertText: true, includeCompletionsWithSnippetText: false, includeCompletionsWithObjectLiteralMethodSnippets: true, + useLabelDetailsInCompletionEntries: true, }, includes: [ { @@ -73,6 +78,9 @@ verify.completions({ completion.SortText.ObjectLiteralProperty(completion.SortText.LocationPriority, "bar")), source: completion.CompletionSource.ObjectLiteralMethodSnippet, insertText: "bar(x: number): void {\n},", + labelDetails: { + detail: "(x: number): void", + }, }, { name: "foo", @@ -85,6 +93,9 @@ verify.completions({ completion.SortText.ObjectLiteralProperty(completion.SortText.LocationPriority, "foo")), source: completion.CompletionSource.ObjectLiteralMethodSnippet, insertText: "foo(x: string): string {\n},", + labelDetails: { + detail: "(x: string): string", + }, }, ], }); @@ -110,6 +121,7 @@ verify.completions({ includeCompletionsWithInsertText: true, includeCompletionsWithSnippetText: false, includeCompletionsWithObjectLiteralMethodSnippets: true, + useLabelDetailsInCompletionEntries: true, }, includes: [ { @@ -123,6 +135,9 @@ verify.completions({ completion.SortText.ObjectLiteralProperty(completion.SortText.LocationPriority, "\"space bar\"")), source: completion.CompletionSource.ObjectLiteralMethodSnippet, insertText: "\"space bar\"(): string {\n},", + labelDetails: { + detail: "(): string", + }, }, ], }); @@ -132,6 +147,7 @@ verify.completions({ includeCompletionsWithInsertText: true, includeCompletionsWithSnippetText: true, includeCompletionsWithObjectLiteralMethodSnippets: true, + useLabelDetailsInCompletionEntries: true, }, includes: [ { @@ -146,6 +162,33 @@ verify.completions({ source: completion.CompletionSource.ObjectLiteralMethodSnippet, isSnippet: true, insertText: "bar(x: number): void {\n $0\n},", + labelDetails: { + detail: "(x: number): void", + }, + }, + ], +}); +verify.completions({ + marker: "a", + preferences: { + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: true, + includeCompletionsWithObjectLiteralMethodSnippets: true, + useLabelDetailsInCompletionEntries: false, + }, + includes: [ + { + name: "bar", + sortText: completion.SortText.ObjectLiteralProperty(completion.SortText.LocationPriority, "bar"), + insertText: undefined, + }, + { + name: "bar(x: number): void", + sortText: completion.SortText.SortBelow( + completion.SortText.ObjectLiteralProperty(completion.SortText.LocationPriority, "bar")), + source: completion.CompletionSource.ObjectLiteralMethodSnippet, + isSnippet: true, + insertText: "bar(x: number): void {\n $0\n},", }, ], }); diff --git a/tests/cases/fourslash/completionsObjectLiteralMethod2.ts b/tests/cases/fourslash/completionsObjectLiteralMethod2.ts index 08e2fa824a79c..6543e75337fb2 100644 --- a/tests/cases/fourslash/completionsObjectLiteralMethod2.ts +++ b/tests/cases/fourslash/completionsObjectLiteralMethod2.ts @@ -24,6 +24,7 @@ verify.completions({ includeCompletionsWithInsertText: true, includeCompletionsWithSnippetText: false, includeCompletionsWithObjectLiteralMethodSnippets: true, + useLabelDetailsInCompletionEntries: true, }, includes: [ { @@ -38,6 +39,9 @@ verify.completions({ source: completion.CompletionSource.ObjectLiteralMethodSnippet, insertText: "foo(f: IFoo): void {\n},", hasAction: true, + labelDetails: { + detail: "(f: IFoo): void", + }, }, ], }); @@ -47,6 +51,7 @@ verify.applyCodeActionFromCompletion("a", { includeCompletionsWithInsertText: true, includeCompletionsWithSnippetText: false, includeCompletionsWithObjectLiteralMethodSnippets: true, + useLabelDetailsInCompletionEntries: true, }, name: "foo", source: completion.CompletionSource.ObjectLiteralMethodSnippet, diff --git a/tests/cases/fourslash/completionsObjectLiteralMethod3.ts b/tests/cases/fourslash/completionsObjectLiteralMethod3.ts index 96aa3467540f1..170df601ac1d1 100644 --- a/tests/cases/fourslash/completionsObjectLiteralMethod3.ts +++ b/tests/cases/fourslash/completionsObjectLiteralMethod3.ts @@ -39,6 +39,7 @@ verify.completions({ includeCompletionsWithInsertText: true, includeCompletionsWithSnippetText: false, includeCompletionsWithObjectLiteralMethodSnippets: true, + useLabelDetailsInCompletionEntries: true, }, includes: [ { @@ -52,6 +53,9 @@ verify.completions({ completion.SortText.ObjectLiteralProperty(completion.SortText.LocationPriority, "M")), source: completion.CompletionSource.ObjectLiteralMethodSnippet, insertText: "M(x: number): void {\n},", + labelDetails: { + detail: "(x: number): void", + }, }, ], }); @@ -93,6 +97,7 @@ verify.completions({ includeCompletionsWithInsertText: true, includeCompletionsWithSnippetText: false, includeCompletionsWithObjectLiteralMethodSnippets: true, + useLabelDetailsInCompletionEntries: true, }, includes: [ { @@ -106,6 +111,9 @@ verify.completions({ completion.SortText.ObjectLiteralProperty(completion.SortText.OptionalMember, "M")), source: completion.CompletionSource.ObjectLiteralMethodSnippet, insertText: "M(x: number): void {\n},", + labelDetails: { + detail: "(x: number): void", + }, }, { name: "N", @@ -118,6 +126,9 @@ verify.completions({ completion.SortText.ObjectLiteralProperty(completion.SortText.LocationPriority, "N")), source: completion.CompletionSource.ObjectLiteralMethodSnippet, insertText: "N(x: string): void {\n},", + labelDetails: { + detail: "(x: string): void", + }, }, { name: "O", @@ -130,6 +141,9 @@ verify.completions({ completion.SortText.ObjectLiteralProperty(completion.SortText.OptionalMember, "O")), source: completion.CompletionSource.ObjectLiteralMethodSnippet, insertText: "O(): void {\n},", + labelDetails: { + detail: "(): void", + }, }, ], }); \ No newline at end of file diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 147bb243091fe..a02e06a574c05 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -648,6 +648,7 @@ declare namespace FourSlashInterface { readonly includeCompletionsWithInsertText?: boolean; readonly includeCompletionsWithClassMemberSnippets?: boolean; readonly includeCompletionsWithObjectLiteralMethodSnippets?: boolean; + readonly useLabelDetailsInCompletionEntries?: boolean; readonly allowIncompleteCompletions?: boolean; /** @deprecated use `includeCompletionsWithInsertText` */ readonly includeInsertTextCompletions?: boolean; @@ -699,6 +700,12 @@ declare namespace FourSlashInterface { readonly documentation?: string; readonly tags?: ReadonlyArray; readonly sourceDisplay?: string; + readonly labelDetails?: ExpectedCompletionEntryLabelDetails; + } + + export interface ExpectedCompletionEntryLabelDetails { + detail?: string; + description?: string; } interface VerifySignatureHelpOptions {