From 5fb7d2aa2f02e28d5be477c9309919e67bb40e0f Mon Sep 17 00:00:00 2001 From: Leila Pearson <873990+leilapearson@users.noreply.github.com> Date: Wed, 12 Aug 2020 16:33:14 -0700 Subject: [PATCH] Compare full filenames This change fixes issue #99955 When comparing file and folder names, after grouping by type if applicable, compare the full name all at once instead of separately comparing the name and the extension. This change makes filename comparisons work the same as they do in the Mac File Explorer and the same as a standard locale-based sort with the numeric option (a form of natural sort) enabled. Windows File Explorer has a different sort order due to treating the dot character differently. Many terminal programs and sites like github also differ, using a unicode-sort instead of a locale-based sort. --- src/vs/base/common/comparers.ts | 68 +++---- src/vs/base/test/browser/comparers.test.ts | 185 ++++++++---------- .../files/browser/views/explorerViewer.ts | 10 +- 3 files changed, 116 insertions(+), 147 deletions(-) diff --git a/src/vs/base/common/comparers.ts b/src/vs/base/common/comparers.ts index a25e2bb9c16..7b45ce5abb6 100644 --- a/src/vs/base/common/comparers.ts +++ b/src/vs/base/common/comparers.ts @@ -33,8 +33,7 @@ const intlFileNameCollatorNumericCaseInsenstive: IdleValue<{ collator: Intl.Coll return { collator: collator }; -}); - +});/** 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 || ''; @@ -49,36 +48,16 @@ export function compareFileNames(one: string | null, other: string | null, caseS return result; } -/** Compares filenames by name then extension, sorting numbers numerically instead of alphabetically. */ -export function compareFileNamesNumeric(one: string | null, other: string | null): number { - const [oneName, oneExtension] = extractNameAndExtension(one, true); - const [otherName, otherExtension] = extractNameAndExtension(other, true); +/** Compares filenames without distinguishing the name from the extension. Disambiguates by length, not unicode comparison. */ +export function compareFileNamesDefault(one: string | null, other: string | null): number { const collatorNumeric = intlFileNameCollatorNumeric.value.collator; - const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsenstive.value.collator; - let result; + one = one || ''; + other = other || ''; - // Check for name differences, comparing numbers numerically instead of alphabetically. - result = compareAndDisambiguateByLength(collatorNumeric, oneName, otherName); - if (result !== 0) { - return result; - } - - // Check for case insensitive extension differences, comparing numbers numerically instead of alphabetically. - result = compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension); - if (result !== 0) { - return result; - } - - // Disambiguate the extension case if needed. - if (oneExtension !== otherExtension) { - return collatorNumeric.compare(oneExtension, otherExtension); - } - - return 0; + // Compare the entire filename - both name and extension - and disambiguate by length if needed + return compareAndDisambiguateByLength(collatorNumeric, one, other); } -const FileNameMatch = /^(.*?)(\.([^.]*))?$/; - export function noIntlCompareFileNames(one: string | null, other: string | null, caseSensitive = false): number { if (!caseSensitive) { one = one && one.toLowerCase(); @@ -123,10 +102,12 @@ export function compareFileExtensions(one: string | null, other: string | null): return result; } -/** Compares filenames by extenson, then by name. Sorts numbers numerically, not alphabetically. */ -export function compareFileExtensionsNumeric(one: string | null, other: string | null): number { - const [oneName, oneExtension] = extractNameAndExtension(one, true); - const [otherName, otherExtension] = extractNameAndExtension(other, true); +/** Compares filenames by extenson, then by full filename */ +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; @@ -137,20 +118,12 @@ export function compareFileExtensionsNumeric(one: string | null, other: string | return result; } - // Compare names. - result = compareAndDisambiguateByLength(collatorNumeric, oneName, otherName); - if (result !== 0) { - return result; - } - - // Disambiguate extension case if needed. - if (oneExtension !== otherExtension) { - return collatorNumeric.compare(oneExtension, otherExtension); - } - - return 0; + // Compare full filenames + return compareAndDisambiguateByLength(collatorNumeric, one, other); } +const FileNameMatch = /^(.*?)(\.([^.]*))?$/; + /** Extracts the name and extension from a full filename, with optional special handling for dotfiles */ function extractNameAndExtension(str?: string | null, dotfilesAsNames = false): [string, string] { const match = str ? FileNameMatch.exec(str) as Array : ([] as Array); @@ -166,6 +139,13 @@ function extractNameAndExtension(str?: string | null, dotfilesAsNames = false): return result; } +/** Extracts the extension from a full filename. Treats dotfiles as names, not extensions. */ +function extractExtension(str?: string | null): string { + const match = str ? FileNameMatch.exec(str) as Array : ([] as Array); + + return (match && match[1] && match[1].charAt(0) !== '.' && match[3]) || ''; +} + function compareAndDisambiguateByLength(collator: Intl.Collator, one: string, other: string) { // Check for differences let result = collator.compare(one, other); diff --git a/src/vs/base/test/browser/comparers.test.ts b/src/vs/base/test/browser/comparers.test.ts index 90dff8c2835..4c40ea2a065 100644 --- a/src/vs/base/test/browser/comparers.test.ts +++ b/src/vs/base/test/browser/comparers.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { compareFileNames, compareFileExtensions, compareFileNamesNumeric, compareFileExtensionsNumeric } from 'vs/base/common/comparers'; +import { compareFileNames, compareFileExtensions, compareFileNamesDefault, compareFileExtensionsDefault } from 'vs/base/common/comparers'; import * as assert from 'assert'; const compareLocale = (a: string, b: string) => a.localeCompare(b); @@ -15,7 +15,7 @@ suite('Comparers', () => { test('compareFileNames', () => { // - // Comparisons with the same results as compareFileNamesNumeric + // Comparisons with the same results as compareFileNamesDefault // // name-only comparisons @@ -28,6 +28,7 @@ suite('Comparers', () => { // name plus extension comparisons assert(compareFileNames('bbb.aaa', 'aaa.bbb') > 0, 'files with extensions are compared first by filename'); + assert(compareFileNames('aggregate.go', 'aggregate_repo.go') > 0, 'compares the whole name all at once by locale'); // dotfile comparisons assert(compareFileNames('.abc', '.abc') === 0, 'equal dotfile names should be equal'); @@ -52,7 +53,7 @@ suite('Comparers', () => { assert(compareFileNames('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); // - // Comparisons with different results than compareFileNamesNumeric + // Comparisons with different results than compareFileNamesDefault // // name-only comparisons @@ -61,9 +62,6 @@ suite('Comparers', () => { assert.notDeepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileNames), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases do not sort in locale order'); assert.notDeepEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileNames), ['email', 'Email', 'émail', 'Émail'].sort(compareLocale), 'the same base characters with different case or accents do not sort in locale order'); - // name plus extension comparisons - assert(compareFileNames('aggregate.go', 'aggregate_repo.go') > 0, 'compares the whole name all at once by locale'); - // numeric comparisons assert(compareFileNames('abc02.txt', 'abc002.txt') > 0, 'filenames with equivalent numbers and leading zeros sort in unicode order'); assert(compareFileNames('abc.txt1', 'abc.txt01') > 0, 'same name plus extensions with equal numbers sort in unicode order'); @@ -75,7 +73,7 @@ suite('Comparers', () => { test('compareFileExtensions', () => { // - // Comparisons with the same results as compareFileExtensionsNumeric + // Comparisons with the same results as compareFileExtensionsDefault // // name-only comparisons @@ -118,12 +116,8 @@ suite('Comparers', () => { assert(compareFileExtensions('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, filenames should be compared'); assert(compareFileExtensions('a10.txt', 'A2.txt') > 0, 'filenames with number and case differences compare numerically'); - // Same extension comparison that has the same result as compareFileExtensionsNumeric, but a different result than compareFileNames - // This is an edge case caused by compareFileNames comparing the whole name all at once instead of the name and then the extension. - assert(compareFileExtensions('aggregate.go', 'aggregate_repo.go') < 0, 'when extensions are equal, names sort in dictionary order'); - // - // Comparisons with different results from compareFileExtensionsNumeric + // Comparisons with different results from compareFileExtensionsDefault // // name-only comparisions @@ -135,6 +129,7 @@ suite('Comparers', () => { // name plus extension comparisons assert(compareFileExtensions('a.MD', 'a.md') !== compareLocale('MD', 'md'), 'case differences in extensions do not sort by locale'); assert(compareFileExtensions('a.md', 'A.md') !== compareLocale('a', 'A'), 'case differences in names do not sort by locale'); + assert(compareFileExtensions('aggregate.go', 'aggregate_repo.go') < 0, 'when extensions are equal, names sort in dictionary order'); // dotfile comparisons assert(compareFileExtensions('.env', '.aaa.env') < 0, 'a dotfile with an extension is treated as a name plus an extension - equal extensions'); @@ -152,145 +147,139 @@ suite('Comparers', () => { }); - test('compareFileNamesNumeric', () => { + test('compareFileNamesDefault', () => { // // Comparisons with the same results as compareFileNames // // name-only comparisons - assert(compareFileNamesNumeric(null, null) === 0, 'null should be equal'); - assert(compareFileNamesNumeric(null, 'abc') < 0, 'null should be come before real values'); - assert(compareFileNamesNumeric('', '') === 0, 'empty should be equal'); - assert(compareFileNamesNumeric('abc', 'abc') === 0, 'equal names should be equal'); - assert(compareFileNamesNumeric('z', 'A') > 0, 'z comes is after A regardless of case'); - assert(compareFileNamesNumeric('Z', 'a') > 0, 'Z comes after a regardless of case'); + assert(compareFileNamesDefault(null, null) === 0, 'null should be equal'); + assert(compareFileNamesDefault(null, 'abc') < 0, 'null should be come before real values'); + assert(compareFileNamesDefault('', '') === 0, 'empty should be equal'); + assert(compareFileNamesDefault('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileNamesDefault('z', 'A') > 0, 'z comes is after A regardless of case'); + assert(compareFileNamesDefault('Z', 'a') > 0, 'Z comes after a regardless of case'); // name plus extension comparisons - assert(compareFileNamesNumeric('file.ext', 'file.ext') === 0, 'equal full names should be equal'); - assert(compareFileNamesNumeric('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); - assert(compareFileNamesNumeric('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); - assert(compareFileNamesNumeric('bbb.aaa', 'aaa.bbb') > 0, 'files should be compared by names even if extensions compare differently'); + assert(compareFileNamesDefault('file.ext', 'file.ext') === 0, 'equal full names should be equal'); + assert(compareFileNamesDefault('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); + assert(compareFileNamesDefault('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); + assert(compareFileNamesDefault('bbb.aaa', 'aaa.bbb') > 0, 'files should be compared by names even if extensions compare differently'); + assert(compareFileNamesDefault('aggregate.go', 'aggregate_repo.go') > 0, 'compares the whole filename in locale order'); // dotfile comparisons - assert(compareFileNamesNumeric('.abc', '.abc') === 0, 'equal dotfile names should be equal'); - assert(compareFileNamesNumeric('.env.', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly'); - assert(compareFileNamesNumeric('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); - assert(compareFileNamesNumeric('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); - assert(compareFileNamesNumeric('.aaa_env', '.aaa.env') < 0, 'and underscore in a dotfile name will sort before a dot'); + assert(compareFileNamesDefault('.abc', '.abc') === 0, 'equal dotfile names should be equal'); + assert(compareFileNamesDefault('.env.', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly'); + assert(compareFileNamesDefault('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileNamesDefault('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + assert(compareFileNamesDefault('.aaa_env', '.aaa.env') < 0, 'and underscore in a dotfile name will sort before a dot'); // dotfile vs non-dotfile comparisons - assert(compareFileNamesNumeric(null, '.abc') < 0, 'null should come before dotfiles'); - assert(compareFileNamesNumeric('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); - assert(compareFileNamesNumeric('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); - assert(compareFileNamesNumeric('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files'); - assert(compareFileNamesNumeric('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files'); + assert(compareFileNamesDefault(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileNamesDefault('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileNamesDefault('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileNamesDefault('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files'); + assert(compareFileNamesDefault('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files'); // numeric comparisons - assert(compareFileNamesNumeric('1', '1') === 0, 'numerically equal full names should be equal'); - assert(compareFileNamesNumeric('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); - assert(compareFileNamesNumeric('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); - assert(compareFileNamesNumeric('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order even when they are multiple digits long'); - assert(compareFileNamesNumeric('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); - assert(compareFileNamesNumeric('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); + assert(compareFileNamesDefault('1', '1') === 0, 'numerically equal full names should be equal'); + assert(compareFileNamesDefault('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); + assert(compareFileNamesDefault('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); + assert(compareFileNamesDefault('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order even when they are multiple digits long'); + assert(compareFileNamesDefault('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); + assert(compareFileNamesDefault('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); // // Comparisons with different results than compareFileNames // // name-only comparisons - assert(compareFileNamesNumeric('a', 'A') === compareLocale('a', 'A'), 'the same letter sorts by locale'); - assert(compareFileNamesNumeric('â', 'Â') === compareLocale('â', 'Â'), 'the same accented letter sorts by locale'); - assert.deepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileNamesNumeric), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases sort in locale order'); - assert.deepEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileNamesNumeric), ['email', 'Email', 'émail', 'Émail'].sort(compareLocale), 'the same base characters with different case or accents sort in locale order'); - - // name plus extensions comparisons - assert(compareFileNamesNumeric('aggregate.go', 'aggregate_repo.go') < 0, 'compares the name first, then the extension'); + assert(compareFileNamesDefault('a', 'A') === compareLocale('a', 'A'), 'the same letter sorts by locale'); + assert(compareFileNamesDefault('â', 'Â') === compareLocale('â', 'Â'), 'the same accented letter sorts by locale'); + assert.deepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileNamesDefault), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases sort in locale order'); + assert.deepEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileNamesDefault), ['email', 'Email', 'émail', 'Émail'].sort(compareLocale), 'the same base characters with different case or accents sort in locale order'); // numeric comparisons - assert(compareFileNamesNumeric('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest number first'); - assert(compareFileNamesNumeric('abc.txt1', 'abc.txt01') < 0, 'same name plus extensions with equal numbers sort shortest number first'); - assert(compareFileNamesNumeric('art01', 'Art01') === compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case compares numerically based on locale'); + assert(compareFileNamesDefault('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest number first'); + assert(compareFileNamesDefault('abc.txt1', 'abc.txt01') < 0, 'same name plus extensions with equal numbers sort shortest number first'); + assert(compareFileNamesDefault('art01', 'Art01') === compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case compares numerically based on locale'); }); - test('compareFileExtensionsNumeric', () => { + test('compareFileExtensionsDefault', () => { // // Comparisons with the same result as compareFileExtensions // // name-only comparisons - assert(compareFileExtensionsNumeric(null, null) === 0, 'null should be equal'); - assert(compareFileExtensionsNumeric(null, 'abc') < 0, 'null should come before real files without extensions'); - assert(compareFileExtensionsNumeric('', '') === 0, 'empty should be equal'); - assert(compareFileExtensionsNumeric('abc', 'abc') === 0, 'equal names should be equal'); - assert(compareFileExtensionsNumeric('z', 'A') > 0, 'z comes after A'); - assert(compareFileExtensionsNumeric('Z', 'a') > 0, 'Z comes after a'); + assert(compareFileExtensionsDefault(null, null) === 0, 'null should be equal'); + assert(compareFileExtensionsDefault(null, 'abc') < 0, 'null should come before real files without extensions'); + assert(compareFileExtensionsDefault('', '') === 0, 'empty should be equal'); + assert(compareFileExtensionsDefault('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileExtensionsDefault('z', 'A') > 0, 'z comes after A'); + assert(compareFileExtensionsDefault('Z', 'a') > 0, 'Z comes after a'); // name plus extension comparisons - assert(compareFileExtensionsNumeric('file.ext', 'file.ext') === 0, 'equal full filenames should be equal'); - assert(compareFileExtensionsNumeric('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); - assert(compareFileExtensionsNumeric('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); - assert(compareFileExtensionsNumeric('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extension first'); - assert(compareFileExtensionsNumeric('agg.go', 'aggrepo.go') < 0, 'shorter names sort before longer names'); - assert(compareFileExtensionsNumeric('agg.go', 'agg_repo.go') < 0, 'shorter names short before longer names even when the longer name contains an underscore'); - assert(compareFileExtensionsNumeric('a.MD', 'b.md') < 0, 'when extensions are the same except for case, the files sort by name'); + assert(compareFileExtensionsDefault('file.ext', 'file.ext') === 0, 'equal full filenames should be equal'); + assert(compareFileExtensionsDefault('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); + assert(compareFileExtensionsDefault('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); + assert(compareFileExtensionsDefault('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extension first'); + assert(compareFileExtensionsDefault('agg.go', 'aggrepo.go') < 0, 'shorter names sort before longer names'); + assert(compareFileExtensionsDefault('a.MD', 'b.md') < 0, 'when extensions are the same except for case, the files sort by name'); // dotfile comparisons - assert(compareFileExtensionsNumeric('.abc', '.abc') === 0, 'equal dotfiles should be equal'); - assert(compareFileExtensionsNumeric('.md', '.Gitattributes') > 0, 'dotfiles sort alphabetically regardless of case'); + assert(compareFileExtensionsDefault('.abc', '.abc') === 0, 'equal dotfiles should be equal'); + assert(compareFileExtensionsDefault('.md', '.Gitattributes') > 0, 'dotfiles sort alphabetically regardless of case'); // dotfile vs non-dotfile comparisons - assert(compareFileExtensionsNumeric(null, '.abc') < 0, 'null should come before dotfiles'); - assert(compareFileExtensionsNumeric('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); - assert(compareFileExtensionsNumeric('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files'); + assert(compareFileExtensionsDefault(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileExtensionsDefault('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileExtensionsDefault('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files'); // numeric comparisons - assert(compareFileExtensionsNumeric('1', '1') === 0, 'numerically equal full names should be equal'); - assert(compareFileExtensionsNumeric('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); - assert(compareFileExtensionsNumeric('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); - assert(compareFileExtensionsNumeric('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order'); - assert(compareFileExtensionsNumeric('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); - assert(compareFileExtensionsNumeric('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); - assert(compareFileExtensionsNumeric('abc2.txt2', 'abc1.txt10') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); - assert(compareFileExtensionsNumeric('txt.abc1', 'txt.abc1') === 0, 'equal extensions with numbers should be equal'); - assert(compareFileExtensionsNumeric('txt.abc1', 'txt.abc2') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); - assert(compareFileExtensionsNumeric('txt.abc2', 'txt.abc10') < 0, 'extensions with numbers should be in numerical order even when they are multiple digits long'); - assert(compareFileExtensionsNumeric('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, filenames should be compared'); - assert(compareFileExtensionsNumeric('a10.txt', 'A2.txt') > 0, 'filenames with number and case differences compare numerically'); - - // Same extension comparison that has the same result as compareFileExtensions, but a different result than compareFileNames - // This is an edge case caused by compareFileNames comparing the whole name all at once instead of the name and then the extension. - assert(compareFileExtensionsNumeric('aggregate.go', 'aggregate_repo.go') < 0, 'when extensions are equal, names sort in dictionary order'); + assert(compareFileExtensionsDefault('1', '1') === 0, 'numerically equal full names should be equal'); + assert(compareFileExtensionsDefault('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); + assert(compareFileExtensionsDefault('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsDefault('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order'); + assert(compareFileExtensionsDefault('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); + assert(compareFileExtensionsDefault('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); + assert(compareFileExtensionsDefault('abc2.txt2', 'abc1.txt10') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsDefault('txt.abc1', 'txt.abc1') === 0, 'equal extensions with numbers should be equal'); + assert(compareFileExtensionsDefault('txt.abc1', 'txt.abc2') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsDefault('txt.abc2', 'txt.abc10') < 0, 'extensions with numbers should be in numerical order even when they are multiple digits long'); + assert(compareFileExtensionsDefault('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, filenames should be compared'); + assert(compareFileExtensionsDefault('a10.txt', 'A2.txt') > 0, 'filenames with number and case differences compare numerically'); // // Comparisons with different results than compareFileExtensions // // name-only comparisons - assert(compareFileExtensionsNumeric('a', 'A') === compareLocale('a', 'A'), 'the same letter of different case sorts by locale'); - assert(compareFileExtensionsNumeric('â', 'Â') === compareLocale('â', 'Â'), 'the same accented letter of different case sorts by locale'); - assert.deepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileExtensionsNumeric), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases sort in locale order'); - assert.deepEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileExtensionsNumeric), ['email', 'Email', 'émail', 'Émail'].sort((a, b) => a.localeCompare(b)), 'the same base characters with different case or accents sort in locale order'); + assert(compareFileExtensionsDefault('a', 'A') === compareLocale('a', 'A'), 'the same letter of different case sorts by locale'); + assert(compareFileExtensionsDefault('â', 'Â') === compareLocale('â', 'Â'), 'the same accented letter of different case sorts by locale'); + assert.deepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileExtensionsDefault), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases sort in locale order'); + assert.deepEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileExtensionsDefault), ['email', 'Email', 'émail', 'Émail'].sort((a, b) => a.localeCompare(b)), 'the same base characters with different case or accents sort in locale order'); // name plus extension comparisons - assert(compareFileExtensionsNumeric('a.MD', 'a.md') === compareLocale('MD', 'md'), 'case differences in extensions sort by locale'); - assert(compareFileExtensionsNumeric('a.md', 'A.md') === compareLocale('a', 'A'), 'case differences in names sort by locale'); + assert(compareFileExtensionsDefault('a.MD', 'a.md') === compareLocale('MD', 'md'), 'case differences in extensions sort by locale'); + assert(compareFileExtensionsDefault('a.md', 'A.md') === compareLocale('a', 'A'), 'case differences in names sort by locale'); + assert(compareFileExtensionsDefault('aggregate.go', 'aggregate_repo.go') > 0, 'names with the same extension sort in full filename locale order'); // dotfile comparisons - assert(compareFileExtensionsNumeric('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); - assert(compareFileExtensionsNumeric('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + assert(compareFileExtensionsDefault('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileExtensionsDefault('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); // dotfile vs non-dotfile comparisons - assert(compareFileExtensionsNumeric('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); - assert(compareFileExtensionsNumeric('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files'); + assert(compareFileExtensionsDefault('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileExtensionsDefault('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files'); // numeric comparisons - assert(compareFileExtensionsNumeric('abc.txt01', 'abc.txt1') > 0, 'extensions with equal numbers should be in shortest-first order'); - assert(compareFileExtensionsNumeric('art01', 'Art01') === compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case compares numerically based on locale'); - assert(compareFileExtensionsNumeric('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest string first'); - assert(compareFileExtensionsNumeric('txt.abc01', 'txt.abc1') > 0, 'extensions with equivalent numbers sort shortest extension first'); + assert(compareFileExtensionsDefault('abc.txt01', 'abc.txt1') > 0, 'extensions with equal numbers should be in shortest-first order'); + assert(compareFileExtensionsDefault('art01', 'Art01') === compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case compares numerically based on locale'); + assert(compareFileExtensionsDefault('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest string first'); + assert(compareFileExtensionsDefault('txt.abc01', 'txt.abc1') > 0, 'extensions with equivalent numbers sort shortest extension first'); }); }); diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 08a100254f8..2d940ea198a 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -29,7 +29,7 @@ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { equals, deepClone } from 'vs/base/common/objects'; import * as path from 'vs/base/common/path'; import { ExplorerItem, NewExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; -import { compareFileExtensionsNumeric, compareFileNamesNumeric } from 'vs/base/common/comparers'; +import { compareFileNamesDefault, compareFileExtensionsDefault } from 'vs/base/common/comparers'; import { fillResourceDataTransfers, CodeDataTransfers, extractResources, containsDragType } from 'vs/workbench/browser/dnd'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IDragAndDropData, DataTransfers } from 'vs/base/browser/dnd'; @@ -665,7 +665,7 @@ export class FileSorter implements ITreeSorter { } if (statA.isDirectory && statB.isDirectory) { - return compareFileNamesNumeric(statA.name, statB.name); + return compareFileNamesDefault(statA.name, statB.name); } break; @@ -699,17 +699,17 @@ export class FileSorter implements ITreeSorter { // Sort Files switch (sortOrder) { case 'type': - return compareFileExtensionsNumeric(statA.name, statB.name); + return compareFileExtensionsDefault(statA.name, statB.name); case 'modified': if (statA.mtime !== statB.mtime) { return (statA.mtime && statB.mtime && statA.mtime < statB.mtime) ? 1 : -1; } - return compareFileNamesNumeric(statA.name, statB.name); + return compareFileNamesDefault(statA.name, statB.name); default: /* 'default', 'mixed', 'filesFirst' */ - return compareFileNamesNumeric(statA.name, statB.name); + return compareFileNamesDefault(statA.name, statB.name); } } }