Skip to content

Commit eeaa595

Browse files
authored
Enable auto imports in member snippet completions (microsoft#46592)
1 parent b0ab2a5 commit eeaa595

File tree

8 files changed

+128
-22
lines changed

8 files changed

+128
-22
lines changed

src/compiler/diagnosticMessages.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6433,6 +6433,11 @@
64336433
"category": "Message",
64346434
"code": 90053
64356435
},
6436+
"Includes imports of types referenced by '{0}'": {
6437+
"category": "Message",
6438+
"code": 90054
6439+
},
6440+
64366441
"Convert function to an ES2015 class": {
64376442
"category": "Message",
64386443
"code": 95001

src/harness/fourslashImpl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -942,7 +942,7 @@ namespace FourSlash {
942942
expected = typeof expected === "string" ? { name: expected } : expected;
943943

944944
if (actual.insertText !== expected.insertText) {
945-
this.raiseError(`Expected completion insert text to be ${expected.insertText}, got ${actual.insertText}`);
945+
this.raiseError(`Completion insert text did not match: ${showTextDiff(expected.insertText || "", actual.insertText || "")}`);
946946
}
947947
const convertedReplacementSpan = expected.replacementSpan && ts.createTextSpanFromRange(expected.replacementSpan);
948948
if (convertedReplacementSpan?.length) {

src/services/codeFixProvider.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,6 @@ namespace ts.codefix {
33
const errorCodeToFixes = createMultiMap<CodeFixRegistration>();
44
const fixIdToRegistration = new Map<string, CodeFixRegistration>();
55

6-
export type DiagnosticAndArguments = DiagnosticMessage | [DiagnosticMessage, string] | [DiagnosticMessage, string, string];
7-
function diagnosticToString(diag: DiagnosticAndArguments): string {
8-
return isArray(diag)
9-
? formatStringFromArgs(getLocaleSpecificMessage(diag[0]), diag.slice(1) as readonly string[])
10-
: getLocaleSpecificMessage(diag);
11-
}
12-
136
export function createCodeFixActionWithoutFixAll(fixName: string, changes: FileTextChanges[], description: DiagnosticAndArguments) {
147
return createCodeFixActionWorker(fixName, diagnosticToString(description), changes, /*fixId*/ undefined, /*fixAllDescription*/ undefined);
158
}

src/services/codefixes/importFixes.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ namespace ts.codefix {
3333
});
3434

3535
export interface ImportAdder {
36+
hasFixes(): boolean;
3637
addImportFromDiagnostic: (diagnostic: DiagnosticWithLocation, context: CodeFixContextBase) => void;
3738
addImportFromExportedSymbol: (exportedSymbol: Symbol, isValidTypeOnlyUseSite?: boolean) => void;
3839
writeFixes: (changeTracker: textChanges.ChangeTracker) => void;
@@ -59,7 +60,7 @@ namespace ts.codefix {
5960
type NewImportsKey = `${0 | 1}|${string}`;
6061
/** Use `getNewImportEntry` for access */
6162
const newImports = new Map<NewImportsKey, Mutable<ImportsCollection & { useRequire: boolean }>>();
62-
return { addImportFromDiagnostic, addImportFromExportedSymbol, writeFixes };
63+
return { addImportFromDiagnostic, addImportFromExportedSymbol, writeFixes, hasFixes };
6364

6465
function addImportFromDiagnostic(diagnostic: DiagnosticWithLocation, context: CodeFixContextBase) {
6566
const info = getFixesInfo(context, diagnostic.code, diagnostic.start, useAutoImportProvider);
@@ -217,6 +218,10 @@ namespace ts.codefix {
217218
insertImports(changeTracker, sourceFile, newDeclarations, /*blankLineBetween*/ true);
218219
}
219220
}
221+
222+
function hasFixes() {
223+
return addToNamespace.length > 0 || importType.length > 0 || addToExisting.size > 0 || newImports.size > 0;
224+
}
220225
}
221226

222227
// Sorted with the preferred fix coming first.

src/services/completions.ts

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ namespace ts.Completions {
5757
*/
5858
export enum CompletionSource {
5959
/** Completions that require `this.` insertion text */
60-
ThisProperty = "ThisProperty/"
60+
ThisProperty = "ThisProperty/",
61+
/** Auto-import that comes attached to a class member snippet */
62+
ClassMemberSnippet = "ClassMemberSnippet/",
6163
}
6264

6365
const enum SymbolOriginInfoKind {
@@ -641,6 +643,7 @@ namespace ts.Completions {
641643
let replacementSpan = getReplacementSpanForContextToken(replacementToken);
642644
let data: CompletionEntryData | undefined;
643645
let isSnippet: true | undefined;
646+
let source = getSourceFromOrigin(origin);
644647
let sourceDisplay;
645648
let hasAction;
646649

@@ -702,7 +705,12 @@ namespace ts.Completions {
702705
preferences.includeCompletionsWithInsertText &&
703706
completionKind === CompletionKind.MemberLike &&
704707
isClassLikeMemberCompletion(symbol, location)) {
705-
({ insertText, isSnippet } = getEntryForMemberCompletion(host, program, options, preferences, name, symbol, location, contextToken));
708+
let importAdder;
709+
({ insertText, isSnippet, importAdder } = getEntryForMemberCompletion(host, program, options, preferences, name, symbol, location, contextToken));
710+
if (importAdder?.hasFixes()) {
711+
hasAction = true;
712+
source = CompletionSource.ClassMemberSnippet;
713+
}
706714
}
707715

708716
const kind = SymbolDisplay.getSymbolKind(typeChecker, symbol, location);
@@ -758,7 +766,7 @@ namespace ts.Completions {
758766
kind,
759767
kindModifiers: SymbolDisplay.getSymbolModifiers(typeChecker, symbol),
760768
sortText,
761-
source: getSourceFromOrigin(origin),
769+
source,
762770
hasAction: hasAction ? true : undefined,
763771
isRecommended: isRecommendedCompletionMatch(symbol, recommendedCompletion, typeChecker) || undefined,
764772
insertText,
@@ -828,7 +836,7 @@ namespace ts.Completions {
828836
symbol: Symbol,
829837
location: Node,
830838
contextToken: Node | undefined,
831-
): { insertText: string, isSnippet?: true } {
839+
): { insertText: string, isSnippet?: true, importAdder?: codefix.ImportAdder } {
832840
const classLikeDeclaration = findAncestor(location, isClassLike);
833841
if (!classLikeDeclaration) {
834842
return { insertText: name };
@@ -921,7 +929,7 @@ namespace ts.Completions {
921929
insertText = printer.printSnippetList(ListFormat.MultiLine, factory.createNodeArray(completionNodes), sourceFile);
922930
}
923931

924-
return { insertText, isSnippet };
932+
return { insertText, isSnippet, importAdder };
925933
}
926934

927935
function getPresentModifiers(contextToken: Node): ModifierFlags {
@@ -1297,6 +1305,7 @@ namespace ts.Completions {
12971305
location: Node;
12981306
origin: SymbolOriginInfo | SymbolOriginInfoExport | SymbolOriginInfoResolvedExport | undefined;
12991307
previousToken: Node | undefined;
1308+
contextToken: Node | undefined;
13001309
readonly isJsxInitializer: IsJsxInitializer;
13011310
readonly isTypeOnlyLocation: boolean;
13021311
}
@@ -1312,11 +1321,13 @@ namespace ts.Completions {
13121321
if (entryId.data) {
13131322
const autoImport = getAutoImportSymbolFromCompletionEntryData(entryId.name, entryId.data, program, host);
13141323
if (autoImport) {
1324+
const { contextToken, previousToken } = getRelevantTokens(position, sourceFile);
13151325
return {
13161326
type: "symbol",
13171327
symbol: autoImport.symbol,
13181328
location: getTouchingPropertyName(sourceFile, position),
1319-
previousToken: findPrecedingToken(position, sourceFile, /*startNode*/ undefined)!,
1329+
previousToken,
1330+
contextToken,
13201331
isJsxInitializer: false,
13211332
isTypeOnlyLocation: false,
13221333
origin: autoImport.origin,
@@ -1333,7 +1344,7 @@ namespace ts.Completions {
13331344
return { type: "request", request: completionData };
13341345
}
13351346

1336-
const { symbols, literals, location, completionKind, symbolToOriginInfoMap, previousToken, isJsxInitializer, isTypeOnlyLocation } = completionData;
1347+
const { symbols, literals, location, completionKind, symbolToOriginInfoMap, contextToken, previousToken, isJsxInitializer, isTypeOnlyLocation } = completionData;
13371348

13381349
const literal = find(literals, l => completionNameForLiteral(sourceFile, preferences, l) === entryId.name);
13391350
if (literal !== undefined) return { type: "literal", literal };
@@ -1345,8 +1356,8 @@ namespace ts.Completions {
13451356
return firstDefined(symbols, (symbol, index): SymbolCompletion | undefined => {
13461357
const origin = symbolToOriginInfoMap[index];
13471358
const info = getCompletionEntryDisplayNameForSymbol(symbol, getEmitScriptTarget(compilerOptions), origin, completionKind, completionData.isJsxIdentifierExpected);
1348-
return info && info.name === entryId.name && getSourceFromOrigin(origin) === entryId.source
1349-
? { type: "symbol" as const, symbol, location, origin, previousToken, isJsxInitializer, isTypeOnlyLocation }
1359+
return info && info.name === entryId.name && (entryId.source === CompletionSource.ClassMemberSnippet && symbol.flags & SymbolFlags.ClassMember || getSourceFromOrigin(origin) === entryId.source)
1360+
? { type: "symbol" as const, symbol, location, origin, contextToken, previousToken, isJsxInitializer, isTypeOnlyLocation }
13501361
: undefined;
13511362
}) || { type: "none" };
13521363
}
@@ -1370,7 +1381,7 @@ namespace ts.Completions {
13701381
): CompletionEntryDetails | undefined {
13711382
const typeChecker = program.getTypeChecker();
13721383
const compilerOptions = program.getCompilerOptions();
1373-
const { name } = entryId;
1384+
const { name, source, data } = entryId;
13741385

13751386
const contextToken = findPrecedingToken(position, sourceFile);
13761387
if (isInString(sourceFile, position, contextToken)) {
@@ -1396,8 +1407,8 @@ namespace ts.Completions {
13961407
}
13971408
}
13981409
case "symbol": {
1399-
const { symbol, location, origin, previousToken } = symbolCompletion;
1400-
const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(origin, symbol, program, host, compilerOptions, sourceFile, position, previousToken, formatContext, preferences, entryId.data);
1410+
const { symbol, location, contextToken, origin, previousToken } = symbolCompletion;
1411+
const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(name, location, contextToken, origin, symbol, program, host, compilerOptions, sourceFile, position, previousToken, formatContext, preferences, data, source);
14011412
return createCompletionDetailsForSymbol(symbol, typeChecker, sourceFile, location, cancellationToken, codeActions, sourceDisplay); // TODO: GH#18217
14021413
}
14031414
case "literal": {
@@ -1433,6 +1444,9 @@ namespace ts.Completions {
14331444
readonly sourceDisplay: SymbolDisplayPart[] | undefined;
14341445
}
14351446
function getCompletionEntryCodeActionsAndSourceDisplay(
1447+
name: string,
1448+
location: Node,
1449+
contextToken: Node | undefined,
14361450
origin: SymbolOriginInfo | SymbolOriginInfoExport | SymbolOriginInfoResolvedExport | undefined,
14371451
symbol: Symbol,
14381452
program: Program,
@@ -1444,6 +1458,7 @@ namespace ts.Completions {
14441458
formatContext: formatting.FormatContext,
14451459
preferences: UserPreferences,
14461460
data: CompletionEntryData | undefined,
1461+
source: string | undefined,
14471462
): CodeActionsAndSourceDisplay {
14481463
if (data?.moduleSpecifier) {
14491464
const { contextToken, previousToken } = getRelevantTokens(position, sourceFile);
@@ -1453,6 +1468,30 @@ namespace ts.Completions {
14531468
}
14541469
}
14551470

1471+
if (source === CompletionSource.ClassMemberSnippet) {
1472+
const { importAdder } = getEntryForMemberCompletion(
1473+
host,
1474+
program,
1475+
compilerOptions,
1476+
preferences,
1477+
name,
1478+
symbol,
1479+
location,
1480+
contextToken);
1481+
if (importAdder) {
1482+
const changes = textChanges.ChangeTracker.with(
1483+
{ host, formatContext, preferences },
1484+
importAdder.writeFixes);
1485+
return {
1486+
sourceDisplay: undefined,
1487+
codeActions: [{
1488+
changes,
1489+
description: diagnosticToString([Diagnostics.Includes_imports_of_types_referenced_by_0, name]),
1490+
}],
1491+
};
1492+
}
1493+
}
1494+
14561495
if (!origin || !(originIsExport(origin) || originIsResolvedExport(origin))) {
14571496
return { codeActions: undefined, sourceDisplay: undefined };
14581497
}

src/services/utilities.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3283,5 +3283,12 @@ namespace ts {
32833283
return newLineCharacter === "\n" ? NewLineKind.LineFeed : NewLineKind.CarriageReturnLineFeed;
32843284
}
32853285

3286+
export type DiagnosticAndArguments = DiagnosticMessage | [DiagnosticMessage, string] | [DiagnosticMessage, string, string];
3287+
export function diagnosticToString(diag: DiagnosticAndArguments): string {
3288+
return isArray(diag)
3289+
? formatStringFromArgs(getLocaleSpecificMessage(diag[0]), diag.slice(1) as readonly string[])
3290+
: getLocaleSpecificMessage(diag);
3291+
}
3292+
32863293
// #endregion
32873294
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @newline: LF
4+
5+
// @Filename: /types1.ts
6+
//// export interface I { foo: string }
7+
8+
// @Filename: /types2.ts
9+
//// import { I } from "./types1";
10+
//// export interface Base { method(p: I): void }
11+
12+
// @Filename: /index.ts
13+
//// import { Base } from "./types2";
14+
//// export class C implements Base {
15+
//// /**/
16+
//// }
17+
18+
goTo.marker("");
19+
verify.completions({
20+
marker: "",
21+
isNewIdentifierLocation: true,
22+
preferences: {
23+
includeCompletionsWithInsertText: true,
24+
includeCompletionsWithSnippetText: false,
25+
includeCompletionsWithClassMemberSnippets: true,
26+
},
27+
includes: [{
28+
name: "method",
29+
sortText: completion.SortText.LocationPriority,
30+
replacementSpan: {
31+
fileName: "",
32+
pos: 0,
33+
end: 0,
34+
},
35+
insertText: "method(p: I): void {\n}\n",
36+
hasAction: true,
37+
source: completion.CompletionSource.ClassMemberSnippet,
38+
}],
39+
});
40+
41+
verify.applyCodeActionFromCompletion("", {
42+
preferences: {
43+
includeCompletionsWithInsertText: true,
44+
includeCompletionsWithSnippetText: false,
45+
includeCompletionsWithClassMemberSnippets: true,
46+
},
47+
name: "method",
48+
source: completion.CompletionSource.ClassMemberSnippet,
49+
description: "Includes imports of types referenced by 'method'",
50+
newFileContent:
51+
`import { I } from "./types1";
52+
import { Base } from "./types2";
53+
export class C implements Base {
54+
55+
}`
56+
});

tests/cases/fourslash/fourslash.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -850,7 +850,8 @@ declare namespace completion {
850850
DeprecatedAutoImportSuggestions = "24"
851851
}
852852
export const enum CompletionSource {
853-
ThisProperty = "ThisProperty/"
853+
ThisProperty = "ThisProperty/",
854+
ClassMemberSnippet = "ClassMemberSnippet/",
854855
}
855856
export const globalThisEntry: Entry;
856857
export const undefinedVarEntry: Entry;

0 commit comments

Comments
 (0)