Skip to content

Commit 993c503

Browse files
authored
Add 'data' property to completion entry for better coordination between completions and completion details (microsoft#42890)
* Add 'data' property to completion entry for better cooperation between completions and completion details * Add doc comment * Update API baselines * Add server test * Test session’s Full result * Fix tests * stableSort to fix server fourslash test * Explicit verification of data parameter
1 parent 41b5abf commit 993c503

22 files changed

+348
-69
lines changed

src/compiler/checker.ts

+1
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,7 @@ namespace ts {
600600
},
601601
tryGetMemberInModuleExports: (name, symbol) => tryGetMemberInModuleExports(escapeLeadingUnderscores(name), symbol),
602602
tryGetMemberInModuleExportsAndProperties: (name, symbol) => tryGetMemberInModuleExportsAndProperties(escapeLeadingUnderscores(name), symbol),
603+
tryFindAmbientModule: moduleName => tryFindAmbientModule(moduleName, /*withAugmentations*/ true),
603604
tryFindAmbientModuleWithoutAugmentations: moduleName => {
604605
// we deliberately exclude augmentations
605606
// since we are only interested in declarations of the module itself

src/compiler/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4165,6 +4165,7 @@ namespace ts {
41654165
/* @internal */ createSymbol(flags: SymbolFlags, name: __String): TransientSymbol;
41664166
/* @internal */ createIndexInfo(type: Type, isReadonly: boolean, declaration?: SignatureDeclaration): IndexInfo;
41674167
/* @internal */ isSymbolAccessible(symbol: Symbol, enclosingDeclaration: Node | undefined, meaning: SymbolFlags, shouldComputeAliasToMarkVisible: boolean): SymbolAccessibilityResult;
4168+
/* @internal */ tryFindAmbientModule(moduleName: string): Symbol | undefined;
41684169
/* @internal */ tryFindAmbientModuleWithoutAugmentations(moduleName: string): Symbol | undefined;
41694170

41704171
/* @internal */ getSymbolWalker(accept?: (symbol: Symbol) => boolean): SymbolWalker;

src/harness/client.ts

+8-9
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,9 @@ namespace ts.server {
205205
isNewIdentifierLocation: false,
206206
entries: response.body!.map<CompletionEntry>(entry => { // TODO: GH#18217
207207
if (entry.replacementSpan !== undefined) {
208-
const { name, kind, kindModifiers, sortText, replacementSpan, hasAction, source, isRecommended } = entry;
208+
const { name, kind, kindModifiers, sortText, replacementSpan, hasAction, source, data, isRecommended } = entry;
209209
// TODO: GH#241
210-
const res: CompletionEntry = { name, kind, kindModifiers, sortText, replacementSpan: this.decodeSpan(replacementSpan, fileName), hasAction, source, isRecommended };
210+
const res: CompletionEntry = { name, kind, kindModifiers, sortText, replacementSpan: this.decodeSpan(replacementSpan, fileName), hasAction, source, data: data as any, isRecommended };
211211
return res;
212212
}
213213

@@ -216,14 +216,13 @@ namespace ts.server {
216216
};
217217
}
218218

219-
getCompletionEntryDetails(fileName: string, position: number, entryName: string, _options: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined): CompletionEntryDetails {
220-
const args: protocol.CompletionDetailsRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), entryNames: [{ name: entryName, source }] };
219+
getCompletionEntryDetails(fileName: string, position: number, entryName: string, _options: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined, _preferences: UserPreferences | undefined, data: unknown): CompletionEntryDetails {
220+
const args: protocol.CompletionDetailsRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), entryNames: [{ name: entryName, source, data }] };
221221

222-
const request = this.processRequest<protocol.CompletionDetailsRequest>(CommandNames.CompletionDetails, args);
223-
const response = this.processResponse<protocol.CompletionDetailsResponse>(request);
224-
Debug.assert(response.body!.length === 1, "Unexpected length of completion details response body.");
225-
const convertedCodeActions = map(response.body![0].codeActions, ({ description, changes }) => ({ description, changes: this.convertChanges(changes, fileName) }));
226-
return { ...response.body![0], codeActions: convertedCodeActions };
222+
const request = this.processRequest<protocol.CompletionDetailsRequest>(CommandNames.CompletionDetailsFull, args);
223+
const response = this.processResponse<protocol.Response>(request);
224+
Debug.assert(response.body.length === 1, "Unexpected length of completion details response body.");
225+
return response.body[0];
227226
}
228227

229228
getCompletionEntrySymbol(_fileName: string, _position: number, _entryName: string): Symbol {

src/harness/fourslashImpl.ts

+19-9
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ namespace FourSlash {
399399
}
400400
const memo = Utils.memoize(
401401
(_version: number, _active: string, _caret: number, _selectEnd: number, _marker: string, ...args: any[]) => (ls[key] as Function)(...args),
402-
(...args) => args.join("|,|")
402+
(...args) => args.map(a => a && typeof a === "object" ? JSON.stringify(a) : a).join("|,|")
403403
);
404404
proxy[key] = (...args: any[]) => memo(
405405
target.languageServiceAdapterHost.getScriptInfo(target.activeFile.fileName)!.version,
@@ -867,7 +867,7 @@ namespace FourSlash {
867867
nameToEntries.set(entry.name, [entry]);
868868
}
869869
else {
870-
if (entries.some(e => e.source === entry.source)) {
870+
if (entries.some(e => e.source === entry.source && this.deepEqual(e.data, entry.data))) {
871871
this.raiseError(`Duplicate completions for ${entry.name}`);
872872
}
873873
entries.push(entry);
@@ -885,8 +885,8 @@ namespace FourSlash {
885885
const name = typeof include === "string" ? include : include.name;
886886
const found = nameToEntries.get(name);
887887
if (!found) throw this.raiseError(`Includes: completion '${name}' not found.`);
888-
assert(found.length === 1, `Must use 'exact' for multiple completions with same name: '${name}'`);
889-
this.verifyCompletionEntry(ts.first(found), include);
888+
if (!found.length) throw this.raiseError(`Includes: no completions with name '${name}' remain unmatched.`);
889+
this.verifyCompletionEntry(found.shift()!, include);
890890
}
891891
}
892892
if (options.excludes) {
@@ -933,7 +933,7 @@ namespace FourSlash {
933933
assert.equal(actual.sortText, expected.sortText || ts.Completions.SortText.LocationPriority, this.messageAtLastKnownMarker(`Actual entry: ${JSON.stringify(actual)}`));
934934

935935
if (expected.text !== undefined) {
936-
const actualDetails = ts.Debug.checkDefined(this.getCompletionEntryDetails(actual.name, actual.source), `No completion details available for name '${actual.name}' and source '${actual.source}'`);
936+
const actualDetails = ts.Debug.checkDefined(this.getCompletionEntryDetails(actual.name, actual.source, actual.data), `No completion details available for name '${actual.name}' and source '${actual.source}'`);
937937
assert.equal(ts.displayPartsToString(actualDetails.displayParts), expected.text, "Expected 'text' property to match 'displayParts' string");
938938
assert.equal(ts.displayPartsToString(actualDetails.documentation), expected.documentation || "", "Expected 'documentation' property to match 'documentation' display parts string");
939939
// TODO: GH#23587
@@ -1254,6 +1254,16 @@ namespace FourSlash {
12541254

12551255
}
12561256

1257+
private deepEqual(a: unknown, b: unknown) {
1258+
try {
1259+
this.assertObjectsEqual(a, b);
1260+
return true;
1261+
}
1262+
catch {
1263+
return false;
1264+
}
1265+
}
1266+
12571267
public verifyDisplayPartsOfReferencedSymbol(expected: ts.SymbolDisplayPart[]) {
12581268
const referencedSymbols = this.findReferencesAtCaret()!;
12591269

@@ -1281,11 +1291,11 @@ namespace FourSlash {
12811291
return this.languageService.getCompletionsAtPosition(this.activeFile.fileName, this.currentCaretPosition, options);
12821292
}
12831293

1284-
private getCompletionEntryDetails(entryName: string, source?: string, preferences?: ts.UserPreferences): ts.CompletionEntryDetails | undefined {
1294+
private getCompletionEntryDetails(entryName: string, source: string | undefined, data: ts.CompletionEntryData | undefined, preferences?: ts.UserPreferences): ts.CompletionEntryDetails | undefined {
12851295
if (preferences) {
12861296
this.configure(preferences);
12871297
}
1288-
return this.languageService.getCompletionEntryDetails(this.activeFile.fileName, this.currentCaretPosition, entryName, this.formatCodeSettings, source, preferences);
1298+
return this.languageService.getCompletionEntryDetails(this.activeFile.fileName, this.currentCaretPosition, entryName, this.formatCodeSettings, source, preferences, data);
12891299
}
12901300

12911301
private getReferencesAtCaret() {
@@ -2796,14 +2806,14 @@ namespace FourSlash {
27962806
public applyCodeActionFromCompletion(markerName: string, options: FourSlashInterface.VerifyCompletionActionOptions) {
27972807
this.goToMarker(markerName);
27982808

2799-
const details = this.getCompletionEntryDetails(options.name, options.source, options.preferences);
2809+
const details = this.getCompletionEntryDetails(options.name, options.source, options.data, options.preferences);
28002810
if (!details) {
28012811
const completions = this.getCompletionListAtCaret(options.preferences)?.entries;
28022812
const matchingName = completions?.filter(e => e.name === options.name);
28032813
const detailMessage = matchingName?.length
28042814
? `\n Found ${matchingName.length} with name '${options.name}' from source(s) ${matchingName.map(e => `'${e.source}'`).join(", ")}.`
28052815
: ` (In fact, there were no completions with name '${options.name}' at all.)`;
2806-
return this.raiseError(`No completions were found for the given name, source, and preferences.` + detailMessage);
2816+
return this.raiseError(`No completions were found for the given name, source/data, and preferences.` + detailMessage);
28072817
}
28082818
const codeActions = details.codeActions;
28092819
if (codeActions?.length !== 1) {

src/harness/fourslashInterfaceImpl.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1701,6 +1701,7 @@ namespace FourSlashInterface {
17011701
export interface VerifyCompletionActionOptions extends NewContentOptions {
17021702
name: string;
17031703
source?: string;
1704+
data?: ts.CompletionEntryData;
17041705
description: string;
17051706
preferences?: ts.UserPreferences;
17061707
}

src/harness/harnessLanguageService.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -474,8 +474,8 @@ namespace Harness.LanguageService {
474474
getCompletionsAtPosition(fileName: string, position: number, preferences: ts.UserPreferences | undefined): ts.CompletionInfo {
475475
return unwrapJSONCallResult(this.shim.getCompletionsAtPosition(fileName, position, preferences));
476476
}
477-
getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: ts.FormatCodeOptions | undefined, source: string | undefined, preferences: ts.UserPreferences | undefined): ts.CompletionEntryDetails {
478-
return unwrapJSONCallResult(this.shim.getCompletionEntryDetails(fileName, position, entryName, JSON.stringify(formatOptions), source, preferences));
477+
getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: ts.FormatCodeOptions | undefined, source: string | undefined, preferences: ts.UserPreferences | undefined, data: ts.CompletionEntryData | undefined): ts.CompletionEntryDetails {
478+
return unwrapJSONCallResult(this.shim.getCompletionEntryDetails(fileName, position, entryName, JSON.stringify(formatOptions), source, preferences, data));
479479
}
480480
getCompletionEntrySymbol(): ts.Symbol {
481481
throw new Error("getCompletionEntrySymbol not implemented across the shim layer.");

src/server/protocol.ts

+7
Original file line numberDiff line numberDiff line change
@@ -2169,6 +2169,7 @@ namespace ts.server.protocol {
21692169
export interface CompletionEntryIdentifier {
21702170
name: string;
21712171
source?: string;
2172+
data?: unknown;
21722173
}
21732174

21742175
/**
@@ -2255,6 +2256,12 @@ namespace ts.server.protocol {
22552256
* in the project package.json.
22562257
*/
22572258
isPackageJsonImport?: true;
2259+
/**
2260+
* A property to be sent back to TS Server in the CompletionDetailsRequest, along with `name`,
2261+
* that allows TS Server to look up the symbol represented by the completion item, disambiguating
2262+
* items with the same name.
2263+
*/
2264+
data?: unknown;
22582265
}
22592266

22602267
/**

src/server/session.ts

+14-6
Original file line numberDiff line numberDiff line change
@@ -1808,14 +1808,14 @@ namespace ts.server {
18081808
if (kind === protocol.CommandTypes.CompletionsFull) return completions;
18091809

18101810
const prefix = args.prefix || "";
1811-
const entries = mapDefined<CompletionEntry, protocol.CompletionEntry>(completions.entries, entry => {
1811+
const entries = stableSort(mapDefined<CompletionEntry, protocol.CompletionEntry>(completions.entries, entry => {
18121812
if (completions.isMemberCompletion || startsWith(entry.name.toLowerCase(), prefix.toLowerCase())) {
1813-
const { name, kind, kindModifiers, sortText, insertText, replacementSpan, hasAction, source, isRecommended, isPackageJsonImport } = entry;
1813+
const { name, kind, kindModifiers, sortText, insertText, replacementSpan, hasAction, source, isRecommended, isPackageJsonImport, data } = entry;
18141814
const convertedSpan = replacementSpan ? toProtocolTextSpan(replacementSpan, scriptInfo) : undefined;
18151815
// Use `hasAction || undefined` to avoid serializing `false`.
1816-
return { name, kind, kindModifiers, sortText, insertText, replacementSpan: convertedSpan, hasAction: hasAction || undefined, source, isRecommended, isPackageJsonImport };
1816+
return { name, kind, kindModifiers, sortText, insertText, replacementSpan: convertedSpan, hasAction: hasAction || undefined, source, isRecommended, isPackageJsonImport, data };
18171817
}
1818-
}).sort((a, b) => compareStringsCaseSensitiveUI(a.name, b.name));
1818+
}), (a, b) => compareStringsCaseSensitiveUI(a.name, b.name));
18191819

18201820
if (kind === protocol.CommandTypes.Completions) {
18211821
if (completions.metadata) (entries as WithMetadata<readonly protocol.CompletionEntry[]>).metadata = completions.metadata;
@@ -1837,8 +1837,8 @@ namespace ts.server {
18371837
const formattingOptions = project.projectService.getFormatCodeOptions(file);
18381838

18391839
const result = mapDefined(args.entryNames, entryName => {
1840-
const { name, source } = typeof entryName === "string" ? { name: entryName, source: undefined } : entryName;
1841-
return project.getLanguageService().getCompletionEntryDetails(file, position, name, formattingOptions, source, this.getPreferences(file));
1840+
const { name, source, data } = typeof entryName === "string" ? { name: entryName, source: undefined, data: undefined } : entryName;
1841+
return project.getLanguageService().getCompletionEntryDetails(file, position, name, formattingOptions, source, this.getPreferences(file), data ? cast(data, isCompletionEntryData) : undefined);
18421842
});
18431843
return simplifiedResult
18441844
? result.map(details => ({ ...details, codeActions: map(details.codeActions, action => this.mapCodeAction(action)) }))
@@ -3118,4 +3118,12 @@ namespace ts.server {
31183118
isDefinition
31193119
};
31203120
}
3121+
3122+
function isCompletionEntryData(data: any): data is CompletionEntryData {
3123+
return data === undefined || data && typeof data === "object"
3124+
&& typeof data.exportName === "string"
3125+
&& (data.fileName === undefined || typeof data.fileName === "string")
3126+
&& (data.ambientModuleName === undefined || typeof data.ambientModuleName === "string"
3127+
&& (data.isPackageJsonImport === undefined || typeof data.isPackageJsonImport === "boolean"));
3128+
}
31213129
}

0 commit comments

Comments
 (0)