Every board root keys once at its store — the hover path stops walking the filesystem
`DragLocality.isSameBoard` resolved symlinks on both URLs, which stats every path component. It ran five-plus times per `dropUpdated` and again per lane per body evaluation through `renderedCards` → `hiddenMembers` — order of 50–100 stat calls per mouse-move, and worse on iCloud-backed paths. A pickup stall sample had it on ~47 of 943 stacks. `BoardRootKey` mints that canonical spelling once, where the root is, and every locality comparison downstream is an `==` on two strings. The drag carriers hold keys rather than URLs, so re-deriving under the cursor is no longer expressible. `rootURL` keeps the user's spelling — the folder name is the display-name fallback, and canonicalizing there would visibly rename a board opened through a pin. The key follows the folder through `relocate(to:)`: a key frozen at open would collide with a new board opened at the vacated path, and two boards comparing as one is a cross-board drag silently behaving as a move. Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
@@ -31,7 +31,7 @@ struct DropTarget: Equatable, Sendable {
|
||||
case trash
|
||||
}
|
||||
|
||||
var boardRoot: URL
|
||||
var boardRoot: BoardRootKey
|
||||
var container: Container
|
||||
var index: Int
|
||||
|
||||
@@ -133,7 +133,7 @@ struct FileDropTarget: Equatable, Sendable {
|
||||
|
||||
/// The board under the cursor — only that board's delegates may commit, and only its lanes draw
|
||||
/// the shadows, exactly as `DropTarget.boardRoot` works for our own sessions.
|
||||
var boardRoot: URL
|
||||
var boardRoot: BoardRootKey
|
||||
|
||||
var landing: Landing
|
||||
|
||||
@@ -175,7 +175,7 @@ struct FileDropTarget: Equatable, Sendable {
|
||||
struct CommittedHold: Equatable, Sendable {
|
||||
|
||||
/// The board whose next applied snapshot retires this hold.
|
||||
var boardRoot: URL
|
||||
var boardRoot: BoardRootKey
|
||||
|
||||
/// That board's `snapshotGeneration` at the moment of the commit.
|
||||
var generation: Int
|
||||
@@ -188,8 +188,8 @@ struct CommittedHold: Equatable, Sendable {
|
||||
/// Whether a snapshot applied on `root` at `generation` retires this hold. A snapshot on another
|
||||
/// board says nothing about this one, and the *same* generation is the one already on screen at
|
||||
/// the commit.
|
||||
func isRetired(byRoot root: URL, generation: Int) -> Bool {
|
||||
DragLocality.isSameBoard(root, boardRoot) && generation > self.generation
|
||||
func isRetired(byRoot root: BoardRootKey, generation: Int) -> Bool {
|
||||
root == boardRoot && generation > self.generation
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,16 +204,12 @@ struct CommittedHold: Equatable, Sendable {
|
||||
/// is already the default. The operation is therefore a function of (source board, board under the
|
||||
/// cursor, modifiers) sampled every frame, not a decision taken at pickup, which is what lets the
|
||||
/// badge track live as the cursor crosses a boundary.
|
||||
///
|
||||
/// Which board a root names is `BoardRootKey`'s question, and it is asked as an `==`: the two sides
|
||||
/// of every comparison below are keys their own stores minted once (`BoardStore.rootKey`), never
|
||||
/// paths re-resolved under the cursor.
|
||||
enum DragLocality {
|
||||
|
||||
/// Whether two roots name the same board. Symlinks are resolved first — a board reached through
|
||||
/// a pinned symlink is the same board as the one reached directly (01-storage-format.md's
|
||||
/// symlink pins) — and the standardized path is the comparison key.
|
||||
static func isSameBoard(_ lhs: URL, _ rhs: URL) -> Bool {
|
||||
lhs.resolvingSymlinksInPath().standardizedFileURL.path
|
||||
== rhs.resolvingSymlinksInPath().standardizedFileURL.path
|
||||
}
|
||||
|
||||
/// The effective operation, live.
|
||||
///
|
||||
/// Two carve-outs, both 04-interactions.md's:
|
||||
@@ -314,7 +310,7 @@ final class DragSession {
|
||||
/// The board the drag started in. Root and store are kept separately because the store may go
|
||||
/// away with its window mid-drag while the root — the left-hand side of the locality
|
||||
/// comparison — stays perfectly usable.
|
||||
private(set) var sourceRoot: URL?
|
||||
private(set) var sourceRoot: BoardRootKey?
|
||||
|
||||
@ObservationIgnored private(set) weak var sourceStore: BoardStore?
|
||||
|
||||
@@ -408,27 +404,26 @@ final class DragSession {
|
||||
/// **The hold freezes the answer**, because `resolveOperation` refuses to move once a release
|
||||
/// has settled: a settled copy keeps its originals on screen and a settled move keeps them
|
||||
/// lifted, in both cases until the echo snapshot lands and the real faces take over.
|
||||
func hiddenMembers(onBoardRooted root: URL) -> Set<ItemID> {
|
||||
guard isActive, container == .board, let sourceRoot,
|
||||
DragLocality.isSameBoard(root, sourceRoot)
|
||||
func hiddenMembers(onBoardRooted root: BoardRootKey) -> Set<ItemID> {
|
||||
guard isActive, container == .board, let sourceRoot, root == sourceRoot
|
||||
else { return [] }
|
||||
return operation == .copy ? [] : memberSet
|
||||
}
|
||||
|
||||
/// The proposal's index when it names this board's strip, else `nil` — the lane strip's shadow
|
||||
/// run position.
|
||||
func stripProposal(onBoardRooted root: URL) -> Int? {
|
||||
func stripProposal(onBoardRooted root: BoardRootKey) -> Int? {
|
||||
guard kind == .lanes, let proposal, proposal.container == .strip,
|
||||
DragLocality.isSameBoard(proposal.boardRoot, root)
|
||||
proposal.boardRoot == root
|
||||
else { return nil }
|
||||
return proposal.index
|
||||
}
|
||||
|
||||
/// The proposal's index when it names `laneID` on this board, else `nil` — the masonry's shadow
|
||||
/// run position, in the lane's logical card order.
|
||||
func laneProposal(onBoardRooted root: URL, laneID: ItemID) -> Int? {
|
||||
func laneProposal(onBoardRooted root: BoardRootKey, laneID: ItemID) -> Int? {
|
||||
guard kind == .cards, let proposal, proposal.container == .lane(laneID),
|
||||
DragLocality.isSameBoard(proposal.boardRoot, root)
|
||||
proposal.boardRoot == root
|
||||
else { return nil }
|
||||
return proposal.index
|
||||
}
|
||||
@@ -443,9 +438,9 @@ final class DragSession {
|
||||
/// **Either kind proposes here** (lanes extended 2026-07-29), so there is no kind clause: what
|
||||
/// makes the proposal legal is `TrashDrop.accepts`, asked at hover and again at release, and a
|
||||
/// second copy of its answer written here could only ever disagree with it.
|
||||
func trashProposal(onBoardRooted root: URL) -> Int? {
|
||||
func trashProposal(onBoardRooted root: BoardRootKey) -> Int? {
|
||||
guard isActive, let proposal, proposal.container == .trash,
|
||||
DragLocality.isSameBoard(proposal.boardRoot, root)
|
||||
proposal.boardRoot == root
|
||||
else { return nil }
|
||||
return proposal.index
|
||||
}
|
||||
@@ -468,9 +463,9 @@ final class DragSession {
|
||||
|
||||
/// The card an external file drag is hovering **on this board**, or `nil` — the attach
|
||||
/// highlight's one input (`CardFaceView`).
|
||||
func fileAttachTarget(onBoardRooted root: URL) -> ItemID? {
|
||||
func fileAttachTarget(onBoardRooted root: BoardRootKey) -> ItemID? {
|
||||
guard let fileTarget,
|
||||
DragLocality.isSameBoard(fileTarget.boardRoot, root),
|
||||
fileTarget.boardRoot == root,
|
||||
case let .attach(cardID) = fileTarget.landing
|
||||
else { return nil }
|
||||
return cardID
|
||||
@@ -487,9 +482,9 @@ final class DragSession {
|
||||
/// The floor restates `FinderDrop.shadowCount`'s, at the render end rather than in place of it: a
|
||||
/// proposal that somehow carried a zero would otherwise open a run of no shadows at all, which is
|
||||
/// a landing spot the user cannot see.
|
||||
func fileLaneProposal(onBoardRooted root: URL, laneID: ItemID) -> (index: Int, count: Int)? {
|
||||
func fileLaneProposal(onBoardRooted root: BoardRootKey, laneID: ItemID) -> (index: Int, count: Int)? {
|
||||
guard let fileTarget,
|
||||
DragLocality.isSameBoard(fileTarget.boardRoot, root),
|
||||
fileTarget.boardRoot == root,
|
||||
case let .create(lane, index) = fileTarget.landing,
|
||||
lane == laneID
|
||||
else { return nil }
|
||||
@@ -576,7 +571,7 @@ final class DragSession {
|
||||
self.container = container
|
||||
self.mixesKinds = mixesKinds
|
||||
self.sourceStore = source
|
||||
self.sourceRoot = source.rootURL
|
||||
self.sourceRoot = source.rootKey
|
||||
self.proposal = nil
|
||||
self.operation = .move
|
||||
// The reload-resolved drag set: vanished members leave it silently, which is what
|
||||
@@ -609,14 +604,14 @@ final class DragSession {
|
||||
/// pure function is.
|
||||
@discardableResult
|
||||
func resolveOperation(
|
||||
destinationRoot: URL,
|
||||
destinationRoot: BoardRootKey,
|
||||
modifiers: NSEvent.ModifierFlags = NSEvent.modifierFlags
|
||||
) -> TransferOperation {
|
||||
guard let kind, let sourceRoot, hold == nil else { return operation }
|
||||
let resolved = DragLocality.operation(
|
||||
kind: kind,
|
||||
container: container,
|
||||
isWithinBoard: DragLocality.isSameBoard(sourceRoot, destinationRoot),
|
||||
isWithinBoard: sourceRoot == destinationRoot,
|
||||
modifiers: modifiers
|
||||
)
|
||||
if resolved != operation { operation = resolved }
|
||||
@@ -656,7 +651,7 @@ final class DragSession {
|
||||
sourceStore?.transient.dragMembers = .empty
|
||||
watchdog?.cancel()
|
||||
watchdog = nil
|
||||
let hold = CommittedHold(boardRoot: store.rootURL, generation: store.snapshotGeneration)
|
||||
let hold = CommittedHold(boardRoot: store.rootKey, generation: store.snapshotGeneration)
|
||||
self.hold = hold
|
||||
let timeout = holdTimeout
|
||||
holdTimeoutTask?.cancel()
|
||||
@@ -684,7 +679,7 @@ final class DragSession {
|
||||
///
|
||||
/// Called from every board window's own snapshot-generation watch, which is why the hold names
|
||||
/// the board it belongs to — a reload on some other board says nothing about this one.
|
||||
func handOff(root: URL, generation: Int) {
|
||||
func handOff(root: BoardRootKey, generation: Int) {
|
||||
guard let hold, hold.isRetired(byRoot: root, generation: generation) else { return }
|
||||
end()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user