Skip to content

fix(macos): give the menu back the commands the web side cannot bind - #527

Merged
PathGao merged 2 commits into
masterfrom
fix/macos-edit-menu-paste
Aug 7, 2026
Merged

fix(macos): give the menu back the commands the web side cannot bind#527
PathGao merged 2 commits into
masterfrom
fix/macos-edit-menu-paste

Conversation

@PathGao

@PathGao PathGao commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Fixes #526.

Stacked on #528 (dompurify pin) so CI can run — merge that one first.

What happens

On macOS nothing can be pasted into a plain input — the theme-import field in Settings, the rename prompt — by keyboard or by right-click. ⌘M and ⌃⌘F do nothing either. All of it broke in 2.6.10 with #339.

Why

#339 reduced the native menu to app-level actions and dropped File/Edit/Window, on the grounds that Markpad keeps its document actions in the in-window controls. That holds for document actions — MarkdownViewer.handleKeyDown really does bind ⌘N/⌘O/⌘S/⌘W/⌘±/⌘0/⌘Tab web-side, and those still work. It does not hold for commands the web side has no way to bind:

  1. Text editing. macOS routes ⌘X/⌘C/⌘V/⌘A and undo through the main menu to the first responder — WKWebView declines them precisely so the menu can. With no Edit menu they go nowhere. Monaco was unaffected only because Editor.svelte implements its own ⌘V against the Rust clipboard commands, which is why this looked field-specific rather than app-wide.
  2. Window-server actions. Minimize and full screen have no web API to bind at all. No menu item, no key equivalent, no shortcut.

Plus one that is not AppKit's doing: handleContextMenu preventDefault()s across the whole document and opens Markpad's preview menu, which has no edit items — so right-clicking a text field offers no Paste either. The modal backdrop does the same to the rename prompt.

Changes

  • Edit menu: Undo, Redo, Cut, Copy, Paste, Select All.
  • Window menu: Minimize, Toggle Full Screen. Full Screen belongs in a View menu by convention; View would hold that one item and nothing else, so it rides in Window — a deliberate deviation, not an oversight.
  • Text fields keep the webview's own editing menu, in the document handler and behind modals.

File stays out, and so do Hide Others / Show All — #281's intent is otherwise untouched.

Monaco keeps its ⌘V: it calls preventDefault(), WebKit reports the key as handled, and the menu never fires. Image paste and URL linkify still go through Editor.svelte.

