Skip to content

Paste and copy the whole multi-selection; extend it with Shift-arrow - #116

Open
pedrohcmeneses wants to merge 2 commits into
momenbasel:mainfrom
pedrohcmeneses:feature/multi-select-paste-copy
Open

pedrohcmeneses wants to merge 2 commits into
momenbasel:mainfrom
pedrohcmeneses:feature/multi-select-paste-copy

Conversation

@pedrohcmeneses

Copy link
Copy Markdown

What

Multi-selection (⌘-click / ⇧-click, #77) only served bulk deletion. This makes it useful for getting clips back out:

  • Return pastes every selected clip and ⌘C copies them, in strip order (not click order).
  • The context-menu Paste, Paste as Plain Text, Paste with Clean Formatting, Paste as Markdown and Copy act on the whole selection when the clicked card is part of it, the same rule Delete already follows. The titles say how many: "Paste 3 Clips to Notes", "Copy 3 Clips".
  • ⇧← / ⇧→ (and ⇧↑ / ⇧↓, which mirror them today) extend or shrink the selection from its anchor, like a Finder list. A bare arrow goes back to a single selection. ⌘← / ⌘→ still switch sections.

How a multi-clip paste is delivered

A new pure type, ClipComposer, turns the selection into pasteboard "steps":

  • neighbouring text clips (text, rich text, links, colors) become one step, joined by line breaks. Formatting is kept when any of them carries RTF; the plain ones ride along in the system font;
  • neighbouring file clips become one file list;
  • an image is always its own step.

One step is an ordinary paste through the existing path. Several steps are written and pasted one after another by PasteService:

  • before every ⌘V it waits for the target app to be frontmost (if the user has moved on, the rest stays unpasted instead of landing somewhere else);
  • the target gets 0.3 s to read one clip before the next replaces it on the pasteboard;
  • a step that wrote nothing (its image file is gone) sends no ⌘V, since the previous clip would be pasted twice;
  • a later paste cancels a sequence still in flight;
  • a text step next to an image or file gets a line break on that side, so the two do not run together.

Why sequential: no single pasteboard write carries text next to images in a form every app accepts. Why a fixed interval: nothing reports that a paste was consumed. A lazy data provider would look like that signal, but other clipboard managers read the pasteboard the moment it changes and would trigger it early.

Where direct paste is not available (the Mac App Store build, Accessibility not granted, "paste directly" off), and for ⌘C, the steps are written as separate pasteboard items in one change. Apps that read every item get them all; the rest take the first.

The converting formats (plain text, clean formatting, Markdown) always produce one text step and leave out clips that have no text form.

Copying, and pasting with "promote on paste" on, promotes the selected clips as a block, keeping their order. No merged clip is added to history.

Two fixes that came with it

Both were hidden until a keyboard path depended on them, and both affect existing behaviour:

  1. selectFirst() did not reset the selection anchor. After navigating and closing the bar, the anchor stayed on the old clip, so the first ⇧-click after reopening selected a range from a clip nobody picked. One line, covered by a test.
  2. The bar opened with the search field focused the first time it was shown after launch (separate commit). The first time a panel becomes key, AppKit makes its only key view first responder. showBar() resigns the field before presenting, but on the first presentation the panel does not exist yet. Return and the plain arrows still worked through the field's delegate, which hid it; ⌘C, ⌘← / ⌘→ and now ⇧-arrow were taken as edits to an empty query and did nothing until the bar had been hidden once.

Tests

  • 16 new unit tests: ClipComposerTests (joining, formatting, mixed selections, file lists, the converting formats, multi-item copy to a private pasteboard) and KeyboardSelectionTests (grow, shrink, crossing the anchor, strip ends, back to single selection, anchor reset, strip order, block promotion).

  • ClipboardStore gained an internal init(baseDirectory:) so these run on a scratch directory. The app still only uses shared.

  • swift build and swift build -Xswiftc -DMAS add no warnings. (The Swift 6.4 toolchain reports two existing ones in SettingsView.swift about a missing import Combine; left alone as unrelated.)

  • End to end on macOS 27.3, Apple Silicon: a build started with --demo on an empty data directory, driven by synthetic key events, checking the pasteboard and a rich-text test document:

    • ⇧→ ⇧→ Return on three text clips: one paste, joined by line breaks;
    • seven arrows, ⇧→ ⇧→ Return across text, image, text: three pastes, in order, landing in the document as text, image, text;
    • ⇧→ ×3, ⇧←, ⌘C: the selection shrinks and three clips are copied;
    • ⇧→ ⇧→, ←, Return: a bare arrow drops the multi-selection, one clip pasted;
    • ⌘→, Return: section switching unchanged.

    In the runs that paste into the document, a test-only build delivered ⌘V straight to the document's process rather than through the HID tap, because an input-sharing tool on that machine was intercepting HID-level events at the time. The sequencing was exercised for real; the HID post itself is the existing, unchanged line.

  • Not tested on Intel. The Mac App Store target was compiled, not run.

UI

No visual change to the strip. The only visible delta is the context-menu titles when several clips are selected, so there is no before/after screenshot.

Not in this PR

  • No setting for the separator or the interval.
  • CHANGELOG.md is untouched; I left it for the release.

https://claude.ai/code/session_01SiCApxeYcYJjeYYByKpN1m

Multi-selection only served bulk deletion. Return, Command-C and the
context-menu Paste and Copy commands now act on every selected clip, in
strip order:

- Text clips (text, rich text, links, colors) are joined by line breaks
  into a single paste; formatting is kept when a clip carries RTF.
- Neighbouring file clips become one file list.
- Images and mixed selections are pasted one clip after another, giving
  the target app time to read each one. Without direct paste they are
  written as separate pasteboard items instead.
- Paste as Plain Text, Clean Formatting and Markdown join the text form
  of every selected clip.

Shift-arrow extends or shrinks the selection from its anchor like a
Finder list; a bare arrow returns to a single selection.

selectFirst() now resets the selection anchor too. It used to leave the
anchor where the previous presentation ended, so the first Shift-click
after reopening the bar selected a range from a clip nobody picked.

Claude-Session: https://claude.ai/code/session_01SiCApvjwVcpmBxjvC4aNjB
The first time a panel becomes key, AppKit makes its only key view, the
search field, first responder. showBar() resigns the field before
presenting, but on the first presentation the panel does not exist yet,
so the bar opened with the query focused until it was hidden once.

Return and the plain arrows still worked through the field's delegate,
which hid the problem; Command-C, Command-arrow and Shift-arrow were
taken as edits to an empty query and did nothing. Resign the field
right after the panel becomes key.

Claude-Session: https://claude.ai/code/session_01SiCApvjwVcpmBxjvC4aNjB

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant