Skip to content

fix(shortcuts): Alt is called Option on a Mac - #651

Merged
PathGao merged 1 commit into
masterfrom
fix/alt-is-called-option-on-a-mac
Aug 12, 2026
Merged

fix(shortcuts): Alt is called Option on a Mac#651
PathGao merged 1 commit into
masterfrom
fix/alt-is-called-option-on-a-mac

Conversation

@PathGao

@PathGao PathGao commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

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, not Option, because the panel already abbreviates every modifier it can — Cmd not Command, Ctrl not Control. Spelling this one out would have been the odd row.

formatChord's docblock stated the assumption outright:

Only Mod is substituted. Everything else — Ctrl, Alt, F5, Shift — is already the literal the key carries.

True for Ctrl and Shift, false for Alt. Caught during hand-testing, when a proposed Cmd+Alt+T binding sent someone looking for a key that isn't labelled that.

Three rows were affected, not two — the new test found the third:

id before (macOS) after
nav-back Alt+Left Opt+Left
nav-forward Alt+Right Opt+Right
view-preview-width Cmd+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 the Mod word:

const PLATFORM_KEY_WORDS = {
	Cmd:  { Mod: 'Cmd',  Alt: 'Opt' },
	Ctrl: { Mod: 'Ctrl', Alt: 'Alt' },
} as const;

I asked for the signature to take a platform instead, on the theory that formatChord could 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: modifierFor is the only producer of 'Cmd' | 'Ctrl', and it returns 'Cmd' iff osType === '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's modifier prop and ~25 call sites in TitleBar, Tab, TabList and MarkdownViewer — and would leave modifierFor with no callers outside shortcuts.ts, breaking assertions in singleImplementationConvention.test.ts (an entry whose whole rationale is the four sites that failed to adopt modifierFor) and platformSource.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 for Mod+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+X cannot pass until someone states what macOS calls that key.

✖ every modifier the panel prints is what that platform calls the key
  AssertionError: view-preview-width: the panel shows "Cmd+Alt+[" on macos,
  but macos calls that key "Opt", not "Alt"

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 through formatChord, so those three rows were the entire user-visible surface.

The machine side cannot desync. toMonacoLabel and keymapHarness parse raw registry chords (Alt+Left), never formatChord output, and MarkdownViewer's altKey branches 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 test 900 → 901 · vitest 356 · check 0 errors.

🤖 Generated with Claude Code

`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
PathGao force-pushed the fix/alt-is-called-option-on-a-mac branch from caa51f9 to b0bd654 Compare August 12, 2026 16:18
@PathGao
PathGao merged commit cfc99ac into master Aug 12, 2026
4 checks passed
@PathGao
PathGao deleted the fix/alt-is-called-option-on-a-mac branch August 12, 2026 16:29
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>
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.

1 participant