A file copied in Finder becomes a card's attachment — ⌘V's reserved clause finally answers

The image branch's precedence ladder always had a second clause: a file URL on the
pasteboard suppresses it, "a different gesture with a different answer" that the
code deliberately declined rather than guessed at. This fills it in: one or more
file URLs paste through the same `importAttachments` a Finder drop takes — one
collision ladder, one folder refusal (`FinderDrop.partition`), one set of banners
— outranking raw image data riding beside it (a Finder-copied image file carries
both; the actual file lands, not a re-encoded copy of its bytes) while still
deferring to the app's own clipboard type. Both ⌘V surfaces read the same
`ClipboardStore.fileURLPayload`, so the board's fallback and the card window's own
branch stay in step by construction rather than by two hand-kept-in-sync checks.

Fixed a real leak in the body editor's paste yield along the way: `public.file-url`
conforms to `public.url`, which `NSTextView` legitimately reads for a pasted
hyperlink, and `NSPasteboard.availableType(from:)` matches by conformance rather
than exact type — so a Finder copy carrying a generic URL representation beside
its file URL would have been silently swallowed as text and never reached the
window's attachment branch at all. The yield now declines outright on any
file-URL pasteboard before the generic capability check runs.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-09 08:38:07 -04:00
parent d0c546179f
commit 0fe92bdf38
9 changed files with 565 additions and 38 deletions
+30
View File
@@ -32,6 +32,12 @@ final class FakePasteboard: ClipboardPasteboard {
/// test can actually make.
private var foreign: [(type: String, data: Data)] = []
/// **A multi-item read, unlike `foreign`** the file-URL branch's own `fileURLs()`, which reads
/// across every pasteboard item rather than the first item's types (`availableTypes()`'s
/// carve-out). `foreign`'s flat `(type, data)` list cannot represent "the same type on several
/// items", which is exactly what a multi-file Finder copy needs seeded.
private var seededFileURLs: [URL] = []
func manifestData() -> Data? { data }
@discardableResult
@@ -53,12 +59,15 @@ final class FakePasteboard: ClipboardPasteboard {
return foreign.first { $0.type == type }?.data
}
func fileURLs() -> [URL] { seededFileURLs }
/// Another app copied: ownership moves, our type is gone, the counter advanced.
func takeOver() {
changeCount += 1
data = nil
text = nil
foreign = []
seededFileURLs = []
}
/// Another app put *these* flavors down a screenshot, a browser's Copy Image, a Finder copy.
@@ -68,6 +77,27 @@ final class FakePasteboard: ClipboardPasteboard {
takeOver()
foreign = payloads
}
/// A Finder copy of one or more **files** every URL as its own pasteboard item, exactly as a
/// real multi-select copy is, with `public.file-url` reported through `availableTypes()` (the
/// first-item read the image branch's precedence classifies) so `carriesFileURL` sees it. `also`
/// seeds types riding beside the file URL on that same first item an image flavor, for the
/// precedence tests where a Finder-copied image file carries both.
func seedFileURLs(_ urls: [URL], also: [(type: String, data: Data)] = []) {
takeOver()
seededFileURLs = urls
foreign = urls.isEmpty ? also : [(UTType.fileURL.identifier, Data())] + also
}
/// A foreign type layered over whatever this pasteboard already holds, counter bumped but nothing
/// cleared a combination `write()` alone can never produce (a real copy's `clearContents()`
/// takes everything with it), but exactly what "the app's own type wins outright" needs to
/// construct to prove the guard actually fires rather than merely never being exercised
/// (`PastedImageClassificationTests.boardItemsWin`'s same synthetic shape, one layer up).
func layerForeign(_ payloads: [(type: String, data: Data)]) {
changeCount += 1
foreign = payloads
}
}
// MARK: - Fixtures