mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 16:03:26 +00:00
fix(file-explorer): sort numbered file names naturally
The File Explorer compared names with bare localeCompare, so numbered files listed 100, 200 before 99. Hoist the numeric collator Source Control file rows already use (#10850) into src/shared and apply it to the local and runtime directory listings, the name-filtered view, and Source Control directory nodes, which were inconsistent with the file rows one line below (#11426).
This commit is contained in:
@@ -30,6 +30,7 @@ import type {
|
||||
} from '../../shared/types'
|
||||
import type { GitHistoryOptions, GitHistoryResult } from '../../shared/git-history'
|
||||
import type { SshMutationExpectation } from '../../shared/ssh-types'
|
||||
import { compareFileNames } from '../../shared/file-name-sort'
|
||||
import { assertSshMutationExpectation } from '../ssh/ssh-connection-generation'
|
||||
import {
|
||||
buildRgArgs,
|
||||
@@ -531,7 +532,7 @@ export function registerFilesystemHandlers(
|
||||
if (a.isDirectory !== b.isDirectory) {
|
||||
return a.isDirectory ? -1 : 1
|
||||
}
|
||||
return a.name.localeCompare(b.name)
|
||||
return compareFileNames(a.name, b.name)
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
recordCrashBreadcrumb(
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
resolveRuntimePath
|
||||
} from '../../shared/cross-platform-path'
|
||||
import { PhysicalExitTracker } from '../../shared/physical-exit-tracker'
|
||||
import { compareFileNames } from '../../shared/file-name-sort'
|
||||
import type {
|
||||
RuntimeFileListResult,
|
||||
RuntimeFileOpenResult,
|
||||
@@ -1277,7 +1278,7 @@ export class RuntimeFileCommands {
|
||||
if (a.isDirectory !== b.isDirectory) {
|
||||
return a.isDirectory ? -1 : 1
|
||||
}
|
||||
return a.name.localeCompare(b.name)
|
||||
return compareFileNames(a.name, b.name)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { joinPath, normalizeRelativePath } from '@/lib/path'
|
||||
import { isClipboardTextByteLengthOverLimit } from '../../../../shared/clipboard-text'
|
||||
import { compareFileNames } from '../../../../shared/file-name-sort'
|
||||
import type { FileExplorerOperationOwner, TreeNode } from './file-explorer-types'
|
||||
import {
|
||||
createFileExplorerRowProjectionFromParts,
|
||||
@@ -232,7 +233,7 @@ function appendNameFilteredEntries(
|
||||
if (a.node.isDirectory !== b.node.isDirectory) {
|
||||
return a.node.isDirectory ? -1 : 1
|
||||
}
|
||||
return a.node.name.localeCompare(b.node.name)
|
||||
return compareFileNames(a.node.name, b.node.name)
|
||||
})
|
||||
for (const entry of sortedEntries) {
|
||||
visibleFlatRows.push(entry.node)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { GitStatusEntry } from '../../../../shared/types'
|
||||
import { fileNameCollator } from '../../../../shared/file-name-sort'
|
||||
|
||||
// Why hoisted: localeCompare with an options object resolves a fresh ICU collator
|
||||
// on every comparison, so a changed-file sort paid for one per O(n log n) step.
|
||||
export const sourceControlPathCollator = new Intl.Collator(undefined, { numeric: true })
|
||||
export const sourceControlPathCollator = fileNameCollator
|
||||
|
||||
export function compareGitStatusEntries(a: GitStatusEntry, b: GitStatusEntry): number {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { normalizeRelativePath } from '@/lib/path'
|
||||
import type { GitStatusEntry, GitStagingArea } from '../../../../shared/types'
|
||||
import { compareFileNames } from '../../../../shared/file-name-sort'
|
||||
import { splitPathSegments } from './path-tree'
|
||||
import { compareGitStatusEntries, sourceControlPathCollator } from './source-control-status-sort'
|
||||
|
||||
@@ -90,7 +91,9 @@ function finalizeDirectoryNode<Entry extends SourceControlTreeEntry, Area extend
|
||||
}
|
||||
}
|
||||
|
||||
directories.sort((a, b) => a.name.localeCompare(b.name))
|
||||
// Why: keep directory ordering consistent with the numeric path collator used
|
||||
// for the file rows one line below.
|
||||
directories.sort((a, b) => compareFileNames(a.name, b.name))
|
||||
files.sort((a, b) => compareEntries(a.entry, b.entry))
|
||||
const fileCount =
|
||||
files.length + directories.reduce((count, directory) => count + directory.fileCount, 0)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { compareFileNames } from './file-name-sort'
|
||||
|
||||
describe('compareFileNames', () => {
|
||||
it('sorts numeric name segments naturally instead of lexicographically', () => {
|
||||
const names = [
|
||||
'1 - item.txt',
|
||||
'100 - item.txt',
|
||||
'2 - item.txt',
|
||||
'200 - item.txt',
|
||||
'409 - item.txt',
|
||||
'41 - item.txt',
|
||||
'410 - item.txt',
|
||||
'9 - item.txt',
|
||||
'99 - item.txt'
|
||||
]
|
||||
expect([...names].sort(compareFileNames)).toEqual([
|
||||
'1 - item.txt',
|
||||
'2 - item.txt',
|
||||
'9 - item.txt',
|
||||
'41 - item.txt',
|
||||
'99 - item.txt',
|
||||
'100 - item.txt',
|
||||
'200 - item.txt',
|
||||
'409 - item.txt',
|
||||
'410 - item.txt'
|
||||
])
|
||||
})
|
||||
|
||||
it('sorts embedded numbers naturally within alphabetical order', () => {
|
||||
expect(['b1.md', 'a10.md', 'a2.md', 'a1.md'].sort(compareFileNames)).toEqual([
|
||||
'a1.md',
|
||||
'a2.md',
|
||||
'a10.md',
|
||||
'b1.md'
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
// Why hoisted: localeCompare with an options object resolves a fresh ICU collator
|
||||
// on every comparison, so a directory sort paid for one per O(n log n) step.
|
||||
// numeric: true orders "99 - a" before "100 - b", matching Finder/Explorer.
|
||||
export const fileNameCollator = new Intl.Collator(undefined, { numeric: true })
|
||||
|
||||
export function compareFileNames(a: string, b: string): number {
|
||||
return fileNameCollator.compare(a, b)
|
||||
}
|
||||
Reference in New Issue
Block a user