Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Sort Order Lexicographic Options setting for Explorer #97272

Merged
merged 13 commits into from
May 6, 2021
Merged
163 changes: 141 additions & 22 deletions src/vs/base/common/comparers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
import { sep } from 'vs/base/common/path';
import { IdleValue } from 'vs/base/common/async';

// When comparing large numbers of strings, such as in sorting large arrays, is better for
// performance to create an Intl.Collator object and use the function provided by its compare
// property than it is to use String.prototype.localeCompare()
// When comparing large numbers of strings it's better for performance to create an
// Intl.Collator object and use the function provided by its compare property
// than it is to use String.prototype.localeCompare()

// A collator with numeric sorting enabled, and no sensitivity to case or to accents
// A collator with numeric sorting enabled, and no sensitivity to case, accents or diacritics.
const intlFileNameCollatorBaseNumeric: IdleValue<{ collator: Intl.Collator, collatorIsNumeric: boolean }> = new IdleValue(() => {
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
return {
Expand All @@ -28,36 +28,66 @@ const intlFileNameCollatorNumeric: IdleValue<{ collator: Intl.Collator }> = new
});

// A collator with numeric sorting enabled, and sensitivity to accents and diacritics but not case.
const intlFileNameCollatorNumericCaseInsenstive: IdleValue<{ collator: Intl.Collator }> = new IdleValue(() => {
const intlFileNameCollatorNumericCaseInsensitive: IdleValue<{ collator: Intl.Collator }> = new IdleValue(() => {
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'accent' });
return {
collator: collator
};
});/** Compares filenames without distinguishing the name from the extension. Disambiguates by unicode comparison. */
});

/** Compares filenames without distinguishing the name from the extension. Disambiguates by unicode comparison. */
export function compareFileNames(one: string | null, other: string | null, caseSensitive = false): number {
const a = one || '';
const b = other || '';
const result = intlFileNameCollatorBaseNumeric.value.collator.compare(a, b);

// Using the numeric option in the collator will
// make compare(`foo1`, `foo01`) === 0. We must disambiguate.
// Using the numeric option will make compare(`foo1`, `foo01`) === 0. Disambiguate.
if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && result === 0 && a !== b) {
return a < b ? -1 : 1;
}

return result;
}

/** Compares filenames without distinguishing the name from the extension. Disambiguates by length, not unicode comparison. */
/** Compares full filenames without grouping by case. */
export function compareFileNamesDefault(one: string | null, other: string | null): number {
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
one = one || '';
other = other || '';

// Compare the entire filename - both name and extension - and disambiguate by length if needed
return compareAndDisambiguateByLength(collatorNumeric, one, other);
}

/** Compares full filenames grouping uppercase names before lowercase. */
export function compareFileNamesUpper(one: string | null, other: string | null) {
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
one = one || '';
other = other || '';

return compareCaseUpperFirst(one, other) || compareAndDisambiguateByLength(collatorNumeric, one, other);
}

/** Compares full filenames grouping lowercase names before uppercase. */
export function compareFileNamesLower(one: string | null, other: string | null) {
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
one = one || '';
other = other || '';

return compareCaseLowerFirst(one, other) || compareAndDisambiguateByLength(collatorNumeric, one, other);
}

/** Compares full filenames by unicode value. */
export function compareFileNamesUnicode(one: string | null, other: string | null) {
one = one || '';
other = other || '';

if (one === other) {
return 0;
}

return one < other ? -1 : 1;
}

export function noIntlCompareFileNames(one: string | null, other: string | null, caseSensitive = false): number {
if (!caseSensitive) {
one = one && one.toLowerCase();
Expand All @@ -78,15 +108,15 @@ export function noIntlCompareFileNames(one: string | null, other: string | null,
return oneExtension < otherExtension ? -1 : 1;
}

/** Compares filenames by extension, then by name. Disambiguates by unicode comparison. */
export function compareFileExtensions(one: string | null, other: string | null): number {
const [oneName, oneExtension] = extractNameAndExtension(one);
const [otherName, otherExtension] = extractNameAndExtension(other);

let result = intlFileNameCollatorBaseNumeric.value.collator.compare(oneExtension, otherExtension);

if (result === 0) {
// Using the numeric option in the collator will
// make compare(`foo1`, `foo01`) === 0. We must disambiguate.
// Using the numeric option will make compare(`foo1`, `foo01`) === 0. Disambiguate.
if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && oneExtension !== otherExtension) {
return oneExtension < otherExtension ? -1 : 1;
}
Expand All @@ -102,24 +132,65 @@ export function compareFileExtensions(one: string | null, other: string | null):
return result;
}

/** Compares filenames by extenson, then by full filename */
/** Compares filenames by extenson, then by full filename. Mixes uppercase and lowercase names together. */
export function compareFileExtensionsDefault(one: string | null, other: string | null): number {
one = one || '';
other = other || '';
const oneExtension = extractExtension(one);
const otherExtension = extractExtension(other);
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsenstive.value.collator;
let result;
const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsensitive.value.collator;

// Check for extension differences, ignoring differences in case and comparing numbers numerically.
result = compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension);
if (result !== 0) {
return result;
return compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension) ||
compareAndDisambiguateByLength(collatorNumeric, one, other);
}

/** Compares filenames by extension, then case, then full filename. Groups uppercase names before lowercase. */
export function compareFileExtensionsUpper(one: string | null, other: string | null): number {
one = one || '';
other = other || '';
const oneExtension = extractExtension(one);
const otherExtension = extractExtension(other);
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsensitive.value.collator;

return compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension) ||
compareCaseUpperFirst(one, other) ||
compareAndDisambiguateByLength(collatorNumeric, one, other);
}

