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
+25 -9
View File
@@ -409,8 +409,8 @@ final class CardBodyTextView: NSTextView {
// MARK: The paste yield
/// **A pasteboard this editor cannot read is never the editor's either** the drop rule above,
/// arrived at the keyboard (04-interactions.md Clipboard, the image-data branch; 05
/// Attachments makes the *window* answer V with an attachment import).
/// arrived at the keyboard (04-interactions.md Clipboard, the image-data and file-URL branches;
/// 05 Attachments makes the *window* answer V with an attachment import).
///
/// The focused-editor rule says a focused text surface wins V, and it still does: any pasteboard
/// carrying something this view can take text, foremost pastes into the text exactly as
@@ -421,10 +421,22 @@ final class CardBodyTextView: NSTextView {
/// must not mean "blocks what it cannot take" so a paste this editor has no reading of is
/// passed to the responder behind it, and the standard validation walks the same path.
///
/// The yield is by *capability*, not by content kind: `readablePasteboardTypes` is AppKit's own
/// statement of what this view would accept, so the expression cannot drift from the paste it
/// guards. In Preview the view is not editable and takes no paste, so there the window's branch
/// simply owns V outright.
/// The yield is by *capability*, not by content kind, for the image branch `readablePasteboardTypes`
/// is AppKit's own statement of what this view would accept, so the expression cannot drift from
/// the paste it guards. In Preview the view is not editable and takes no paste, so there the
/// window's branch simply owns V outright.
///
/// **The file-URL branch needs one content check, and here is why.** `acceptableDragTypes` above
/// excludes `.fileURL` outright, but `readablePasteboardTypes`' own answer already omits it too
/// `importsGraphics` is `false` on this view, so AppKit does not offer it. The leak is one level
/// up: `public.file-url` *conforms to* `public.url`, which this view legitimately does read (so a
/// dragged or pasted web link still lands as text), and `NSPasteboard.availableType(from:)`
/// matches by conformance, not exact type so a Finder copy that also declares a generic URL
/// representation beside its file URL would still read as "text this editor takes" through sheer
/// ancestry, and the paste would never reach the window at all. `PastedImage.carriesFileURL` is
/// the one predicate this whole feature already classifies file-URL pasteboards with
/// (`ClipboardStore.fileURLPayload`); reusing it here rather than a second UTI walk is what keeps
/// this check in step with that one.
/// The pasteboard the yield reads the general one in the app; tests hand in their own so the
/// suite never touches the machine's (`FakePasteboard`'s reason, one seam over).
@@ -432,11 +444,15 @@ final class CardBodyTextView: NSTextView {
/// Whether this editor itself would take the current pasteboard.
private var takesPasteboardAsText: Bool {
isEditable && yieldPasteboard.availableType(from: readablePasteboardTypes) != nil
guard isEditable else { return false }
// A file URL is never this editor's, whatever generic ancestor type rides beside it on the
// pasteboard (the doc block above) the file-URL branch owns this paste instead.
guard !PastedImage.carriesFileURL((yieldPasteboard.types ?? []).map(\.rawValue)) else { return false }
return yieldPasteboard.availableType(from: readablePasteboardTypes) != nil
}
/// The responder behind this view that answers `paste:` the card window's image branch when it
/// is armed (`cardWindowImagePaste`; SwiftUI's bridge responds only while the handler is
/// The responder behind this view that answers `paste:` the card window's file or image branch
/// when either is armed (`cardWindowPaste`; SwiftUI's bridge responds only while the handler is
/// attached), and `nil` when nothing behind would take the paste either.
private var pasteYieldTarget: NSResponder? {
var responder = nextResponder