zudo-text

検索したい単語を入力

いつでも検索バーを開ける

Paste as Plain Text

Paste as Plain Text (editor-paste-as-plain-text, default Mod+Shift+V) inserts the clipboard's text/plain into the focused body editor as one CodeMirror transaction. It never routes through the EditorPane DOM paste handler, so it never imports images or files, and it never rewrites Markdown characters. The renderer owns the command on every entry point; the native macOS Edit menu only forwards to it. User-facing behaviour lives in the manual's エディタ page (manual/src/content/docs/basics/editor.mdx).

Where the code lives

PieceFile
Executor, ownership fence, HTML-to-text conversion, notification copytauri-app/renderer/lib/paste-as-plain-text.ts
Command palette entry (editor-paste-as-plain-text, "Paste as Plain Text", category Editor)tauri-app/renderer/data/app-commands.tsx
Shortcut key shortcuts.pasteAsPlainText, default ["Mod+Shift+V"], validationpackages/app-defaults/src/types.ts, packages/app-defaults/src/defaults.ts, packages/app-defaults/src/validate-settings.ts
Settings → Shortcuts row (Editor group)tauri-app/renderer/components/settings/sections/shortcut-groups.ts
Shortcut engine binding, command callback, workspace-switch invalidationtauri-app/renderer/app.tsx
Editor context menu rowtauri-app/renderer/components/editor-pane/editor-context-menu.ts
Native Edit menu entry (MenuKind::RendererCommand, macOS only)tauri-app/core/src/menu_spec.rs
Bridge clipboard contract (readText, isReadSupported, optional readHtml)packages/backend-bridge/src/types.ts
Help catalog entrytauri-app/renderer/data/help-catalog/index.ts

Entry points

The shortcut, palette and context-menu entry points end in pasteAsPlainText() with no view argument, so they share target resolution and the fence. The native Edit menu goes through the source-aware pasteAsPlainTextFromMenu() instead (below):

  • Shortcut engine — the pasteAsPlainText binding uses suppressIfTargetMatches with isPlainTextPasteTarget(). The key is consumed only when the event target is inside a body .cm-editor. The Spreadsheet board (its one-shot "Paste values only" gesture), the Terminal pane, plain inputs nested in editor chrome (Vim prompt, search panel), and dialog inputs keep their own Mod+Shift+V handling.

  • Command palette — the palette runs the action before it closes, so document.activeElement is the palette input at execution time. Target resolution therefore uses the last-focused body view (getActiveEditorView()), not the active element. The executor calls view.focus() after inserting.

  • Editor context menu — a row next to Paste, hidden on read-only editors and disabled when clipboard.isReadSupported() is false (the same rule as Paste).

  • Native macOS Edit menu — the former edit_paste_and_match_style native entry (accelerator Shift+CmdOrCtrl+V, label "Paste and Match Style") is now MenuKind::RendererCommand { command_id: "editor-paste-as-plain-text" } labelled "Paste as Plain Text". It dispatches through dispatch_renderer_command → zudotext-native-menu-command → useNativeMenuCommands → runCommandById, like the other renderer menu items, except that useNativeMenuCommands takes a menuActions map and app.tsx maps this id to pasteAsPlainTextFromMenu(). runCommandById still applies its disabled/status gate before running the override. The earlier navigator.clipboard.readText() + execCommand('insertText') eval in tauri-app/src/native/menu.rs is gone, which also removes the WebKit "Paste" permission popup from this path. See App Lifecycle for the full menu dispatch table.