Validation

  • npm run check 0 errors · npm test 843 pass · cargo test 141 pass

  • Paste into the theme-import field by ⌘V and by right-click → Paste: confirmed by hand on macOS 26.

  • ⌘M and ⌃⌘F measured on a dev build, before and after, by window size — AXFullScreen is unreliable for this window's overlay title bar:

    ⌃⌘F ⌘M
    without Window menu 708x923 → 708x923 not minimized
    with it 708x923 → 1470x33 minimized

    Control for "did the keystroke arrive at all": ⇧⌘M, which is bound web-side, took the window count 1 → 2 in the same session.

  • ⌘` still cycles windows either way — it is a system binding, not a menu key equivalent — so no menu item is owed for it.

  • Tests updated to state the new intent: issue281MinimalMacosMenu.test.ts still keeps File and the document shortcuts out, and now requires Edit and Window; menuModalGuards.test.ts asserts the backdrop consumes context menus outside text fields.

🤖 Generated with Claude Code

The minimal macOS menu (#281) dropped File, Edit and Window because
Markpad keeps its document actions in the in-window controls. Edit is
not a document menu, though: macOS routes Cmd+X/C/V/A and undo through
the main menu to the first responder, so removing it left every native
input in the app — the theme-import field, the rename prompt — unable
to paste at all. Monaco was unaffected only because it implements its
own Cmd+V against the Rust clipboard commands.

Put back the six standard Edit items. Monaco still wins its own
shortcut: it calls preventDefault(), so WebKit reports the key as
handled and the menu never fires.

The document context menu had the second half of the same problem — it
preventDefault()s over the whole document, so right-clicking a text
field replaced the webview's Cut/Copy/Paste menu with a preview menu
that has no edit items. Text fields now keep the native menu, in the
document handler and behind modals alike.

Fixes #526

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@PathGao
PathGao force-pushed the fix/macos-edit-menu-paste branch from 3df1ba4 to f25676d Compare August 7, 2026 17:07
@PathGao
PathGao changed the base branch from master to chore/dompurify-3413 August 7, 2026 17:07
@PathGao

PathGao commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto #528. npm audit started failing on GHSA-55q2-fjhq-7xh7 (dompurify <= 3.4.12, published after master's last green run), which killed the test job before it reached npm test — nothing to do with this change, and it hits every open PR. #528 moves the pin; this PR now sits on top of it so its own checks can actually run.

Merge order: #528, then this one.

Same shape as the Edit menu, one layer out. Minimize and full screen
are window-server actions with no web-side equivalent — nothing in
`handleKeyDown` can bind them — so with no menu item to carry the key
equivalent they simply do not fire. Measured on a dev build, by window
size rather than AXFullScreen, which this window's overlay title bar
reports unreliably:

    no Window menu   ⌃⌘F: 708x923 -> 708x923    ⌘M: not minimized
    with it          ⌃⌘F: 708x923 -> 1470x33    ⌘M: minimized

Full Screen belongs in a View menu by convention. View would hold that
one item and nothing else, since Markpad's view actions are all
in-window per #281, so it rides in Window instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@PathGao PathGao changed the title fix(macos): restore the Edit menu so text fields can paste fix(macos): give the menu back the commands the web side cannot bind Aug 7, 2026
Base automatically changed from chore/dompurify-3413 to master August 7, 2026 17:27
@PathGao
PathGao merged commit e21501e into master Aug 7, 2026
4 checks passed
@PathGao
PathGao deleted the fix/macos-edit-menu-paste branch August 7, 2026 17:34
PathGao pushed a commit that referenced this pull request Aug 8, 2026
Monaco writes a styled `text/html` flavour beside the plain text on a copy.
Everything Markpad produces IS plain text, so pasting into Word or Outlook gave
coloured monospace instead of the Markdown that was copied — the styled flavour
has no audience here. Item 3 of the audit in #393; `false` is what that issue
recommends.

It was in the first draft of this branch and taken out when cut, copy and paste
were collapsed onto three functions of our own, on the reasoning that Monaco's
own copy had become unreachable. It had not.

Those three cover ⌘X/⌘C/⌘V and the editor's context menu. macOS has a third way
in that reaches none of them: Edit > Copy in the menu bar is a
`PredefinedMenuItem::copy` (#527), which asks the WEBVIEW to perform its own
copy. So without this option the menu bar puts a different clipboard on the
pasteboard than the other two routes do, from the same selection.

The test says that rather than saying the option is set, and asserts the menu
bar route still exists — so if `PredefinedMenuItem::copy` ever goes, this comes
up for review instead of sitting there as an option nobody remembers the reason
for.
PathGao added a commit that referenced this pull request Aug 8, 2026
* fix(clipboard): one cut, one copy, one paste (#207)

Right-clicking in the editor and choosing Paste did nothing. The menu half of
that report was fixed in #266; this is the clipboard itself, and the answer
turned out to be smaller than the diagnosis.

Markpad is a webview, not Electron, so Monaco's `platform.isNative` is false
and its clipboard code takes the branch written for the web. There it decides
whether to register a Paste action at all by asking whether the API *exists*:

```js
const supportsPaste = (typeof navigator.clipboard === 'undefined' || isFirefox)
    ? document.queryCommandSupported('paste') : true;
```

`navigator.clipboard` is an object in any Chromium webview, so the action
registers and the menu item is drawn — the check is never for permission.
`readText()` then rejects, `BrowserClipboardService` catches it and returns
`''`, and `if (clipboardText !== '')` declines to paste. No error, no feedback.

wry leaves the webview's `clipboard` attribute off, which on Windows means the
WebView2 `PermissionRequested` handler allowing CLIPBOARD_READ is never
registered. Turning it on is the obvious fix, the plumbing is intact all the
way down — `enable_clipboard_access()` → `webview_attributes.clipboard` →
`.with_clipboard(...)` → `add_PermissionRequested(...)` — and **it does not
work**. Built it, ran it on Windows, right-clicked Paste: still nothing.

tauri-apps/tauri#12007 has been open since December 2024 on exactly this, and
the answer the ecosystem gives is the one Markpad already had for ⌘V: go
through Rust. The official `tauri-plugin-clipboard-manager` exists to be "an
alternative to the native navigator.clipboard methods".

Three operations had six implementations. ⌘X was the browser's, ⌘C and ⌘V were
ours through Rust, and the three menu items were Monaco's — of which Paste
cannot work in a webview at all, and Cut and Copy are dead on Linux, where the
same wry default gates `set_javascript_can_access_clipboard`.

Now: three functions, six entry points.

```
        keyboard                     menu
cut     ⌘X ─┐                   Cut ─┐
copy    ⌘C ─┼→ cutToClipboard   Copy ┼→ same three functions
paste   ⌘V ─┘  copyToClipboard  Paste┘
               pasteFromClipboard          → invoke → arboard
