Enable auto imports in member snippet completions (#46592)
This commit is contained in:
parent
b0ab2a54bb
commit
eeaa595196
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
56
tests/cases/fourslash/completionsOverridingMethod8.ts
Normal file
56
tests/cases/fourslash/completionsOverridingMethod8.ts
Normal 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 {
|
||||
|
||||
}`
|
||||
});
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue