diff --git a/src/compiler/types.ts b/src/compiler/types.ts index e56bba5ab4859..56e07e4b65a43 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -10074,6 +10074,7 @@ export interface UserPreferences { readonly organizeImportsNumericCollation?: boolean; readonly organizeImportsAccentCollation?: boolean; readonly organizeImportsCaseFirst?: "upper" | "lower" | false; + readonly organizeImportsTypeOrder?: "first" | "last" | "inline"; readonly excludeLibrarySymbolsInNavTo?: boolean; } diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index 5763b24c6bde6..2842fd6896a5f 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -3521,10 +3521,12 @@ export class TestState { actualTextArray.push(text); // Undo changes to perform next fix - const span = change.textChanges[0].span; - const deletedText = originalContent.substr(span.start, change.textChanges[0].span.length); - const insertedText = change.textChanges[0].newText; - this.editScriptAndUpdateMarkers(fileName, span.start, span.start + insertedText.length, deletedText); + for (const textChange of change.textChanges) { + const span = textChange.span; + const deletedText = originalContent.slice(span.start, span.start + textChange.span.length); + const insertedText = textChange.newText; + this.editScriptAndUpdateMarkers(fileName, span.start, span.start + insertedText.length, deletedText); + } } if (expectedTextArray.length !== actualTextArray.length) { this.raiseError(`Expected ${expectedTextArray.length} import fixes, got ${actualTextArray.length}:\n\n${actualTextArray.join("\n\n" + "-".repeat(20) + "\n\n")}`); diff --git a/src/server/protocol.ts b/src/server/protocol.ts index ae6c5bc055143..e0964cea6d9d6 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -3643,6 +3643,13 @@ export interface UserPreferences { * Default: `false` */ readonly organizeImportsCaseFirst?: "upper" | "lower" | false; + /** + * Indicates where named type-only imports should sort. "inline" sorts named imports without regard to if the import is + * type-only. + * + * Default: `last` + */ + readonly organizeImportsTypeOrder?: "last" | "first" | "inline"; /** * Indicates whether {@link ReferencesResponseItem.lineText} is supported. diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 97c5bca540b54..22cd9693ab98f 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -53,6 +53,7 @@ import { getSourceFileOfNode, getSymbolId, getTokenAtPosition, + getTokenPosOfNode, getTypeKeywordOfTypeOnlyImport, getUniqueSymbolId, hostGetCanonicalFileName, @@ -1406,14 +1407,14 @@ function promoteFromTypeOnly( if (aliasDeclaration.parent.elements.length > 1 && sortKind) { const newSpecifier = factory.updateImportSpecifier(aliasDeclaration, /*isTypeOnly*/ false, aliasDeclaration.propertyName, aliasDeclaration.name); const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, sortKind === SortKind.CaseInsensitive); - const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(aliasDeclaration.parent.elements, newSpecifier, comparer); - if (aliasDeclaration.parent.elements.indexOf(aliasDeclaration) !== insertionIndex) { + const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(aliasDeclaration.parent.elements, newSpecifier, comparer, preferences); + if (insertionIndex !== aliasDeclaration.parent.elements.indexOf(aliasDeclaration)) { changes.delete(sourceFile, aliasDeclaration); changes.insertImportSpecifierAtIndex(sourceFile, newSpecifier, aliasDeclaration.parent, insertionIndex); return aliasDeclaration; } } - changes.deleteRange(sourceFile, aliasDeclaration.getFirstToken()!); + changes.deleteRange(sourceFile, { pos: getTokenPosOfNode(aliasDeclaration.getFirstToken()!), end: getTokenPosOfNode(aliasDeclaration.propertyName ?? aliasDeclaration.name) }); return aliasDeclaration; } else { @@ -1538,7 +1539,7 @@ function doAddExistingFix( // type-only, there's no need to ask for the insertion index - it's 0. const insertionIndex = promoteFromTypeOnly && !spec.isTypeOnly ? 0 - : OrganizeImports.getImportSpecifierInsertionIndex(existingSpecifiers, spec, comparer); + : OrganizeImports.getImportSpecifierInsertionIndex(existingSpecifiers, spec, comparer, preferences); changes.insertImportSpecifierAtIndex(sourceFile, spec, clause.namedBindings as NamedImports, insertionIndex); } } diff --git a/src/services/organizeImports.ts b/src/services/organizeImports.ts index 777541679b7da..b9c2f7e89096f 100644 --- a/src/services/organizeImports.ts +++ b/src/services/organizeImports.ts @@ -23,8 +23,10 @@ import { flatMap, formatting, getNewLineOrDefaultFromHost, + getStringComparer, getUILocale, group, + groupBy, Identifier, identity, ImportClause, @@ -94,7 +96,7 @@ export function organizeImports( const processImportsOfSameModuleSpecifier = (importGroup: readonly ImportDeclaration[]) => { if (shouldRemove) importGroup = removeUnusedImports(importGroup, sourceFile, program); - if (shouldCombine) importGroup = coalesceImportsWorker(importGroup, comparer, sourceFile); + if (shouldCombine) importGroup = coalesceImportsWorker(importGroup, comparer, sourceFile, preferences); if (shouldSort) importGroup = stableSort(importGroup, (s1, s2) => compareImportsOrRequireStatements(s1, s2, comparer)); return importGroup; }; @@ -104,7 +106,7 @@ export function organizeImports( // Exports are always used if (mode !== OrganizeImportsMode.RemoveUnused) { // All of the old ExportDeclarations in the file, in syntactic order. - getTopLevelExportGroups(sourceFile).forEach(exportGroupDecl => organizeImportsWorker(exportGroupDecl, group => coalesceExportsWorker(group, comparer))); + getTopLevelExportGroups(sourceFile).forEach(exportGroupDecl => organizeImportsWorker(exportGroupDecl, group => coalesceExportsWorker(group, comparer, preferences))); } for (const ambientModule of sourceFile.statements.filter(isAmbientModule)) { @@ -116,7 +118,7 @@ export function organizeImports( // Exports are always used if (mode !== OrganizeImportsMode.RemoveUnused) { const ambientModuleExportDecls = ambientModule.body.statements.filter(isExportDeclaration); - organizeImportsWorker(ambientModuleExportDecls, group => coalesceExportsWorker(group, comparer)); + organizeImportsWorker(ambientModuleExportDecls, group => coalesceExportsWorker(group, comparer, preferences)); } } @@ -310,12 +312,12 @@ function getExternalModuleName(specifier: Expression | undefined) { * @deprecated Only used for testing * @internal */ -export function coalesceImports(importGroup: readonly ImportDeclaration[], ignoreCase: boolean, sourceFile?: SourceFile): readonly ImportDeclaration[] { +export function coalesceImports(importGroup: readonly ImportDeclaration[], ignoreCase: boolean, sourceFile?: SourceFile, preferences?: UserPreferences): readonly ImportDeclaration[] { const comparer = getOrganizeImportsOrdinalStringComparer(ignoreCase); - return coalesceImportsWorker(importGroup, comparer, sourceFile); + return coalesceImportsWorker(importGroup, comparer, sourceFile, preferences); } -function coalesceImportsWorker(importGroup: readonly ImportDeclaration[], comparer: Comparer, sourceFile?: SourceFile): readonly ImportDeclaration[] { +function coalesceImportsWorker(importGroup: readonly ImportDeclaration[], comparer: Comparer, sourceFile?: SourceFile, preferences?: UserPreferences): readonly ImportDeclaration[] { if (importGroup.length === 0) { return importGroup; } @@ -374,7 +376,7 @@ function coalesceImportsWorker(importGroup: readonly ImportDeclaration[], compar newImportSpecifiers.push(...getNewImportSpecifiers(namedImports)); const sortedImportSpecifiers = factory.createNodeArray( - sortSpecifiers(newImportSpecifiers, comparer), + sortSpecifiers(newImportSpecifiers, comparer, preferences), firstNamedImport?.importClause.namedBindings.elements.hasTrailingComma, ); @@ -491,18 +493,17 @@ function getCategorizedImports(importGroup: readonly ImportDeclaration[]) { * @deprecated Only used for testing * @internal */ -export function coalesceExports(exportGroup: readonly ExportDeclaration[], ignoreCase: boolean) { +export function coalesceExports(exportGroup: readonly ExportDeclaration[], ignoreCase: boolean, preferences?: UserPreferences) { const comparer = getOrganizeImportsOrdinalStringComparer(ignoreCase); - return coalesceExportsWorker(exportGroup, comparer); + return coalesceExportsWorker(exportGroup, comparer, preferences); } -function coalesceExportsWorker(exportGroup: readonly ExportDeclaration[], comparer: Comparer) { +function coalesceExportsWorker(exportGroup: readonly ExportDeclaration[], comparer: Comparer, preferences?: UserPreferences) { if (exportGroup.length === 0) { return exportGroup; } const { exportWithoutClause, namedExports, typeOnlyExports } = getCategorizedExports(exportGroup); - const coalescedExports: ExportDeclaration[] = []; if (exportWithoutClause) { @@ -516,7 +517,7 @@ function coalesceExportsWorker(exportGroup: readonly ExportDeclaration[], compar const newExportSpecifiers: ExportSpecifier[] = []; newExportSpecifiers.push(...flatMap(exportGroup, i => i.exportClause && isNamedExports(i.exportClause) ? i.exportClause.elements : emptyArray)); - const sortedExportSpecifiers = sortSpecifiers(newExportSpecifiers, comparer); + const sortedExportSpecifiers = sortSpecifiers(newExportSpecifiers, comparer, preferences); const exportDecl = exportGroup[0]; coalescedExports.push( @@ -583,13 +584,20 @@ function updateImportDeclarationAndClause( ); } -function sortSpecifiers(specifiers: readonly T[], comparer: Comparer) { - return stableSort(specifiers, (s1, s2) => compareImportOrExportSpecifiers(s1, s2, comparer)); +function sortSpecifiers(specifiers: readonly T[], comparer: Comparer, preferences?: UserPreferences): readonly T[] { + return stableSort(specifiers, (s1, s2) => compareImportOrExportSpecifiers(s1, s2, comparer, preferences)); } /** @internal */ -export function compareImportOrExportSpecifiers(s1: T, s2: T, comparer: Comparer): Comparison { - return compareBooleans(s1.isTypeOnly, s2.isTypeOnly) || comparer(s1.name.text, s2.name.text); +export function compareImportOrExportSpecifiers(s1: T, s2: T, comparer: Comparer, preferences?: UserPreferences): Comparison { + switch (preferences?.organizeImportsTypeOrder) { + case "first": + return compareBooleans(s2.isTypeOnly, s1.isTypeOnly) || comparer(s1.name.text, s2.name.text); + case "inline": + return comparer(s1.name.text, s2.name.text); + default: + return compareBooleans(s1.isTypeOnly, s2.isTypeOnly) || comparer(s1.name.text, s2.name.text); + } } /** @@ -721,11 +729,51 @@ class ImportSpecifierSortingCache implements MemoizeCache<[readonly ImportSpecif /** @internal */ export const detectImportSpecifierSorting = memoizeCached((specifiers: readonly ImportSpecifier[], preferences: UserPreferences): SortKind => { - if (!arrayIsSorted(specifiers, (s1, s2) => compareBooleans(s1.isTypeOnly, s2.isTypeOnly))) { - return SortKind.None; + // If types are not sorted as specified, then imports are assumed to be unsorted. + // If there is no type sorting specification, we default to "last" and move on to case sensitivity detection. + switch (preferences.organizeImportsTypeOrder) { + case "first": + if (!arrayIsSorted(specifiers, (s1, s2) => compareBooleans(s2.isTypeOnly, s1.isTypeOnly))) return SortKind.None; + break; + case "inline": + if ( + !arrayIsSorted(specifiers, (s1, s2) => { + const comparer = getStringComparer(/*ignoreCase*/ true); + return comparer(s1.name.text, s2.name.text); + }) + ) { + return SortKind.None; + } + break; + default: + if (!arrayIsSorted(specifiers, (s1, s2) => compareBooleans(s1.isTypeOnly, s2.isTypeOnly))) return SortKind.None; + break; } + const collateCaseSensitive = getOrganizeImportsComparer(preferences, /*ignoreCase*/ false); const collateCaseInsensitive = getOrganizeImportsComparer(preferences, /*ignoreCase*/ true); + + if (preferences.organizeImportsTypeOrder !== "inline") { + const { type: regularImports, regular: typeImports } = groupBy(specifiers, s => s.isTypeOnly ? "type" : "regular"); + const regularCaseSensitivity = regularImports?.length + ? detectSortCaseSensitivity(regularImports, specifier => specifier.name.text, collateCaseSensitive, collateCaseInsensitive) + : undefined; + const typeCaseSensitivity = typeImports?.length + ? detectSortCaseSensitivity(typeImports, specifier => specifier.name.text ?? "", collateCaseSensitive, collateCaseInsensitive) + : undefined; + if (regularCaseSensitivity === undefined) { + return typeCaseSensitivity ?? SortKind.None; + } + if (typeCaseSensitivity === undefined) { + return regularCaseSensitivity; + } + if (regularCaseSensitivity === SortKind.None || typeCaseSensitivity === SortKind.None) { + return SortKind.None; + } + return typeCaseSensitivity & regularCaseSensitivity; + } + + // else inline return detectSortCaseSensitivity(specifiers, specifier => specifier.name.text, collateCaseSensitive, collateCaseInsensitive); }, new ImportSpecifierSortingCache()); @@ -736,8 +784,8 @@ export function getImportDeclarationInsertionIndex(sortedImports: readonly AnyIm } /** @internal */ -export function getImportSpecifierInsertionIndex(sortedImports: readonly ImportSpecifier[], newImport: ImportSpecifier, comparer: Comparer) { - const index = binarySearch(sortedImports, newImport, identity, (s1, s2) => compareImportOrExportSpecifiers(s1, s2, comparer)); +export function getImportSpecifierInsertionIndex(sortedImports: readonly ImportSpecifier[], newImport: ImportSpecifier, comparer: Comparer, preferences: UserPreferences) { + const index = binarySearch(sortedImports, newImport, identity, (s1, s2) => compareImportOrExportSpecifiers(s1, s2, comparer, preferences)); return index < 0 ? ~index : index; } diff --git a/src/testRunner/unittests/services/organizeImports.ts b/src/testRunner/unittests/services/organizeImports.ts index be6809a0ade23..e2970ef0d84a1 100644 --- a/src/testRunner/unittests/services/organizeImports.ts +++ b/src/testRunner/unittests/services/organizeImports.ts @@ -239,10 +239,10 @@ describe("unittests:: services:: organizeImports", () => { assertListEqual(actualCoalescedExports, expectedCoalescedExports); }); - it("Sort specifiers - type-only", () => { + it("Sort specifiers - type-only-inline", () => { const sortedImports = parseImports(`import { type z, y, type x, c, type b, a } from "lib";`); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); - const expectedCoalescedImports = parseImports(`import { a, c, y, type b, type x, type z } from "lib";`); + const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true, ts.getSourceFileOfNode(sortedImports[0]), { organizeImportsTypeOrder: "inline" }); + const expectedCoalescedImports = parseImports(`import { a, type b, c, type x, y, type z } from "lib";`); assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index c723fbebb0a16..fe1a414ef6f51 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -2933,6 +2933,13 @@ declare namespace ts { * Default: `false` */ readonly organizeImportsCaseFirst?: "upper" | "lower" | false; + /** + * Indicates where named type-only imports should sort. "inline" sorts named imports without regard to if the import is + * type-only. + * + * Default: `last` + */ + readonly organizeImportsTypeOrder?: "last" | "first" | "inline"; /** * Indicates whether {@link ReferencesResponseItem.lineText} is supported. */ @@ -8784,6 +8791,7 @@ declare namespace ts { readonly organizeImportsNumericCollation?: boolean; readonly organizeImportsAccentCollation?: boolean; readonly organizeImportsCaseFirst?: "upper" | "lower" | false; + readonly organizeImportsTypeOrder?: "first" | "last" | "inline"; readonly excludeLibrarySymbolsInNavTo?: boolean; } /** Represents a bigint literal value without requiring bigint support */ diff --git a/tests/cases/fourslash/autoImportTypeImport1.ts b/tests/cases/fourslash/autoImportTypeImport1.ts new file mode 100644 index 0000000000000..40422df03d2e7 --- /dev/null +++ b/tests/cases/fourslash/autoImportTypeImport1.ts @@ -0,0 +1,46 @@ +/// + +// @verbatimModuleSyntax: true +// @target: esnext + +// @Filename: /foo.ts +//// export const A = 1; +//// export type B = { x: number }; +//// export type C = 1; +//// export class D = { y: string }; + +// @Filename: /test.ts +//// import { A, D, type C } from './foo'; +//// const b: B/**/ | C; +//// console.log(A, D); + +goTo.marker(""); + +// importFixes should only place the import in sorted position if the existing imports are sorted as specified, +// otherwise the import should be placed at the end +verify.importFixAtPosition([ +`import { A, D, type C, type B } from './foo'; +const b: B | C; +console.log(A, D);`], + /*errorCode*/ undefined, + { organizeImportsTypeOrder: "inline" } + // `type B` is added to the end since the existing imports are not sorted as specified +); + +verify.importFixAtPosition([ +`import { A, D, type B, type C } from './foo'; +const b: B | C; +console.log(A, D);`], + /*errorCode*/ undefined, + { organizeImportsTypeOrder: "last" } + // `type B` is added to the sorted position since the existing imports *are* sorted as specified +); + +verify.importFixAtPosition([ +`import { A, D, type C, type B } from './foo'; +const b: B | C; +console.log(A, D);`], + /*errorCode*/ undefined, + { organizeImportsTypeOrder: "first" } + // `type B` is added to the end (default behavior) since the existing imports are not sorted as specified +); \ No newline at end of file diff --git a/tests/cases/fourslash/autoImportTypeImport2.ts b/tests/cases/fourslash/autoImportTypeImport2.ts new file mode 100644 index 0000000000000..195a12980c2f8 --- /dev/null +++ b/tests/cases/fourslash/autoImportTypeImport2.ts @@ -0,0 +1,43 @@ +/// + +// @verbatimModuleSyntax: true +// @target: esnext + +// @Filename: /foo.ts +//// export const A = 1; +//// export type B = { x: number }; +//// export type C = 1; +//// export class D = { y: string }; + +// @Filename: /test.ts +//// import { A, type C, D } from './foo'; +//// const b: B/**/ | C; +//// console.log(A, D); + +goTo.marker(""); + +// importFixes should only place the import in sorted position if the existing imports are sorted as specified, +// otherwise the import should be placed at the end +verify.importFixAtPosition([ +`import { A, type B, type C, D } from './foo'; +const b: B | C; +console.log(A, D);`], + /*errorCode*/ undefined, + { organizeImportsTypeOrder: "inline" } +); + +verify.importFixAtPosition([ +`import { A, type C, D, type B } from './foo'; +const b: B | C; +console.log(A, D);`], + /*errorCode*/ undefined, + { organizeImportsTypeOrder: "last" } +); + +verify.importFixAtPosition([ +`import { A, type C, D, type B } from './foo'; +const b: B | C; +console.log(A, D);`], + /*errorCode*/ undefined, + { organizeImportsTypeOrder: "first" } +); \ No newline at end of file diff --git a/tests/cases/fourslash/autoImportTypeImport3.ts b/tests/cases/fourslash/autoImportTypeImport3.ts new file mode 100644 index 0000000000000..50594e625817c --- /dev/null +++ b/tests/cases/fourslash/autoImportTypeImport3.ts @@ -0,0 +1,44 @@ +/// + +// @verbatimModuleSyntax: true +// @target: esnext + +// @Filename: /foo.ts +//// export const A = 1; +//// export type B = { x: number }; +//// export type C = 1; +//// export class D = { y: string }; + +// @Filename: /test.ts +//// import { A, type B, type C } from './foo'; +//// const b: B | C; +//// console.log(A, D/**/); + +goTo.marker(""); + +// importFixes should only place the import in sorted position if the existing imports are sorted as specified, +// otherwise the import should be placed at the end (regardless of if it's a regular or type-only import) +verify.importFixAtPosition([ +`import { A, D, type B, type C } from './foo'; +const b: B | C; +console.log(A, D);`], + /*errorCode*/ undefined, + { organizeImportsIgnoreCase: true, organizeImportsTypeOrder : "last" } +); + +verify.importFixAtPosition([ +`import { A, type B, type C, D } from './foo'; +const b: B | C; +console.log(A, D);`], + /*errorCode*/ undefined, + { organizeImportsIgnoreCase: true, organizeImportsTypeOrder : "inline" } +); + +verify.importFixAtPosition([ +`import { A, type B, type C, D } from './foo'; +const b: B | C; +console.log(A, D);`], + /*errorCode*/ undefined, + { organizeImportsIgnoreCase: true, organizeImportsTypeOrder : "first" } + // `D` is added to the end since `A, type B, type C` is not sorted to "first" +); \ No newline at end of file diff --git a/tests/cases/fourslash/autoImportTypeImport4.ts b/tests/cases/fourslash/autoImportTypeImport4.ts new file mode 100644 index 0000000000000..82dff821438f7 --- /dev/null +++ b/tests/cases/fourslash/autoImportTypeImport4.ts @@ -0,0 +1,111 @@ +/// + +// @verbatimModuleSyntax: true +// @target: esnext + +// @Filename: /exports1.ts +//// export const a = 0; +//// export const A = 1; +//// export const b = 2; +//// export const B = 3; +//// export const c = 4; +//// export const C = 5; +//// export type x = 6; +//// export const X = 7; +//// export const Y = 8; +//// export const Z = 9; + +// @Filename: /exports2.ts +//// export const d = 0; +//// export const D = 1; +//// export const e = 2; +//// export const E = 3; + +// @Filename: /index0.ts +//// import { A, B, C } from "./exports1"; +//// a/*0*/; +//// b; + +// @Filename: /index1.ts +//// import { A, B, C, type Y, type Z } from "./exports1"; +//// a/*1*/; +//// b; + +// @Filename: /index2.ts +//// import { A, a, B, b, type Y, type Z } from "./exports1"; +//// import { E } from "./exports2"; +//// d/*2*/ + +// addition of correctly sorted type imports should not affect behavior as shown in autoImportSortCaseSensitivity1.ts +goTo.marker("0"); +verify.importFixAtPosition([ + `import { A, B, C, a } from "./exports1";\na;\nb;`, + `import { A, B, C, b } from "./exports1";\na;\nb;`, +], + /*errorCode*/ undefined, + { organizeImportsTypeOrder : "last" }); +verify.importFixAtPosition([ + `import { a, A, B, C } from "./exports1";\na;\nb;`, + `import { A, b, B, C } from "./exports1";\na;\nb;` +], + /*errorCode*/ undefined, + { organizeImportsIgnoreCase: true, organizeImportsTypeOrder : "last" }); + +goTo.marker("1"); +verify.importFixAtPosition([ + `import { A, B, C, a, type Y, type Z } from "./exports1";\na;\nb;`, + `import { A, B, C, b, type Y, type Z } from "./exports1";\na;\nb;`, +], + /*errorCode*/ undefined, + { organizeImportsTypeOrder : "last" }); +verify.importFixAtPosition([ + `import { a, A, B, C, type Y, type Z } from "./exports1";\na;\nb;`, + `import { A, b, B, C, type Y, type Z } from "./exports1";\na;\nb;` +], + /*errorCode*/ undefined, + { organizeImportsIgnoreCase: true, organizeImportsTypeOrder : "last" }); + +// if we sort inline and sensitive, then all upper case imports should be sorted before any lower case imports +verify.importFixAtPosition([ + `import { A, B, C, type Y, type Z, a } from "./exports1";\na;\nb;`, + `import { A, B, C, type Y, type Z, b } from "./exports1";\na;\nb;`, +], + /*errorCode*/ undefined, + { organizeImportsTypeOrder : "inline" }); +verify.importFixAtPosition([ + `import { a, A, B, C, type Y, type Z } from "./exports1";\na;\nb;`, + `import { A, b, B, C, type Y, type Z } from "./exports1";\na;\nb;` +], + /*errorCode*/ undefined, + { organizeImportsIgnoreCase: true, organizeImportsTypeOrder : "inline" }); + +goTo.marker("2"); +verify.importFixAtPosition([ +`import { A, a, B, b, type Y, type Z } from "./exports1"; +import { d, E } from "./exports2"; +d`, +], + /*errorCode*/ undefined, + { organizeImportsTypeOrder : "last" }); +verify.importFixAtPosition([ +`import { A, a, B, b, type Y, type Z } from "./exports1"; +import { E, d } from "./exports2"; +d` +], + /*errorCode*/ undefined, + { organizeImportsIgnoreCase: false, organizeImportsTypeOrder : "last" }); + +verify.importFixAtPosition([ +`import { A, a, B, b, type Y, type Z } from "./exports1"; +import { d, E } from "./exports2"; +d`, +], + /*errorCode*/ undefined, + { organizeImportsTypeOrder : "last" }); +verify.importFixAtPosition([ +`import { A, a, B, b, type Y, type Z } from "./exports1"; +import { E, d } from "./exports2"; +d` +], + /*errorCode*/ undefined, + { organizeImportsIgnoreCase: false, organizeImportsTypeOrder : "last" }); \ No newline at end of file diff --git a/tests/cases/fourslash/autoImportTypeImport5.ts b/tests/cases/fourslash/autoImportTypeImport5.ts new file mode 100644 index 0000000000000..a0800d3f236f0 --- /dev/null +++ b/tests/cases/fourslash/autoImportTypeImport5.ts @@ -0,0 +1,86 @@ +/// + +// @verbatimModuleSyntax: true +// @target: esnext + +// @Filename: /exports1.ts +//// export const a = 0; +//// export const A = 1; +//// export const b = 2; +//// export const B = 3; +//// export const c = 4; +//// export const C = 5; +//// export type x = 6; +//// export const X = 7; +//// export type y = 8 +//// export const Y = 9; +//// export const Z = 10; + +// @Filename: /exports2.ts +//// export const d = 0; +//// export const D = 1; +//// export const e = 2; +//// export const E = 3; + +// @Filename: /index0.ts +//// import { type X, type Y, type Z } from "./exports1"; +//// const foo: x/*0*/; +//// const bar: y; + +// @Filename: /index1.ts +//// import { A, B, type X, type Y, type Z } from "./exports1"; +//// const foo: x/*1*/; +//// const bar: y; + +// addition of correctly sorted regular imports should not affect correctly sorted type imports +goTo.marker("0"); +verify.importFixAtPosition([ + `import { type X, type Y, type Z, type x } from "./exports1";\nconst foo: x;\nconst bar: y;`, + `import { type X, type Y, type Z, type y } from "./exports1";\nconst foo: x;\nconst bar: y;`, +], + /*errorCode*/ undefined, + { organizeImportsTypeOrder : "last" }); +verify.importFixAtPosition([ + `import { type x, type X, type Y, type Z } from "./exports1";\nconst foo: x;\nconst bar: y;`, + `import { type X, type y, type Y, type Z } from "./exports1";\nconst foo: x;\nconst bar: y;`, +], + /*errorCode*/ undefined, + { organizeImportsIgnoreCase: true, organizeImportsTypeOrder : "last" }); +verify.importFixAtPosition([ + `import { type X, type Y, type Z, type x } from "./exports1";\nconst foo: x;\nconst bar: y;`, + `import { type X, type Y, type Z, type y } from "./exports1";\nconst foo: x;\nconst bar: y;`, +], + /*errorCode*/ undefined, + { organizeImportsTypeOrder : "inline" }); +verify.importFixAtPosition([ + `import { type x, type X, type Y, type Z } from "./exports1";\nconst foo: x;\nconst bar: y;`, + `import { type X, type y, type Y, type Z } from "./exports1";\nconst foo: x;\nconst bar: y;`, +], + /*errorCode*/ undefined, + { organizeImportsIgnoreCase: true, organizeImportsTypeOrder : "inline" }); +verify.importFixAtPosition([ + `import { type X, type Y, type Z, type x } from "./exports1";\nconst foo: x;\nconst bar: y;`, + `import { type X, type Y, type Z, type y } from "./exports1";\nconst foo: x;\nconst bar: y;`, +], + /*errorCode*/ undefined, + { organizeImportsTypeOrder : "first" }); +verify.importFixAtPosition([ + `import { type x, type X, type Y, type Z } from "./exports1";\nconst foo: x;\nconst bar: y;`, + `import { type X, type y, type Y, type Z } from "./exports1";\nconst foo: x;\nconst bar: y;`, +], + /*errorCode*/ undefined, + { organizeImportsIgnoreCase: true, organizeImportsTypeOrder : "first" }); + +goTo.marker("1"); +verify.importFixAtPosition([ + `import { A, B, type X, type Y, type Z, type x } from "./exports1";\nconst foo: x;\nconst bar: y;`, + `import { A, B, type X, type Y, type Z, type y } from "./exports1";\nconst foo: x;\nconst bar: y;`, +], + /*errorCode*/ undefined, + { organizeImportsTypeOrder : "last" }); +verify.importFixAtPosition([ + `import { A, B, type x, type X, type Y, type Z } from "./exports1";\nconst foo: x;\nconst bar: y;`, + `import { A, B, type X, type y, type Y, type Z } from "./exports1";\nconst foo: x;\nconst bar: y;`, +], + /*errorCode*/ undefined, + { organizeImportsIgnoreCase: true, organizeImportsTypeOrder : "last" }); \ No newline at end of file diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index b61f947e488b1..5daa7c0b3167e 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -670,6 +670,7 @@ declare namespace FourSlashInterface { readonly organizeImportsNumericCollation?: boolean; readonly organizeImportsAccentCollation?: boolean; readonly organizeImportsCaseFirst?: "upper" | "lower" | false; + readonly organizeImportsTypeOrder?: "first" | "last" | "inline"; } interface InlayHintsOptions extends UserPreferences { readonly includeInlayParameterNameHints?: "none" | "literals" | "all"; diff --git a/tests/cases/fourslash/importNameCodeFix_importType7.ts b/tests/cases/fourslash/importNameCodeFix_importType7.ts index 7d28f3019654b..c74a3c2c22837 100644 --- a/tests/cases/fourslash/importNameCodeFix_importType7.ts +++ b/tests/cases/fourslash/importNameCodeFix_importType7.ts @@ -22,3 +22,31 @@ verify.importFixAtPosition([ type SomeInterface, } from "./exports.js"; new SomePig`]); + +verify.importFixAtPosition([ +`import { + SomePig, + type SomeInterface, +} from "./exports.js"; +new SomePig`], +/*errorCode*/ undefined, +{ organizeImportsTypeOrder: "last" }); + +verify.importFixAtPosition([ +`import { + type SomeInterface, + SomePig, +} from "./exports.js"; +new SomePig`], + /*errorCode*/ undefined, + { organizeImportsTypeOrder: "inline" } +); + +verify.importFixAtPosition([ +`import { + type SomeInterface, + SomePig, +} from "./exports.js"; +new SomePig`], +/*errorCode*/ undefined, +{ organizeImportsTypeOrder: "first" }); diff --git a/tests/cases/fourslash/organizeImports1.ts b/tests/cases/fourslash/organizeImports1.ts index 58306c8161caa..53ee2c25b1fe1 100644 --- a/tests/cases/fourslash/organizeImports1.ts +++ b/tests/cases/fourslash/organizeImports1.ts @@ -18,26 +18,26 @@ //// console.log(a, B, b, c, C, d, D); //// console.log(e, f, F, g, G, H, h); -// verify.organizeImports( -// `import { -// a, -// b, -// b as B, -// c, -// c as C, -// d, d as D, -// e, -// f, -// f as F, -// g, -// g as G, -// h, h as H -// } from './foo'; +verify.organizeImports( +`import { + a, + b, + b as B, + c, + c as C, + d, d as D, + e, + f, + f as F, + g, + g as G, + h, h as H +} from './foo'; -// console.log(a, B, b, c, C, d, D); -// console.log(e, f, F, g, G, H, h);`, -// /*mode*/ undefined, -// { organizeImportsIgnoreCase: true }); +console.log(a, B, b, c, C, d, D); +console.log(e, f, F, g, G, H, h);`, +/*mode*/ undefined, +{ organizeImportsIgnoreCase: true }); verify.organizeImports( `import { diff --git a/tests/cases/fourslash/organizeImports16.ts b/tests/cases/fourslash/organizeImports16.ts index 93aa5a81f2cb8..be7150c047e0f 100644 --- a/tests/cases/fourslash/organizeImports16.ts +++ b/tests/cases/fourslash/organizeImports16.ts @@ -9,22 +9,25 @@ verify.organizeImports( interface Use extends A {} console.log(a, b);`); +edit.replaceLine(0, 'import { a, A, b } from "foo1";'); verify.organizeImports( -`import { a, A, b } from "foo"; +`import { a, A, b } from "foo1"; interface Use extends A {} console.log(a, b);`, /*mode*/ undefined, { organizeImportsIgnoreCase: "auto" }); +edit.replaceLine(0, 'import { a, A, b } from "foo2";'); verify.organizeImports( -`import { a, A, b } from "foo"; +`import { a, A, b } from "foo2"; interface Use extends A {} console.log(a, b);`, /*mode*/ undefined, { organizeImportsIgnoreCase: true }); +edit.replaceLine(0, 'import { a, A, b } from "foo3";'); verify.organizeImports( -`import { A, a, b } from "foo"; +`import { A, a, b } from "foo3"; interface Use extends A {} console.log(a, b);`, /*mode*/ undefined, diff --git a/tests/cases/fourslash/organizeImportsType1.ts b/tests/cases/fourslash/organizeImportsType1.ts new file mode 100644 index 0000000000000..998cb2f041123 --- /dev/null +++ b/tests/cases/fourslash/organizeImportsType1.ts @@ -0,0 +1,46 @@ +/// + +// @allowSyntheticDefaultImports: true +// @moduleResolution: node +// @noUnusedLocals: true +// @target: es2018 + +//// import { A } from "foo"; +//// import { type B } from "foo"; +//// import { C } from "foo"; +//// import { type E } from "foo"; +//// import { D } from "foo"; +//// +//// console.log(A, B, C, D, E); + +// default behavior is "last" +verify.organizeImports( +`import { A, C, D, type B, type E } from "foo"; + +console.log(A, B, C, D, E);` +); + +verify.organizeImports( +`import { A, type B, C, D, type E } from "foo"; + +console.log(A, B, C, D, E);`, + undefined, + { organizeImportsTypeOrder : "inline" } +); + + +verify.organizeImports( +`import { type B, type E, A, C, D } from "foo"; + +console.log(A, B, C, D, E);`, + undefined, + { organizeImportsTypeOrder : "first" } +); + +verify.organizeImports( +`import { A, C, D, type B, type E } from "foo"; + +console.log(A, B, C, D, E);`, + undefined, + { organizeImportsTypeOrder : "last" } +); \ No newline at end of file diff --git a/tests/cases/fourslash/organizeImportsType2.ts b/tests/cases/fourslash/organizeImportsType2.ts new file mode 100644 index 0000000000000..57b2b4db26950 --- /dev/null +++ b/tests/cases/fourslash/organizeImportsType2.ts @@ -0,0 +1,50 @@ +/// + +// @allowSyntheticDefaultImports: true +// @moduleResolution: node +// @noUnusedLocals: true +// @target: es2018 + +//// type A = string; +//// type B = string; +//// const C = "hello"; +//// export { A, type B, C }; + +// default behavior is equivalent to "last" +verify.organizeImports( +`type A = string; +type B = string; +const C = "hello"; +export { A, C, type B }; +` +); + +verify.organizeImports( +`type A = string; +type B = string; +const C = "hello"; +export { A, type B, C }; +`, + undefined, + { organizeImportsTypeOrder : "inline" } +); + +verify.organizeImports( +`type A = string; +type B = string; +const C = "hello"; +export { type B, A, C }; +`, + undefined, + { organizeImportsTypeOrder : "first" } +); + +verify.organizeImports( +`type A = string; +type B = string; +const C = "hello"; +export { A, C, type B }; +`, + undefined, + { organizeImportsTypeOrder : "last" } +); \ No newline at end of file diff --git a/tests/cases/fourslash/organizeImportsType3.ts b/tests/cases/fourslash/organizeImportsType3.ts new file mode 100644 index 0000000000000..4ed9a24f04cb5 --- /dev/null +++ b/tests/cases/fourslash/organizeImportsType3.ts @@ -0,0 +1,29 @@ +/// + +//// import { +//// d, +//// type d as D, +//// type c, +//// c as C, +//// b, +//// b as B, +//// type A, +//// a +//// } from './foo'; +//// console.log(A, a, B, b, c, C, d, D); + +verify.organizeImports( +`import { + type A, + b as B, + c as C, + type d as D, + a, + b, + type c, + d +} from './foo'; +console.log(A, a, B, b, c, C, d, D);`, + /*mode*/ undefined, + { organizeImportsIgnoreCase: false, organizeImportsTypeOrder: "inline" } +); diff --git a/tests/cases/fourslash/organizeImportsType4.ts b/tests/cases/fourslash/organizeImportsType4.ts new file mode 100644 index 0000000000000..da76b5f17bbea --- /dev/null +++ b/tests/cases/fourslash/organizeImportsType4.ts @@ -0,0 +1,29 @@ +/// + +//// import { +//// d, +//// type d as D, +//// type c, +//// c as C, +//// b, +//// b as B, +//// type A, +//// a +//// } from './foo'; +//// console.log(A, a, B, b, c, C, d, D); + +verify.organizeImports( +`import { + type A, + a, + b, + b as B, + type c, + c as C, + d, + type d as D +} from './foo'; +console.log(A, a, B, b, c, C, d, D);`, + /*mode*/ undefined, + { organizeImportsIgnoreCase: true, organizeImportsTypeOrder: "inline" } +); diff --git a/tests/cases/fourslash/organizeImportsType5.ts b/tests/cases/fourslash/organizeImportsType5.ts new file mode 100644 index 0000000000000..1f8bb8dd602a7 --- /dev/null +++ b/tests/cases/fourslash/organizeImportsType5.ts @@ -0,0 +1,29 @@ +/// + +//// import { +//// d, +//// type d as D, +//// type c, +//// c as C, +//// b, +//// b as B, +//// type A, +//// a +//// } from './foo'; +//// console.log(A, a, B, b, c, C, d, D); + +verify.organizeImports( +`import { + type A, + b as B, + c as C, + type d as D, + a, + b, + type c, + d +} from './foo'; +console.log(A, a, B, b, c, C, d, D);`, + /*mode*/ undefined, + { organizeImportsIgnoreCase: "auto", organizeImportsTypeOrder: "inline" } +); diff --git a/tests/cases/fourslash/organizeImportsType6.ts b/tests/cases/fourslash/organizeImportsType6.ts new file mode 100644 index 0000000000000..c38fc504e07b1 --- /dev/null +++ b/tests/cases/fourslash/organizeImportsType6.ts @@ -0,0 +1,36 @@ +/// + +//// import { type a, A, b } from "foo"; +//// interface Use extends A {} +//// console.log(a, b); + +verify.organizeImports( +`import { type a, A, b } from "foo"; +interface Use extends A {} +console.log(a, b);`, + /*mode*/ undefined, + { organizeImportsTypeOrder: "inline" }); + +edit.replaceLine(0, 'import { type a, A, b } from "foo1";'); +verify.organizeImports( +`import { type a, A, b } from "foo1"; +interface Use extends A {} +console.log(a, b);`, + /*mode*/ undefined, + { organizeImportsIgnoreCase: "auto", organizeImportsTypeOrder: "inline" }); + +edit.replaceLine(0, 'import { type a, A, b } from "foo2";'); +verify.organizeImports( +`import { type a, A, b } from "foo2"; +interface Use extends A {} +console.log(a, b);`, + /*mode*/ undefined, + { organizeImportsIgnoreCase: true, organizeImportsTypeOrder: "inline" }); + +edit.replaceLine(0, 'import { type a, A, b } from "foo3";'); +verify.organizeImports( +`import { A, type a, b } from "foo3"; +interface Use extends A {} +console.log(a, b);`, + /*mode*/ undefined, + { organizeImportsIgnoreCase: false, organizeImportsTypeOrder: "inline" }); \ No newline at end of file diff --git a/tests/cases/fourslash/organizeImportsType7.ts b/tests/cases/fourslash/organizeImportsType7.ts new file mode 100644 index 0000000000000..72fd2af92c9db --- /dev/null +++ b/tests/cases/fourslash/organizeImportsType7.ts @@ -0,0 +1,36 @@ +/// + +//// import { a, type A, b } from "foo"; +//// interface Use extends A {} +//// console.log(a, b); + +verify.organizeImports( +`import { a, type A, b } from "foo"; +interface Use extends A {} +console.log(a, b);`, + /*mode*/ undefined, + { organizeImportsTypeOrder: "inline" }); + +edit.replaceLine(0, 'import { a, type A, b } from "foo1";'); +verify.organizeImports( +`import { a, type A, b } from "foo1"; +interface Use extends A {} +console.log(a, b);`, + /*mode*/ undefined, + { organizeImportsIgnoreCase: "auto", organizeImportsTypeOrder: "inline" }); + +edit.replaceLine(0, 'import { a, type A, b } from "foo2";'); +verify.organizeImports( +`import { a, type A, b } from "foo2"; +interface Use extends A {} +console.log(a, b);`, + /*mode*/ undefined, + { organizeImportsIgnoreCase: true, organizeImportsTypeOrder: "inline"}); + +edit.replaceLine(0, 'import { a, type A, b } from "foo3";'); +verify.organizeImports( +`import { type A, a, b } from "foo3"; +interface Use extends A {} +console.log(a, b);`, + /*mode*/ undefined, + { organizeImportsIgnoreCase: false, organizeImportsTypeOrder: "inline" }); \ No newline at end of file diff --git a/tests/cases/fourslash/organizeImportsType8.ts b/tests/cases/fourslash/organizeImportsType8.ts new file mode 100644 index 0000000000000..da43f893431df --- /dev/null +++ b/tests/cases/fourslash/organizeImportsType8.ts @@ -0,0 +1,53 @@ +/// + +//// import { type A, type a, b, B } from "foo"; +//// console.log(a, b, A, B); + +verify.organizeImports( +`import { type A, type a, b, B } from "foo"; +console.log(a, b, A, B);`, + /*mode*/ undefined, + { organizeImportsIgnoreCase: "auto", organizeImportsTypeOrder: "inline" } +); + +edit.replaceLine(0, 'import { type A, type a, b, B } from "foo1";'); +verify.organizeImports( +`import { type A, type a, b, B } from "foo1"; +console.log(a, b, A, B);`, + /*mode*/ undefined, + { organizeImportsIgnoreCase: "auto", organizeImportsTypeOrder: "first" } +); + +edit.replaceLine(0, 'import { type A, type a, b, B } from "foo2";'); +verify.organizeImports( +`import { B, b, type A, type a } from "foo2"; +console.log(a, b, A, B);`, + /*mode*/ undefined, + { organizeImportsIgnoreCase: "auto", organizeImportsTypeOrder: "last" } +); + +// default behavior is { organizeImportsTypeOrder: "last" } + +edit.replaceLine(0, 'import { type A, type a, b, B } from "foo3";'); +verify.organizeImports( +`import { B, b, type A, type a } from "foo3"; +console.log(a, b, A, B);`, + /*mode*/ undefined, + { organizeImportsIgnoreCase: "auto" } +); + +edit.replaceLine(0, 'import { type A, type a, b, B } from "foo4";'); +verify.organizeImports( +`import { b, B, type A, type a } from "foo4"; +console.log(a, b, A, B);`, + /*mode*/ undefined, + { organizeImportsIgnoreCase: true } +); + +edit.replaceLine(0, 'import { type A, type a, b, B } from "foo5";'); +verify.organizeImports( +`import { B, b, type A, type a } from "foo5"; +console.log(a, b, A, B);`, + /*mode*/ undefined, + { organizeImportsIgnoreCase: false } +); \ No newline at end of file diff --git a/tests/cases/fourslash/organizeImportsType9.ts b/tests/cases/fourslash/organizeImportsType9.ts new file mode 100644 index 0000000000000..efe6107e020b6 --- /dev/null +++ b/tests/cases/fourslash/organizeImportsType9.ts @@ -0,0 +1,48 @@ +/// + +//// import { type a, type A, b, B } from "foo"; +//// console.log(a, b, A, B); + +verify.organizeImports( +`import { type a, type A, b, B } from "foo"; +console.log(a, b, A, B);`, + /*mode*/ undefined, + { organizeImportsIgnoreCase: "auto", organizeImportsTypeOrder: "inline" }); + +edit.replaceLine(0, 'import { type a, type A, b, B } from "foo1";'); +verify.organizeImports( +`import { type a, type A, b, B } from "foo1"; +console.log(a, b, A, B);`, + /*mode*/ undefined, + { organizeImportsIgnoreCase: "auto", organizeImportsTypeOrder: "first"} +); + +edit.replaceLine(0, 'import { type a, type A, b, B } from "foo2";'); +verify.organizeImports( +`import { B, b, type A, type a } from "foo2"; +console.log(a, b, A, B);`, + /*mode*/ undefined, + { organizeImportsIgnoreCase: "auto", organizeImportsTypeOrder: "last" } +); + +edit.replaceLine(0, 'import { type a, type A, b, B } from "foo3";'); +verify.organizeImports( +`import { B, b, type A, type a } from "foo3"; +console.log(a, b, A, B);`, + /*mode*/ undefined, + { organizeImportsIgnoreCase: "auto" } +); + +edit.replaceLine(0, 'import { type a, type A, b, B } from "foo4";'); +verify.organizeImports( +`import { b, B, type a, type A } from "foo4"; +console.log(a, b, A, B);`, + /*mode*/ undefined, + { organizeImportsIgnoreCase: true }); + +edit.replaceLine(0, 'import { type a, type A, b, B } from "foo5";'); +verify.organizeImports( +`import { B, b, type A, type a } from "foo5"; +console.log(a, b, A, B);`, + /*mode*/ undefined, + { organizeImportsIgnoreCase: false }); \ No newline at end of file