```

Both halves are free to take. Monaco leaves ⌘X/⌘C/⌘V unbound in a browser by
design ("browsers do that for us" — clipboard.js), so the key slots were
available. And its editor context menu holds nothing else this app can use:
`gotoSymbol` and `inlayHints` need language providers Markpad does not
register, and Copy As / Share are empty in standalone. So `contextmenu: false`,
and the menu the preview pane already had is drawn for the editor too —
already translated six ways, already styled, and now the same on both sides.

That also settles what #266 could not. Its fix was to stop covering Monaco's
menu, which was right for the overlay bug and left the reader pointed at a
menu whose Paste silently did nothing.

Nothing here depends on a webview permission, and nothing imports a Monaco
internal, so there is no upgrade that can quietly take it away.

Three existing tests changed, each because the thing it described moved:

- `issue261EditorPdf` pinned "Monaco must receive the event before the document
  menu prevents it". The invariant is now that the editor's branch runs before
  every other rule — including the carve-out that leaves text fields their
  native menu, which would otherwise claim every right-click in the editor,
  because Monaco takes input through a hidden `<textarea>`.
- `editorOptionWiring` pinned the whole-line-on-empty-selection rule inside
  `custom-copy`. It lives in `clipboardTextForSelection` now, where cut reads
  it too — a cut has to remove exactly what a copy would have taken. A new
  assertion holds the line: `clipboard_write_text` may appear exactly twice in
  the component.
- `imageUndoKeepsFile` located the paste body as "the `addCommand` callback
  that reads a clipboard image". It is a named function now, and the lookup
  asserts no inline paste has appeared beside it.

* fix(clipboard): keep the two editor-menu entries this app can use

Drawing the editor's context menu ourselves took away everything Monaco used
to contribute to it, and two of those worked: **Command Palette** and **Change
All Occurrences**. Neither needs a language provider, so both were there in
every build, and both disappeared. Found by looking, not by a test.

The rest of what Monaco offers there — Go to Symbol, Quick Fix, Refactor,
Format Document, Rename — is gated on providers Markdown has none of and never
appeared in this app, which is what made the earlier claim that "nothing else
is usable" wrong rather than merely imprecise. The audit that produced it
searched for `MenuId.EditorContext`, and standalone actions register through
`contextMenuOpts` instead.

They come back translated, which is a gain rather than parity: Monaco's menu is
English whatever language the app is in.

`menu.commandPalette` already existed — the shortcuts pane uses it — so it is
reused rather than declared a second time in the same object, where the later
one would have silently won.

`menu.changeAllOccurrences` is new, in all 26 languages. A missing key does not
throw: `t()` falls back to English and then to the key itself, so a forgotten
locale ships either an English label between translated siblings or the literal
string `menu.changeAllOccurrences`. Its neighbours (cut, copy, paste,
commandPalette) all carry 26, and the new test holds every label this menu asks
for to that count.

* fix(clipboard): copy plain text from the menu bar too (#393)

Monaco writes a styled `text/html` flavour beside the plain text on a copy.
Everything Markpad produces IS plain text, so pasting into Word or Outlook gave
coloured monospace instead of the Markdown that was copied — the styled flavour
has no audience here. Item 3 of the audit in #393; `false` is what that issue
recommends.

It was in the first draft of this branch and taken out when cut, copy and paste
were collapsed onto three functions of our own, on the reasoning that Monaco's
own copy had become unreachable. It had not.

Those three cover ⌘X/⌘C/⌘V and the editor's context menu. macOS has a third way
in that reaches none of them: Edit > Copy in the menu bar is a
`PredefinedMenuItem::copy` (#527), which asks the WEBVIEW to perform its own
copy. So without this option the menu bar puts a different clipboard on the
pasteboard than the other two routes do, from the same selection.

The test says that rather than saying the option is set, and asserts the menu
bar route still exists — so if `PredefinedMenuItem::copy` ever goes, this comes
up for review instead of sitting there as an option nobody remembers the reason
for.

* fix(clipboard): print the chord beside the two menu entries that need it

Monaco's context menu showed a shortcut next to every item and ours showed
none — a regression from drawing the menu ourselves, and the kind that is
invisible until someone goes looking for a command they used to reach that way.

Only the bottom two get one. Command Palette is `F1` and Change All
Occurrences is `Mod+F2`, and for the second in particular the menu entry is
most of how anyone learns the chord exists. Cut, copy and paste do not: those
are OS conventions rather than app shortcuts — `shortcuts.ts` says so about the
same three — and printing them costs a column of width in every language to
tell people something they already know.

`formatChord` rather than two literals, so the Mac and Windows spellings cannot
drift apart. Confirmed against Monaco's own registrations: `KeyCode.F1` with no
modifier, and `KeyMod.CtrlCmd | KeyCode.F2`.

* fix(clipboard): leave the editor focused after a clipboard action

Reported from Windows as "⌘Z stopped working". It had not: paste from the
context menu inserted the text and left focus on the menu item, so there was no
caret and the next keystroke went to the document instead of the editor. The
visible symptom named a different feature than the broken one.

⌘X/⌘C/⌘V never needed a `focus()` — a keybinding fires with the editor focused
by definition — which is exactly why it was missing once those same functions
were given a second entry point. `cutToClipboard` was written fresh and had it;
`pasteFromClipboard` was lifted whole out of the ⌘V command and carried the
assumption with it.

All three focus first now, matching `runEditorAction`, and the test asserts the
order rather than the presence: focusing after the work leaves the same gap for
anything that reads the selection.

---------

Co-authored-by: PathGao <gaoyanbo@gaoyanbodeMacBook-Air.local>
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.

Pasting text to theme import field fails

1 participant