Skip to content
91 changes: 71 additions & 20 deletions src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -865,32 +865,25 @@ export const enum SortKind {
}

/** @internal */
export function detectSortCaseSensitivity(array: readonly string[], useEslintOrdering?: boolean): SortKind;
/** @internal */
export function detectSortCaseSensitivity<T>(array: readonly T[], useEslintOrdering: boolean, getString: (element: T) => string): SortKind;
/** @internal */
export function detectSortCaseSensitivity<T>(array: readonly T[], useEslintOrdering?: boolean, getString?: (element: T) => string): SortKind {
export function detectSortCaseSensitivity<T>(
array: readonly T[],
getString: (element: T) => string,
compareStringsCaseSensitive: Comparer<string>,
compareStringsCaseInsensitive: Comparer<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) {

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;
}
Expand Down Expand Up @@ -2048,6 +2041,64 @@ export function memoizeWeak<A extends object, T>(callback: (arg: A) => T): (arg:
};
}

/** @internal */
export interface MemoizeCache<A extends any[], T> {
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<A extends any[], T>(callback: (...args: A) => T, cache: MemoizeCache<A, T>): (...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<T>(...args: ((t: T) => T)[]): (t: T) => T;
/** @internal */
export function compose<T>(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,
Expand Down
5 changes: 5 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
53 changes: 53 additions & 0 deletions src/server/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
26 changes: 14 additions & 12 deletions src/services/codefixes/importFixes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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)];
Expand All @@ -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 {
Expand Down Expand Up @@ -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
) {
Expand Down Expand Up @@ -1348,36 +1349,37 @@ 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
// 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, ignoreCaseForSorting);
: OrganizeImports.getImportSpecifierInsertionIndex(existingSpecifiers, spec, comparer);
changes.insertImportSpecifierAtIndex(sourceFile, spec, clause.namedBindings as NamedImports, insertionIndex);
}
}
Expand Down
Loading