mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(language-service): honor quote style preference when generating imports
The quick fix and completion auto-import always generated the module specifier with double quotes, ignoring the user's quote preference and the style used in the file.
The generated import now respects the `quotePreference` from the TypeScript user preferences. When the preference is `auto` (or absent), the style is inferred from the first existing import in the file, mirroring TypeScript's own behavior. Files with no imports
keep the previous double-quote default.
Fixes #67108
(cherry picked from commit 0f0d52e9c1)
This commit is contained in:
@@ -95,6 +95,7 @@ function getCodeActions({typeCheckInfo, start, compiler, tsLs, preferences}: Cod
|
||||
currMatch,
|
||||
tsLs,
|
||||
preferences.includeCompletionsForModuleExports,
|
||||
preferences,
|
||||
) ?? [];
|
||||
|
||||
codeActions.push(
|
||||
|
||||
@@ -865,6 +865,7 @@ export class CompletionBuilder<N extends TmplAstNode | AST> {
|
||||
directive,
|
||||
this.tsLS,
|
||||
options?.includeCompletionsWithInsertText,
|
||||
options,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
@@ -1325,6 +1326,7 @@ export class CompletionBuilder<N extends TmplAstNode | AST> {
|
||||
directive,
|
||||
this.tsLS,
|
||||
preferences?.includeCompletionsWithInsertText,
|
||||
preferences,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
|
||||
@@ -375,6 +375,7 @@ export function updateImportsForTypescriptFile(
|
||||
symbolName: string,
|
||||
declarationName: string,
|
||||
moduleSpecifier: string,
|
||||
preferences?: ts.UserPreferences,
|
||||
): [ts.TextChange[], string] {
|
||||
// The trait might already be imported, possibly under a different name. If so, determine the
|
||||
// local name of the imported trait.
|
||||
@@ -421,11 +422,40 @@ export function updateImportsForTypescriptFile(
|
||||
// TODO: Why does the compiler insist this is null?
|
||||
span.start = lastImport!.getStart() + lastImport!.getWidth();
|
||||
}
|
||||
const newImportDeclaration = generateImport(symbolName, importName, moduleSpecifier);
|
||||
const newImportDeclaration = generateImport(
|
||||
symbolName,
|
||||
importName,
|
||||
moduleSpecifier,
|
||||
shouldUseSingleQuotes(file, preferences),
|
||||
);
|
||||
const importString = '\n' + printNode(newImportDeclaration, file);
|
||||
return [[{span, newText: importString}], importName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether new string literals in the given file should use single quotes, based on the
|
||||
* user's `quotePreference`. When the preference is `auto` (or absent), the style is inferred
|
||||
* from the first existing import in the file, mirroring TypeScript's own behavior. Defaults
|
||||
* to double quotes when there is nothing to infer from.
|
||||
*/
|
||||
export function shouldUseSingleQuotes(
|
||||
file: ts.SourceFile,
|
||||
preferences: ts.UserPreferences | undefined,
|
||||
): boolean {
|
||||
if (preferences?.quotePreference === 'single') {
|
||||
return true;
|
||||
}
|
||||
if (preferences?.quotePreference === 'double') {
|
||||
return false;
|
||||
}
|
||||
for (const statement of file.statements) {
|
||||
if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
|
||||
return file.text[statement.moduleSpecifier.getStart(file)] === `'`;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a given Angular trait, such as an NgModule or standalone Component, by adding
|
||||
* `importName` to the list of imports on the decorator arguments.
|
||||
@@ -590,13 +620,14 @@ export function generateImport(
|
||||
localName: string,
|
||||
exportedSpecifierName: string | null,
|
||||
rawModuleSpecifier: string,
|
||||
useSingleQuotes = false,
|
||||
): ts.ImportDeclaration {
|
||||
let propName: ts.Identifier | undefined;
|
||||
if (exportedSpecifierName !== null && exportedSpecifierName !== localName) {
|
||||
propName = ts.factory.createIdentifier(exportedSpecifierName);
|
||||
}
|
||||
const name = ts.factory.createIdentifier(localName);
|
||||
const moduleSpec = ts.factory.createStringLiteral(rawModuleSpecifier);
|
||||
const moduleSpec = ts.factory.createStringLiteral(rawModuleSpecifier, useSingleQuotes);
|
||||
let importClauseName: ts.Identifier | undefined;
|
||||
let importBindings: ts.NamedImportBindings | undefined;
|
||||
|
||||
@@ -692,6 +723,7 @@ export function getCodeActionToImportTheDirectiveDeclaration(
|
||||
directive: PotentialDirective | PotentialPipe,
|
||||
tsLs: ts.LanguageService,
|
||||
includeCompletionsForModuleExports?: boolean,
|
||||
preferences?: ts.UserPreferences,
|
||||
): ts.CodeAction[] | undefined {
|
||||
const codeActions: ts.CodeAction[] = [];
|
||||
|
||||
@@ -741,6 +773,7 @@ export function getCodeActionToImportTheDirectiveDeclaration(
|
||||
potentialImport.symbolName,
|
||||
declarationName,
|
||||
potentialImport.moduleSpecifier,
|
||||
preferences,
|
||||
);
|
||||
importName = generatedImportName;
|
||||
fileImportChanges.push(...importChanges);
|
||||
@@ -753,6 +786,7 @@ export function getCodeActionToImportTheDirectiveDeclaration(
|
||||
'forwardRef',
|
||||
declarationName,
|
||||
'@angular/core',
|
||||
preferences,
|
||||
);
|
||||
fileImportChanges.push(...forwardRefImports);
|
||||
forwardRefName = generatedForwardRefName;
|
||||
|
||||
@@ -593,6 +593,80 @@ describe('code fixes', () => {
|
||||
const fixFile = project.openFile('foo.ts');
|
||||
fixFile.moveCursorToText('<¦bar>');
|
||||
|
||||
const codeActions = project.getCodeFixesAtPosition('foo.ts', fixFile.cursor, fixFile.cursor, [
|
||||
diags[0].code,
|
||||
]);
|
||||
const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
|
||||
actionChangesMatch(actionChanges, `Import BarComponent from './bar' on FooComponent`, [
|
||||
[``, `import { BarComponent } from './bar';`],
|
||||
[``, `, imports: [BarComponent]`],
|
||||
]);
|
||||
});
|
||||
|
||||
it('respects an explicit double quote preference for the generated import', () => {
|
||||
const standaloneFiles = {
|
||||
'foo.ts': `
|
||||
import {Component} from '@angular/core';
|
||||
@Component({
|
||||
selector: 'foo',
|
||||
template: '<bar></bar>'
|
||||
})
|
||||
export class FooComponent {}
|
||||
`,
|
||||
'bar.ts': `
|
||||
import {Component} from '@angular/core';
|
||||
@Component({
|
||||
selector: 'bar',
|
||||
template: '<div>bar</div>'
|
||||
})
|
||||
export class BarComponent {}
|
||||
`,
|
||||
};
|
||||
|
||||
const project = createModuleAndProjectWithDeclarations(env, 'test', {}, {}, standaloneFiles);
|
||||
const diags = project.getDiagnosticsForFile('foo.ts');
|
||||
const fixFile = project.openFile('foo.ts');
|
||||
fixFile.moveCursorToText('<¦bar>');
|
||||
|
||||
const codeActions = project.getCodeFixesAtPosition(
|
||||
'foo.ts',
|
||||
fixFile.cursor,
|
||||
fixFile.cursor,
|
||||
[diags[0].code],
|
||||
{quotePreference: 'double'},
|
||||
);
|
||||
const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
|
||||
actionChangesMatch(actionChanges, `Import BarComponent from './bar' on FooComponent`, [
|
||||
[``, `import { BarComponent } from "./bar";`],
|
||||
[``, `, imports: [BarComponent]`],
|
||||
]);
|
||||
});
|
||||
|
||||
it('matches the quote style of existing imports for the generated import', () => {
|
||||
const standaloneFiles = {
|
||||
'foo.ts': `
|
||||
import {Component} from "@angular/core";
|
||||
@Component({
|
||||
selector: 'foo',
|
||||
template: '<bar></bar>'
|
||||
})
|
||||
export class FooComponent {}
|
||||
`,
|
||||
'bar.ts': `
|
||||
import {Component} from '@angular/core';
|
||||
@Component({
|
||||
selector: 'bar',
|
||||
template: '<div>bar</div>'
|
||||
})
|
||||
export class BarComponent {}
|
||||
`,
|
||||
};
|
||||
|
||||
const project = createModuleAndProjectWithDeclarations(env, 'test', {}, {}, standaloneFiles);
|
||||
const diags = project.getDiagnosticsForFile('foo.ts');
|
||||
const fixFile = project.openFile('foo.ts');
|
||||
fixFile.moveCursorToText('<¦bar>');
|
||||
|
||||
const codeActions = project.getCodeFixesAtPosition('foo.ts', fixFile.cursor, fixFile.cursor, [
|
||||
diags[0].code,
|
||||
]);
|
||||
@@ -640,7 +714,7 @@ describe('code fixes', () => {
|
||||
]);
|
||||
const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
|
||||
actionChangesMatch(actionChanges, `Import BarModule from './bar' on FooComponent`, [
|
||||
[``, `import { BarModule } from "./bar";`],
|
||||
[``, `import { BarModule } from './bar';`],
|
||||
[``, `, imports: [BarModule]`],
|
||||
]);
|
||||
});
|
||||
@@ -682,7 +756,7 @@ describe('code fixes', () => {
|
||||
]);
|
||||
const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
|
||||
actionChangesMatch(actionChanges, `Import BarComponent from './bar' on FooModule`, [
|
||||
[``, `import { BarComponent } from "./bar";`],
|
||||
[``, `import { BarComponent } from './bar';`],
|
||||
[`imports: []`, `imports: [BarComponent]`],
|
||||
]);
|
||||
});
|
||||
@@ -721,7 +795,7 @@ describe('code fixes', () => {
|
||||
const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
|
||||
|
||||
actionChangesMatch(actionChanges, `Import BarPipe from './bar' on FooComponent`, [
|
||||
[``, `import { BarPipe } from "./bar";`],
|
||||
[``, `import { BarPipe } from './bar';`],
|
||||
['', `, imports: [BarPipe]`],
|
||||
]);
|
||||
});
|
||||
@@ -768,11 +842,11 @@ describe('code fixes', () => {
|
||||
]);
|
||||
const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
|
||||
actionChangesMatch(actionChanges, `Import BarModule from './bar' on FooComponent`, [
|
||||
[``, `import { BarModule } from "./bar";`],
|
||||
[``, `import { BarModule } from './bar';`],
|
||||
[``, `, imports: [BarModule]`],
|
||||
]);
|
||||
actionChangesMatch(actionChanges, `Import Bar2Module from './bar' on FooComponent`, [
|
||||
[``, `import { Bar2Module } from "./bar";`],
|
||||
[``, `import { Bar2Module } from './bar';`],
|
||||
[``, `, imports: [Bar2Module]`],
|
||||
]);
|
||||
});
|
||||
@@ -808,7 +882,7 @@ describe('code fixes', () => {
|
||||
]);
|
||||
const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
|
||||
actionChangesMatch(actionChanges, `Import BarComponent from './bar' on FooComponent`, [
|
||||
[``, `import BarComponent from "./bar";`],
|
||||
[``, `import BarComponent from './bar';`],
|
||||
[``, `, imports: [BarComponent]`],
|
||||
]);
|
||||
});
|
||||
@@ -948,7 +1022,7 @@ describe('code fixes', () => {
|
||||
]);
|
||||
const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
|
||||
actionChangesMatch(actionChanges, `Import MatCard from '@angular/common' on FooComponent`, [
|
||||
[``, `import { MatCard } from "@angular/common";`],
|
||||
[``, `import { MatCard } from '@angular/common';`],
|
||||
[``, `, imports: [MatCard]`],
|
||||
]);
|
||||
});
|
||||
@@ -988,7 +1062,7 @@ describe('code fixes', () => {
|
||||
);
|
||||
const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
|
||||
actionChangesMatch(actionChanges, `Import BarComponent from '@app/bar' on FooComponent`, [
|
||||
[``, `import { BarComponent } from "@app/bar";`],
|
||||
[``, `import { BarComponent } from '@app/bar';`],
|
||||
[``, `, imports: [BarComponent]`],
|
||||
]);
|
||||
});
|
||||
@@ -1041,7 +1115,7 @@ describe('code fixes', () => {
|
||||
actionChanges,
|
||||
`Import NewBarComponent3 from '@app/index' on FooComponent`,
|
||||
[
|
||||
[``, `import { NewBarComponent3 } from "@app/index";`],
|
||||
[``, `import { NewBarComponent3 } from '@app/index';`],
|
||||
[``, `, imports: [NewBarComponent3]`],
|
||||
],
|
||||
);
|
||||
@@ -1197,7 +1271,7 @@ describe('code fixes', () => {
|
||||
actionChanges,
|
||||
`Import BarModule from '../component/share/bar.module' on FooComponent`,
|
||||
[
|
||||
[``, `import { BarModule } from "../component/share/bar.module";`],
|
||||
[``, `import { BarModule } from '../component/share/bar.module';`],
|
||||
[``, `, imports: [BarModule]`],
|
||||
],
|
||||
);
|
||||
@@ -1255,7 +1329,7 @@ describe('code fixes', () => {
|
||||
|
||||
const actionChanges = allChangesForCodeActions(appModuleContents, codeActions);
|
||||
actionChangesMatch(actionChanges, `Import BarComponent from './bar' on AppModule`, [
|
||||
[``, `import { BarComponent } from "./bar";`],
|
||||
[``, `import { BarComponent } from './bar';`],
|
||||
[`imports: []`, `imports: [BarComponent]`],
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1599,7 +1599,7 @@ describe('completions', () => {
|
||||
'start': 303,
|
||||
'length': 0,
|
||||
},
|
||||
'newText': '\nimport { HighlightDirective } from "@app/highlight";',
|
||||
'newText': "\nimport { HighlightDirective } from '@app/highlight';",
|
||||
},
|
||||
{
|
||||
'span': {
|
||||
@@ -1805,7 +1805,7 @@ describe('completions', () => {
|
||||
'start': 303,
|
||||
'length': 0,
|
||||
},
|
||||
'newText': '\nimport { HighlightDirective } from "@app/highlight";',
|
||||
'newText': "\nimport { HighlightDirective } from '@app/highlight';",
|
||||
},
|
||||
{
|
||||
'span': {
|
||||
|
||||
@@ -216,6 +216,7 @@ export class Project {
|
||||
start: number,
|
||||
end: number,
|
||||
errorCodes: readonly number[],
|
||||
preferences: ts.UserPreferences = {},
|
||||
): readonly ts.CodeFixAction[] {
|
||||
const fileName = absoluteFrom(`/${this.name}/${projectFileName}`);
|
||||
return this.ngLS.getCodeFixesAtPosition(
|
||||
@@ -226,6 +227,7 @@ export class Project {
|
||||
{},
|
||||
{
|
||||
includeCompletionsForModuleExports: true,
|
||||
...preferences,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2420,7 +2420,7 @@ export class AppComponent {
|
||||
'changes': {
|
||||
[APP_COMPONENT_MODULE_URI]: [
|
||||
{
|
||||
'newText': '\nimport { BarComponent } from "./bar.component";',
|
||||
'newText': "\nimport { BarComponent } from './bar.component';",
|
||||
'range': {
|
||||
// Line numbers adjusted for HighlightDirective import
|
||||
'start': {'line': 6, 'character': 57},
|
||||
|
||||
Reference in New Issue
Block a user