Skip to content

Commit

Permalink
Add label details to completion entry (#48429)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
gabritto committed Mar 30, 2022
1 parent e25f04a commit f57bdaa
Show file tree
Hide file tree
Showing 15 changed files with 200 additions and 10 deletions.
1 change: 1 addition & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" */
Expand Down
2 changes: 2 additions & 0 deletions src/harness/fourslashImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
Expand Down
6 changes: 6 additions & 0 deletions src/harness/fourslashInterfaceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down
24 changes: 24 additions & 0 deletions src/server/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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" */
Expand Down
35 changes: 33 additions & 2 deletions src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
}
});

Expand Down
28 changes: 21 additions & 7 deletions src/services/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ namespace ts.Completions {
interface SymbolOriginInfoObjectLiteralMethod extends SymbolOriginInfo {
importAdder: codefix.ImportAdder,
insertText: string,
sourceDisplay: SymbolDisplayPart[],
labelDetails: CompletionEntryLabelDetails,
isSnippet?: true,
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -842,6 +847,7 @@ namespace ts.Completions {
insertText,
replacementSpan,
sourceDisplay,
labelDetails,
isSnippet,
isPackageJsonImport: originIsPackageJsonImport(origin) || undefined,
isImportStatementCompletion: !!importCompletionNode || undefined,
Expand Down Expand Up @@ -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;

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

};

Expand Down
6 changes: 6 additions & 0 deletions src/services/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1232,6 +1232,7 @@ namespace ts {
hasAction?: true;
source?: string;
sourceDisplay?: SymbolDisplayPart[];
labelDetails?: CompletionEntryLabelDetails;
isRecommended?: true;
isFromUncheckedFile?: true;
isPackageJsonImport?: true;
Expand All @@ -1247,6 +1248,11 @@ namespace ts {
data?: CompletionEntryData;
}

export interface CompletionEntryLabelDetails {
detail?: string;
description?: string;
}

export interface CompletionEntryDetails {
name: string;
kind: ScriptElementKind;
Expand Down
3 changes: 2 additions & 1 deletion src/testRunner/unittests/tsserver/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions src/testRunner/unittests/tsserver/partialSemanticServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import { something } from "something";
data: undefined,
sourceDisplay: undefined,
isSnippet: undefined,
labelDetails: undefined,
};
}
});
Expand Down
29 changes: 29 additions & 0 deletions tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" */
Expand Down Expand Up @@ -6485,6 +6486,7 @@ declare namespace ts {
hasAction?: true;
source?: string;
sourceDisplay?: SymbolDisplayPart[];
labelDetails?: CompletionEntryLabelDetails;
isRecommended?: true;
isFromUncheckedFile?: true;
isPackageJsonImport?: true;
Expand All @@ -6499,6 +6501,10 @@ declare namespace ts {
*/
data?: CompletionEntryData;
}
interface CompletionEntryLabelDetails {
detail?: string;
description?: string;
}
interface CompletionEntryDetails {
name: string;
kind: ScriptElementKind;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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" */
Expand Down
6 changes: 6 additions & 0 deletions tests/baselines/reference/api/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" */
Expand Down Expand Up @@ -6485,6 +6486,7 @@ declare namespace ts {
hasAction?: true;
source?: string;
sourceDisplay?: SymbolDisplayPart[];
labelDetails?: CompletionEntryLabelDetails;
isRecommended?: true;
isFromUncheckedFile?: true;
isPackageJsonImport?: true;
Expand All @@ -6499,6 +6501,10 @@ declare namespace ts {
*/
data?: CompletionEntryData;
}
interface CompletionEntryLabelDetails {
detail?: string;
description?: string;
}
interface CompletionEntryDetails {
name: string;
kind: ScriptElementKind;
Expand Down

0 comments on commit f57bdaa

Please sign in to comment.