Edit-menu target (#7628)

The palette cannot see the real focus, but a native menu click does not move it, so the Edit-menu path resolves its target from document.activeElement, in this order:

  1. Ordinary text field — a <textarea>, or an <input> of type text, search, url, email or tel, that is not disabled or read-only and not inside a .cm-editor, the Spreadsheet board or the Terminal. The clipboard text replaces the field's selection with document.execCommand("insertText") (native undo, real input event), falling back to setRangeText plus a dispatched input event where execCommand is missing or returns false. email has no selection API, so that fallback appends the text through the native value setter (which React-controlled inputs observe) instead. This covers Settings fields and the palette's own search input when the palette is open. Clipboard outcomes map as for the body editor. After the read the field must still be document.activeElement, connected and writable, with the same value and selection, the same workspace and generation, and document.hasFocus(); otherwise the result is stale and nothing is inserted.

  2. Read-only note body editor — focus inside a body editor whose only failing check is state.readOnly → no-target, the same result and message as the palette path. The clipboard is not read.

  3. Excluded surface — password, number and other non-eligible inputs, a generic [contenteditable], the Spreadsheet board, the Terminal, inputs in editor chrome (Vim : prompt, search panel), an inline editor → excluded (#7638). The clipboard is not read, the focused field and the remembered note are left untouched, and the command never falls through to the note. number is excluded because it has no selection API and appending arbitrary clipboard text produces an invalid value that the browser sanitizes to empty; the command does not attempt numeric parsing or caret emulation. Ordinary Mod+V paste into these surfaces is unaffected.

  4. The remembered body view — as for the palette, but no-target when focus sits in a [role="dialog"]/[role="alertdialog"]/<dialog> that neither contains the view nor hosts a frame (or, when focus has dropped to <body> after a click on a non-focusable spot in a modal, when any [aria-modal="true"] element is such a modal), and stale when such a modal took focus during the clipboard read. The paste never lands behind a modal and never steals its focus.

There is no generic contenteditable or password handling, and the shortcut binding is unchanged: in an input it is suppressed before any preventDefault, so Mod+Shift+V falls through to the browser.

Executor contract

pasteAsPlainText({ view?, owner }) returns one of "inserted" | "empty" | "unavailable" | "stale" | "no-target", and pasteAsPlainTextFromMenu({ owner }) can additionally return "excluded". notifyPlainTextPasteResult() maps four of them to an info notification:

ResultWhenNotification
insertedThe text was inserted.none
emptyreadText() returned nothing and there was no usable HTML fallback (empty, image-only, or RTF-only clipboard)."The clipboard has no plain text to paste"
unavailablereadText() or readHtml() threw."Couldn't read plain text from the clipboard"
no-targetNo live, visible, editable body view owned by the active frame (including a focused read-only note on the Edit-menu path), or document.hasFocus() is false."Focus an editable note to paste as plain text"
excludedEdit-menu path only: focus sits on an excluded surface (Terminal, password or number field, …). The message is deliberately neutral and does not point the user at a note."Paste as Plain Text isn't available here"
staleThe target changed during the async read.none

The insert is a single dispatch with userEvent: "input.paste" that replaces every selection range (each cursor of a multi-cursor selection gets the text, as with a native paste) and places the cursor after the inserted text. One Undo reverts it and Redo restores it. The text goes through state.toText() unchanged: no Markdown escaping, stripping, or reinterpretation (#904/#905).

Ownership fence

Before the read, the executor validates the view (connected, visible, not read-only, not an inline-editor host, not inside the Spreadsheet board, Terminal, or a non-frame dialog, and inside the active frame) and captures the frame id, workspace id, document, main selection, and a module-level generation counter. After the read it returns stale without inserting if any of these changed, or if the view was disconnected or became read-only. invalidatePlainTextPaste() bumps the generation; app.tsx calls it on workspace switch. A focus move alone is not staleness, because the palette path moves focus by design; only the Edit-menu path adds the modal re-check described above.

Renderer commands still dispatch to the main window. The requirement never to paste into another window's document is met on the renderer side by the document.hasFocus() check, not by routing the command to the focused webview.

Clipboard sources by adapter

Adaptertext/plainHTML-only clipboardImage-only / empty
Tauri (desktop)readText()readHtml is absent; whether the plugin's readText() resolves "" (→ empty) or rejects (→ unavailable, which the unit test models) is unverified on a real Mac (#7624). Either way a notification shows and nothing is insertedempty, notification
Mock / REST (web)readText()readHtml() via navigator.clipboard.read() → htmlToPlainText()empty, notification

text/plain always wins when present; HTML is consulted only when readText() returns an empty string. htmlToPlainText() parses with an inert DOMParser document, drops script/style/template and similar subtrees, maps block and paragraph elements to line breaks and table cells to tabs, preserves whitespace inside pre, and returns text only, never markup. RTF is never parsed: on both platforms an RTF payload is usable only through the text/plain representation that source apps write alongside it.

Verification

Unit coverage sits next to the executor and binding (tauri-app/renderer/lib/paste-as-plain-text.test.ts, paste-as-plain-text-binding.test.ts), the menu routing test (tauri-app/renderer/hooks/use-native-menu-commands.test.tsx), the command and shortcut tests under tauri-app/renderer/data/__tests__/, and the menu_spec.rs tests in tauri-app/core. e2e/paste-as-plain-text.spec.ts "native Edit menu target" dispatches the menu event in Chromium and WebKit with a Settings field, a password field or a number field focused. Its "spreadsheet ownership" @interactive group needs no clipboard permission, so it runs in the WebKit exam lane as well as Chromium: it holds the real Mod+Shift+V keys on a focused cell and delivers one synthetic ClipboardEvent while they are down, because neither headless engine on macOS turns that chord into a paste event (#7748). It checks the one-shot values-only intent against translated formula pastes before and after, exactly one paste reaching the board, no bridge clipboard read, and both body editors unchanged. The installed-macOS leg (native Edit menu, its ⇧⌘V accelerator display, and real system clipboard) needs a real macOS host and is tracked as a deferred-verification issue linked from epic #7610 (#7624).