Enable auto imports in member snippet completions (#46592)

This commit is contained in:
Andrew Branch 2021-10-29 14:43:32 -07:00 committed by GitHub
parent b0ab2a54bb
commit eeaa595196
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 128 additions and 22 deletions

View file

@ -6433,6 +6433,11 @@
"category": "Message",
"code": 90053
},
"Includes imports of types referenced by '{0}'": {
"category": "Message",
"code": 90054
},
"Convert function to an ES2015 class": {
"category": "Message",
"code": 95001

View file

@ -942,7 +942,7 @@ namespace FourSlash {
expected = typeof expected === "string" ? { name: expected } : expected;
if (actual.insertText !== expected.insertText) {
this.raiseError(`Expected completion insert text to be ${expected.insertText}, got ${actual.insertText}`);
this.raiseError(`Completion insert text did not match: ${showTextDiff(expected.insertText || "", actual.insertText || "")}`);
}
const convertedReplacementSpan = expected.replacementSpan && ts.createTextSpanFromRange(expected.replacementSpan);
if (convertedReplacementSpan?.length) {

View file

@ -3,13 +3,6 @@ namespace ts.codefix {
const errorCodeToFixes = createMultiMap<CodeFixRegistration>();
const fixIdToRegistration = new Map<string, CodeFixRegistration>();
export type DiagnosticAndArguments = DiagnosticMessage | [DiagnosticMessage, string] | [DiagnosticMessage, string, string];
function diagnosticToString(diag: DiagnosticAndArguments): string {
return isArray(diag)
? formatStringFromArgs(getLocaleSpecificMessage(diag[0]), diag.slice(1) as readonly string[])
: getLocaleSpecificMessage(diag);
}
export function createCodeFixActionWithoutFixAll(fixName: string, changes: FileTextChanges[], description: DiagnosticAndArguments) {
return createCodeFixActionWorker(fixName, diagnosticToString(description), changes, /*fixId*/ undefined, /*fixAllDescription*/ undefined);
}

View file

@ -33,6 +33,7 @@ namespace ts.codefix {
});
export interface ImportAdder {
hasFixes(): boolean;
addImportFromDiagnostic: (diagnostic: DiagnosticWithLocation, context: CodeFixContextBase) => void;
addImportFromExportedSymbol: (exportedSymbol: Symbol, isValidTypeOnlyUseSite?: boolean) => void;
writeFixes: (changeTracker: textChanges.ChangeTracker) => void;
@ -59,7 +60,7 @@ namespace ts.codefix {
type NewImportsKey = `${0 | 1}|${string}`;
/** Use `getNewImportEntry` for access */
const newImports = new Map<NewImportsKey, Mutable<ImportsCollection & { useRequire: boolean }>>();
return { addImportFromDiagnostic, addImportFromExportedSymbol, writeFixes };
return { addImportFromDiagnostic, addImportFromExportedSymbol, writeFixes, hasFixes };
function addImportFromDiagnostic(diagnostic: DiagnosticWithLocation, context: CodeFixContextBase) {
const info = getFixesInfo(context, diagnostic.code, diagnostic.start, useAutoImportProvider);
@ -217,6 +218,10 @@ namespace ts.codefix {
insertImports(changeTracker, sourceFile, newDeclarations, /*blankLineBetween*/ true);
}
}
function hasFixes() {
return addToNamespace.length > 0 || importType.length > 0 || addToExisting.size > 0 || newImports.size > 0;
}
}
// Sorted with the preferred fix coming first.

View file

@ -57,7 +57,9 @@ namespace ts.Completions {
*/
export enum CompletionSource {
/** Completions that require `this.` insertion text */
ThisProperty = "ThisProperty/"
ThisProperty = "ThisProperty/",
/** Auto-import that comes attached to a class member snippet */
ClassMemberSnippet = "ClassMemberSnippet/",
}
const enum SymbolOriginInfoKind {
@ -641,6 +643,7 @@ namespace ts.Completions {
let replacementSpan = getReplacementSpanForContextToken(replacementToken);
let data: CompletionEntryData | undefined;
let isSnippet: true | undefined;
let source = getSourceFromOrigin(origin);
let sourceDisplay;
let hasAction;
@ -702,7 +705,12 @@ namespace ts.Completions {
preferences.includeCompletionsWithInsertText &&
completionKind === CompletionKind.MemberLike &&
isClassLikeMemberCompletion(symbol, location)) {
({ insertText, isSnippet } = getEntryForMemberCompletion(host, program, options, preferences, name, symbol, location, contextToken));
let importAdder;
({ insertText, isSnippet, importAdder } = getEntryForMemberCompletion(host, program, options, preferences, name, symbol, location, contextToken));
if (importAdder?.hasFixes()) {
hasAction = true;
source = CompletionSource.ClassMemberSnippet;
}
}
const kind = SymbolDisplay.getSymbolKind(typeChecker, symbol, location);
@ -758,7 +766,7 @@ namespace ts.Completions {
kind,
kindModifiers: SymbolDisplay.getSymbolModifiers(typeChecker, symbol),
sortText,
source: getSourceFromOrigin(origin),
source,
hasAction: hasAction ? true : undefined,
isRecommended: isRecommendedCompletionMatch(symbol, recommendedCompletion, typeChecker) || undefined,
insertText,
@ -828,7 +836,7 @@ namespace ts.Completions {
symbol: Symbol,
location: Node,
contextToken: Node | undefined,
): { insertText: string, isSnippet?: true } {
): { insertText: string, isSnippet?: true, importAdder?: codefix.ImportAdder } {
const classLikeDeclaration = findAncestor(location, isClassLike);
if (!classLikeDeclaration) {
return { insertText: name };
@ -921,7 +929,7 @@ namespace ts.Completions {
insertText = printer.printSnippetList(ListFormat.MultiLine, factory.createNodeArray(completionNodes), sourceFile);
}
return { insertText, isSnippet };
return { insertText, isSnippet, importAdder };
}
function getPresentModifiers(contextToken: Node): ModifierFlags {
@ -1297,6 +1305,7 @@ namespace ts.Completions {
location: Node;
origin: SymbolOriginInfo | SymbolOriginInfoExport | SymbolOriginInfoResolvedExport | undefined;
previousToken: Node | undefined;
contextToken: Node | undefined;
readonly isJsxInitializer: IsJsxInitializer;
readonly isTypeOnlyLocation: boolean;
}
@ -1312,11 +1321,13 @@ namespace ts.Completions {
if (entryId.data) {
const autoImport = getAutoImportSymbolFromCompletionEntryData(entryId.name, entryId.data, program, host);
if (autoImport) {
const { contextToken, previousToken } = getRelevantTokens(position, sourceFile);
return {
type: "symbol",
symbol: autoImport.symbol,
location: getTouchingPropertyName(sourceFile, position),
previousToken: findPrecedingToken(position, sourceFile, /*startNode*/ undefined)!,
previousToken,
contextToken,
isJsxInitializer: false,
isTypeOnlyLocation: false,
origin: autoImport.origin,
@ -1333,7 +1344,7 @@ namespace ts.Completions {
return { type: "request", request: completionData };
}
const { symbols, literals, location, completionKind, symbolToOriginInfoMap, previousToken, isJsxInitializer, isTypeOnlyLocation } = completionData;
const { symbols, literals, location, completionKind, symbolToOriginInfoMap, contextToken, previousToken, isJsxInitializer, isTypeOnlyLocation } = completionData;
const literal = find(literals, l => completionNameForLiteral(sourceFile, preferences, l) === entryId.name);
if (literal !== undefined) return { type: "literal", literal };
@ -1345,8 +1356,8 @@ namespace ts.Completions {
return firstDefined(symbols, (symbol, index): SymbolCompletion | undefined => {
const origin = symbolToOriginInfoMap[index];
const info = getCompletionEntryDisplayNameForSymbol(symbol, getEmitScriptTarget(compilerOptions), origin, completionKind, completionData.isJsxIdentifierExpected);
return info && info.name === entryId.name && getSourceFromOrigin(origin) === entryId.source
? { type: "symbol" as const, symbol, location, origin, previousToken, isJsxInitializer, isTypeOnlyLocation }
return info && info.name === entryId.name && (entryId.source === CompletionSource.ClassMemberSnippet && symbol.flags & SymbolFlags.ClassMember || getSourceFromOrigin(origin) === entryId.source)
? { type: "symbol" as const, symbol, location, origin, contextToken, previousToken, isJsxInitializer, isTypeOnlyLocation }
: undefined;
}) || { type: "none" };
}
@ -1370,7 +1381,7 @@ namespace ts.Completions {
): CompletionEntryDetails | undefined {
const typeChecker = program.getTypeChecker();
const compilerOptions = program.getCompilerOptions();
const { name } = entryId;
const { name, source, data } = entryId;
const contextToken = findPrecedingToken(position, sourceFile);
if (isInString(sourceFile, position, contextToken)) {
@ -1396,8 +1407,8 @@ namespace ts.Completions {
}
}
case "symbol": {
const { symbol, location, origin, previousToken } = symbolCompletion;
const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(origin, symbol, program, host, compilerOptions, sourceFile, position, previousToken, formatContext, preferences, entryId.data);
const { symbol, location, contextToken, origin, previousToken } = symbolCompletion;
const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(name, location, contextToken, origin, symbol, program, host, compilerOptions, sourceFile, position, previousToken, formatContext, preferences, data, source);
return createCompletionDetailsForSymbol(symbol, typeChecker, sourceFile, location, cancellationToken, codeActions, sourceDisplay); // TODO: GH#18217
}
case "literal": {
@ -1433,6 +1444,9 @@ namespace ts.Completions {
readonly sourceDisplay: SymbolDisplayPart[] | undefined;
}
function getCompletionEntryCodeActionsAndSourceDisplay(
name: string,
location: Node,
contextToken: Node | undefined,
origin: SymbolOriginInfo | SymbolOriginInfoExport | SymbolOriginInfoResolvedExport | undefined,
symbol: Symbol,
program: Program,
@ -1444,6 +1458,7 @@ namespace ts.Completions {
formatContext: formatting.FormatContext,
preferences: UserPreferences,
data: CompletionEntryData | undefined,
source: string | undefined,
): CodeActionsAndSourceDisplay {
if (data?.moduleSpecifier) {
const { contextToken, previousToken } = getRelevantTokens(position, sourceFile);
@ -1453,6 +1468,30 @@ namespace ts.Completions {
}
}
if (source === CompletionSource.ClassMemberSnippet) {
const { importAdder } = getEntryForMemberCompletion(
host,
program,
compilerOptions,
preferences,
name,
symbol,
location,
contextToken);
if (importAdder) {
const changes = textChanges.ChangeTracker.with(
{ host, formatContext, preferences },
importAdder.writeFixes);
return {
sourceDisplay: undefined,
codeActions: [{
changes,
description: diagnosticToString([Diagnostics.Includes_imports_of_types_referenced_by_0, name]),
}],
};
}
}
if (!origin || !(originIsExport(origin) || originIsResolvedExport(origin))) {
return { codeActions: undefined, sourceDisplay: undefined };
}

View file

@ -3283,5 +3283,12 @@ namespace ts {
return newLineCharacter === "\n" ? NewLineKind.LineFeed : NewLineKind.CarriageReturnLineFeed;
}
export type DiagnosticAndArguments = DiagnosticMessage | [DiagnosticMessage, string] | [DiagnosticMessage, string, string];
export function diagnosticToString(diag: DiagnosticAndArguments): string {
return isArray(diag)
? formatStringFromArgs(getLocaleSpecificMessage(diag[0]), diag.slice(1) as readonly string[])
: getLocaleSpecificMessage(diag);
}
// #endregion
}

View file

@ -0,0 +1,56 @@
/// <reference path="fourslash.ts" />
// @newline: LF
// @Filename: /types1.ts
//// export interface I { foo: string }
// @Filename: /types2.ts
//// import { I } from "./types1";
//// export interface Base { method(p: I): void }
// @Filename: /index.ts
//// import { Base } from "./types2";
//// export class C implements Base {
//// /**/
//// }
goTo.marker("");
verify.completions({
marker: "",
isNewIdentifierLocation: true,
preferences: {
includeCompletionsWithInsertText: true,
includeCompletionsWithSnippetText: false,
includeCompletionsWithClassMemberSnippets: true,
},
includes: [{
name: "method",
sortText: completion.SortText.LocationPriority,
replacementSpan: {
fileName: "",
pos: 0,
end: 0,
},
insertText: "method(p: I): void {\n}\n",
hasAction: true,
source: completion.CompletionSource.ClassMemberSnippet,
}],
});
verify.applyCodeActionFromCompletion("", {
preferences: {
includeCompletionsWithInsertText: true,
includeCompletionsWithSnippetText: false,
includeCompletionsWithClassMemberSnippets: true,
},
name: "method",
source: completion.CompletionSource.ClassMemberSnippet,
description: "Includes imports of types referenced by 'method'",
newFileContent:
`import { I } from "./types1";
import { Base } from "./types2";
export class C implements Base {
}`
});

View file

@ -850,7 +850,8 @@ declare namespace completion {
DeprecatedAutoImportSuggestions = "24"
}
export const enum CompletionSource {
ThisProperty = "ThisProperty/"
ThisProperty = "ThisProperty/",
ClassMemberSnippet = "ClassMemberSnippet/",
}
export const globalThisEntry: Entry;
export const undefinedVarEntry: Entry;