Skip to content

Commit 39fc8d2

Browse files
committed
Bugfix: File ops now process selection in pane order
- Copy/move/delete used to process selected items in Cmd+click order, not pane sort order, because `SvelteSet<number>` iteration preserves insertion. Cmd+clicking rows 15, 5, 10 sent paths to the backend as `[15, 5, 10]`; the backend faithfully copied them in that order, surprising users who expected top-to-bottom processing. - `selection-state.svelte.ts::getSelectedIndices()` now sorts ascending before returning. Visible-index ascending = pane sort order (the listing cache is sorted at fetch time). The `SvelteSet` itself stays insertion-ordered; only the read-out is sorted. All five FE call sites (copy/move/delete/trash/clipboard) flow through this single helper. - For SMB ≥3 files the volume-copy concurrent path's spawn order now matches pane order; completion races by design (`concurrency=8`). - Added Vitest cases for ascending order after non-monotonic toggles and from non-sorted `setSelectedIndices` input. - Updated `file-explorer/CLAUDE.md` § Selection / Implementation to document the visible-index-ascending guarantee.
1 parent 0fbafeb commit 39fc8d2

3 files changed

Lines changed: 26 additions & 1 deletion

File tree

apps/desktop/src/lib/file-explorer/CLAUDE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ Dual-pane file explorer with keyboard-driven navigation, file selection, sorting
3636
- **State**: `SvelteSet<number>` (from `svelte/reactivity`) in `FilePane.svelte`. O(1) add/remove/has
3737
- **Preserved on sort/filter**: `resort_listing` accepts `selectedIndices[]`, returns `newSelectedIndices[]`
3838
- **Write operations receive indices**: backend resolves to paths from cached listing
39+
- **Visible-index ascending order**: `selection-state.svelte.ts::getSelectedIndices()` sorts ascending before
40+
returning, so write ops process selections top-to-bottom in pane sort order regardless of Cmd+click sequence. The
41+
`SvelteSet` itself remains insertion-ordered; only the read-out for callers is sorted.
3942
- **Visual**: three-tier `--color-selection-fg` cascade (red, Total-Commander-style):
4043
- `--color-selection-fg-primary` (strong red — `#cc0000` light, `#ff4040` dark) applies on the selection bg.
4144
- `--color-selection-fg-cursor` (`#b80808` / `#ff8c8c`) takes over when the row is also under the cursor

apps/desktop/src/lib/file-explorer/pane/selection-state.svelte.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,11 @@ export function createSelectionState(options?: { onChanged?: () => void }) {
180180
}
181181

182182
function getSelectedIndices(): number[] {
183-
return Array.from(selectedIndices)
183+
// Ascending visible-index order = pane sort order (the listing cache is
184+
// sorted at fetch time). Write ops process this array top-to-bottom, so
185+
// the user sees files copied/moved/deleted in the order they appear in
186+
// the pane, regardless of the order they were Cmd+clicked in.
187+
return Array.from(selectedIndices).sort((a, b) => a - b)
184188
}
185189

186190
function setSelectedIndices(indices: number[]) {

apps/desktop/src/lib/file-explorer/pane/selection-state.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,24 @@ describe('createSelectionState', () => {
135135
state.setSelectedIndices([1])
136136
expect(onChanged).toHaveBeenCalled()
137137
})
138+
139+
it('returns indices in ascending order regardless of selection sequence', () => {
140+
// Visible-index ascending is pane-sort order (the listing cache is sorted
141+
// at fetch time). Write ops process this Vec top-to-bottom, so the user
142+
// sees the same files copied/moved/deleted first as the ones at the top
143+
// of the pane, even when they Cmd+clicked them in a non-monotonic order.
144+
const state = createSelectionState()
145+
state.toggleAt(15, false)
146+
state.toggleAt(5, false)
147+
state.toggleAt(10, false)
148+
expect(state.getSelectedIndices()).toEqual([5, 10, 15])
149+
})
150+
151+
it('returns ascending order even when setSelectedIndices is given non-sorted input', () => {
152+
const state = createSelectionState()
153+
state.setSelectedIndices([12, 3, 8, 1])
154+
expect(state.getSelectedIndices()).toEqual([1, 3, 8, 12])
155+
})
138156
})
139157

140158
describe('clearSelection', () => {

0 commit comments

Comments
 (0)