Skip to content

Commit a7e172b

Browse files
author
Andy
authored
Support multiple completions with the same name but different source module (#19455) (#19496)
* Support multiple completions with the same name but different source module * Use optional parameters for source * Simplify use of `uniques` * Update test * Fix `undefined` error
1 parent c35e90e commit a7e172b

23 files changed

+301
-117
lines changed

src/harness/fourslash.ts

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -783,13 +783,13 @@ namespace FourSlash {
783783
});
784784
}
785785

786-
public verifyCompletionListContains(symbol: string, text?: string, documentation?: string, kind?: string, spanIndex?: number, hasAction?: boolean) {
786+
public verifyCompletionListContains(entryId: ts.Completions.CompletionEntryIdentifier, text?: string, documentation?: string, kind?: string, spanIndex?: number, hasAction?: boolean) {
787787
const completions = this.getCompletionListAtCaret();
788788
if (completions) {
789-
this.assertItemInCompletionList(completions.entries, symbol, text, documentation, kind, spanIndex, hasAction);
789+
this.assertItemInCompletionList(completions.entries, entryId, text, documentation, kind, spanIndex, hasAction);
790790
}
791791
else {
792-
this.raiseError(`No completions at position '${this.currentCaretPosition}' when looking for '${symbol}'.`);
792+
this.raiseError(`No completions at position '${this.currentCaretPosition}' when looking for '${JSON.stringify(entryId)}'.`);
793793
}
794794
}
795795

@@ -804,7 +804,7 @@ namespace FourSlash {
804804
* @param expectedKind the kind of symbol (see ScriptElementKind)
805805
* @param spanIndex the index of the range that the completion item's replacement text span should match
806806
*/
807-
public verifyCompletionListDoesNotContain(symbol: string, expectedText?: string, expectedDocumentation?: string, expectedKind?: string, spanIndex?: number) {
807+
public verifyCompletionListDoesNotContain(entryId: ts.Completions.CompletionEntryIdentifier, expectedText?: string, expectedDocumentation?: string, expectedKind?: string, spanIndex?: number) {
808808
const that = this;
809809
let replacementSpan: ts.TextSpan;
810810
if (spanIndex !== undefined) {
@@ -833,14 +833,14 @@ namespace FourSlash {
833833

834834
const completions = this.getCompletionListAtCaret();
835835
if (completions) {
836-
let filterCompletions = completions.entries.filter(e => e.name === symbol);
836+
let filterCompletions = completions.entries.filter(e => e.name === entryId.name && e.source === entryId.source);
837837
filterCompletions = expectedKind ? filterCompletions.filter(e => e.kind === expectedKind) : filterCompletions;
838838
filterCompletions = filterCompletions.filter(filterByTextOrDocumentation);
839839
if (filterCompletions.length !== 0) {
840840
// After filtered using all present criterion, if there are still symbol left in the list
841841
// then these symbols must meet the criterion for Not supposed to be in the list. So we
842842
// raise an error
843-
let error = "Completion list did contain \'" + symbol + "\'.";
843+
let error = `Completion list did contain '${JSON.stringify(entryId)}\'.`;
844844
const details = this.getCompletionEntryDetails(filterCompletions[0].name);
845845
if (expectedText) {
846846
error += "Expected text: " + expectedText + " to equal: " + ts.displayPartsToString(details.displayParts) + ".";
@@ -1130,8 +1130,8 @@ Actual: ${stringify(fullActual)}`);
11301130
return this.languageService.getCompletionsAtPosition(this.activeFile.fileName, this.currentCaretPosition);
11311131
}
11321132

1133-
private getCompletionEntryDetails(entryName: string) {
1134-
return this.languageService.getCompletionEntryDetails(this.activeFile.fileName, this.currentCaretPosition, entryName, this.formatCodeSettings);
1133+
private getCompletionEntryDetails(entryName: string, source?: string) {
1134+
return this.languageService.getCompletionEntryDetails(this.activeFile.fileName, this.currentCaretPosition, entryName, this.formatCodeSettings, source);
11351135
}
11361136

11371137
private getReferencesAtCaret() {
@@ -1640,7 +1640,7 @@ Actual: ${stringify(fullActual)}`);
16401640
const longestNameLength = max(entries, m => m.name.length);
16411641
const longestKindLength = max(entries, m => m.kind.length);
16421642
entries.sort((m, n) => m.sortText > n.sortText ? 1 : m.sortText < n.sortText ? -1 : m.name > n.name ? 1 : m.name < n.name ? -1 : 0);
1643-
const membersString = entries.map(m => `${pad(m.name, longestNameLength)} ${pad(m.kind, longestKindLength)} ${m.kindModifiers}`).join("\n");
1643+
const membersString = entries.map(m => `${pad(m.name, longestNameLength)} ${pad(m.kind, longestKindLength)} ${m.kindModifiers} ${m.source === undefined ? "" : m.source}`).join("\n");
16441644
Harness.IO.log(membersString);
16451645
}
16461646

@@ -2296,13 +2296,13 @@ Actual: ${stringify(fullActual)}`);
22962296
public applyCodeActionFromCompletion(markerName: string, options: FourSlashInterface.VerifyCompletionActionOptions) {
22972297
this.goToMarker(markerName);
22982298

2299-
const actualCompletion = this.getCompletionListAtCaret().entries.find(e => e.name === options.name);
2299+
const actualCompletion = this.getCompletionListAtCaret().entries.find(e => e.name === options.name && e.source === options.source);
23002300

23012301
if (!actualCompletion.hasAction) {
23022302
this.raiseError(`Completion for ${options.name} does not have an associated action.`);
23032303
}
23042304

2305-
const details = this.getCompletionEntryDetails(options.name);
2305+
const details = this.getCompletionEntryDetails(options.name, actualCompletion.source);
23062306
if (details.codeActions.length !== 1) {
23072307
this.raiseError(`Expected one code action, got ${details.codeActions.length}`);
23082308
}
@@ -2984,33 +2984,35 @@ Actual: ${stringify(fullActual)}`);
29842984

29852985
private assertItemInCompletionList(
29862986
items: ts.CompletionEntry[],
2987-
name: string,
2987+
entryId: ts.Completions.CompletionEntryIdentifier,
29882988
text: string | undefined,
29892989
documentation: string | undefined,
29902990
kind: string | undefined,
29912991
spanIndex: number | undefined,
29922992
hasAction: boolean | undefined,
29932993
) {
29942994
for (const item of items) {
2995-
if (item.name === name) {
2996-
if (documentation !== undefined || text !== undefined) {
2997-
const details = this.getCompletionEntryDetails(item.name);
2995+
if (item.name === entryId.name && item.source === entryId.source) {
2996+
if (documentation !== undefined || text !== undefined || entryId.source !== undefined) {
2997+
const details = this.getCompletionEntryDetails(item.name, item.source);
29982998

29992999
if (documentation !== undefined) {
3000-
assert.equal(ts.displayPartsToString(details.documentation), documentation, this.assertionMessageAtLastKnownMarker("completion item documentation for " + name));
3000+
assert.equal(ts.displayPartsToString(details.documentation), documentation, this.assertionMessageAtLastKnownMarker("completion item documentation for " + entryId));
30013001
}
30023002
if (text !== undefined) {
3003-
assert.equal(ts.displayPartsToString(details.displayParts), text, this.assertionMessageAtLastKnownMarker("completion item detail text for " + name));
3003+
assert.equal(ts.displayPartsToString(details.displayParts), text, this.assertionMessageAtLastKnownMarker("completion item detail text for " + entryId));
30043004
}
3005+
3006+
assert.deepEqual(details.source, entryId.source === undefined ? undefined : [ts.textPart(entryId.source)]);
30053007
}
30063008

30073009
if (kind !== undefined) {
3008-
assert.equal(item.kind, kind, this.assertionMessageAtLastKnownMarker("completion item kind for " + name));
3010+
assert.equal(item.kind, kind, this.assertionMessageAtLastKnownMarker("completion item kind for " + entryId));
30093011
}
30103012

30113013
if (spanIndex !== undefined) {
30123014
const span = this.getTextSpanForRangeAtIndex(spanIndex);
3013-
assert.isTrue(TestState.textSpansEqual(span, item.replacementSpan), this.assertionMessageAtLastKnownMarker(stringify(span) + " does not equal " + stringify(item.replacementSpan) + " replacement span for " + name));
3015+
assert.isTrue(TestState.textSpansEqual(span, item.replacementSpan), this.assertionMessageAtLastKnownMarker(stringify(span) + " does not equal " + stringify(item.replacementSpan) + " replacement span for " + entryId));
30143016
}
30153017

30163018
assert.equal(item.hasAction, hasAction);
@@ -3021,7 +3023,7 @@ Actual: ${stringify(fullActual)}`);
30213023

30223024
const itemsString = items.map(item => stringify({ name: item.name, kind: item.kind })).join(",\n");
30233025

3024-
this.raiseError(`Expected "${stringify({ name, text, documentation, kind })}" to be in list [${itemsString}]`);
3026+
this.raiseError(`Expected "${stringify({ entryId, text, documentation, kind })}" to be in list [${itemsString}]`);
30253027
}
30263028

30273029
private findFile(indexOrName: any) {
@@ -3732,12 +3734,15 @@ namespace FourSlashInterface {
37323734

37333735
// Verifies the completion list contains the specified symbol. The
37343736
// completion list is brought up if necessary
3735-
public completionListContains(symbol: string, text?: string, documentation?: string, kind?: string, spanIndex?: number, hasAction?: boolean) {
3737+
public completionListContains(entryId: string | ts.Completions.CompletionEntryIdentifier, text?: string, documentation?: string, kind?: string, spanIndex?: number, hasAction?: boolean) {
3738+
if (typeof entryId === "string") {
3739+
entryId = { name: entryId, source: undefined };
3740+
}
37363741
if (this.negative) {
3737-
this.state.verifyCompletionListDoesNotContain(symbol, text, documentation, kind, spanIndex);
3742+
this.state.verifyCompletionListDoesNotContain(entryId, text, documentation, kind, spanIndex);
37383743
}
37393744
else {
3740-
this.state.verifyCompletionListContains(symbol, text, documentation, kind, spanIndex, hasAction);
3745+
this.state.verifyCompletionListContains(entryId, text, documentation, kind, spanIndex, hasAction);
37413746
}
37423747
}
37433748

@@ -4492,6 +4497,7 @@ namespace FourSlashInterface {
44924497

44934498
export interface VerifyCompletionActionOptions extends NewContentOptions {
44944499
name: string;
4500+
source?: string;
44954501
description: string;
44964502
}
44974503
}

src/server/protocol.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1625,7 +1625,12 @@ namespace ts.server.protocol {
16251625
/**
16261626
* Names of one or more entries for which to obtain details.
16271627
*/
1628-
entryNames: string[];
1628+
entryNames: (string | CompletionEntryIdentifier)[];
1629+
}
1630+
1631+
export interface CompletionEntryIdentifier {
1632+
name: string;
1633+
source: string;
16291634
}
16301635

16311636
/**
@@ -1685,6 +1690,10 @@ namespace ts.server.protocol {
16851690
* made to avoid errors. The CompletionEntryDetails will have these actions.
16861691
*/
16871692
hasAction?: true;
1693+
/**
1694+
* Identifier (not necessarily human-readable) identifying where this completion came from.
1695+
*/
1696+
source?: string;
16881697
}
16891698

16901699
/**
@@ -1722,6 +1731,11 @@ namespace ts.server.protocol {
17221731
* The associated code actions for this entry
17231732
*/
17241733
codeActions?: CodeAction[];
1734+
1735+
/**
1736+
* Human-readable description of the `source` from the CompletionEntry.
1737+
*/
1738+
source?: SymbolDisplayPart[];
17251739
}
17261740

17271741
export interface CompletionsResponse extends Response {

src/server/session.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,10 +1188,10 @@ namespace ts.server {
11881188
if (simplifiedResult) {
11891189
return mapDefined<CompletionEntry, protocol.CompletionEntry>(completions && completions.entries, entry => {
11901190
if (completions.isMemberCompletion || (entry.name.toLowerCase().indexOf(prefix.toLowerCase()) === 0)) {
1191-
const { name, kind, kindModifiers, sortText, replacementSpan, hasAction } = entry;
1191+
const { name, kind, kindModifiers, sortText, replacementSpan, hasAction, source } = entry;
11921192
const convertedSpan = replacementSpan ? this.decorateSpan(replacementSpan, scriptInfo) : undefined;
11931193
// Use `hasAction || undefined` to avoid serializing `false`.
1194-
return { name, kind, kindModifiers, sortText, replacementSpan: convertedSpan, hasAction: hasAction || undefined };
1194+
return { name, kind, kindModifiers, sortText, replacementSpan: convertedSpan, hasAction: hasAction || undefined, source };
11951195
}
11961196
}).sort((a, b) => compareStrings(a.name, b.name));
11971197
}
@@ -1206,8 +1206,9 @@ namespace ts.server {
12061206
const position = this.getPosition(args, scriptInfo);
12071207
const formattingOptions = project.projectService.getFormatCodeOptions(file);
12081208

1209-
return mapDefined(args.entryNames, entryName => {
1210-
const details = project.getLanguageService().getCompletionEntryDetails(file, position, entryName, formattingOptions);
1209+
return mapDefined<string | protocol.CompletionEntryIdentifier, protocol.CompletionEntryDetails>(args.entryNames, entryName => {
1210+
const { name, source } = typeof entryName === "string" ? { name: entryName, source: undefined } : entryName;
1211+
const details = project.getLanguageService().getCompletionEntryDetails(file, position, name, formattingOptions, source);
12111212
if (details) {
12121213
const mappedCodeActions = map(details.codeActions, action => this.mapCodeAction(action, scriptInfo));
12131214
return { ...details, codeActions: mappedCodeActions };

src/services/codefixes/importFixes.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,11 @@ namespace ts.codefix {
249249
const lastImportDeclaration = findLast(sourceFile.statements, isAnyImportSyntax);
250250

251251
const moduleSpecifierWithoutQuotes = stripQuotes(moduleSpecifier);
252-
const importDecl = createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, createImportClauseOfKind(kind, symbolName), createStringLiteralWithQuoteStyle(sourceFile, moduleSpecifierWithoutQuotes));
252+
const importDecl = createImportDeclaration(
253+
/*decorators*/ undefined,
254+
/*modifiers*/ undefined,
255+
createImportClauseOfKind(kind, symbolName),
256+
createStringLiteralWithQuoteStyle(sourceFile, moduleSpecifierWithoutQuotes));
253257
const changes = ChangeTracker.with(context, changeTracker => {
254258
if (lastImportDeclaration) {
255259
changeTracker.insertNodeAfter(sourceFile, lastImportDeclaration, importDecl, { suffix: newLineCharacter });
@@ -279,13 +283,14 @@ namespace ts.codefix {
279283
}
280284

281285
function createImportClauseOfKind(kind: ImportKind, symbolName: string) {
286+
const id = createIdentifier(symbolName);
282287
switch (kind) {
283288
case ImportKind.Default:
284-
return createImportClause(createIdentifier(symbolName), /*namedBindings*/ undefined);
289+
return createImportClause(id, /*namedBindings*/ undefined);
285290
case ImportKind.Namespace:
286-
return createImportClause(/*name*/ undefined, createNamespaceImport(createIdentifier(symbolName)));
291+
return createImportClause(/*name*/ undefined, createNamespaceImport(id));
287292
case ImportKind.Named:
288-
return createImportClause(/*name*/ undefined, createNamedImports([createImportSpecifier(/*propertyName*/ undefined, createIdentifier(symbolName))]));
293+
return createImportClause(/*name*/ undefined, createNamedImports([createImportSpecifier(/*propertyName*/ undefined, id)]));
289294
default:
290295
Debug.assertNever(kind);
291296
}
@@ -529,7 +534,7 @@ namespace ts.codefix {
529534
declarations: ReadonlyArray<AnyImportSyntax>): ImportCodeAction {
530535
const fromExistingImport = firstDefined(declarations, declaration => {
531536
if (declaration.kind === SyntaxKind.ImportDeclaration && declaration.importClause) {
532-
const changes = tryUpdateExistingImport(ctx, ctx.kind, declaration.importClause);
537+
const changes = tryUpdateExistingImport(ctx, declaration.importClause);
533538
if (changes) {
534539
const moduleSpecifierWithoutQuotes = stripQuotes(declaration.moduleSpecifier.getText());
535540
return createCodeAction(
@@ -559,8 +564,8 @@ namespace ts.codefix {
559564
return expression && isStringLiteral(expression) ? expression.text : undefined;
560565
}
561566

562-
function tryUpdateExistingImport(context: SymbolContext, kind: ImportKind, importClause: ImportClause): FileTextChanges[] | undefined {
563-
const { symbolName, sourceFile } = context;
567+
function tryUpdateExistingImport(context: SymbolContext & { kind: ImportKind }, importClause: ImportClause): FileTextChanges[] | undefined {
568+
const { symbolName, sourceFile, kind } = context;
564569
const { name, namedBindings } = importClause;
565570
switch (kind) {
566571
case ImportKind.Default:

0 commit comments

Comments
 (0)