fix(shortcuts): Alt is called Option on a Mac - #651
Merged
Conversation
`formatChord` substituted only `Mod`, and its docblock defended that:
"Everything else — `Ctrl`, `Alt`, `F5`, `Shift` — is already the literal
the key carries." False for `Alt`. Apple keyboards print `⌥ option`; the
word "Alt" is on no modern Mac keyboard. The panel was telling Mac users
to press a key that does not exist by that name, on three rows:
nav-back Alt+Left -> Option+Left
nav-forward Alt+Right -> Option+Right
view-preview-width Cmd+Alt+[ ] -> Cmd+Option+[ ]
The third was found by the new test, not by hand — which is the argument
for having it. Windows and Linux render exactly as before.
WHERE THE SUBSTITUTION WENT
Inside `formatChord`, in a table of per-platform key words keyed by the
`Mod` word, not in a second `if`. `Cmd` IS macOS: `modifierFor` is the
only producer of these two values, so the argument callers already pass
carries the platform. The function can ask which platform it renders for
after all — it just had to read its argument as the platform tag it is.
Considered giving `formatChord` (and `shortcutLabel`) a platform instead,
which would remove the proxy rather than lean on it. Not worth it here:
the modifier travels through `EditorToolbarTool.shortcut`'s signature,
EditorToolbar's `modifier` prop, and ~25 call sites in TitleBar, Tab,
TabList and MarkdownViewer; it would leave `modifierFor` with no callers
outside this file, and `singleImplementationConvention.test.ts` and
`platformSource.spec.ts` both hold assertions about those exact call
shapes. Six source files and four test files to move one word. The table
is where the next modifier lands either way.
GUARDRAIL
`scripts/shortcutRegistry.test.ts` enumerates the modifier tokens across
SHORTCUTS — every `+`-separated part but the last — and requires each to
be listed in MODIFIER_NAMES with BOTH of its platform names, then that
the panel render those names. A row added later with an unclassified
modifier fails outright instead of defaulting to "same word everywhere",
which is the assumption that produced this defect. Red-first: it failed
on `view-preview-width` with `'Alt' !== 'Option'`.
NOT DONE: switching the panel to ⌘⌥⇧⌃. That is a separate question about
matching macOS convention wholesale — the panel uses word form throughout
(Cmd, Ctrl, Shift), and mixing one symbol into words reads worse than
either. Worth raising as its own change.
No i18n string hard-codes a key name; the locale table was checked.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PathGao
force-pushed
the
fix/alt-is-called-option-on-a-mac
branch
from
August 12, 2026 16:18
caa51f9 to
b0bd654
Compare
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.
The shortcuts panel told macOS users to press a key their keyboard does not have. Apple keyboards print
⌥ option; the word "Alt" appears on no modern Mac keyboard.Rendered as
Opt, notOption, because the panel already abbreviates every modifier it can —Cmdnot Command,Ctrlnot Control. Spelling this one out would have been the odd row.formatChord's docblock stated the assumption outright:True for
CtrlandShift, false forAlt. Caught during hand-testing, when a proposedCmd+Alt+Tbinding sent someone looking for a key that isn't labelled that.Three rows were affected, not two — the new test found the third:
nav-backAlt+LeftOpt+Leftnav-forwardAlt+RightOpt+Rightview-preview-widthCmd+Alt+[/]Cmd+Opt+[/]Windows and Linux output is byte-identical, verified by rendering both.
Where the substitution went
A per-platform word table inside
formatChord, keyed by theModword:I asked for the signature to take a platform instead, on the theory that
formatChordcould not know which platform it was rendering for and so any second modifier would have to be a special case. That premise is wrong in a useful way:modifierForis the only producer of'Cmd' | 'Ctrl', and it returns'Cmd'iffosType === 'macos'. The argument is already a total encoding of the platform distinction — it only had to be read as a platform tag rather than as one output word. The table is therefore the general fix, not a second special case beside the first.The signature change was traced before being rejected: a platform argument propagates through
shortcutLabel,EditorToolbarTool.shortcut's public function type,EditorToolbar'smodifierprop and ~25 call sites inTitleBar,Tab,TabListandMarkdownViewer— and would leavemodifierForwith no callers outsideshortcuts.ts, breaking assertions insingleImplementationConvention.test.ts(an entry whose whole rationale is the four sites that failed to adoptmodifierFor) andplatformSource.spec.ts. Six source files and four test files to move one word.The guardrail is the point
This is the shape the codebase has been closing all session: a translation mechanism existed and one member never joined. A one-line rename leaves the next modifier free to repeat it.
The new test enumerates modifier tokens across
SHORTCUTS— every+-separated part except the last, sequence-aware forMod+K T— requires each to appear in a table carrying both platform names, then requires the panel to render them positionally on both platforms.The part that stops the repeat: an unlisted token fails outright rather than defaulting to "same word everywhere" — that default is exactly the assumption that produced this defect. A row added later with
Meta+Xcannot pass until someone states what macOS calls that key.Scope checks
No i18n string hard-codes a key name. The locale table was grepped for
Alt+, quoted'Alt',Option+and⌥; every hit is a Spanish/Portuguese/Italian/Turkish word (Alternar,Altrimenti,Altı). Every displayed chord routes throughformatChord, so those three rows were the entire user-visible surface.The machine side cannot desync.
toMonacoLabelandkeymapHarnessparse raw registry chords (Alt+Left), neverformatChordoutput, andMarkdownViewer'saltKeybranches read the DOM event. Display and binding stay independent.Not done deliberately: the ⌘⌥⇧⌃ symbol set. That is a separate question about matching macOS convention wholesale; the panel uses word form throughout, and mixing one symbol into words would be worse than either choice.
npm test900 → 901 ·vitest356 ·check0 errors.🤖 Generated with Claude Code