mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +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). * fix(file-explorer): natural sort on SSH funnels, relay, and pickers Adversarial-review round 1 rework: - Both readDir funnels short-circuited to the SSH filesystem provider before the patched sort, so SSH workspaces kept lexicographic order; re-sort locally after the provider returns (the remote relay may be an older build), and fix the relay's own comparator for relay-native consumers. - sortDirEntries (shared, unit-tested) owns the directories-first + natural-order listing contract used by every funnel. - compareFileNames breaks numeric-collation ties ('2' vs '02') by code units so sibling order stays total instead of readdir order, and pins the collator locale to 'en' so every host produces one order. - The SSH folder browser and runtime server dir picker now match the Explorer they browse into. - Ordering pinned by tests at the relay, source-control tree, and shared helper. * fix(mobile): natural sort in the mobile file explorer Mobile re-sorted host readDir results with bare localeCompare, undoing the host funnel's natural order (round-2 review). Reuse the shared comparator and pin the order in the mobile suite. * fix(file-explorer): natural sort at the renderer choke point and remaining ties Round-3 review: the remote-runtime RPC and paired-web routes return the host's order verbatim, so re-sort in readFileExplorerDirectory where every desktop route converges; pin the SSH funnel with a handler-level test; and route Source Control path compares through compareFileNames so numeric-collation ties share one total order with the Explorer. * docs(file-name-sort): state the real perf baseline in the hoist comment * refactor(source-control): drop the dead collator export; pin the test oracle locale * fix(file-listings): cover remaining natural-sort surfaces
64 lines
2.2 KiB
TypeScript
64 lines
2.2 KiB
TypeScript
import { lstat, opendir } from 'node:fs/promises'
|
|
import { compareFileNames } from './file-name-sort'
|
|
import { isFileListingCancellation, throwIfFileListingCancelled } from './file-listing-cancellation'
|
|
import { isQuickOpenReadableDirectory } from './quick-open-directory-validation'
|
|
import {
|
|
assertQuickOpenReaddirDeadline,
|
|
consumeQuickOpenReaddirEntryBudget,
|
|
consumeQuickOpenReaddirPathBudget,
|
|
isQuickOpenReaddirBudgetError,
|
|
type QuickOpenReaddirBudget
|
|
} from './quick-open-readdir-budget'
|
|
|
|
export type QuickOpenDirectoryEntry = {
|
|
name: string
|
|
kind: 'directory' | 'file' | 'symlink' | 'other'
|
|
}
|
|
|
|
export async function readQuickOpenDirectoryEntries(opts: {
|
|
absPath: string
|
|
allowSymlinkedRoot: boolean
|
|
budget: QuickOpenReaddirBudget
|
|
signal?: AbortSignal
|
|
}): Promise<QuickOpenDirectoryEntry[]> {
|
|
try {
|
|
const stat = await lstat(opts.absPath)
|
|
if (!isQuickOpenReadableDirectory(stat, opts.allowSymlinkedRoot)) {
|
|
return []
|
|
}
|
|
|
|
const entries: QuickOpenDirectoryEntry[] = []
|
|
const directory = await opendir(opts.absPath)
|
|
throwIfFileListingCancelled(opts.signal)
|
|
assertQuickOpenReaddirDeadline(opts.budget)
|
|
for await (const entry of directory) {
|
|
throwIfFileListingCancelled(opts.signal)
|
|
assertQuickOpenReaddirDeadline(opts.budget)
|
|
consumeQuickOpenReaddirEntryBudget(opts.budget)
|
|
consumeQuickOpenReaddirPathBudget(opts.budget, entry.name)
|
|
entries.push({
|
|
name: entry.name,
|
|
kind: entry.isDirectory()
|
|
? 'directory'
|
|
: entry.isFile()
|
|
? 'file'
|
|
: entry.isSymbolicLink()
|
|
? 'symlink'
|
|
: 'other'
|
|
})
|
|
}
|
|
entries.sort((left, right) => compareFileNames(left.name, right.name))
|
|
|
|
// Why: discard buffered names if the path became a symlink while its
|
|
// directory handle was open; descendants must never escape the root.
|
|
const statAfterRead = await lstat(opts.absPath)
|
|
return isQuickOpenReadableDirectory(statAfterRead, opts.allowSymlinkedRoot) ? entries : []
|
|
} catch (error) {
|
|
if (isQuickOpenReaddirBudgetError(error) || isFileListingCancellation(error)) {
|
|
throw error
|
|
}
|
|
// Permission denied or a vanished subtree must not hide readable siblings.
|
|
return []
|
|
}
|
|
}
|