/** Compares filenames by extension, then case, then full filename. Groups lowercase names before uppercase. */
export function compareFileExtensionsLower(one: string | null, other: string | null): number {
one = one || '';
other = other || '';
const oneExtension = extractExtension(one);
const otherExtension = extractExtension(other);
const collatorNumeric = intlFileNameCollatorNumeric.value.collator;
const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsensitive.value.collator;

return compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension) ||
compareCaseLowerFirst(one, other) ||
compareAndDisambiguateByLength(collatorNumeric, one, other);
}

/** Compares filenames by case-insensitive extension unicode value, then by full filename unicode value. */
export function compareFileExtensionsUnicode(one: string | null, other: string | null) {
one = one || '';
other = other || '';
const oneExtension = extractExtension(one).toLowerCase();
const otherExtension = extractExtension(other).toLowerCase();

// Check for extension differences
if (oneExtension !== otherExtension) {
return oneExtension < otherExtension ? -1 : 1;
}

// Compare full filenames
return compareAndDisambiguateByLength(collatorNumeric, one, other);
// Check for full filename differences.
if (one !== other) {
return one < other ? -1 : 1;
}

return 0;
}

const FileNameMatch = /^(.*?)(\.([^.]*))?$/;
Expand All @@ -130,7 +201,7 @@ function extractNameAndExtension(str?: string | null, dotfilesAsNames = false):

let result: [string, string] = [(match && match[1]) || '', (match && match[3]) || ''];

// if the dotfilesAsNames option is selected, treat an empty filename with an extension,
// if the dotfilesAsNames option is selected, treat an empty filename with an extension
// or a filename that starts with a dot, as a dotfile name
if (dotfilesAsNames && (!result[0] && result[1] || result[0] && result[0].charAt(0) === '.')) {
result = [result[0] + '.' + result[1], ''];
Expand Down Expand Up @@ -162,6 +233,54 @@ function compareAndDisambiguateByLength(collator: Intl.Collator, one: string, ot
return 0;
}

/** @returns `true` if the string is starts with a lowercase letter. Otherwise, `false`. */
function startsWithLower(string: string) {
const character = string.charAt(0);

return (character.toLocaleUpperCase() !== character) ? true : false;
}

/** @returns `true` if the string starts with an uppercase letter. Otherwise, `false`. */
function startsWithUpper(string: string) {
const character = string.charAt(0);

return (character.toLocaleLowerCase() !== character) ? true : false;
}

/**
* Compares the case of the provided strings - lowercase before uppercase
*
* @returns
* ```text
* -1 if one is lowercase and other is uppercase
* 1 if one is uppercase and other is lowercase
* 0 otherwise
* ```
*/
function compareCaseLowerFirst(one: string, other: string): number {
if (startsWithLower(one) && startsWithUpper(other)) {
return -1;
}
return (startsWithUpper(one) && startsWithLower(other)) ? 1 : 0;
}

/**
* Compares the case of the provided strings - uppercase before lowercase
*
* @returns
* ```text
* -1 if one is uppercase and other is lowercase
* 1 if one is lowercase and other is uppercase
* 0 otherwise
* ```
*/
function compareCaseUpperFirst(one: string, other: string): number {
if (startsWithUpper(one) && startsWithLower(other)) {
return -1;
}
return (startsWithLower(one) && startsWithUpper(other)) ? 1 : 0;
}

function comparePathComponents(one: string, other: string, caseSensitive = false): number {
if (!caseSensitive) {
one = one && one.toLowerCase();
Expand Down
Loading