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
This commit is contained in:
Gabriela Araujo Britto 2022-03-30 16:45:56 -03:00 committed by GitHub
parent e25f04a30f
commit f57bdaa097
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 200 additions and 10 deletions

View File

@ -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" */

View File

@ -983,6 +983,8 @@ namespace FourSlash {
assert.equal<boolean | undefined>(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`);

View File

@ -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[]

View File

@ -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" */

View File

@ -1837,10 +1837,41 @@ namespace ts.server {
const prefix = args.prefix || "";
const entries = mapDefined<CompletionEntry, protocol.CompletionEntry>(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
};
}
});

View File

@ -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 };
};

View File

@ -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;

View File

@ -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.

View File

@ -75,6 +75,7 @@ import { something } from "something";
data: undefined,
sourceDisplay: undefined,
isSnippet: undefined,
labelDetails: undefined,
};
}
});

View File

@ -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" */

View File

@ -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;

View File

@ -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},",
},
],
});

View File

@ -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,

View File

@ -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",
},
},
],
});

View File

@ -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<JSDocTagInfo>;
readonly sourceDisplay?: string;
readonly labelDetails?: ExpectedCompletionEntryLabelDetails;
}
export interface ExpectedCompletionEntryLabelDetails {
detail?: string;
description?: string;
}
interface VerifySignatureHelpOptions {