diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 3961e094cde6b..8be7299567361 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -944,16 +944,53 @@ export function sortAndDeduplicate(array: readonly T[], comparer?: Comparer(array: readonly T[], comparer: Comparer) { if (array.length < 2) return true; - let prevElement = array[0]; - for (const element of array.slice(1)) { - if (comparer(prevElement, element) === Comparison.GreaterThan) { + for (let i = 1, len = array.length; i < len; i++) { + if (comparer(array[i - 1], array[i]) === Comparison.GreaterThan) { return false; } - prevElement = element; } return true; } +/** @internal */ +export const enum SortKind { + None = 0, + CaseSensitive = 1 << 0, + CaseInsensitive = 1 << 1, + Both = CaseSensitive | CaseInsensitive, +} + +/** @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 { + 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) { + kind &= ~SortKind.CaseSensitive; + } + if (kind & SortKind.CaseInsensitive && caseInsensitiveComparer(prevElement, element) === Comparison.GreaterThan) { + kind &= ~SortKind.CaseInsensitive; + } + if (kind === SortKind.None) { + return kind; + } + } + return kind; +} + /** @internal */ export function arrayIsEqualTo(array1: readonly T[] | undefined, array2: readonly T[] | undefined, equalityComparer: (a: T, b: T, index: number) => boolean = equateValues): boolean { if (!array1 || !array2) { @@ -2144,6 +2181,23 @@ export function memoizeOne(c }; } +/** + * A version of `memoize` that supports a single non-primitive argument, stored as keys of a WeakMap. + * + * @internal + */ +export function memoizeWeak(callback: (arg: A) => T): (arg: A) => T { + const map = new WeakMap(); + return (arg: A) => { + let value = map.get(arg); + if (value === undefined && !map.has(arg)) { + value = callback(arg); + map.set(arg, 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))`. @@ -2293,6 +2347,27 @@ export function compareStringsCaseInsensitive(a: string, b: string) { return a < b ? Comparison.LessThan : a > b ? Comparison.GreaterThan : Comparison.EqualTo; } +/** + * `compareStringsCaseInsensitive` transforms letters to uppercase for unicode reasons, + * while eslint's `sort-imports` rule transforms letters to lowercase. Which one you choose + * affects the relative order of letters and ASCII characters 91-96, of which `_` is a + * valid character in an identifier. So if we used `compareStringsCaseInsensitive` for + * import sorting, TypeScript and eslint would disagree about the correct case-insensitive + * sort order for `__String` and `Foo`. Since eslint's whole job is to create consistency + * by enforcing nitpicky details like this, it makes way more sense for us to just adopt + * their convention so users can have auto-imports without making eslint angry. + * + * @internal + */ +export function compareStringsCaseInsensitiveEslintCompatible(a: string, b: string) { + if (a === b) return Comparison.EqualTo; + if (a === undefined) return Comparison.LessThan; + if (b === undefined) return Comparison.GreaterThan; + a = a.toLowerCase(); + b = b.toLowerCase(); + return a < b ? Comparison.LessThan : a > b ? Comparison.GreaterThan : Comparison.EqualTo; +} + /** * Compare two strings using a case-sensitive ordinal comparison. * diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 773d64df907ae..8ddfa5eef6c4d 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -9729,6 +9729,7 @@ export interface UserPreferences { readonly includeInlayEnumMemberValueHints?: boolean; readonly allowRenameOfImportPath?: boolean; readonly autoImportFileExcludePatterns?: string[]; + readonly organizeImportsIgnoreCase?: "auto" | boolean; } /** Represents a bigint literal value without requiring bigint support */ diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index ac7c0c40ad3b5..4e2ef19a3b15a 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -535,8 +535,8 @@ export class TestState { } } - public verifyOrganizeImports(newContent: string, mode?: ts.OrganizeImportsMode) { - const changes = this.languageService.organizeImports({ fileName: this.activeFile.fileName, type: "file", mode }, this.formatCodeSettings, ts.emptyOptions); + public verifyOrganizeImports(newContent: string, mode?: ts.OrganizeImportsMode, preferences?: ts.UserPreferences) { + const changes = this.languageService.organizeImports({ fileName: this.activeFile.fileName, type: "file", mode }, this.formatCodeSettings, preferences); this.applyChanges(changes); this.verifyFileContent(this.activeFile.fileName, newContent); } diff --git a/src/harness/fourslashInterfaceImpl.ts b/src/harness/fourslashInterfaceImpl.ts index af1044d6fee6b..c312a40bcb8f6 100644 --- a/src/harness/fourslashInterfaceImpl.ts +++ b/src/harness/fourslashInterfaceImpl.ts @@ -626,8 +626,8 @@ export class Verify extends VerifyNegatable { this.state.noMoveToNewFile(); } - public organizeImports(newContent: string, mode?: ts.OrganizeImportsMode): void { - this.state.verifyOrganizeImports(newContent, mode); + public organizeImports(newContent: string, mode?: ts.OrganizeImportsMode, preferences?: ts.UserPreferences): void { + this.state.verifyOrganizeImports(newContent, mode, preferences); } } diff --git a/src/server/protocol.ts b/src/server/protocol.ts index f1133fe1cad3b..76c80264ce968 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -3516,6 +3516,7 @@ export interface UserPreferences { readonly includeInlayFunctionLikeReturnTypeHints?: boolean; readonly includeInlayEnumMemberValueHints?: boolean; readonly autoImportFileExcludePatterns?: string[]; + readonly organizeImportsIgnoreCase?: "auto" | boolean; /** * Indicates whether {@link ReferencesResponseItem.lineText} is supported. diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 2f0c90737549a..9339ca90c43be 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -118,6 +118,7 @@ import { skipAlias, some, sort, + SortKind, SourceFile, stableSort, startsWith, @@ -164,15 +165,14 @@ registerCodeFix({ const { errorCode, preferences, sourceFile, span, program } = context; const info = getFixInfos(context, errorCode, span.start, /*useAutoImportProvider*/ true); if (!info) return undefined; - const quotePreference = getQuotePreference(sourceFile, preferences); return info.map(({ fix, symbolName, errorIdentifierText }) => codeActionForFix( context, sourceFile, symbolName, fix, /*includeSymbolNameInDescription*/ symbolName !== errorIdentifierText, - quotePreference, - program.getCompilerOptions())); + program.getCompilerOptions(), + preferences)); }, fixIds: [importFixId], getAllCodeActions: context => { @@ -358,7 +358,8 @@ function createImportAdderWorker(sourceFile: SourceFile, program: Program, useAu importClauseOrBindingPattern, defaultImport, arrayFrom(namedImports.entries(), ([name, addAsTypeOnly]) => ({ addAsTypeOnly, name })), - compilerOptions); + compilerOptions, + preferences); }); let newDeclarations: AnyImportOrRequireStatement | readonly AnyImportOrRequireStatement[] | undefined; @@ -516,7 +517,8 @@ export function getImportCompletionAction( symbolName, fix, /*includeSymbolNameInDescription*/ false, - getQuotePreference(sourceFile, preferences), compilerOptions)) + compilerOptions, + preferences)) }; } @@ -526,7 +528,7 @@ export function getPromoteTypeOnlyCompletionAction(sourceFile: SourceFile, symbo const symbolName = single(getSymbolNamesToImport(sourceFile, program.getTypeChecker(), symbolToken, compilerOptions)); const fix = getTypeOnlyPromotionFix(sourceFile, symbolToken, symbolName, program); const includeSymbolNameInDescription = symbolName !== symbolToken.text; - return fix && codeFixActionToCodeAction(codeActionForFix({ host, formatContext, preferences }, sourceFile, symbolName, fix, includeSymbolNameInDescription, QuotePreference.Double, compilerOptions)); + return fix && codeFixActionToCodeAction(codeActionForFix({ host, formatContext, preferences }, sourceFile, symbolName, fix, includeSymbolNameInDescription, compilerOptions, preferences)); } function getImportFixForSymbol(sourceFile: SourceFile, exportInfos: readonly SymbolExportInfo[], moduleSymbol: Symbol, program: Program, useNamespaceInfo: { position: number, symbolName: string } | undefined, isValidTypeOnlyUseSite: boolean, useRequire: boolean, host: LanguageServiceHost, preferences: UserPreferences) { @@ -1175,14 +1177,15 @@ function getExportEqualsImportKind(importingFile: SourceFile, compilerOptions: C return allowSyntheticDefaults ? ImportKind.Default : ImportKind.CommonJS; } -function codeActionForFix(context: textChanges.TextChangesContext, sourceFile: SourceFile, symbolName: string, fix: ImportFix, includeSymbolNameInDescription: boolean, quotePreference: QuotePreference, compilerOptions: CompilerOptions): CodeFixAction { +function codeActionForFix(context: textChanges.TextChangesContext, sourceFile: SourceFile, symbolName: string, fix: ImportFix, includeSymbolNameInDescription: boolean, compilerOptions: CompilerOptions, preferences: UserPreferences): CodeFixAction { let diag!: DiagnosticAndArguments; const changes = textChanges.ChangeTracker.with(context, tracker => { - diag = codeActionForFixWorker(tracker, sourceFile, symbolName, fix, includeSymbolNameInDescription, quotePreference, compilerOptions); + diag = codeActionForFixWorker(tracker, sourceFile, symbolName, fix, includeSymbolNameInDescription, compilerOptions, preferences); }); return createCodeFixAction(importFixName, changes, diag, importFixId, Diagnostics.Add_all_missing_imports); } -function codeActionForFixWorker(changes: textChanges.ChangeTracker, sourceFile: SourceFile, symbolName: string, fix: ImportFix, includeSymbolNameInDescription: boolean, quotePreference: QuotePreference, compilerOptions: CompilerOptions): DiagnosticAndArguments { +function codeActionForFixWorker(changes: textChanges.ChangeTracker, sourceFile: SourceFile, symbolName: string, fix: ImportFix, includeSymbolNameInDescription: boolean, compilerOptions: CompilerOptions, preferences: UserPreferences): DiagnosticAndArguments { + const quotePreference = getQuotePreference(sourceFile, preferences); switch (fix.kind) { case ImportFixKind.UseNamespace: addNamespaceQualifier(changes, sourceFile, fix); @@ -1198,7 +1201,8 @@ function codeActionForFixWorker(changes: textChanges.ChangeTracker, sourceFile: importClauseOrBindingPattern, importKind === ImportKind.Default ? { name: symbolName, addAsTypeOnly } : undefined, importKind === ImportKind.Named ? [{ name: symbolName, addAsTypeOnly }] : emptyArray, - compilerOptions); + compilerOptions, + preferences); const moduleSpecifierWithoutQuotes = stripQuotes(moduleSpecifier); return includeSymbolNameInDescription ? [Diagnostics.Import_0_from_1, symbolName, moduleSpecifierWithoutQuotes] @@ -1239,10 +1243,11 @@ function promoteFromTypeOnly(changes: textChanges.ChangeTracker, aliasDeclaratio switch (aliasDeclaration.kind) { case SyntaxKind.ImportSpecifier: if (aliasDeclaration.isTypeOnly) { - if (aliasDeclaration.parent.elements.length > 1 && OrganizeImports.importSpecifiersAreSorted(aliasDeclaration.parent.elements)) { + const sortKind = OrganizeImports.detectImportSpecifierSorting(aliasDeclaration.parent.elements); + 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); + const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(aliasDeclaration.parent.elements, newSpecifier, sortKind === SortKind.CaseInsensitive); changes.insertImportSpecifierAtIndex(sourceFile, newSpecifier, aliasDeclaration.parent, insertionIndex); } else { @@ -1273,7 +1278,7 @@ function promoteFromTypeOnly(changes: textChanges.ChangeTracker, aliasDeclaratio if (convertExistingToTypeOnly) { const namedImports = tryCast(importClause.namedBindings, isNamedImports); if (namedImports && namedImports.elements.length > 1) { - if (OrganizeImports.importSpecifiersAreSorted(namedImports.elements) && + if (OrganizeImports.detectImportSpecifierSorting(namedImports.elements) && aliasDeclaration.kind === SyntaxKind.ImportSpecifier && namedImports.elements.indexOf(aliasDeclaration) !== 0 ) { @@ -1299,6 +1304,7 @@ function doAddExistingFix( defaultImport: Import | undefined, namedImports: readonly Import[], compilerOptions: CompilerOptions, + preferences: UserPreferences, ): void { if (clause.kind === SyntaxKind.ObjectBindingPattern) { if (defaultImport) { @@ -1326,21 +1332,45 @@ function doAddExistingFix( } if (namedImports.length) { + // sort case sensitivity: + // - if the user preference is explicit, use that + // - otherwise, if there are enough existing import specifiers in this import to detect unambiguously, use that + // - otherwise, detect from other imports in the file + let ignoreCaseForSorting: boolean | undefined; + if (typeof preferences.organizeImportsIgnoreCase === "boolean") { + ignoreCaseForSorting = preferences.organizeImportsIgnoreCase; + } + else if (existingSpecifiers) { + const targetImportSorting = OrganizeImports.detectImportSpecifierSorting(existingSpecifiers); + if (targetImportSorting !== SortKind.Both) { + ignoreCaseForSorting = targetImportSorting === SortKind.CaseInsensitive; + } + } + if (ignoreCaseForSorting === undefined) { + ignoreCaseForSorting = OrganizeImports.detectSorting(sourceFile) === SortKind.CaseInsensitive; + } + const newSpecifiers = stableSort( namedImports.map(namedImport => factory.createImportSpecifier( (!clause.isTypeOnly || promoteFromTypeOnly) && needsTypeOnly(namedImport), /*propertyName*/ undefined, factory.createIdentifier(namedImport.name))), - OrganizeImports.compareImportOrExportSpecifiers); - - if (existingSpecifiers?.length && OrganizeImports.importSpecifiersAreSorted(existingSpecifiers)) { + (s1, s2) => OrganizeImports.compareImportOrExportSpecifiers(s1, s2, ignoreCaseForSorting)); + + // 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); + if (specifierSort && !(ignoreCaseForSorting && specifierSort === SortKind.CaseSensitive)) { for (const spec of newSpecifiers) { // Organize imports puts type-only import specifiers last, so if we're // adding a non-type-only specifier and converting all the other ones to // 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); + : OrganizeImports.getImportSpecifierInsertionIndex(existingSpecifiers, spec, ignoreCaseForSorting); changes.insertImportSpecifierAtIndex(sourceFile, spec, clause.namedBindings as NamedImports, insertionIndex); } } diff --git a/src/services/organizeImports.ts b/src/services/organizeImports.ts index 103b156c9c4f4..881c2d6471501 100644 --- a/src/services/organizeImports.ts +++ b/src/services/organizeImports.ts @@ -3,10 +3,12 @@ import { arrayIsSorted, binarySearch, compareBooleans, - compareStringsCaseInsensitive, + compareStringsCaseInsensitiveEslintCompatible, + compareStringsCaseSensitive, compareValues, Comparison, createScanner, + detectSortCaseSensitivity, EmitFlags, emptyArray, ExportDeclaration, @@ -14,6 +16,7 @@ import { Expression, factory, FileTextChanges, + find, FindAllReferences, flatMap, formatting, @@ -39,6 +42,7 @@ import { LanguageServiceHost, length, map, + memoizeWeak, NamedImportBindings, NamedImports, NamespaceImport, @@ -48,7 +52,7 @@ import { Scanner, setEmitFlags, some, - SortedReadonlyArray, + SortKind, SourceFile, stableSort, suppressLeadingTrivia, @@ -81,22 +85,26 @@ export function organizeImports( 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 processImportsOfSameModuleSpecifier = (importGroup: readonly ImportDeclaration[]) => { - const processedDeclarations = maybeCoalesce(maybeRemove(importGroup, sourceFile, program), sourceFile); + const processedDeclarations = maybeCoalesce(maybeRemove(importGroup, sourceFile, program), ignoreCase, sourceFile); return shouldSort ? stableSort(processedDeclarations, (s1, s2) => compareImportsOrRequireStatements(s1, s2)) : processedDeclarations; }; - // All of the old ImportDeclarations in the file, in syntactic order. - const topLevelImportGroupDecls = groupImportsByNewlineContiguous(sourceFile, sourceFile.statements.filter(isImportDeclaration)); topLevelImportGroupDecls.forEach(importGroupDecl => organizeImportsWorker(importGroupDecl, processImportsOfSameModuleSpecifier)); // Exports are always used if (mode !== OrganizeImportsMode.RemoveUnused) { // All of the old ExportDeclarations in the file, in syntactic order. const topLevelExportDecls = sourceFile.statements.filter(isExportDeclaration); - organizeImportsWorker(topLevelExportDecls, coalesceExports); + organizeImportsWorker(topLevelExportDecls, group => coalesceExports(group, ignoreCase)); } for (const ambientModule of sourceFile.statements.filter(isAmbientModule)) { @@ -108,7 +116,7 @@ export function organizeImports( // Exports are always used if (mode !== OrganizeImportsMode.RemoveUnused) { const ambientModuleExportDecls = ambientModule.body.statements.filter(isExportDeclaration); - organizeImportsWorker(ambientModuleExportDecls, coalesceExports); + organizeImportsWorker(ambientModuleExportDecls, group => coalesceExports(group, ignoreCase)); } } @@ -130,13 +138,13 @@ export function organizeImports( suppressLeadingTrivia(oldImportDecls[0]); const oldImportGroups = shouldCombine - ? group(oldImportDecls, importDecl => getExternalModuleName(importDecl.moduleSpecifier!)!) + ? group(oldImportDecls, importDecl => getExternalModuleName(importDecl.moduleSpecifier)!) : [oldImportDecls]; const sortedImportGroups = shouldSort ? stableSort(oldImportGroups, (group1, group2) => compareModuleSpecifiers(group1[0].moduleSpecifier, group2[0].moduleSpecifier)) : oldImportGroups; const newImportDecls = flatMap(sortedImportGroups, importGroup => - getExternalModuleName(importGroup[0].moduleSpecifier!) + getExternalModuleName(importGroup[0].moduleSpecifier) ? coalesce(importGroup) : importGroup); @@ -170,7 +178,7 @@ function groupImportsByNewlineContiguous(sourceFile: SourceFile, importDecls: Im const groupImports: ImportDeclaration[][] = []; let groupIndex = 0; for (const topLevelImportDecl of importDecls) { - if (isNewGroup(sourceFile, topLevelImportDecl, scanner)) { + if (groupImports[groupIndex] && isNewGroup(sourceFile, topLevelImportDecl, scanner)) { groupIndex++; } @@ -288,7 +296,7 @@ function hasModuleDeclarationMatchingSpecifier(sourceFile: SourceFile, moduleSpe && moduleName.text === moduleSpecifierText); } -function getExternalModuleName(specifier: Expression) { +function getExternalModuleName(specifier: Expression | undefined) { return specifier !== undefined && isStringLiteralLike(specifier) ? specifier.text : undefined; @@ -300,13 +308,13 @@ function getExternalModuleName(specifier: Expression) { * * @internal */ -export function coalesceImports(importGroup: readonly ImportDeclaration[], sourceFile?: SourceFile) { +export function coalesceImports(importGroup: readonly ImportDeclaration[], ignoreCase?: boolean, 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) { @@ -355,7 +363,7 @@ export function coalesceImports(importGroup: readonly ImportDeclaration[], sourc newImportSpecifiers.push(...getNewImportSpecifiers(namedImports)); const sortedImportSpecifiers = factory.createNodeArray( - sortSpecifiers(newImportSpecifiers), + sortSpecifiers(newImportSpecifiers, ignoreCase), (namedImports[0]?.importClause!.namedBindings as NamedImports)?.elements.hasTrailingComma ); @@ -454,7 +462,7 @@ function getCategorizedImports(importGroup: readonly ImportDeclaration[]) { * * @internal */ -export function coalesceExports(exportGroup: readonly ExportDeclaration[]) { +export function coalesceExports(exportGroup: readonly ExportDeclaration[], ignoreCase: boolean) { if (exportGroup.length === 0) { return exportGroup; } @@ -474,7 +482,7 @@ export function coalesceExports(exportGroup: readonly ExportDeclaration[]) { const newExportSpecifiers: ExportSpecifier[] = []; newExportSpecifiers.push(...flatMap(exportGroup, i => i.exportClause && isNamedExports(i.exportClause) ? i.exportClause.elements : emptyArray)); - const sortedExportSpecifiers = sortSpecifiers(newExportSpecifiers); + const sortedExportSpecifiers = sortSpecifiers(newExportSpecifiers, ignoreCase); const exportDecl = exportGroup[0]; coalescedExports.push( @@ -538,15 +546,22 @@ function updateImportDeclarationAndClause( importDeclaration.assertClause); } -function sortSpecifiers(specifiers: readonly T[]) { - return stableSort(specifiers, compareImportOrExportSpecifiers); +function sortSpecifiers(specifiers: readonly T[], ignoreCase?: boolean) { + return stableSort(specifiers, (s1, s2) => compareImportOrExportSpecifiers(s1, s2, ignoreCase)); } /** @internal */ -export function compareImportOrExportSpecifiers(s1: T, s2: T): Comparison { - return compareBooleans(s1.isTypeOnly, s2.isTypeOnly) - || compareIdentifiers(s1.propertyName || s1.name, s2.propertyName || s2.name) - || compareIdentifiers(s1.name, s2.name); +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); } /** @@ -554,16 +569,14 @@ export function compareImportOrExportSpecifiers { - return arrayIsSorted(imports, compareImportsOrRequireStatements); +export function detectSorting(sourceFile: SourceFile): SortKind { + return detectSortingWorker( + groupImportsByNewlineContiguous(sourceFile, sourceFile.statements.filter(isImportDeclaration))); +} + +function detectSortingWorker(importGroups: ImportDeclaration[][]): SortKind { + 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 ?? ""); + if (!sortState) { + return sortState; + } + } + + // Check import specifiers + const declarationWithNamedImports = find( + importGroup, + i => tryCast(i.importClause?.namedBindings, isNamedImports)?.elements.length! > 1); + if (declarationWithNamedImports) { + sortState &= detectImportSpecifierSorting((declarationWithNamedImports.importClause!.namedBindings as NamedImports).elements); + if (!sortState) { + return sortState; + } + } + + // Quit as soon as we've disambiguated. There's a chance that something later will disagree with what we've + // found so far, but this function is only intended to infer a preference, not validate the whole file for + // consistent and correct sorting. + if (sortState !== SortKind.Both) { + return sortState; + } + } + return sortState; } /** @internal */ -export function importSpecifiersAreSorted(imports: readonly ImportSpecifier[]): imports is SortedReadonlyArray { - return arrayIsSorted(imports, compareImportOrExportSpecifiers); +export function detectImportDeclarationSorting(imports: readonly AnyImportOrRequireStatement[]): SortKind { + return detectSortCaseSensitivity(imports, /*useEslintOrdering*/ true, s => getExternalModuleName(getModuleSpecifierExpression(s)) || ""); } /** @internal */ -export function getImportDeclarationInsertionIndex(sortedImports: SortedReadonlyArray, newImport: AnyImportOrRequireStatement) { - const index = binarySearch(sortedImports, newImport, identity, compareImportsOrRequireStatements); +export const detectImportSpecifierSorting = memoizeWeak((specifiers: readonly ImportSpecifier[]): SortKind => { + if (!arrayIsSorted(specifiers, (s1, s2) => compareBooleans(s1.isTypeOnly, s2.isTypeOnly))) { + return SortKind.None; + } + return detectSortCaseSensitivity(specifiers, /*useEslintOrdering*/ true, specifier => specifier.name.text); +}); + +/** @internal */ +export function getImportDeclarationInsertionIndex(sortedImports: readonly AnyImportOrRequireStatement[], newImport: AnyImportOrRequireStatement, ignoreCase?: boolean) { + const index = binarySearch(sortedImports, newImport, identity, (a, b) => compareImportsOrRequireStatements(a, b, ignoreCase)); return index < 0 ? ~index : index; } /** @internal */ -export function getImportSpecifierInsertionIndex(sortedImports: SortedReadonlyArray, newImport: ImportSpecifier) { - const index = binarySearch(sortedImports, newImport, identity, compareImportOrExportSpecifiers); +export function getImportSpecifierInsertionIndex(sortedImports: readonly ImportSpecifier[], newImport: ImportSpecifier, ignoreCase?: boolean) { + const index = binarySearch(sortedImports, newImport, identity, (s1, s2) => compareImportOrExportSpecifiers(s1, s2, ignoreCase)); return index < 0 ? ~index : index; } /** @internal */ -export function compareImportsOrRequireStatements(s1: AnyImportOrRequireStatement, s2: AnyImportOrRequireStatement) { - return compareModuleSpecifiers(getModuleSpecifierExpression(s1), getModuleSpecifierExpression(s2)) || compareImportKind(s1, s2); +export function compareImportsOrRequireStatements(s1: AnyImportOrRequireStatement, s2: AnyImportOrRequireStatement, ignoreCase?: boolean) { + return compareModuleSpecifiers(getModuleSpecifierExpression(s1), getModuleSpecifierExpression(s2), ignoreCase) || compareImportKind(s1, s2); } function compareImportKind(s1: AnyImportOrRequireStatement, s2: AnyImportOrRequireStatement) { diff --git a/src/services/utilities.ts b/src/services/utilities.ts index ac93d22ba7699..5d5204d7fba93 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -2545,7 +2545,7 @@ export function insertImports(changes: textChanges.ChangeTracker, sourceFile: So if (!existingImportStatements.length) { changes.insertNodesAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween); } - else if (existingImportStatements && OrganizeImports.importsAreSorted(existingImportStatements)) { + else if (existingImportStatements && OrganizeImports.detectImportDeclarationSorting(existingImportStatements)) { for (const newImport of sortedNewImports) { const insertionIndex = OrganizeImports.getImportDeclarationInsertionIndex(existingImportStatements, newImport); if (insertionIndex === 0) { diff --git a/src/testRunner/unittests/services/organizeImports.ts b/src/testRunner/unittests/services/organizeImports.ts index 8188d9a2cfd22..6f0d500b8011f 100644 --- a/src/testRunner/unittests/services/organizeImports.ts +++ b/src/testRunner/unittests/services/organizeImports.ts @@ -38,20 +38,20 @@ describe("unittests:: services:: organizeImports", () => { function assertSortsBefore(importString1: string, importString2: string) { const [{moduleSpecifier: moduleSpecifier1}, {moduleSpecifier: moduleSpecifier2}] = parseImports(importString1, importString2); - assert.equal(ts.OrganizeImports.compareModuleSpecifiers(moduleSpecifier1, moduleSpecifier2), ts.Comparison.LessThan); - assert.equal(ts.OrganizeImports.compareModuleSpecifiers(moduleSpecifier2, moduleSpecifier1), ts.Comparison.GreaterThan); + assert.equal(ts.OrganizeImports.compareModuleSpecifiers(moduleSpecifier1, moduleSpecifier2, /*ignoreCase*/ true), ts.Comparison.LessThan); + assert.equal(ts.OrganizeImports.compareModuleSpecifiers(moduleSpecifier2, moduleSpecifier1, /*ignoreCase*/ true), ts.Comparison.GreaterThan); } }); describe("Coalesce imports", () => { it("No imports", () => { - assert.isEmpty(ts.OrganizeImports.coalesceImports([])); + assert.isEmpty(ts.OrganizeImports.coalesceImports([], /*ignoreCase*/ true)); }); it("Sort specifiers - case-insensitive", () => { const sortedImports = parseImports(`import { default as M, a as n, B, y, Z as O } from "lib";`); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = parseImports(`import { a as n, B, default as M, y, Z as O } from "lib";`); + const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const expectedCoalescedImports = parseImports(`import { B, default as M, a as n, Z as O, y } from "lib";`); assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -59,7 +59,7 @@ describe("unittests:: services:: organizeImports", () => { const sortedImports = parseImports( `import "lib";`, `import "lib";`); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); + const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = parseImports(`import "lib";`); assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -68,7 +68,7 @@ describe("unittests:: services:: organizeImports", () => { const sortedImports = parseImports( `import * as x from "lib";`, `import * as y from "lib";`); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); + const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = sortedImports; assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -77,7 +77,7 @@ describe("unittests:: services:: organizeImports", () => { const sortedImports = parseImports( `import x from "lib";`, `import y from "lib";`); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); + const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = parseImports(`import { default as x, default as y } from "lib";`); assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -86,7 +86,7 @@ describe("unittests:: services:: organizeImports", () => { const sortedImports = parseImports( `import { x } from "lib";`, `import { y as z } from "lib";`); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); + const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = parseImports(`import { x, y as z } from "lib";`); assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -95,7 +95,7 @@ describe("unittests:: services:: organizeImports", () => { const sortedImports = parseImports( `import "lib";`, `import * as x from "lib";`); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); + const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = sortedImports; assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -104,7 +104,7 @@ describe("unittests:: services:: organizeImports", () => { const sortedImports = parseImports( `import "lib";`, `import x from "lib";`); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); + const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = sortedImports; assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -113,7 +113,7 @@ describe("unittests:: services:: organizeImports", () => { const sortedImports = parseImports( `import "lib";`, `import { x } from "lib";`); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); + const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = sortedImports; assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -122,7 +122,7 @@ describe("unittests:: services:: organizeImports", () => { const sortedImports = parseImports( `import * as x from "lib";`, `import y from "lib";`); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); + const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = parseImports( `import y, * as x from "lib";`); assertListEqual(actualCoalescedImports, expectedCoalescedImports); @@ -132,7 +132,7 @@ describe("unittests:: services:: organizeImports", () => { const sortedImports = parseImports( `import * as x from "lib";`, `import { y } from "lib";`); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); + const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = sortedImports; assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -141,7 +141,7 @@ describe("unittests:: services:: organizeImports", () => { const sortedImports = parseImports( `import x from "lib";`, `import { y } from "lib";`); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); + const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = parseImports( `import x, { y } from "lib";`); assertListEqual(actualCoalescedImports, expectedCoalescedImports); @@ -157,7 +157,7 @@ describe("unittests:: services:: organizeImports", () => { `import * as x from "lib";`, `import z from "lib";`, `import { a } from "lib";`); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); + const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = parseImports( `import "lib";`, `import * as x from "lib";`, @@ -172,7 +172,7 @@ describe("unittests:: services:: organizeImports", () => { `import * as x from "lib";`, `import * as y from "lib";`, `import z from "lib";`); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); + const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = sortedImports; assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -182,7 +182,7 @@ describe("unittests:: services:: organizeImports", () => { `import type { x } from "lib";`, `import type { y } from "lib";`, `import { z } from "lib";`); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); + const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = parseImports( `import { z } from "lib";`, `import type { x, y } from "lib";`); @@ -196,7 +196,7 @@ describe("unittests:: services:: organizeImports", () => { `import type z from "lib";`); // Default import could be rewritten as a named import to combine with `x`, // but seems of debatable merit. - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); + const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = actualCoalescedImports; assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -204,19 +204,19 @@ describe("unittests:: services:: organizeImports", () => { describe("Coalesce exports", () => { it("No exports", () => { - assert.isEmpty(ts.OrganizeImports.coalesceExports([])); + assert.isEmpty(ts.OrganizeImports.coalesceExports([], /*ignoreCase*/ true)); }); it("Sort specifiers - case-insensitive", () => { const sortedExports = parseExports(`export { default as M, a as n, B, y, Z as O } from "lib";`); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports); - const expectedCoalescedExports = parseExports(`export { a as n, B, default as M, y, Z as O } from "lib";`); + const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true); + const expectedCoalescedExports = parseExports(`export { B, default as M, a as n, Z as O, y } from "lib";`); assertListEqual(actualCoalescedExports, expectedCoalescedExports); }); it("Sort specifiers - type-only", () => { const sortedImports = parseImports(`import { type z, y, type x, c, type b, a } from "lib";`); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports); + const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = parseImports(`import { a, c, y, type b, type x, type z } from "lib";`); assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -225,7 +225,7 @@ describe("unittests:: services:: organizeImports", () => { const sortedExports = parseExports( `export * from "lib";`, `export * from "lib";`); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports); + const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true); const expectedCoalescedExports = parseExports(`export * from "lib";`); assertListEqual(actualCoalescedExports, expectedCoalescedExports); }); @@ -234,7 +234,7 @@ describe("unittests:: services:: organizeImports", () => { const sortedExports = parseExports( `export { x };`, `export { y as z };`); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports); + const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true); const expectedCoalescedExports = parseExports(`export { x, y as z };`); assertListEqual(actualCoalescedExports, expectedCoalescedExports); }); @@ -243,7 +243,7 @@ describe("unittests:: services:: organizeImports", () => { const sortedExports = parseExports( `export { x } from "lib";`, `export { y as z } from "lib";`); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports); + const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true); const expectedCoalescedExports = parseExports(`export { x, y as z } from "lib";`); assertListEqual(actualCoalescedExports, expectedCoalescedExports); }); @@ -252,7 +252,7 @@ describe("unittests:: services:: organizeImports", () => { const sortedExports = parseExports( `export * from "lib";`, `export { y } from "lib";`); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports); + const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true); const expectedCoalescedExports = sortedExports; assertListEqual(actualCoalescedExports, expectedCoalescedExports); }); @@ -262,9 +262,9 @@ describe("unittests:: services:: organizeImports", () => { `export { x };`, `export { y as w, z as default };`, `export { w as q };`); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports); + const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true); const expectedCoalescedExports = parseExports( - `export { w as q, x, y as w, z as default };`); + `export { z as default, w as q, y as w, x };`); assertListEqual(actualCoalescedExports, expectedCoalescedExports); }); @@ -273,10 +273,10 @@ describe("unittests:: services:: organizeImports", () => { `export { x as a, y } from "lib";`, `export * from "lib";`, `export { z as b } from "lib";`); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports); + const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true); const expectedCoalescedExports = parseExports( `export * from "lib";`, - `export { x as a, y, z as b } from "lib";`); + `export { x as a, z as b, y } from "lib";`); assertListEqual(actualCoalescedExports, expectedCoalescedExports); }); @@ -284,7 +284,7 @@ describe("unittests:: services:: organizeImports", () => { const sortedExports = parseExports( `export { x };`, `export type { y };`); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports); + const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true); const expectedCoalescedExports = sortedExports; assertListEqual(actualCoalescedExports, expectedCoalescedExports); }); @@ -293,7 +293,7 @@ describe("unittests:: services:: organizeImports", () => { const sortedExports = parseExports( `export type { x };`, `export type { y };`); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports); + const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true); const expectedCoalescedExports = parseExports( `export type { x, y };`); assertListEqual(actualCoalescedExports, expectedCoalescedExports); diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 0af2b39653975..f9bff3ab7690d 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -2802,6 +2802,7 @@ declare namespace ts { readonly includeInlayFunctionLikeReturnTypeHints?: boolean; readonly includeInlayEnumMemberValueHints?: boolean; readonly autoImportFileExcludePatterns?: string[]; + readonly organizeImportsIgnoreCase?: "auto" | boolean; /** * Indicates whether {@link ReferencesResponseItem.lineText} is supported. */ @@ -8382,6 +8383,7 @@ declare namespace ts { readonly includeInlayEnumMemberValueHints?: boolean; readonly allowRenameOfImportPath?: boolean; readonly autoImportFileExcludePatterns?: string[]; + readonly organizeImportsIgnoreCase?: "auto" | boolean; } /** 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 eaf5c1e2626fa..ca62f91f244ec 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -4448,6 +4448,7 @@ declare namespace ts { readonly includeInlayEnumMemberValueHints?: boolean; readonly allowRenameOfImportPath?: boolean; readonly autoImportFileExcludePatterns?: string[]; + readonly organizeImportsIgnoreCase?: "auto" | boolean; } /** Represents a bigint literal value without requiring bigint support */ interface PseudoBigInt { diff --git a/tests/baselines/reference/organizeImports/Syntax_Error_Imports_skipDestructiveCodeActions.ts b/tests/baselines/reference/organizeImports/Syntax_Error_Imports_skipDestructiveCodeActions.ts index e77a10ab0d826..da5102320fefc 100644 --- a/tests/baselines/reference/organizeImports/Syntax_Error_Imports_skipDestructiveCodeActions.ts +++ b/tests/baselines/reference/organizeImports/Syntax_Error_Imports_skipDestructiveCodeActions.ts @@ -10,7 +10,7 @@ D; // ==ORGANIZED== import * as NS from "lib"; -import D, { class, class, class, F1, F2 } from "lib"; +import D, { F1, F2, class, class, class } from "lib"; class class class; D; diff --git a/tests/cases/fourslash/autoImportSortCaseSensitivity1.ts b/tests/cases/fourslash/autoImportSortCaseSensitivity1.ts new file mode 100644 index 0000000000000..993b3ec855972 --- /dev/null +++ b/tests/cases/fourslash/autoImportSortCaseSensitivity1.ts @@ -0,0 +1,44 @@ +/// + +// @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; + +// @Filename: /exports2.ts +//// export const d = 0; +//// export const D = 1; +//// export const e = 2; +//// export const E = 3; + +// Ambiguous in whole file: use user preference, default to case-sensitive + +// @Filename: /index0.ts +//// import { A, B, C } from "./exports1"; +//// a/*0*/ + +// Ambiguous in target import: use user preference, look at other imports in file + +// @Filename: /index1.ts +//// import { A, a, B, b } from "./exports1"; +//// import { E } from "./exports2"; +//// d/*1*/ + +goTo.marker("0"); +verify.importFixAtPosition([`import { A, B, C, a } from "./exports1";\na`]); +verify.importFixAtPosition([`import { a, A, B, C } from "./exports1";\na`], + /*errorCode*/ undefined, + { organizeImportsIgnoreCase: true }); + +goTo.marker("1"); +verify.importFixAtPosition([ +`import { A, a, B, b } from "./exports1"; +import { d, E } from "./exports2"; +d`]); +verify.importFixAtPosition([ +`import { A, a, B, b } from "./exports1"; +import { E, d } from "./exports2"; +d`], /*errorCode*/ undefined, { organizeImportsIgnoreCase: false }); \ No newline at end of file diff --git a/tests/cases/fourslash/autoImportSortCaseSensitivity2.ts b/tests/cases/fourslash/autoImportSortCaseSensitivity2.ts new file mode 100644 index 0000000000000..750cb35eccfde --- /dev/null +++ b/tests/cases/fourslash/autoImportSortCaseSensitivity2.ts @@ -0,0 +1,33 @@ +/// + +// @Filename: /a.ts +////export interface HasBar { bar: number } +////export function hasBar(x: unknown): x is HasBar { return x && typeof x.bar === "number" } +////export function foo() {} +////export type __String = string; + +// @Filename: /b.ts +////import { __String, HasBar, hasBar } from "./a"; +////f/**/; + +verify.completions({ + marker: "", + includes: { + name: "foo", + source: "/a", + sourceDisplay: "./a", + text: "function foo(): void", + kind: "function", + kindModifiers: "export", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions + }, + preferences: { includeCompletionsForModuleExports: true }, +}); +verify.applyCodeActionFromCompletion("", { + name: "foo", + source: "/a", + description: `Update import from "./a"`, + newFileContent: `import { __String, foo, HasBar, hasBar } from "./a"; +f;`, +}); \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixInferFromUsageContextualImport1.ts b/tests/cases/fourslash/codeFixInferFromUsageContextualImport1.ts index 5e4a4ca3542ff..f41399a478ac2 100644 --- a/tests/cases/fourslash/codeFixInferFromUsageContextualImport1.ts +++ b/tests/cases/fourslash/codeFixInferFromUsageContextualImport1.ts @@ -20,7 +20,7 @@ goTo.file("/b.ts"); verify.codeFix({ description: "Infer parameter types from usage", newFileContent: -`import { getEmail, User } from "./a"; +`import { User, getEmail } from "./a"; export function f(user: User) { getEmail(user); diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 7d106808e7982..411550e66f5c6 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -447,7 +447,7 @@ declare namespace FourSlashInterface { generateTypes(...options: GenerateTypesOptions[]): void; - organizeImports(newContent: string, mode?: ts.OrganizeImportsMode): void; + organizeImports(newContent: string, mode?: ts.OrganizeImportsMode, preferences?: UserPreferences): void; toggleLineComment(newFileContent: string): void; toggleMultilineComment(newFileContent: string): void; @@ -671,6 +671,7 @@ declare namespace FourSlashInterface { readonly providePrefixAndSuffixTextForRename?: boolean; readonly allowRenameOfImportPath?: boolean; readonly autoImportFileExcludePatterns?: readonly string[]; + readonly organizeImportsIgnoreCase?: "auto" | boolean; } interface InlayHintsOptions extends UserPreferences { readonly includeInlayParameterNameHints?: "none" | "literals" | "all"; diff --git a/tests/cases/fourslash/organizeImports1.ts b/tests/cases/fourslash/organizeImports1.ts index 0ce6e4d0267f0..58306c8161caa 100644 --- a/tests/cases/fourslash/organizeImports1.ts +++ b/tests/cases/fourslash/organizeImports1.ts @@ -18,22 +18,44 @@ //// 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'; + +// 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 { + b as B, + c as C, + d as D, + f as F, + g as G, + h as H, a, b, - b as B, c, - c as C, - d, d as D, + d, e, f, - f as F, g, - g as G, - h, h as H + h } from './foo'; console.log(a, B, b, c, C, d, D); -console.log(e, f, F, g, G, H, h);` -); \ No newline at end of file +console.log(e, f, F, g, G, H, h);`, /*mode*/ undefined, { organizeImportsIgnoreCase: false }); diff --git a/tests/cases/fourslash/organizeImports13.ts b/tests/cases/fourslash/organizeImports13.ts index 3cbccba0d3ba5..22f1aff59c36a 100644 --- a/tests/cases/fourslash/organizeImports13.ts +++ b/tests/cases/fourslash/organizeImports13.ts @@ -23,6 +23,31 @@ ////interface Use extends Type1, Type2, Type3, Type4, Type5, Type6, Type7, Type8, Type9 {} ////console.log(func1, func2, func3, func4, func5, func6, func7, func8, func9); +verify.organizeImports( +`import { + Type1, + Type2, + Type3, + Type4, + Type5, + Type6, + Type7, + Type8, + Type9, + func1, + func2, + func3, + func4, + func5, + func6, + func7, + func8, + func9, +} from "foo"; +interface Use extends Type1, Type2, Type3, Type4, Type5, Type6, Type7, Type8, Type9 {} +console.log(func1, func2, func3, func4, func5, func6, func7, func8, func9);` +); + verify.organizeImports( `import { func1, @@ -45,5 +70,6 @@ verify.organizeImports( Type9, } from "foo"; interface Use extends Type1, Type2, Type3, Type4, Type5, Type6, Type7, Type8, Type9 {} -console.log(func1, func2, func3, func4, func5, func6, func7, func8, func9);` -); +console.log(func1, func2, func3, func4, func5, func6, func7, func8, func9);`, +/*mode*/ undefined, +{ organizeImportsIgnoreCase: true }); diff --git a/tests/cases/fourslash/organizeImports16.ts b/tests/cases/fourslash/organizeImports16.ts new file mode 100644 index 0000000000000..93aa5a81f2cb8 --- /dev/null +++ b/tests/cases/fourslash/organizeImports16.ts @@ -0,0 +1,31 @@ +/// + +//// import { a, A, b } from "foo"; +//// interface Use extends A {} +//// console.log(a, b); + +verify.organizeImports( +`import { a, A, b } from "foo"; +interface Use extends A {} +console.log(a, b);`); + +verify.organizeImports( +`import { a, A, b } from "foo"; +interface Use extends A {} +console.log(a, b);`, + /*mode*/ undefined, + { organizeImportsIgnoreCase: "auto" }); + +verify.organizeImports( +`import { a, A, b } from "foo"; +interface Use extends A {} +console.log(a, b);`, + /*mode*/ undefined, + { organizeImportsIgnoreCase: true }); + +verify.organizeImports( +`import { A, a, b } from "foo"; +interface Use extends A {} +console.log(a, b);`, + /*mode*/ undefined, + { organizeImportsIgnoreCase: false }); \ No newline at end of file