fix(editor): give Ctrl/Cmd+Shift+Z back to redo - #648
Merged
Conversation
Zen mode was bound to CtrlCmd|Shift|KeyZ. On macOS that is redo's ONLY editor binding: Monaco's `mac` override replaces the default rule rather than adding to it, so the Ctrl+Y that Windows and Linux keep does not exist there. `addAction` registers at weight 1000 and every Monaco default sits at 0 or 100, so zen mode did not share the key, it took it — confirmed by hand on a debug build, where Cmd+Shift+Z entered zen mode and nothing redid. Undo still worked, so the edit was recoverable only by retyping it. The Edit menu's native `PredefinedMenuItem::redo` does not save the user; the hand test settles that, and it is left alone. Zen mode moves to Ctrl/Cmd+Shift+D — distraction-free, unclaimed in Monaco on all three platforms, unclaimed by the document-level handler and by the two Rust accelerators, not reserved by macOS, and a neighbour of the app's existing Ctrl/Cmd+Shift row. Redo keeps the chord; giving redo a second binding would have papered over taking a key that was already correctly assigned. THE GUARDRAIL Moving one key leaves the next person free to do the same thing again, and nothing in the suite could see it: the only description of Monaco's keymap was eleven chords hand-copied into formatShortcutKeymap.test.ts, and what it could not see was what it did not list. `scripts/monacoChordOwnership.spec.ts` reads the keymap instead. It imports `monaco-editor` under jsdom and dumps `KeybindingsRegistry.getDefaultKeybindings()` once per platform (366 / 299 / 293 keybindings over 223 / 165 / 161 distinct chords on macOS / Windows / Linux), then fails on any `addAction` or `addCommand` chord Monaco already assigns. Deliberate overrides stay possible through a named allow-list where each row argues why that particular loss is acceptable, and rows are checked for staleness so the table cannot only grow. The chords whose loss is unrecoverable — undo, redo, select all, cut, copy, paste, save — are held separately and cannot be allow-listed at all. The dump is taken in Monaco's web branch, not its native one: the app runs in a WebView, where clipboard.js leaves Ctrl/Cmd+X/C/V unbound "since browsers do that for us", which is why Editor.svelte binds them itself. A native-mode dump would have demanded three allow-list entries for a conflict the shipped app does not have. The allow-list starts with eight rows, all pre-existing: fmt-italic over triggerSuggest, fmt-underline over cursorUndo, fmt-quote over inPlaceReplace.down, view-toggle-edit over findWithSelection (macOS only), view-toggle-live over expandLineSelection, file-reveal over refactor (Windows and Linux only), and the two `when`-gated list-key commands on Enter and Tab. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PathGao
added a commit
that referenced
this pull request
Aug 12, 2026
#645 and #648 were each green and semantically conflict. #648 landed the chord-ownership guardrail with allow-list rows naming `indentListItemOnTab`; #645 had already renamed that handler to `handleTabKey` and added three more key registrations. Merged, the rows are stale AND four chords are undeclared, so master went red. The guardrail is working, not misfiring: `Mod+Enter` and `Mod+Shift+Enter` genuinely take `insertLineAfter`/`insertLineBefore` from Monaco, and nothing said so out loud. Every row now names the chord it takes, on which platforms, and why the loss is acceptable — for these four the answer is that the key is specialised rather than taken, and the handler re-triggers the core command everywhere its own branch declines. Falsified by deleting the `handleModEnterKey` row: red on all three platforms with the exact command list it displaces. Co-authored-by: PathGao <gaoyanbo@gaoyanbodeMacBook-Air.local> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
PathGao
added a commit
that referenced
this pull request
Aug 12, 2026
…he chord (#652) * fix(editor): stop Mod+Shift+K from deleting a line `Mod+Shift+K` is `editor.action.deleteLines` — VS Code's convention, inherited by standalone Monaco and inherited again by this app. It silently destroys the line the caret is on, and it was being mis-fired. None of the mainstream Markdown editors binds it: Typora, Obsidian, iA Writer and Bear all leave that chord alone. It is a code-editor key that leaked into a prose editor. A REMOVAL RULE, not a no-op command on the same chord. A rule whose command id starts with `-` is what Monaco's own resolver reads as "drop this default" (`KeybindingResolver.handleRemovals`); binding a do-nothing command instead would leave the key dead AND leave a fake command sitting in front of Monaco's. The rules are global to the Monaco module rather than to one editor, so the disposable is released with the editor — this component is rebuilt every time a tab goes to reading mode and back, and one rule per mount would pile up. The COMMAND stays. Delete Line keeps its command palette entry, the same rule the table delete verbs on this branch live under: dropping a key is not dropping a command, and a destructive verb keeps a home. The test reads Monaco rather than a copy of it, the way listContinuation.test.ts pins facts about `coreCommands.js`: the `-` prefix is read out of `keybindingResolver.js`, Delete Line's chord out of `linesOperations.js`, and the rule the app registers is evaluated with Monaco's real `KeyMod`/`KeyCode`. So an upstream rename or re-chord fails here instead of leaving a rule that quietly matches nothing, and nothing of ours may take the freed chord. Originally verified on `feat/tab-through-a-table` as d8bebdc and never pushed, so it missed #645. Reapplied here: #648 deleted `MONACO_DEFAULTS` from formatShortcutKeymap.test.ts in the meantime, and the commit's only dependency on it was a comment, so the test itself carried over intact. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(shortcuts): Mod+K inserts a link, and the chords move off Mod+K THE DECISION. Ctrl/Cmd+K is Insert Link in Typora, Bear, iA Writer and GitHub — the only command in the whole keymap survey where four independent sources agree exactly — and this app's link button had no keyboard shortcut at all. Insert Table moves from the `Mod+K T` sequence to `Mod+Alt+T` (Typora's and Bear's macOS chord), Zen Mode from `Mod+Shift+D` to `Mod+Alt+Z`, and the three list buttons — which had actions and no keys — get `Mod+Shift+7`, `Mod+Shift+8` and `Mod+Shift+9`. CHORD SEQUENCES ARE GONE, which is what started this: `Mod+K` then a second key was awkward enough to press that people stopped. Every binding here is one keystroke now, and `shortcutRegistry.test.ts` holds that as a rule rather than as a habit — for both keyboard layers and for the panel, so a new sequence on some other prefix fails too. WHAT MOD+K COSTS, enumerated rather than summarised. Monaco binds no complete keybinding there; it binds 31 chord SEQUENCES behind it, carrying 30 commands, identically on all three platforms. Twenty are folding, the other ten are add/remove comment line, format selection, reveal definition aside, peek widget focus, set/select-from selection anchor, move selection to next find match, show hover and trim trailing whitespace. All thirty keep their command palette entry. The folding bulk is smaller than it looks, and this was checked rather than assumed: nothing in `src/` registers a folding-range provider, and Monaco's own `basic-languages/markdown/markdown.js` declares folding only through `<!-- #region -->` markers nobody writes. What those twenty commands operate on is indentation folding. The folding this app's users actually have is the preview-side system in `foldState.ts`, which is click-driven and untouched. No replacement fold chords are added. THE GUARDRAIL COULD NOT SEE ANY OF THAT. `monacoChordOwnership.spec.ts` keys Monaco's map by the FULL chord, so `Meta+K` is not in it and the ownership rule matched nothing — it would have waved the largest keymap change this app has made straight through. A second rule now catches a chord claimed as a whole key that Monaco uses as a PREFIX, and DELIBERATE_OVERRIDES grew a `namespace` row shape whose staleness check looks behind the prefix instead of at it. INSERT COLUMN HAS NO CHORD, AND THIS IS THE ONE OPEN QUESTION. It was on `Mod+K C`, which can no longer be typed. `Mod+Alt+C` was the intended replacement and is not available: on macOS it is Monaco's `toggleFindCaseSensitive`, bound whenever the editor has focus, and registered with `registerEditorCommand` rather than `registerEditorAction` — so it has no command palette entry to fall back on, and taking it would leave find-case-sensitivity reachable only by mouse. That is the one thing no override in that file is allowed to do, so no collision was shipped. Insert Column is palette-only until a chord is chosen; the free letters on all three platforms are A B D E G H I J K M N O Q S T U V X Y Z. `Mod+Shift+9` for the task list has no precedent and the comments say so rather than implying one: GitHub documents no task-list chord, nor do Typora or Obsidian, and the two editors that do bind one disagree and are both occupied here (Bear's `Cmd+T` is New File; iA Writer's `Opt+Cmd+L` is Monaco's `toggleFindInSelection` on macOS). It is the digit adjacent to GitHub's documented 7 and 8, and nothing more. Every chord was verified against Monaco's real keybinding registry dumped per platform under jsdom, not against a hand-copied list, and against the app's own registry, every addAction/addCommand, the document handler in MarkdownViewer.svelte and the Rust menu accelerators. Tests: 902 pass, vitest 357, check 0 errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tests): stop asserting the two platforms differ by one word #651 made `Alt` render as `Opt` on macOS. Two expectations still spelled the pre-#651 output: one literal `Cmd+Alt+T`, and one assertion that the Cmd rendering IS the Ctrl rendering with `Ctrl` swapped for `Cmd`. That second one is a copy of the assumption #651 removed — "only Mod is substituted". Re-adding an Alt→Opt swap beside it would put the word map in a third place, so it now proves only what it is for: that the two renderings are still the same chord, same length and same final key. `shortcutRegistry.test.ts` owns the per-platform naming and fails on its own when the map breaks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: PathGao <gaoyanbo@gaoyanbodeMacBook-Air.local> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
toggle-zen-modewas registered onCtrlCmd|Shift|KeyZ. Dynamic keybindings register at weight 1000 against Monaco's 100, so it won — and Monaco assigns that chord to redo.bindToCurrentPlatformreturns the platform object whole rather than merging it, so the defaultsecondaryis discarded on macOS:Cmd+Shift+ZonlyCtrl+Y+Ctrl+Shift+ZCtrl+YsurvivesCtrl+Shift+Zis the GTK convention, so it is the likelier pressConfirmed by hand on a debug build. The native
PredefinedMenuItem::redoin the Edit menu does not save it.Windows and Linux look milder but read worse: macOS gives no response, while there the screen switches to zen mode — unrelated to what was pressed. And the panel has no Redo row (it is Monaco's, never registered), so nothing tells that user
Ctrl+Yexists.The fix is to move zen mode, not to give redo a second key
The defect is that we took a chord already correctly assigned. Handing the victim a replacement key buries that instead of fixing it.
Zen mode goes to
Mod+Shift+D— free in Monaco on all three platforms (its only D chords areMod+D,Ctrl+D,Mod+K Mod+D), free in the registry, in everyaddAction/addCommand, inMarkdownViewer's document handler, and among the Rust accelerators; not macOS system-reserved.F8was rejected because Apple keyboards needFnfor it, and VS Code'sMod+K Zsits in a namespace that may itself move.The guardrail is the point
One key moved leaves the next person free to repeat this, and the symptom is silent — a basic operation quietly disappears.
scripts/monacoChordOwnership.spec.tsnow checks everyaddActionandaddCommandbinding against Monaco's real defaults on all three platforms. Four rules: no unexplained collision, no stale allow-list row, the unrecoverable set still belongs to its owner, and the unrecoverable set has no allow-list escape hatch — so undo, redo, cut, copy, paste, save and select-all cannot be taken by writing a justification.Every allow-list entry it forced, all eight pre-existing:
fmt-italicMod+ItriggerSuggestfmt-underlineMod+UcursorUndofmt-quoteMod+Shift+.inPlaceReplace.downview-toggle-editCmd+EfindWithSelectionview-toggle-liveMod+LexpandLineSelectionfile-revealCtrl+Shift+Reditor.action.refactorcontinueListOnEnterEnteracceptSelectedSuggestionet al.indentListItemOnTabTabtabet al.None is the size of the zen-mode one — every command stays in the command palette, and the two list keys are
when-gated and fall through. Two are worth an eye:fmt-underlineovercursorUndois the nearest thing to an undo-family casualty (cursor history, not text), andview-toggle-edit/file-revealcollide on only some platforms, which the table now records rather than leaving to be rediscovered.Two measurement corrections
The right Monaco snapshot is the web branch, not the native one.
clipboard.jsbindsMod+X/C/Vonly whenisNative, and Markpad runs in a WebView: 366 / 299 / 293 rows rather than 369 / 305. Using the native branch would have forced three bogus allow-list entries for chords the shipped app legitimately owns.Faking
process.platformis not sufficient —platform.jschecks for a Nodeprocessbeforenavigator, soprocess.versions.nodehas to be hidden across the import and the platform then comes from the user agent.isNative === falseandisMacintoshare both asserted after import so the mode cannot silently flip.One deletion beyond the brief
MONACO_DEFAULTSinformatShortcutKeymap.test.ts— eleven hand-copied chords that described themselves as the weakest assertion in the file. The new spec reads the same data for real, so the copy was a second source of truth for exactly the thing being checked. Its one deliberate exception (Quote overinPlaceReplace.down) moved into the allow-list, so coverage is strictly stronger.Falsification
Guardrail written with zen mode absent from the allow-list, run against the unfixed tree: 3 of 5 red, naming
toggle-zen-mode takes Shift+Meta+Z from Monaco's redoon all three platforms. Mutatingfmt-strikethroughontoMod+Shift+Kfails the same way. Adding an allow-list row for zen mode onShift+Meta+Zfails with "that chord is redo" — the escape hatch is genuinely closed.npm test843 → 842 (the deleted copy) ·vitest351 → 356 ·check0 errors.🤖 Generated with Claude Code