feat(language-service): Allow auto-imports to suggest multiple possible imports. (#47787)

This change allows the language service to suggest imports for *all* directives returned from the compiler, and for *all* import specifiers for each directive. The first change has immediate effect, since there could already be multiple directives with the same selector. The second change is future-proofing; the compiler currently only returns a single best specifier, but will return more in the future.

PR Close #47787
This commit is contained in:
Dylan Hunn
2022-10-17 14:37:35 +02:00
committed by Pawel Kozlowski
parent d436a19555
commit 5f0b53c735
@@ -39,6 +39,8 @@ export const missingImportMeta: CodeActionMeta = {
function getCodeActions(
{templateInfo, start, compiler, formatOptions, preferences, errorCode, tsLs}:
CodeActionContext) {
let codeActions: ts.CodeFixAction[] = [];
const checker = compiler.getTemplateTypeChecker();
const tsChecker = compiler.programDriver.getProgram().getTypeChecker();
@@ -64,46 +66,45 @@ function getCodeActions(
}
const importOn = owningNgModule ?? templateInfo.component;
// Find all possible importable directives with a matching selector, and take one of them.
// In the future, we could handle multiple matches as additional quick fixes.
// Find all possible importable directives with a matching selector.
const allPossibleDirectives = checker.getPotentialTemplateDirectives(templateInfo.component);
const matchingDirectives =
getDirectiveMatchesForElementTag(missingElement, allPossibleDirectives);
if (matchingDirectives.size === 0) {
return [];
}
const bestMatch: PotentialDirective = matchingDirectives.values().next().value;
const bestMatchSymbol = bestMatch.tsSymbol.valueDeclaration;
const matches = matchingDirectives.values();
// Get possible trait imports corresponding to the recommended directive. Only use the
// compiler's best import; in the future, we could suggest multiple imports if they exist.
const potentialImports = checker.getPotentialImportsFor(bestMatch, importOn);
if (potentialImports.length === 0) {
return [];
}
const potentialImport = potentialImports[0];
// Generate suggestions for each possible match.
for (let currMatch of matches) {
const currMatchSymbol = currMatch.tsSymbol.valueDeclaration;
// Update the imports on the TypeScript file and Angular decorator.
let [fileImportChanges, importName] = updateImportsForTypescriptFile(
tsChecker, importOn.getSourceFile(), potentialImport, bestMatchSymbol.getSourceFile());
let traitImportChanges = updateImportsForAngularTrait(checker, importOn, importName);
// Get possible trait imports corresponding to the recommended directive.
const potentialImports = checker.getPotentialImportsFor(currMatch, importOn);
// All quick fixes should always update the trait import; however, the TypeScript import might
// already be present.
if (traitImportChanges.length === 0) {
return [];
// For each possible import specifier, create a suggestion.
for (let potentialImport of potentialImports) {
// Update the imports on the TypeScript file and Angular decorator.
let [fileImportChanges, importName] = updateImportsForTypescriptFile(
tsChecker, importOn.getSourceFile(), potentialImport, currMatchSymbol.getSourceFile());
let traitImportChanges = updateImportsForAngularTrait(checker, importOn, importName);
// All quick fixes should always update the trait import; however, the TypeScript import might
// already be present.
if (traitImportChanges.length === 0) {
continue;
}
// Create a code action for this import.
codeActions.push({
fixName: FixIdForCodeFixesAll.FIX_MISSING_IMPORT,
description: `Import ${importName} from '${potentialImport.moduleSpecifier}' on ${
importOn.name!.text}`,
changes: [{
fileName: importOn.getSourceFile().fileName,
textChanges: [...fileImportChanges, ...traitImportChanges],
}]
});
}
}
// Create the code action to insert the new imports.
const codeActions: ts.CodeFixAction[] = [{
fixName: FixIdForCodeFixesAll.FIX_MISSING_IMPORT,
description:
`Import ${importName} from '${potentialImport.moduleSpecifier}' on ${importOn.name!.text}`,
changes: [{
fileName: importOn.getSourceFile().fileName,
textChanges: [...fileImportChanges, ...traitImportChanges],
}],
}];
return codeActions;
}