diff --git a/src/compiler/core.ts b/src/compiler/core.ts index f8827baf4df72..6b620569054cd 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -865,32 +865,25 @@ export const enum SortKind { } /** @internal */ -export function detectSortCaseSensitivity(array: readonly string[], useEslintOrdering?: boolean): SortKind; -/** @internal */ -export function detectSortCaseSensitivity(array: readonly T[], useEslintOrdering: boolean, getString: (element: T) => string): SortKind; -/** @internal */ -export function detectSortCaseSensitivity(array: readonly T[], useEslintOrdering?: boolean, getString?: (element: T) => string): SortKind { +export function detectSortCaseSensitivity( + array: readonly T[], + getString: (element: T) => string, + compareStringsCaseSensitive: Comparer, + compareStringsCaseInsensitive: Comparer, +): SortKind { let kind = SortKind.Both; if (array.length < 2) return kind; - const caseSensitiveComparer = getString - ? (a: T, b: T) => compareStringsCaseSensitive(getString(a), getString(b)) - : compareStringsCaseSensitive as (a: T | undefined, b: T | undefined) => Comparison; - const compareCaseInsensitive = useEslintOrdering ? compareStringsCaseInsensitiveEslintCompatible : compareStringsCaseInsensitive; - const caseInsensitiveComparer = getString - ? (a: T, b: T) => compareCaseInsensitive(getString(a), getString(b)) - : compareCaseInsensitive as (a: T | undefined, b: T | undefined) => Comparison; - for (let i = 1, len = array.length; i < len; i++) { - const prevElement = array[i - 1]; - const element = array[i]; - if (kind & SortKind.CaseSensitive && caseSensitiveComparer(prevElement, element) === Comparison.GreaterThan) { + + let prevElement = getString(array[0]); + for (let i = 1, len = array.length; i < len && kind !== SortKind.None; i++) { + const element = getString(array[i]); + if (kind & SortKind.CaseSensitive && compareStringsCaseSensitive(prevElement, element) > 0) { kind &= ~SortKind.CaseSensitive; } - if (kind & SortKind.CaseInsensitive && caseInsensitiveComparer(prevElement, element) === Comparison.GreaterThan) { + if (kind & SortKind.CaseInsensitive && compareStringsCaseInsensitive(prevElement, element) > 0) { kind &= ~SortKind.CaseInsensitive; } - if (kind === SortKind.None) { - return kind; - } + prevElement = element; } return kind; } @@ -2048,6 +2041,64 @@ export function memoizeWeak(callback: (arg: A) => T): (arg: }; } +/** @internal */ +export interface MemoizeCache { + has(args: A): boolean; + get(args: A): T | undefined; + set(args: A, value: T): void; +} + +/** + * A version of `memoize` that supports multiple arguments, backed by a provided cache. + * + * @internal + */ +export function memoizeCached(callback: (...args: A) => T, cache: MemoizeCache): (...args: A) => T { + return (...args: A) => { + let value = cache.get(args); + if (value === undefined && !cache.has(args)) { + value = callback(...args); + cache.set(args, value); + } + return value!; + }; +} + +/** + * High-order function, composes functions. Note that functions are composed inside-out; + * for example, `compose(a, b)` is the equivalent of `x => b(a(x))`. + * + * @param args The functions to compose. + * + * @internal + */ +export function compose(...args: ((t: T) => T)[]): (t: T) => T; +/** @internal */ +export function compose(a: (t: T) => T, b: (t: T) => T, c: (t: T) => T, d: (t: T) => T, e: (t: T) => T): (t: T) => T { + if (!!e) { + const args: ((t: T) => T)[] = []; + for (let i = 0; i < arguments.length; i++) { + args[i] = arguments[i]; + } + + return t => reduceLeft(args, (u, f) => f(u), t); + } + else if (d) { + return t => d(c(b(a(t)))); + } + else if (c) { + return t => c(b(a(t))); + } + else if (b) { + return t => b(a(t)); + } + else if (a) { + return t => a(t); + } + else { + return t => t; + } +} /** @internal */ export const enum AssertionLevel { None = 0, diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 760c2a97170e7..368223f98abc9 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -9832,6 +9832,11 @@ export interface UserPreferences { readonly allowRenameOfImportPath?: boolean; readonly autoImportFileExcludePatterns?: string[]; readonly organizeImportsIgnoreCase?: "auto" | boolean; + readonly organizeImportsCollation?: "ordinal" | "unicode"; + readonly organizeImportsLocale?: string; + readonly organizeImportsNumericCollation?: boolean; + readonly organizeImportsAccentCollation?: boolean; + readonly organizeImportsCaseFirst?: "upper" | "lower" | false; } /** Represents a bigint literal value without requiring bigint support */ diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 76c80264ce968..4240ae95c2ddb 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -3516,7 +3516,60 @@ export interface UserPreferences { readonly includeInlayFunctionLikeReturnTypeHints?: boolean; readonly includeInlayEnumMemberValueHints?: boolean; readonly autoImportFileExcludePatterns?: string[]; + + /** + * Indicates whether imports should be organized in a case-insensitive manner. + */ readonly organizeImportsIgnoreCase?: "auto" | boolean; + /** + * Indicates whether imports should be organized via an "ordinal" (binary) comparison using the numeric value + * of their code points, or via "unicode" collation (via the + * [Unicode Collation Algorithm](https://unicode.org/reports/tr10/#Scope)) using rules associated with the locale + * specified in {@link organizeImportsCollationLocale}. + * + * Default: `"ordinal"`. + */ + readonly organizeImportsCollation?: "ordinal" | "unicode"; + /** + * Indicates the locale to use for "unicode" collation. If not specified, the locale `"en"` is used as an invariant + * for the sake of consistent sorting. Use `"auto"` to use the detected UI locale. + * + * This preference is ignored if {@link organizeImportsCollation} is not `"unicode"`. + * + * Default: `"en"` + */ + readonly organizeImportsCollationLocale?: string; + /** + * Indicates whether numeric collation should be used for digit sequences in strings. When `true`, will collate + * strings such that `a1z < a2z < a100z`. When `false`, will collate strings such that `a1z < a100z < a2z`. + * + * This preference is ignored if {@link organizeImportsCollation} is not `"unicode"`. + * + * Default: `false` + */ + readonly organizeImportsNumericCollation?: boolean; + /** + * Indicates whether accents and other diacritic marks are considered unequal for the purpose of collation. When + * `true`, characters with accents and other diacritics will be collated in the order defined by the locale specified + * in {@link organizeImportsCollationLocale}. + * + * This preference is ignored if {@link organizeImportsCollation} is not `"unicode"`. + * + * Default: `true` + */ + readonly organizeImportsAccentCollation?: boolean; + /** + * Indicates whether upper case or lower case should sort first. When `false`, the default order for the locale + * specified in {@link organizeImportsCollationLocale} is used. + * + * This preference is ignored if {@link organizeImportsCollation} is not `"unicode"`. This preference is also + * ignored if we are using case-insensitive sorting, which occurs when {@link organizeImportsIgnoreCase} is `true`, + * or if {@link organizeImportsIgnoreCase} is `"auto"` and the auto-detected case sensitivity is determined to be + * case-insensitive. + * + * Default: `false` + */ + readonly organizeImportsCaseFirst?: "upper" | "lower" | false; /** * Indicates whether {@link ReferencesResponseItem.lineText} is supported. diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 5b8cef4f560ce..bb04c3a658998 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -375,7 +375,7 @@ function createImportAdderWorker(sourceFile: SourceFile, program: Program, useAu newDeclarations = combine(newDeclarations, declarations); }); if (newDeclarations) { - insertImports(changeTracker, sourceFile, newDeclarations, /*blankLineBetween*/ true); + insertImports(changeTracker, sourceFile, newDeclarations, /*blankLineBetween*/ true, preferences); } } @@ -1221,14 +1221,14 @@ function codeActionForFixWorker(changes: textChanges.ChangeTracker, sourceFile: const defaultImport: Import | undefined = importKind === ImportKind.Default ? { name: symbolName, addAsTypeOnly } : undefined; const namedImports: Import[] | undefined = importKind === ImportKind.Named ? [{ name: symbolName, addAsTypeOnly }] : undefined; const namespaceLikeImport = importKind === ImportKind.Namespace || importKind === ImportKind.CommonJS ? { importKind, name: symbolName, addAsTypeOnly } : undefined; - insertImports(changes, sourceFile, getDeclarations(moduleSpecifier, quotePreference, defaultImport, namedImports, namespaceLikeImport), /*blankLineBetween*/ true); + insertImports(changes, sourceFile, getDeclarations(moduleSpecifier, quotePreference, defaultImport, namedImports, namespaceLikeImport), /*blankLineBetween*/ true, preferences); return includeSymbolNameInDescription ? [Diagnostics.Import_0_from_1, symbolName, moduleSpecifier] : [Diagnostics.Add_import_from_0, moduleSpecifier]; } case ImportFixKind.PromoteTypeOnly: { const { typeOnlyAliasDeclaration } = fix; - const promotedDeclaration = promoteFromTypeOnly(changes, typeOnlyAliasDeclaration, compilerOptions, sourceFile); + const promotedDeclaration = promoteFromTypeOnly(changes, typeOnlyAliasDeclaration, compilerOptions, sourceFile, preferences); return promotedDeclaration.kind === SyntaxKind.ImportSpecifier ? [Diagnostics.Remove_type_from_import_of_0_from_1, symbolName, getModuleSpecifierText(promotedDeclaration.parent.parent)] : [Diagnostics.Remove_type_from_import_declaration_from_0, getModuleSpecifierText(promotedDeclaration)]; @@ -1244,17 +1244,18 @@ function getModuleSpecifierText(promotedDeclaration: ImportClause | ImportEquals : cast(promotedDeclaration.parent.moduleSpecifier, isStringLiteral).text; } -function promoteFromTypeOnly(changes: textChanges.ChangeTracker, aliasDeclaration: TypeOnlyAliasDeclaration, compilerOptions: CompilerOptions, sourceFile: SourceFile) { +function promoteFromTypeOnly(changes: textChanges.ChangeTracker, aliasDeclaration: TypeOnlyAliasDeclaration, compilerOptions: CompilerOptions, sourceFile: SourceFile, preferences: UserPreferences) { // See comment in `doAddExistingFix` on constant with the same name. const convertExistingToTypeOnly = compilerOptions.preserveValueImports && compilerOptions.isolatedModules; switch (aliasDeclaration.kind) { case SyntaxKind.ImportSpecifier: if (aliasDeclaration.isTypeOnly) { - const sortKind = OrganizeImports.detectImportSpecifierSorting(aliasDeclaration.parent.elements); + const sortKind = OrganizeImports.detectImportSpecifierSorting(aliasDeclaration.parent.elements, preferences); if (aliasDeclaration.parent.elements.length > 1 && sortKind) { changes.delete(sourceFile, aliasDeclaration); const newSpecifier = factory.updateImportSpecifier(aliasDeclaration, /*isTypeOnly*/ false, aliasDeclaration.propertyName, aliasDeclaration.name); - const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(aliasDeclaration.parent.elements, newSpecifier, sortKind === SortKind.CaseInsensitive); + const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, sortKind === SortKind.CaseInsensitive); + const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(aliasDeclaration.parent.elements, newSpecifier, comparer); changes.insertImportSpecifierAtIndex(sourceFile, newSpecifier, aliasDeclaration.parent, insertionIndex); } else { @@ -1285,7 +1286,7 @@ function promoteFromTypeOnly(changes: textChanges.ChangeTracker, aliasDeclaratio if (convertExistingToTypeOnly) { const namedImports = tryCast(importClause.namedBindings, isNamedImports); if (namedImports && namedImports.elements.length > 1) { - if (OrganizeImports.detectImportSpecifierSorting(namedImports.elements) && + if (OrganizeImports.detectImportSpecifierSorting(namedImports.elements, preferences) && aliasDeclaration.kind === SyntaxKind.ImportSpecifier && namedImports.elements.indexOf(aliasDeclaration) !== 0 ) { @@ -1348,28 +1349,29 @@ function doAddExistingFix( ignoreCaseForSorting = preferences.organizeImportsIgnoreCase; } else if (existingSpecifiers) { - const targetImportSorting = OrganizeImports.detectImportSpecifierSorting(existingSpecifiers); + const targetImportSorting = OrganizeImports.detectImportSpecifierSorting(existingSpecifiers, preferences); if (targetImportSorting !== SortKind.Both) { ignoreCaseForSorting = targetImportSorting === SortKind.CaseInsensitive; } } if (ignoreCaseForSorting === undefined) { - ignoreCaseForSorting = OrganizeImports.detectSorting(sourceFile) === SortKind.CaseInsensitive; + ignoreCaseForSorting = OrganizeImports.detectSorting(sourceFile, preferences) === SortKind.CaseInsensitive; } + const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, ignoreCaseForSorting); const newSpecifiers = stableSort( namedImports.map(namedImport => factory.createImportSpecifier( (!clause.isTypeOnly || promoteFromTypeOnly) && needsTypeOnly(namedImport), /*propertyName*/ undefined, factory.createIdentifier(namedImport.name))), - (s1, s2) => OrganizeImports.compareImportOrExportSpecifiers(s1, s2, ignoreCaseForSorting)); + (s1, s2) => OrganizeImports.compareImportOrExportSpecifiers(s1, s2, comparer)); // The sorting preference computed earlier may or may not have validated that these particular // import specifiers are sorted. If they aren't, `getImportSpecifierInsertionIndex` will return // nonsense. So if there are existing specifiers, even if we know the sorting preference, we // need to ensure that the existing specifiers are sorted according to the preference in order // to do a sorted insertion. - const specifierSort = existingSpecifiers?.length && OrganizeImports.detectImportSpecifierSorting(existingSpecifiers); + const specifierSort = existingSpecifiers?.length && OrganizeImports.detectImportSpecifierSorting(existingSpecifiers, preferences); if (specifierSort && !(ignoreCaseForSorting && specifierSort === SortKind.CaseSensitive)) { for (const spec of newSpecifiers) { // Organize imports puts type-only import specifiers last, so if we're @@ -1377,7 +1379,7 @@ function doAddExistingFix( // type-only, there's no need to ask for the insertion index - it's 0. const insertionIndex = convertExistingToTypeOnly && !spec.isTypeOnly ? 0 - : OrganizeImports.getImportSpecifierInsertionIndex(existingSpecifiers, spec, ignoreCaseForSorting); + : OrganizeImports.getImportSpecifierInsertionIndex(existingSpecifiers, spec, comparer); changes.insertImportSpecifierAtIndex(sourceFile, spec, clause.namedBindings as NamedImports, insertionIndex); } } diff --git a/src/services/organizeImports.ts b/src/services/organizeImports.ts index 881c2d6471501..a090e64823240 100644 --- a/src/services/organizeImports.ts +++ b/src/services/organizeImports.ts @@ -3,6 +3,7 @@ import { arrayIsSorted, binarySearch, compareBooleans, + Comparer, compareStringsCaseInsensitiveEslintCompatible, compareStringsCaseSensitive, compareValues, @@ -18,12 +19,15 @@ import { FileTextChanges, find, FindAllReferences, + firstOrUndefined, flatMap, formatting, getNewLineOrDefaultFromHost, + getUILocale, group, Identifier, identity, + ImportClause, ImportDeclaration, ImportOrExportSpecifier, ImportSpecifier, @@ -42,7 +46,8 @@ import { LanguageServiceHost, length, map, - memoizeWeak, + MemoizeCache, + memoizeCached, NamedImportBindings, NamedImports, NamespaceImport, @@ -83,19 +88,16 @@ export function organizeImports( const shouldSort = mode === OrganizeImportsMode.SortAndCombine || mode === OrganizeImportsMode.All; const shouldCombine = shouldSort; // These are currently inseparable, but I draw a distinction for clarity and in case we add modes in the future. const shouldRemove = mode === OrganizeImportsMode.RemoveUnused || mode === OrganizeImportsMode.All; - const maybeRemove = shouldRemove ? removeUnusedImports : identity; - const maybeCoalesce = shouldCombine ? coalesceImports : identity; // All of the old ImportDeclarations in the file, in syntactic order. const topLevelImportGroupDecls = groupImportsByNewlineContiguous(sourceFile, sourceFile.statements.filter(isImportDeclaration)); - const ignoreCase = typeof preferences.organizeImportsIgnoreCase === "boolean" - ? preferences.organizeImportsIgnoreCase - : shouldSort && detectSortingWorker(topLevelImportGroupDecls) === SortKind.CaseInsensitive; + + const comparer = getOrganizeImportsComparerWithDetection(preferences, shouldSort ? () => detectSortingWorker(topLevelImportGroupDecls, preferences) === SortKind.CaseInsensitive : undefined); const processImportsOfSameModuleSpecifier = (importGroup: readonly ImportDeclaration[]) => { - const processedDeclarations = maybeCoalesce(maybeRemove(importGroup, sourceFile, program), ignoreCase, sourceFile); - return shouldSort - ? stableSort(processedDeclarations, (s1, s2) => compareImportsOrRequireStatements(s1, s2)) - : processedDeclarations; + if (shouldRemove) importGroup = removeUnusedImports(importGroup, sourceFile, program); + if (shouldCombine) importGroup = coalesceImportsWorker(importGroup, comparer, sourceFile); + if (shouldSort) importGroup = stableSort(importGroup, (s1, s2) => compareImportsOrRequireStatements(s1, s2, comparer)); + return importGroup; }; topLevelImportGroupDecls.forEach(importGroupDecl => organizeImportsWorker(importGroupDecl, processImportsOfSameModuleSpecifier)); @@ -104,7 +106,7 @@ export function organizeImports( if (mode !== OrganizeImportsMode.RemoveUnused) { // All of the old ExportDeclarations in the file, in syntactic order. const topLevelExportDecls = sourceFile.statements.filter(isExportDeclaration); - organizeImportsWorker(topLevelExportDecls, group => coalesceExports(group, ignoreCase)); + organizeImportsWorker(topLevelExportDecls, group => coalesceExportsWorker(group, comparer)); } 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 => coalesceExports(group, ignoreCase)); + organizeImportsWorker(ambientModuleExportDecls, group => coalesceExportsWorker(group, comparer)); } } @@ -141,7 +143,7 @@ export function organizeImports( ? group(oldImportDecls, importDecl => getExternalModuleName(importDecl.moduleSpecifier)!) : [oldImportDecls]; const sortedImportGroups = shouldSort - ? stableSort(oldImportGroups, (group1, group2) => compareModuleSpecifiers(group1[0].moduleSpecifier, group2[0].moduleSpecifier)) + ? stableSort(oldImportGroups, (group1, group2) => compareModuleSpecifiersWorker(group1[0].moduleSpecifier, group2[0].moduleSpecifier, comparer)) : oldImportGroups; const newImportDecls = flatMap(sortedImportGroups, importGroup => getExternalModuleName(importGroup[0].moduleSpecifier) @@ -306,15 +308,20 @@ function getExternalModuleName(specifier: Expression | undefined) { /** * @param importGroup a list of ImportDeclarations, all with the same module name. * + * @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): readonly ImportDeclaration[] { + const comparer = getOrganizeImportsOrdinalStringComparer(ignoreCase); + return coalesceImportsWorker(importGroup, comparer, sourceFile); +} + +function coalesceImportsWorker(importGroup: readonly ImportDeclaration[], comparer: Comparer, sourceFile?: SourceFile): readonly ImportDeclaration[] { if (importGroup.length === 0) { return importGroup; } const { importWithoutClause, typeOnlyImports, regularImports } = getCategorizedImports(importGroup); - const compareIdentifiers = ignoreCase ? compareIdentifiersCaseInsensitive : compareIdentifiersCaseSensitive; const coalescedImports: ImportDeclaration[] = []; if (importWithoutClause) { @@ -330,59 +337,58 @@ export function coalesceImports(importGroup: readonly ImportDeclaration[], ignor // Add the namespace import to the existing default ImportDeclaration. const defaultImport = defaultImports[0]; coalescedImports.push( - updateImportDeclarationAndClause(defaultImport, defaultImport.importClause!.name, namespaceImports[0].importClause!.namedBindings)); // TODO: GH#18217 + updateImportDeclarationAndClause(defaultImport, defaultImport.importClause.name, namespaceImports[0].importClause.namedBindings)); continue; } const sortedNamespaceImports = stableSort(namespaceImports, (i1, i2) => - compareIdentifiers((i1.importClause!.namedBindings as NamespaceImport).name, (i2.importClause!.namedBindings as NamespaceImport).name)); // TODO: GH#18217 + comparer(i1.importClause.namedBindings.name.text, i2.importClause.namedBindings.name.text)); for (const namespaceImport of sortedNamespaceImports) { // Drop the name, if any coalescedImports.push( - updateImportDeclarationAndClause(namespaceImport, /*name*/ undefined, namespaceImport.importClause!.namedBindings)); // TODO: GH#18217 + updateImportDeclarationAndClause(namespaceImport, /*name*/ undefined, namespaceImport.importClause.namedBindings)); } - if (defaultImports.length === 0 && namedImports.length === 0) { + const firstDefaultImport = firstOrUndefined(defaultImports); + const firstNamedImport = firstOrUndefined(namedImports); + const importDecl = firstDefaultImport ?? firstNamedImport; + if (!importDecl) { continue; } let newDefaultImport: Identifier | undefined; const newImportSpecifiers: ImportSpecifier[] = []; if (defaultImports.length === 1) { - newDefaultImport = defaultImports[0].importClause!.name; + newDefaultImport = defaultImports[0].importClause.name; } else { for (const defaultImport of defaultImports) { newImportSpecifiers.push( - factory.createImportSpecifier(/*isTypeOnly*/ false, factory.createIdentifier("default"), defaultImport.importClause!.name!)); // TODO: GH#18217 + factory.createImportSpecifier(/*isTypeOnly*/ false, factory.createIdentifier("default"), defaultImport.importClause.name)); } } newImportSpecifiers.push(...getNewImportSpecifiers(namedImports)); const sortedImportSpecifiers = factory.createNodeArray( - sortSpecifiers(newImportSpecifiers, ignoreCase), - (namedImports[0]?.importClause!.namedBindings as NamedImports)?.elements.hasTrailingComma + sortSpecifiers(newImportSpecifiers, comparer), + firstNamedImport?.importClause.namedBindings.elements.hasTrailingComma ); - const importDecl = defaultImports.length > 0 - ? defaultImports[0] - : namedImports[0]; - const newNamedImports = sortedImportSpecifiers.length === 0 ? newDefaultImport ? undefined : factory.createNamedImports(emptyArray) - : namedImports.length === 0 - ? factory.createNamedImports(sortedImportSpecifiers) - : factory.updateNamedImports(namedImports[0].importClause!.namedBindings as NamedImports, sortedImportSpecifiers); // TODO: GH#18217 + : firstNamedImport + ? factory.updateNamedImports(firstNamedImport.importClause.namedBindings, sortedImportSpecifiers) + : factory.createNamedImports(sortedImportSpecifiers); if (sourceFile && newNamedImports && - namedImports[0]?.importClause!.namedBindings && - !rangeIsOnSingleLine(namedImports[0].importClause.namedBindings, sourceFile) + firstNamedImport?.importClause.namedBindings && + !rangeIsOnSingleLine(firstNamedImport.importClause.namedBindings, sourceFile) ) { setEmitFlags(newNamedImports, EmitFlags.MultiLine); } @@ -394,7 +400,7 @@ export function coalesceImports(importGroup: readonly ImportDeclaration[], ignor coalescedImports.push( updateImportDeclarationAndClause(importDecl, newDefaultImport, /*namedBindings*/ undefined)); coalescedImports.push( - updateImportDeclarationAndClause(namedImports[0] ?? importDecl, /*name*/ undefined, newNamedImports)); + updateImportDeclarationAndClause(firstNamedImport ?? importDecl, /*name*/ undefined, newNamedImports)); } else { coalescedImports.push( @@ -403,13 +409,30 @@ export function coalesceImports(importGroup: readonly ImportDeclaration[], ignor } return coalescedImports; - } +type ImportDeclarationWithDefaultImport = ImportDeclaration & { + readonly importClause: ImportClause & { + readonly name: Identifier; + }; +}; + +type ImportDeclarationWithNamespaceImport = ImportDeclaration & { + readonly importClause: ImportClause & { + readonly namedBindings: NamespaceImport; + }; +}; + +type ImportDeclarationWithNamedImports = ImportDeclaration & { + readonly importClause: ImportClause & { + readonly namedBindings: NamedImports; + }; +}; + interface ImportGroup { - defaultImports: ImportDeclaration[]; - namespaceImports: ImportDeclaration[]; - namedImports: ImportDeclaration[]; + defaultImports: ImportDeclarationWithDefaultImport[]; + namespaceImports: ImportDeclarationWithNamespaceImport[]; + namedImports: ImportDeclarationWithNamedImports[]; } /* @@ -436,15 +459,15 @@ function getCategorizedImports(importGroup: readonly ImportDeclaration[]) { const { name, namedBindings } = importDeclaration.importClause; if (name) { - group.defaultImports.push(importDeclaration); + group.defaultImports.push(importDeclaration as ImportDeclarationWithDefaultImport); } if (namedBindings) { if (isNamespaceImport(namedBindings)) { - group.namespaceImports.push(importDeclaration); + group.namespaceImports.push(importDeclaration as ImportDeclarationWithNamespaceImport); } else { - group.namedImports.push(importDeclaration); + group.namedImports.push(importDeclaration as ImportDeclarationWithNamedImports); } } } @@ -460,9 +483,15 @@ function getCategorizedImports(importGroup: readonly ImportDeclaration[]) { /** * @param exportGroup a list of ExportDeclarations, all with the same module name. * + * @deprecated Only used for testing * @internal */ export function coalesceExports(exportGroup: readonly ExportDeclaration[], ignoreCase: boolean) { + const comparer = getOrganizeImportsOrdinalStringComparer(ignoreCase); + return coalesceExportsWorker(exportGroup, comparer); +} + +function coalesceExportsWorker(exportGroup: readonly ExportDeclaration[], comparer: Comparer) { if (exportGroup.length === 0) { return exportGroup; } @@ -482,7 +511,7 @@ export function coalesceExports(exportGroup: readonly ExportDeclaration[], ignor const newExportSpecifiers: ExportSpecifier[] = []; newExportSpecifiers.push(...flatMap(exportGroup, i => i.exportClause && isNamedExports(i.exportClause) ? i.exportClause.elements : emptyArray)); - const sortedExportSpecifiers = sortSpecifiers(newExportSpecifiers, ignoreCase); + const sortedExportSpecifiers = sortSpecifiers(newExportSpecifiers, comparer); const exportDecl = exportGroup[0]; coalescedExports.push( @@ -546,37 +575,33 @@ function updateImportDeclarationAndClause( importDeclaration.assertClause); } -function sortSpecifiers(specifiers: readonly T[], ignoreCase?: boolean) { - return stableSort(specifiers, (s1, s2) => compareImportOrExportSpecifiers(s1, s2, ignoreCase)); +function sortSpecifiers(specifiers: readonly T[], comparer: Comparer) { + return stableSort(specifiers, (s1, s2) => compareImportOrExportSpecifiers(s1, s2, comparer)); } /** @internal */ -export function compareImportOrExportSpecifiers(s1: T, s2: T, ignoreCase?: boolean): Comparison { - const compareIdentifiers = ignoreCase ? compareIdentifiersCaseInsensitive : compareIdentifiersCaseSensitive; - return compareBooleans(s1.isTypeOnly, s2.isTypeOnly) || compareIdentifiers(s1.name, s2.name); -} - -function compareIdentifiersCaseSensitive(s1: Identifier, s2: Identifier) { - return compareStringsCaseSensitive(s1.text, s2.text); -} - -function compareIdentifiersCaseInsensitive(s1: Identifier, s2: Identifier) { - return compareStringsCaseInsensitiveEslintCompatible(s1.text, s2.text); +export function compareImportOrExportSpecifiers(s1: T, s2: T, comparer: Comparer): Comparison { + return compareBooleans(s1.isTypeOnly, s2.isTypeOnly) || comparer(s1.name.text, s2.name.text); } /** * Exported for testing * + * @deprecated Only used for testing * @internal */ export function compareModuleSpecifiers(m1: Expression | undefined, m2: Expression | undefined, ignoreCase?: boolean) { + const comparer = getOrganizeImportsOrdinalStringComparer(!!ignoreCase); + return compareModuleSpecifiersWorker(m1, m2, comparer); +} + +function compareModuleSpecifiersWorker(m1: Expression | undefined, m2: Expression | undefined, comparer: Comparer) { const name1 = m1 === undefined ? undefined : getExternalModuleName(m1); const name2 = m2 === undefined ? undefined : getExternalModuleName(m2); - const compareStrings = ignoreCase ? compareStringsCaseInsensitiveEslintCompatible : compareStringsCaseSensitive; return compareBooleans(name1 === undefined, name2 === undefined) || compareBooleans(isExternalModuleNameRelative(name1!), isExternalModuleNameRelative(name2!)) || // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - compareStrings(name1!, name2!); // I don't know why eslint is wrong but this one is necessary + comparer(name1!, name2!); // I don't know why eslint is wrong but this one is necessary } function getModuleSpecifierExpression(declaration: AnyImportOrRequireStatement): Expression | undefined { @@ -591,17 +616,24 @@ function getModuleSpecifierExpression(declaration: AnyImportOrRequireStatement): } /** @internal */ -export function detectSorting(sourceFile: SourceFile): SortKind { +export function detectSorting(sourceFile: SourceFile, preferences: UserPreferences): SortKind { return detectSortingWorker( - groupImportsByNewlineContiguous(sourceFile, sourceFile.statements.filter(isImportDeclaration))); + groupImportsByNewlineContiguous(sourceFile, sourceFile.statements.filter(isImportDeclaration)), + preferences); } -function detectSortingWorker(importGroups: ImportDeclaration[][]): SortKind { +function detectSortingWorker(importGroups: ImportDeclaration[][], preferences: UserPreferences): SortKind { + const collateCaseSensitive = getOrganizeImportsComparer(preferences, /*ignoreCase*/ false); + const collateCaseInsensitive = getOrganizeImportsComparer(preferences, /*ignoreCase*/ true); let sortState = SortKind.Both; for (const importGroup of importGroups) { // Check module specifiers if (importGroup.length > 1) { - sortState &= detectSortCaseSensitivity(importGroup, /*useEslintOrdering*/ true, i => tryCast(i.moduleSpecifier, isStringLiteral)?.text ?? ""); + sortState &= detectSortCaseSensitivity( + importGroup, + i => tryCast(i.moduleSpecifier, isStringLiteral)?.text ?? "", + collateCaseSensitive, + collateCaseInsensitive); if (!sortState) { return sortState; } @@ -612,7 +644,7 @@ function detectSortingWorker(importGroups: ImportDeclaration[][]): SortKind { importGroup, i => tryCast(i.importClause?.namedBindings, isNamedImports)?.elements.length! > 1); if (declarationWithNamedImports) { - sortState &= detectImportSpecifierSorting((declarationWithNamedImports.importClause!.namedBindings as NamedImports).elements); + sortState &= detectImportSpecifierSorting((declarationWithNamedImports.importClause!.namedBindings as NamedImports).elements, preferences); if (!sortState) { return sortState; } @@ -629,33 +661,66 @@ function detectSortingWorker(importGroups: ImportDeclaration[][]): SortKind { } /** @internal */ -export function detectImportDeclarationSorting(imports: readonly AnyImportOrRequireStatement[]): SortKind { - return detectSortCaseSensitivity(imports, /*useEslintOrdering*/ true, s => getExternalModuleName(getModuleSpecifierExpression(s)) || ""); +export function detectImportDeclarationSorting(imports: readonly AnyImportOrRequireStatement[], preferences: UserPreferences): SortKind { + const collateCaseSensitive = getOrganizeImportsComparer(preferences, /*ignoreCase*/ false); + const collateCaseInsensitive = getOrganizeImportsComparer(preferences, /*ignoreCase*/ true); + return detectSortCaseSensitivity( + imports, + s => getExternalModuleName(getModuleSpecifierExpression(s)) || "", + collateCaseSensitive, + collateCaseInsensitive + ); +} + +class ImportSpecifierSortingCache implements MemoizeCache<[readonly ImportSpecifier[], UserPreferences], SortKind> { + private _lastPreferences: UserPreferences | undefined; + private _cache: WeakMap | undefined; + + has([specifiers, preferences]: [readonly ImportSpecifier[], UserPreferences]) { + if (this._lastPreferences !== preferences || !this._cache) return false; + return this._cache.has(specifiers); + } + + get([specifiers, preferences]: [readonly ImportSpecifier[], UserPreferences]) { + if (this._lastPreferences !== preferences || !this._cache) return undefined; + return this._cache.get(specifiers); + } + + set([specifiers, preferences]: [readonly ImportSpecifier[], UserPreferences], value: SortKind) { + if (this._lastPreferences !== preferences) { + this._lastPreferences = preferences; + this._cache = undefined; + } + this._cache ??= new WeakMap(); + this._cache.set(specifiers, value); + } } /** @internal */ -export const detectImportSpecifierSorting = memoizeWeak((specifiers: readonly ImportSpecifier[]): SortKind => { +export const detectImportSpecifierSorting = memoizeCached((specifiers: readonly ImportSpecifier[], preferences: UserPreferences): SortKind => { if (!arrayIsSorted(specifiers, (s1, s2) => compareBooleans(s1.isTypeOnly, s2.isTypeOnly))) { return SortKind.None; } - return detectSortCaseSensitivity(specifiers, /*useEslintOrdering*/ true, specifier => specifier.name.text); -}); + const collateCaseSensitive = getOrganizeImportsComparer(preferences, /*ignoreCase*/ false); + const collateCaseInsensitive = getOrganizeImportsComparer(preferences, /*ignoreCase*/ true); + return detectSortCaseSensitivity(specifiers, specifier => specifier.name.text, collateCaseSensitive, collateCaseInsensitive); +}, new ImportSpecifierSortingCache()); /** @internal */ -export function getImportDeclarationInsertionIndex(sortedImports: readonly AnyImportOrRequireStatement[], newImport: AnyImportOrRequireStatement, ignoreCase?: boolean) { - const index = binarySearch(sortedImports, newImport, identity, (a, b) => compareImportsOrRequireStatements(a, b, ignoreCase)); +export function getImportDeclarationInsertionIndex(sortedImports: readonly AnyImportOrRequireStatement[], newImport: AnyImportOrRequireStatement, comparer: Comparer) { + const index = binarySearch(sortedImports, newImport, identity, (a, b) => compareImportsOrRequireStatements(a, b, comparer)); return index < 0 ? ~index : index; } /** @internal */ -export function getImportSpecifierInsertionIndex(sortedImports: readonly ImportSpecifier[], newImport: ImportSpecifier, ignoreCase?: boolean) { - const index = binarySearch(sortedImports, newImport, identity, (s1, s2) => compareImportOrExportSpecifiers(s1, s2, ignoreCase)); +export function getImportSpecifierInsertionIndex(sortedImports: readonly ImportSpecifier[], newImport: ImportSpecifier, comparer: Comparer) { + const index = binarySearch(sortedImports, newImport, identity, (s1, s2) => compareImportOrExportSpecifiers(s1, s2, comparer)); return index < 0 ? ~index : index; } /** @internal */ -export function compareImportsOrRequireStatements(s1: AnyImportOrRequireStatement, s2: AnyImportOrRequireStatement, ignoreCase?: boolean) { - return compareModuleSpecifiers(getModuleSpecifierExpression(s1), getModuleSpecifierExpression(s2), ignoreCase) || compareImportKind(s1, s2); +export function compareImportsOrRequireStatements(s1: AnyImportOrRequireStatement, s2: AnyImportOrRequireStatement, comparer: Comparer) { + return compareModuleSpecifiersWorker(getModuleSpecifierExpression(s1), getModuleSpecifierExpression(s2), comparer) || compareImportKind(s1, s2); } function compareImportKind(s1: AnyImportOrRequireStatement, s2: AnyImportOrRequireStatement) { @@ -699,3 +764,51 @@ function tryGetNamedBindingElements(namedImport: ImportDeclaration) { ? namedImport.importClause.namedBindings.elements : undefined; } + +function getOrganizeImportsOrdinalStringComparer(ignoreCase: boolean) { + return ignoreCase ? compareStringsCaseInsensitiveEslintCompatible : compareStringsCaseSensitive; +} + +function getOrganizeImportsUnicodeStringComparer(ignoreCase: boolean, preferences: UserPreferences): Comparer { + const resolvedLocale = getOrganizeImportsLocale(preferences); + const caseFirst = preferences.organizeImportsCaseFirst ?? false; + const numeric = preferences.organizeImportsNumericCollation ?? false; + const accents = preferences.organizeImportsAccentCollation ?? true; + const sensitivity = + ignoreCase ? + accents ? "accent" : "base" : + accents ? "variant" : "case"; + + const collator = new Intl.Collator(resolvedLocale, { + usage: "sort", + caseFirst: caseFirst || "false", + sensitivity, + numeric, + }); + + // `compare` is a bound method, so we do not need to close over `collator`. + return collator.compare; +} + +function getOrganizeImportsLocale(preferences: UserPreferences): string { + let locale = preferences.organizeImportsLocale; + if (locale === "auto") locale = getUILocale(); + if (locale === undefined) locale = "en"; + + const supportedLocales = Intl.Collator.supportedLocalesOf(locale); + const resolvedLocale = supportedLocales.length ? supportedLocales[0] : "en"; + return resolvedLocale; +} + +/** @internal */ +export function getOrganizeImportsComparer(preferences: UserPreferences, ignoreCase: boolean): Comparer { + const collation = preferences.organizeImportsCollation ?? "ordinal"; + return collation === "unicode" ? + getOrganizeImportsUnicodeStringComparer(ignoreCase, preferences) : + getOrganizeImportsOrdinalStringComparer(ignoreCase); +} + +function getOrganizeImportsComparerWithDetection(preferences: UserPreferences, detectIgnoreCase?: () => boolean): Comparer { + const ignoreCase = typeof preferences.organizeImportsIgnoreCase === "boolean" ? preferences.organizeImportsIgnoreCase : detectIgnoreCase?.() ?? false; + return getOrganizeImportsComparer(preferences, ignoreCase); +} diff --git a/src/services/refactors/moveToNewFile.ts b/src/services/refactors/moveToNewFile.ts index 7950f25f5a6cf..f762111174d91 100644 --- a/src/services/refactors/moveToNewFile.ts +++ b/src/services/refactors/moveToNewFile.ts @@ -282,7 +282,7 @@ function getNewStatementsAndRemoveFromOldFile( const quotePreference = getQuotePreference(oldFile, preferences); const importsFromNewFile = createOldFileImportsFromNewFile(oldFile, usage.oldFileImportsFromNewFile, newFilename, program, host, useEsModuleSyntax, quotePreference); if (importsFromNewFile) { - insertImports(changes, oldFile, importsFromNewFile, /*blankLineBetween*/ true); + insertImports(changes, oldFile, importsFromNewFile, /*blankLineBetween*/ true, preferences); } deleteUnusedOldImports(oldFile, toMove.all, changes, usage.unusedImportsFromOldFile, checker); diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 553ef7d5281a5..d49afd59be64e 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -305,6 +305,7 @@ import { skipAlias, skipOuterExpressions, some, + SortKind, SourceFile, SourceFileLike, SourceMapper, @@ -2534,17 +2535,20 @@ export function findModifier(node: Node, kind: Modifier["kind"]): Modifier | und } /** @internal */ -export function insertImports(changes: textChanges.ChangeTracker, sourceFile: SourceFile, imports: AnyImportOrRequireStatement | readonly AnyImportOrRequireStatement[], blankLineBetween: boolean): void { +export function insertImports(changes: textChanges.ChangeTracker, sourceFile: SourceFile, imports: AnyImportOrRequireStatement | readonly AnyImportOrRequireStatement[], blankLineBetween: boolean, preferences: UserPreferences): void { const decl = isArray(imports) ? imports[0] : imports; const importKindPredicate: (node: Node) => node is AnyImportOrRequireStatement = decl.kind === SyntaxKind.VariableStatement ? isRequireVariableStatement : isAnyImportSyntax; const existingImportStatements = filter(sourceFile.statements, importKindPredicate); - const sortedNewImports = isArray(imports) ? stableSort(imports, OrganizeImports.compareImportsOrRequireStatements) : [imports]; + let sortKind = isArray(imports) ? OrganizeImports.detectImportDeclarationSorting(imports, preferences) : SortKind.Both; + const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, sortKind === SortKind.CaseInsensitive); + const sortedNewImports = isArray(imports) ? stableSort(imports, (a, b) => OrganizeImports.compareImportsOrRequireStatements(a, b, comparer)) : [imports]; if (!existingImportStatements.length) { changes.insertNodesAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween); } - else if (existingImportStatements && OrganizeImports.detectImportDeclarationSorting(existingImportStatements)) { + else if (existingImportStatements && (sortKind = OrganizeImports.detectImportDeclarationSorting(existingImportStatements, preferences))) { + const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, sortKind === SortKind.CaseInsensitive); for (const newImport of sortedNewImports) { - const insertionIndex = OrganizeImports.getImportDeclarationInsertionIndex(existingImportStatements, newImport); + const insertionIndex = OrganizeImports.getImportDeclarationInsertionIndex(existingImportStatements, newImport, comparer); if (insertionIndex === 0) { // If the first import is top-of-file, insert after the leading comment which is likely the header. const options = existingImportStatements[0] === sourceFile.statements[0] ? diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 35d7f3eab8a10..db45c13166b5b 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -2802,7 +2802,59 @@ declare namespace ts { readonly includeInlayFunctionLikeReturnTypeHints?: boolean; readonly includeInlayEnumMemberValueHints?: boolean; readonly autoImportFileExcludePatterns?: string[]; + /** + * Indicates whether imports should be organized in a case-insensitive manner. + */ readonly organizeImportsIgnoreCase?: "auto" | boolean; + /** + * Indicates whether imports should be organized via an "ordinal" (binary) comparison using the numeric value + * of their code points, or via "unicode" collation (via the + * [Unicode Collation Algorithm](https://unicode.org/reports/tr10/#Scope)) using rules associated with the locale + * specified in {@link organizeImportsCollationLocale}. + * + * Default: `"ordinal"`. + */ + readonly organizeImportsCollation?: "ordinal" | "unicode"; + /** + * Indicates the locale to use for "unicode" collation. If not specified, the locale `"en"` is used as an invariant + * for the sake of consistent sorting. Use `"auto"` to use the detected UI locale. + * + * This preference is ignored if {@link organizeImportsCollation} is not `"unicode"`. + * + * Default: `"en"` + */ + readonly organizeImportsCollationLocale?: string; + /** + * Indicates whether numeric collation should be used for digit sequences in strings. When `true`, will collate + * strings such that `a1z < a2z < a100z`. When `false`, will collate strings such that `a1z < a100z < a2z`. + * + * This preference is ignored if {@link organizeImportsCollation} is not `"unicode"`. + * + * Default: `false` + */ + readonly organizeImportsNumericCollation?: boolean; + /** + * Indicates whether accents and other diacritic marks are considered unequal for the purpose of collation. When + * `true`, characters with accents and other diacritics will be collated in the order defined by the locale specified + * in {@link organizeImportsCollationLocale}. + * + * This preference is ignored if {@link organizeImportsCollation} is not `"unicode"`. + * + * Default: `true` + */ + readonly organizeImportsAccentCollation?: boolean; + /** + * Indicates whether upper case or lower case should sort first. When `false`, the default order for the locale + * specified in {@link organizeImportsCollationLocale} is used. + * + * This preference is ignored if {@link organizeImportsCollation} is not `"unicode"`. This preference is also + * ignored if we are using case-insensitive sorting, which occurs when {@link organizeImportsIgnoreCase} is `true`, + * or if {@link organizeImportsIgnoreCase} is `"auto"` and the auto-detected case sensitivity is determined to be + * case-insensitive. + * + * Default: `false` + */ + readonly organizeImportsCaseFirst?: "upper" | "lower" | false; /** * Indicates whether {@link ReferencesResponseItem.lineText} is supported. */ @@ -8430,6 +8482,11 @@ declare namespace ts { readonly allowRenameOfImportPath?: boolean; readonly autoImportFileExcludePatterns?: string[]; readonly organizeImportsIgnoreCase?: "auto" | boolean; + readonly organizeImportsCollation?: "ordinal" | "unicode"; + readonly organizeImportsLocale?: string; + readonly organizeImportsNumericCollation?: boolean; + readonly organizeImportsAccentCollation?: boolean; + readonly organizeImportsCaseFirst?: "upper" | "lower" | false; } /** Represents a bigint literal value without requiring bigint support */ interface PseudoBigInt { diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 6c4dd8cf96b43..646049011a60b 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -4489,6 +4489,11 @@ declare namespace ts { readonly allowRenameOfImportPath?: boolean; readonly autoImportFileExcludePatterns?: string[]; readonly organizeImportsIgnoreCase?: "auto" | boolean; + readonly organizeImportsCollation?: "ordinal" | "unicode"; + readonly organizeImportsLocale?: string; + readonly organizeImportsNumericCollation?: boolean; + readonly organizeImportsAccentCollation?: boolean; + readonly organizeImportsCaseFirst?: "upper" | "lower" | false; } /** Represents a bigint literal value without requiring bigint support */ interface PseudoBigInt { diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 7353ce9b4da3f..8d3250c15e79b 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -674,6 +674,11 @@ declare namespace FourSlashInterface { readonly allowRenameOfImportPath?: boolean; readonly autoImportFileExcludePatterns?: readonly string[]; readonly organizeImportsIgnoreCase?: "auto" | boolean; + readonly organizeImportsCollation?: "unicode" | "ordinal"; + readonly organizeImportsLocale?: string; + readonly organizeImportsNumericCollation?: boolean; + readonly organizeImportsAccentCollation?: boolean; + readonly organizeImportsCaseFirst?: "upper" | "lower" | false; } interface InlayHintsOptions extends UserPreferences { readonly includeInlayParameterNameHints?: "none" | "literals" | "all"; diff --git a/tests/cases/fourslash/importNameCodeFix_order2.ts b/tests/cases/fourslash/importNameCodeFix_order2.ts new file mode 100644 index 0000000000000..c5fcbe57a168f --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFix_order2.ts @@ -0,0 +1,53 @@ +/// + +// @Filename: /a.ts +////export const _aB: number; +////export const _Ab: number; +////export const aB: number; +////export const Ab: number; + +// @Filename: /b.ts +////[|import { +//// _aB, +//// _Ab, +//// Ab, +////} from "./a"; +////aB;|] + +// @Filename: /c.ts +////[|import { +//// _aB, +//// _Ab, +//// Ab, +////} from "./a"; +////aB;|] + +// the import in 'b.ts' isn't sorted per ordinal comparison, so the import is added to the end of the list +goTo.file("/b.ts"); +verify.importFixAtPosition([ +`import { + _aB, + _Ab, + Ab, + aB, +} from "./a"; +aB;`, +], undefined, { + organizeImportsIgnoreCase: false, + organizeImportsCollation: "ordinal" +}); + +// the import in 'c.ts' *is* sorted per natural collation, so the import is added before `Ab` +goTo.file("/c.ts"); +verify.importFixAtPosition([ +`import { + _aB, + _Ab, + aB, + Ab, +} from "./a"; +aB;`, +], undefined, { + organizeImportsIgnoreCase: false, + organizeImportsCollation: "unicode" +}); diff --git a/tests/cases/fourslash/organizeImportsPathsUnicode1.ts b/tests/cases/fourslash/organizeImportsPathsUnicode1.ts new file mode 100644 index 0000000000000..5495da0d03aa0 --- /dev/null +++ b/tests/cases/fourslash/organizeImportsPathsUnicode1.ts @@ -0,0 +1,30 @@ +/// + +//// import * as Ab from "./Ab"; +//// import * as _aB from "./_aB"; +//// import * as aB from "./aB"; +//// import * as _Ab from "./_Ab"; +//// +//// console.log(_aB, _Ab, aB, Ab); + +verify.organizeImports( +`import * as Ab from "./Ab"; +import * as _Ab from "./_Ab"; +import * as _aB from "./_aB"; +import * as aB from "./aB"; + +console.log(_aB, _Ab, aB, Ab);`, /*mode*/ undefined, { + organizeImportsIgnoreCase: false, + organizeImportsCollation: "ordinal", +}); + +verify.organizeImports( +`import * as _aB from "./_aB"; +import * as _Ab from "./_Ab"; +import * as aB from "./aB"; +import * as Ab from "./Ab"; + +console.log(_aB, _Ab, aB, Ab);`, /*mode*/ undefined, { + organizeImportsIgnoreCase: false, + organizeImportsCollation: "unicode", +}); diff --git a/tests/cases/fourslash/organizeImportsPathsUnicode2.ts b/tests/cases/fourslash/organizeImportsPathsUnicode2.ts new file mode 100644 index 0000000000000..bca471c32590b --- /dev/null +++ b/tests/cases/fourslash/organizeImportsPathsUnicode2.ts @@ -0,0 +1,29 @@ +/// + +//// import * as a2 from "./a2"; +//// import * as a100 from "./a100"; +//// import * as a1 from "./a1"; +//// +//// console.log(a1, a2, a100); + +verify.organizeImports( +`import * as a1 from "./a1"; +import * as a100 from "./a100"; +import * as a2 from "./a2"; + +console.log(a1, a2, a100);`, /*mode*/ undefined, { + organizeImportsIgnoreCase: false, + organizeImportsCollation: "unicode", + organizeImportsNumericCollation: false, +}); + +verify.organizeImports( +`import * as a1 from "./a1"; +import * as a2 from "./a2"; +import * as a100 from "./a100"; + +console.log(a1, a2, a100);`, /*mode*/ undefined, { + organizeImportsIgnoreCase: false, + organizeImportsCollation: "unicode", + organizeImportsNumericCollation: true, +}); diff --git a/tests/cases/fourslash/organizeImportsPathsUnicode3.ts b/tests/cases/fourslash/organizeImportsPathsUnicode3.ts new file mode 100644 index 0000000000000..05e3571bca09a --- /dev/null +++ b/tests/cases/fourslash/organizeImportsPathsUnicode3.ts @@ -0,0 +1,29 @@ +/// + +//// import * as B from "./B"; +//// import * as À from "./À"; +//// import * as A from "./A"; +//// +//// console.log(A, À, B); + +verify.organizeImports( +`import * as À from "./À"; +import * as A from "./A"; +import * as B from "./B"; + +console.log(A, À, B);`, /*mode*/ undefined, { + organizeImportsIgnoreCase: false, + organizeImportsCollation: "unicode", + organizeImportsAccentCollation: false, +}); + +verify.organizeImports( +`import * as A from "./A"; +import * as À from "./À"; +import * as B from "./B"; + +console.log(A, À, B);`, /*mode*/ undefined, { + organizeImportsIgnoreCase: false, + organizeImportsCollation: "unicode", + organizeImportsAccentCollation: true, +}); diff --git a/tests/cases/fourslash/organizeImportsPathsUnicode4.ts b/tests/cases/fourslash/organizeImportsPathsUnicode4.ts new file mode 100644 index 0000000000000..ca7159953515a --- /dev/null +++ b/tests/cases/fourslash/organizeImportsPathsUnicode4.ts @@ -0,0 +1,32 @@ +/// + +//// import * as Ab from "./Ab"; +//// import * as _aB from "./_aB"; +//// import * as aB from "./aB"; +//// import * as _Ab from "./_Ab"; +//// +//// console.log(_aB, _Ab, aB, Ab); + +verify.organizeImports( +`import * as _Ab from "./_Ab"; +import * as _aB from "./_aB"; +import * as Ab from "./Ab"; +import * as aB from "./aB"; + +console.log(_aB, _Ab, aB, Ab);`, /*mode*/ undefined, { + organizeImportsIgnoreCase: false, + organizeImportsCollation: "unicode", + organizeImportsCaseFirst: "upper", +}); + +verify.organizeImports( +`import * as _aB from "./_aB"; +import * as _Ab from "./_Ab"; +import * as aB from "./aB"; +import * as Ab from "./Ab"; + +console.log(_aB, _Ab, aB, Ab);`, /*mode*/ undefined, { + organizeImportsIgnoreCase: false, + organizeImportsCollation: "unicode", + organizeImportsCaseFirst: "lower", +}); diff --git a/tests/cases/fourslash/organizeImportsUnicode1.ts b/tests/cases/fourslash/organizeImportsUnicode1.ts new file mode 100644 index 0000000000000..c673180a638c0 --- /dev/null +++ b/tests/cases/fourslash/organizeImportsUnicode1.ts @@ -0,0 +1,36 @@ +/// + +//// import { +//// Ab, +//// _aB, +//// aB, +//// _Ab, +//// } from './foo'; +//// +//// console.log(_aB, _Ab, aB, Ab); + +verify.organizeImports( +`import { + Ab, + _Ab, + _aB, + aB, +} from './foo'; + +console.log(_aB, _Ab, aB, Ab);`, /*mode*/ undefined, { + organizeImportsIgnoreCase: false, + organizeImportsCollation: "ordinal", +}); + +verify.organizeImports( +`import { + _aB, + _Ab, + aB, + Ab, +} from './foo'; + +console.log(_aB, _Ab, aB, Ab);`, /*mode*/ undefined, { + organizeImportsIgnoreCase: false, + organizeImportsCollation: "unicode", +}); diff --git a/tests/cases/fourslash/organizeImportsUnicode2.ts b/tests/cases/fourslash/organizeImportsUnicode2.ts new file mode 100644 index 0000000000000..338f3440f6984 --- /dev/null +++ b/tests/cases/fourslash/organizeImportsUnicode2.ts @@ -0,0 +1,35 @@ +/// + +//// import { +//// a2, +//// a100, +//// a1, +//// } from './foo'; +//// +//// console.log(a1, a2, a100); + +verify.organizeImports( +`import { + a1, + a100, + a2, +} from './foo'; + +console.log(a1, a2, a100);`, /*mode*/ undefined, { + organizeImportsIgnoreCase: false, + organizeImportsCollation: "unicode", + organizeImportsNumericCollation: false, +}); + +verify.organizeImports( +`import { + a1, + a2, + a100, +} from './foo'; + +console.log(a1, a2, a100);`, /*mode*/ undefined, { + organizeImportsIgnoreCase: false, + organizeImportsCollation: "unicode", + organizeImportsNumericCollation: true, +}); diff --git a/tests/cases/fourslash/organizeImportsUnicode3.ts b/tests/cases/fourslash/organizeImportsUnicode3.ts new file mode 100644 index 0000000000000..f1b6e28b849d9 --- /dev/null +++ b/tests/cases/fourslash/organizeImportsUnicode3.ts @@ -0,0 +1,35 @@ +/// + +//// import { +//// B, +//// À, +//// A, +//// } from './foo'; +//// +//// console.log(A, À, B); + +verify.organizeImports( +`import { + À, + A, + B, +} from './foo'; + +console.log(A, À, B);`, /*mode*/ undefined, { + organizeImportsIgnoreCase: false, + organizeImportsCollation: "unicode", + organizeImportsAccentCollation: false, +}); + +verify.organizeImports( +`import { + A, + À, + B, +} from './foo'; + +console.log(A, À, B);`, /*mode*/ undefined, { + organizeImportsIgnoreCase: false, + organizeImportsCollation: "unicode", + organizeImportsAccentCollation: true, +}); diff --git a/tests/cases/fourslash/organizeImportsUnicode4.ts b/tests/cases/fourslash/organizeImportsUnicode4.ts new file mode 100644 index 0000000000000..5eeb2d2daa05d --- /dev/null +++ b/tests/cases/fourslash/organizeImportsUnicode4.ts @@ -0,0 +1,38 @@ +/// + +//// import { +//// Ab, +//// _aB, +//// aB, +//// _Ab, +//// } from './foo'; +//// +//// console.log(_aB, _Ab, aB, Ab); + +verify.organizeImports( +`import { + _Ab, + _aB, + Ab, + aB, +} from './foo'; + +console.log(_aB, _Ab, aB, Ab);`, /*mode*/ undefined, { + organizeImportsIgnoreCase: false, + organizeImportsCollation: "unicode", + organizeImportsCaseFirst: "upper", +}); + +verify.organizeImports( +`import { + _aB, + _Ab, + aB, + Ab, +} from './foo'; + +console.log(_aB, _Ab, aB, Ab);`, /*mode*/ undefined, { + organizeImportsIgnoreCase: false, + organizeImportsCollation: "unicode", + organizeImportsCaseFirst: "lower", +});