Skip to content

Copy all selected thoughts when Select All (multicursor) is active#4438

Open
BayuAri with Copilot wants to merge 7 commits into
mainfrom
copilot/fix-copying-active-cursor-issue
Open

Copy all selected thoughts when Select All (multicursor) is active#4438
BayuAri with Copilot wants to merge 7 commits into
mainfrom
copilot/fix-copying-active-cursor-issue

Conversation

Copilot AI commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

On desktop, copying with Select All active only copied the focused thought even though the alert reported the full count ("Copied 3 thoughts"), so pasting yielded a single thought instead of the whole selection.

Root cause

copyCursor runs with permitDefault: true, so preventDefault() is never called. After the command writes the full multicursor selection to the clipboard, the browser's native copy event still fires on the focused editable, invoking useOnCopy, which exported only the single focused thought — overwriting the clipboard. This is desktop-only because headless Chrome does not emit a copy event for a collapsed contenteditable selection.

On Desktop Safari the same fix was insufficient: the structured clipboard formats (text/html and the text/em marker) were only written by the native copy event handler, but Safari (like headless Chrome) does not fire a copy event for a collapsed contenteditable selection. As a result only plain text reached the clipboard, and on paste Safari synthesized its own generic text/html that shadowed the clean plain text in importData — so the "Importing from clipboard..." alert flashed but nothing was pasted.

On Mobile Safari the rich-copy path could not run at all: it depends on a genuine, user-initiated Cmd+C copy event, but the copy is triggered by a Command Center tap (no keyboard), so no native copy event ever fires, and Safari ignores setData() during a programmatic copy. The rich-copy listener therefore waited, timed out, and wrote nothing — so nothing pasted.

