feat(shortcuts): a read-only shortcuts reference, and one registry behind it - #488
Merged
Conversation
"What is the shortcut for X" was answered in three places that nothing held together: `editorToolbar.ts`, fourteen hard-coded literals in TitleBar's markup, and the handlers themselves. Only the first was tested, and only since #480. A shortcuts panel carrying its own list would have been a fourth copy, and the one users would trust most -- so this adds `src/lib/utils/shortcuts.ts` and points all three display sites at it. Net sources of truth: three down to one, plus the handlers the test fires at. The panel is a sixth Settings category, read-only. Remapping needs conflict detection, persistence, reset-to-default and chord capture inside a webview, and is a product-direction call; a reference gets most of the value with none of it. Settings was chosen over a standalone modal because it is already reachable (Ctrl+,) and needs no new global chord -- and a new chord would have to be cleared against Monaco's defaults, monaco-vim and both layers, which is the exact cost the read-only design avoids. Obsidian files Hotkeys under Settings too; VS Code's dedicated editor is a remapping UI, which this is not. Two literals were already wrong, and neither had a test: * The menu printed `Mod+Shift+S` beside Save As. Nothing binds that chord. The save branch is `cmdOrCtrl && key === 's'` with no Shift guard, so the advertised keystroke ran a plain Save -- writing the current file instead of asking where to put it. `saveContentAs` has no keyboard path at all. The label is removed rather than the binding invented: giving Save As a real chord is a behaviour change and the maintainer's call. * `showTooltip` took a suffix and composed `${modifier}+${suffix}`, so the Reload-from-Disk tooltip read "Ctrl+F5" for a binding that is plain F5 -- while the menu row two lines below it correctly said F5. It now takes a complete chord from the registry. Zero new command strings: every label reuses a dictionary key that already exists for a menu or context-menu entry, and the four group headings are the existing menu-bar categories. One new key total, `settings.shortcuts`, English only, per the policy i18nCoverage.test.ts states. The keymap harness from #480 moves to `scripts/keymapHarness.ts` so the new contract test uses the same one rather than a parallel model. It also records assignments now: `Mod+,` and the zoom chords work by writing a variable, and a call-only recorder reported them as dead keys. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
shortcuts.ts re-exported a type nothing imports from it. The panel, the menu and the toolbar all take their language from the i18n module directly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PathGao
added a commit
that referenced
this pull request
Aug 6, 2026
…ry (#493) #488 added the registry and checked every row against the code that implements it. Every assertion ran one way — registry to reality — so a chord the app binds that never reached the table was invisible to the whole file. `no registry entry is unverifiable` cannot see it: there is no entry to check. This adds the other direction, for both layers #488 left uncovered. Run against 5c1f83f before any fix, it is red twice: keyboard-reachable commands the shortcuts panel never mentions + [ 'isFullWidth=' ] native menu accelerators the shortcuts panel never mentions + [ 'CmdOrCtrl+,' ] `Mod+Alt+[` and `Mod+Alt+]` step the preview max width and were in no menu, no tooltip and no panel -- as undiscoverable as a shortcut gets. `CmdOrCtrl+,` is claimed by the native macOS menu, a third layer above the other two, and the registry knew about the document-level half only. Save As is the third item, and it is not what the completeness test found -- reality contains no Save As binding, so no reality-to-registry test can flag it. It is a real defect all the same, and #488 only half-addressed it. The app menu has printed `Mod+Shift+S` beside Save As for as long as the menu has existed; nothing ever bound it, and the save branch matched `cmdOrCtrl && key === 's'` with no Shift guard, so the advertised keystroke ran a plain Save -- silently overwriting the file the user was asking to write somewhere else. `saveContentAs` had no keyboard path at all, its only caller being the menu button. #488 deleted the false label. This binds the chord instead, which is what the menu always promised and what Ctrl+Shift+S means in every editor the guide's "align with mainstream practice" test would have you check, and puts the label back through the registry. Behaviour change, stated plainly: `Mod+Shift+S` used to save the current file and now opens the Save As dialog. Anyone who learned the accidental behaviour gets the documented one. `saveFromReadingMode.test.ts` sliced the plain-save branch from the guard text that had to change. Re-anchored, not weakened -- breaking the body still fails three of its five assertions. Verified against 5c1f83f: npm test 772 -> 774 (the two new tests), npm run check 0 errors, cargo test 125, npm run build clean, npm audit 0. Co-authored-by: PathGao <gaoyanbo@gaoyanbodeMacBook-Air.local> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Closed
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.
Users cannot discover the shortcuts. This adds a read-only reference — and,
because the answer already existed in three places, one registry the reference,
the app menu and the toolbar tooltips all read from.
Not a remapping UI. That needs conflict detection, persistence,
reset-to-default and chord capture inside a webview, and it is a product
direction call rather than a gap. A read-only reference gets most of the value
with none of that.
The problem this had to avoid
"What is the shortcut for X" was answered in three independent places:
src/lib/utils/editorToolbar.ts—shortcut?: (modifier) => stringsrc/lib/components/TitleBar.svelte— hard-coded literals in markupeditor.addActionkeybindings, andMarkdownViewer.svelte's documentkeydownA panel with its own list would have been a fourth copy, and the one users
would trust most. So the panel does not get a list:
src/lib/utils/shortcuts.tsis one registry that (1), (2) and the panel all read, and
scripts/shortcutRegistry.test.tsfires every chord in it at (3). Sources oftruth go from three to one.
Verifying the three-copy claim
Counted rather than assumed.
TitleBar.sveltehad 15menu-shortcutspans:14 chords plus one that reuses the class for the word "Reset" on the zoom
control. A fourth copy also existed that the brief did not mention —
showTooltip(e, text, 'Shift+B'), a suffix that the function composed into`${modifier}+${suffix}`, at 5 call sites. No test covered any of them.Two of them were already wrong
1. Save As advertised a chord that does not exist, and the chord did something
else. The menu printed
Mod+Shift+Sbeside Save As. Nothing binds it. Thesave branch is
so
Mod+Shift+Sfell into plain Save — silently writing the current fileinstead of asking where to put it.
saveContentAshas no keyboard path atall; its only caller is the menu button. Confirmed by firing the chord at the
real handler on all three platforms:
I removed the false label rather than invent the binding. Giving Save As a
real chord is a behaviour change (
Mod+Shift+Swould newly open a dialoginstead of saving) and it is yours to make. The two options, if you want it:
add
!e.shiftKeyto the save branch plus ashiftKey && key === 's'branchcalling
saveContentAs, or leave Save As menu-only as it is now.2. The Reload-from-Disk tooltip said
Ctrl+F5; the binding is plainF5.Because
showTooltipprefixed the modifier onto whatever suffix it was handed,'F5'becameCtrl+F5— while the menu row two lines below it correctly saidF5. Same fact, two copies, disagreeing. Fixed by making the parameter acomplete chord from the registry.
The registry
src/lib/utils/shortcuts.ts— 34 entries. Each carries a stable id (the Monacoaction id where one exists, which is also the toolbar tool id), an i18n key, its
chords, a display group, and the name of the thing that implements it. That
last field is what makes the rows checkable; a row that names nothing fails the
suite.
Chords are templates:
Modrenders asCmd/Ctrl, and a literalCtrlstaysCtrl— which is what tab cycling actually binds on macOS, sinceCmd+Tabisthe system switcher.
Where the panel lives
A sixth Settings category, argued rather than assumed:
Mod+,, kebab menu) and the categorysidebar exists, so it costs no new entry point.
cleared against Monaco's standalone defaults,
monaco-vim's keymap and bothapp layers — the exact cost the read-only design exists to avoid.
editor (
Ctrl+K Ctrl+S) is a remapping UI; this is not one, so borrowingits entry point would promise editing.
i18nCoverage.test.tsasserts the set of panes containing labelled controls isappearance/editor/files/preview; a read-only pane adds no<label for>, sothat stays true.
i18n — one new string
I checked what is enforced before choosing.
i18nCoverage.test.tsgates onEnglish only ("Per-locale completeness is REPORTED, never asserted"); the
26-locale bar in
editorContextMenuI18n.test.tsapplies to a hand-maintainedlist, not to new keys.
So the registry mints no command names: every label reuses a key that
already exists for a menu or context-menu entry, and the four group headings are
the existing menu-bar categories (
menu.file/edit/view/window). Onenew key total —
settings.shortcuts, for the nav item — English only, followingthe policy the test states.
Reusing keys bought full 26-locale coverage for 31 of 38 labels. It did not
for seven, and the test told me so when I claimed otherwise:
menu.moveToWindow,menu.windowmenu.reloadFromDisk,menu.findmenu.openFileLocation,menu.back,menu.forwardEvery one is a key the app menu already renders today, so the panel inherits
an existing gap rather than creating one, and
t()falls back to English exactlyas the menu does. They are pinned in
PARTIALLY_TRANSLATEDso a newunder-translated label fails. That table is a ready-made translation worklist.
Tests
scripts/shortcutRegistry.test.ts, 16 tests. The contract is every chord thepanel shows is the chord that actually fires.
The #480 harness moved to
scripts/keymapHarness.tsand both files import it —extended, not duplicated, so the registry cannot be checked against a more
forgiving model of the app than #480's tests use.
formatShortcutKeymap.test.tskeeps all 10 of its tests, unchanged in substance.
One harness fix, found by writing the new tests:
documentKeymaprecordedonly calls, and
Mod+,and the zoom chords do their work by assigning(
showSettings = true,zoomLevel = …). They looked like dead keys. The scopeproxy's
settrap discarded the write; it now records it, which is theempty-iteration failure one level down.
Mathalso had to be handed inexplicitly —
has: () => truemeant the scope claimed it too, soMath.minreturned a recording stub instead of a number.
Falsification
Every assertion was broken, the failure read, and the change reverted.
fmt-quote→Mod+Shift+Q)macOS: the panel would show Cmd+Shift+Q for fmt-quote, but the editor binds it to Shift+Meta+.Editor.svelteShift+E→Shift+G)macOS: the panel would show Cmd+Shift+E for fmt-inline-code, but the editor binds it to Shift+Meta+Gapp-find→Mod+G)macOS: the panel would show Cmd+G for app-find, but that chord does nothing outside the editorMod+Shift+SmacOS: Shift+Meta+S is advertised as file-save-as (saveContentAs) but runs saveContent+/−in the handlerMeta+= zooms in on macOSevery menu chord comes from shortcutLabel()a chord literal is back in editorToolbar.ts; it belongs in shortcuts.tsmade-up names no implementation, so nothing here can confirm its chord firesmacOS: Meta+1 is advertised by fmt-heading-1 and fmt-heading-2app-exit defers to the native CmdOrCtrl+X, which the menu does not claimfmt-boldrow (binding kept)fmt-boldas unexplainedchecking 4 label keys— the vacuity guards firelabelKeyi18nCoveragemenu.boldish <- src/lib/utils/shortcuts.tsThe last one confirms the existing
labelKey: '…'harvest already reaches theregistry, so panel labels are checked by the repo's own i18n gate, not only by
mine.
One assertion was deleted for being untestable. "The toolbar hint equals the
registry" survived deleting a registry row — once
editorToolbar.tsderives fromshortcuts.ts, both sides are the same copy. It is replaced by a check that nochord literal remains in that file; the toolbar-vs-reality link stays where it
is real, in
formatShortcutKeymap.test.ts, which compares the rendered hintagainst the keybinding
Editor.svelteregisters.What I migrated, and what I did not
Migrated: all 14 TitleBar chord literals, all 5 tooltip suffixes, and every
editorToolbar.tshint. No shortcut literal remains in either file, and twotests keep it that way.
Left behind, deliberately:
TabList.svelte's hard-coded(Ctrl+T)tooltip, which says Ctrl on macOStoo. feat(editor): shortcuts for the six unbound formatting commands, and one meaning for Ctrl+T #480 named it as out of scope and it stays that way — a separate file
with a separate mechanism.
Mod+C/Mod+V). OS conventions, andMod+Vis abare
addCommandwith no id, so listing Copy without Paste is a worsehalf-answer. Recorded in
NOT_ADVERTISEDwith that reason, and the test failsif the binding disappears.
view-toggle-split's second binding (OEM_102, the extra backslash key onISO keyboards) is not displayed — same gesture, not a second chord to teach.
Known weakness in the panel:
tab-move-window(Mod+Shift+M) reads"Move to", which is the app's own name for that family but is vague standing
alone.
carryActiveTabToNextWindowmoves the tab to the next window, or detachesit to a new one when there is only one — so
menu.moveToNewWindowwould beactively wrong. I kept the vague-but-true label rather than mint a string or drop
a real shortcut; say the word and I will do either.
Verification
Against the current
masterbaseline (7aa2aa9), not #480's numbers:+16 tests is exactly
shortcutRegistry.test.ts.formatShortcutKeymap.test.tsstays at 10 across the harness extraction, so nothing was lost in the move.
Not verified
declares — the actions
Editor.svelteregisters and the brancheshandleKeyDowntakes, both executed rather than pattern-matched — plus thenative accelerators in
lib.rs. What a running WebView does with these chords,how
monaco-vimshadows them, and Monaco's own resolution order are reasonedfrom source, not observed.
per-platform rows come from Monaco's
decodeKeybindingrun once perOperatingSystem.shortcutSections()output was inspected as text, but no screenshot exists —layout, the
kbdstyling in each theme, and whether 34 rows scroll comfortablyin the 420px settings modal are unconfirmed.
Cmd+Shift+Erather than
⇧⌘E. That matches what the app menu already prints.Based on
master(7aa2aa9), which already contains #480, #479 and #481. Mybrief said to stack this on #480's branch; #480 merged while I was working, so
this is a plain PR against master.
🤖 Generated with Claude Code