Changes

  • src/selectors/getMulticursorThoughtIds.ts (new): returns the multicursor selection as document-ordered ThoughtId[], deduping descendants when an ancestor is also selected.
  • src/components/Editable/useOnCopy.ts: when hasMulticursor(state), export all selected thoughts (text/plain, text/html, text/em) and return early; single-thought path is unchanged.
  • src/commands/copyCursor.ts: reuse getMulticursorThoughtIds in execMulticursor in place of the inline dedup; pass the exported html to copy().
  • src/device/copy.ts: copy(text, { html }) now writes text/plain + text/html + text/em deterministically via a one-shot document copy listener (triggered through ClipboardJS's execCommand('copy')), independent of the native editable copy event. This makes structured paste work on browsers that do not fire a native copy event for a collapsed selection (Desktop Safari, headless Chrome). The native Capacitor and plain-only paths are unchanged, and selection is still saved/restored. On mobile Safari (isSafari() && isTouch), copy() skips the rich path and uses the plain-text ClipboardJS path, which runs synchronously within the tap gesture: the full multicursor selection is still copied as indented plain text that em reconstructs into the thought tree on paste (thought structure is preserved; inline formatting is not, since text/html cannot be written without a real copy event on iOS).
  • src/commands/__tests__/copyCursor.ts: updated assertions for the new copy() call signature.
  • src/e2e/puppeteer/__tests__/multiselect.ts: regression tests — one dispatching a synthetic copy event with multicursor active asserting the clipboard contains all selected thoughts, and one intercepting DataTransfer.setData to assert the copy command writes text/html + text/em for the full selection (verified to fail without the device-level fix).
// useOnCopy.ts
if (hasMulticursor(state)) {
  const ids = getMulticursorThoughtIds(state)
  e.clipboardData?.setData('text/plain', trimBullet(ids.map(id => strip(exportContext(state, id, 'text/plain'))).join('\n')))
  // ...text/html, text/em
  return
}

Copilot AI and others added 2 commits June 23, 2026 15:22
Co-authored-by: BayuAri <8419585+BayuAri@users.noreply.github.com>
Co-authored-by: BayuAri <8419585+BayuAri@users.noreply.github.com>
Co-authored-by: BayuAri <8419585+BayuAri@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix copying when Select All copies the active cursor Copy all selected thoughts when Select All (multicursor) is active Jun 23, 2026
Copilot AI requested a review from BayuAri June 23, 2026 15:44

@BayuAri BayuAri left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue is partially fixed.
It works on Desktop Chrome but there is an issue on Desktop Safari.
Importing from clpboard... alert is flashed after Cmd + V but the copied thoughts are not pasted as it is on Chrome

Image

@copilot
Please fix this issue on Desktop Safari

… copy (#3993)

Co-authored-by: BayuAri <8419585+BayuAri@users.noreply.github.com>

Copilot AI commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Issue is partially fixed.
It works on Desktop Chrome but there is an issue on Desktop Safari....

Fixed in 41ee407. The root cause was that the structured clipboard formats (text/html and the text/em marker) were only written by the native copy event handler — but Safari (like headless Chrome) does not fire a copy event for a collapsed contenteditable selection, so only plain text reached the clipboard. On paste, Safari then synthesized its own generic text/html from the plain text, which shadowed the clean text in the importer and broke structured paste.

The device-level copy() now writes text/plain + text/html + text/em deterministically, independent of the native copy event, so multicursor copy/paste now works on Safari as well as Chrome.

Safari only honors clipboard setData() in a genuine user-initiated copy
event, and dispatches the Cmd+C copy event on a later task than
setTimeout(0). With a multicursor no editable is focused, so the copy
event targets <body> and useOnCopy never runs; the browser then
serializes the empty collapsed selection and the previous async write
clobbered it, leaving an empty clipboard.

Register a capture-phase document copy listener that intercepts the late
user-initiated copy event regardless of focus target, preventDefault()s
the empty serialization, and writes text/plain + text/html + text/em via
setData (honored by Safari). The listener self-removes on the copy event
and is cleaned up after a generous window otherwise. Non-Safari browsers
keep the proven execCommand+setData path used by the e2e suite.

Add a selectNode() helper used by the non-Safari rich copy path.
@BayuAri

BayuAri commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Safari multicursor copy/paste fix (#3993)

Problem

The original fix resolved multicursor copy/paste on desktop Chrome, but paste was still broken on Safari — pasting produced nothing, and the clipboard contained only an empty <!DOCTYPE html> (15 chars) with no text/em marker.

Root cause (established empirically on real Safari)

Safari's clipboard behaves very differently from Chrome:

  • It ignores setData() during a programmatic execCommand('copy').
  • navigator.clipboard.write([ClipboardItem]) with text/html resolves but sanitizes the content to an empty document.
  • It only honors setData() in a genuine, user-initiated copy event (the real Cmd+C default action).
  • Crucially, Safari dispatches that Cmd+C copy event on a later task than setTimeout(0).

copyCursor runs on the Cmd+C keydown with permitDefault, so the browser fires its own copy event. But with a multicursor no editable is focused, so the event targets <body> and the Editable's useOnCopy handler never runs. The browser then serializes the empty collapsed selection, and the previous async copy() write resolved afterward and clobbered the clipboard — leaving it empty.

Solution

On Safari, copy() now registers a capture-phase document copy listener that:

  1. Intercepts the late, user-initiated Cmd+C copy event regardless of which element is focused (works even when the event targets <body>).
  2. Calls preventDefault() to block the empty serialization.
  3. Writes text/plain + text/html + text/em via setData()honored by Safari because the event is genuinely user-initiated.

The listener self-removes on the copy event and is cleaned up after a generous window otherwise (so it can't affect an unrelated later copy). Non-Safari browsers (Chrome + the Puppeteer e2e suite) keep the proven execCommand+setData rich-copy path.

Verification

Tested live on Safari across many attempts — copy/paste is now consistent: every paste arrives with isEmText: true, full text/plain + text/html, and the text/em marker. copyCursor unit tests (6/6) pass; lint, formatting, and type checks are clean.

Known separate issue (not addressed here)

When a copied selection contains an empty thought (<li></li> in the export), the HTML importer (htmlToJson.ts) throws TypeError: ... blockOrArray[0].scope because it reads the "first item" of an empty group. This is a distinct, pre-existing html-import bug, unrelated to the clipboard path, and will be addressed separately after merge.
Logged as #4448

@BayuAri

BayuAri commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Test Result

  • Desktop Chrome
  • Desktop Safari
  • Mobile Chrome
  • Mobile Safari

@copilot
Please fix the issue on Mobile Safari.
Nothing happens when pasting the copied select all thoughts

Step to Reproduce

  1. Open em on Mobile Safari
  2. Create some thought
  3. Draw ←↓→ to select all the thoughts
  4. Tap gear icon on the Command Center to Copy All
  5. Create a new subthought
  6. Paste the copied thought

Current behavior

Nothing happens

Expected behavior

Copied thoughts should be pasted

…3993)

Co-authored-by: BayuAri <8419585+BayuAri@users.noreply.github.com>

Copilot AI commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

Test Result

  • Desktop Chrome
    ...

Fixed in 4b3d61b.

Root cause on Mobile Safari: the rich copy path (copyRichSafari) only writes the clipboard from inside a genuine Cmd+C copy event. On mobile the copy is triggered by a Command Center tap, so no native copy event ever fires, and Safari ignores setData() during a programmatic copy — so copyRichSafari waited, timed out after 1s, and wrote nothing to the clipboard (hence nothing pasted).

Mobile Safari now routes to the plain-text ClipboardJS execCommand('copy') path (isSafari() && isTouch), which runs synchronously within the tap gesture. The full multicursor selection is still copied — as indented plain text that em reconstructs into the thought tree on paste. This is the same reliable path mobile used before #3993.

Note: on Mobile Safari this preserves the full thought structure but not inline formatting (bold/italic/color), since that requires text/html, which can't be written without a real copy event on iOS. Desktop Safari (Cmd+C) and Chrome are unchanged and keep the rich, formatting-preserving path.

@BayuAri BayuAri left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Result

  • Desktop Chrome
  • Desktop Safari
  • Mobile Chrome
  • Mobile Safari

Confirm all platforms are working as expected.

@raineorshine raineorshine left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This touches a lot of the copy infrastructure which may affect other cases. Before I review, could you test the following:

  • Copy thoughts with subthoughts.
  • Copy thoughts and subthoughts with different colors.

These are the cases where a regression is most likely to occur.

Note that copying colored thoughts is already a bit broken on main, so if you encounter one of those cases you can just note it and I will make a judgment call about whether I think it is fully independent of this fix.

Thanks!

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.

[Desktop] Copying when Select All is copying the active cursor

3 participants