Skip to content

feat(shortcuts): a read-only shortcuts reference, and one registry behind it - #488

Merged
PathGao merged 2 commits into
masterfrom
feat/shortcuts-reference
Aug 6, 2026
Merged

feat(shortcuts): a read-only shortcuts reference, and one registry behind it#488
PathGao merged 2 commits into
masterfrom
feat/shortcuts-reference

Conversation

@PathGao

@PathGao PathGao commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

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:

# where held by a test?
1 src/lib/utils/editorToolbar.tsshortcut?: (modifier) => string yes, since #480
2 src/lib/components/TitleBar.svelte — hard-coded literals in markup no
3 the handlers — editor.addAction keybindings, and MarkdownViewer.svelte's document keydown is the truth

A 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.ts
is one registry that (1), (2) and the panel all read, and
scripts/shortcutRegistry.test.ts fires every chord in it at (3). Sources of
truth go from three to one.

Verifying the three-copy claim

Counted rather than assumed. TitleBar.svelte had 15 menu-shortcut spans:
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+S beside Save As. Nothing binds it. The
save branch is

if (cmdOrCtrl && key === 's') {          // no !e.shiftKey

so Mod+Shift+S fell into plain Save — silently writing the current file
instead of asking where to put it. saveContentAs has no keyboard path at
all
; its only caller is the menu button. Confirmed by firing the chord at the
real handler on all three platforms:

macOS    Shift+Meta+S   -> saveContent
Windows  Ctrl+Shift+S   -> saveContent
Linux    Ctrl+Shift+S   -> saveContent

I removed the false label rather than invent the binding. Giving Save As a
real chord is a behaviour change (Mod+Shift+S would newly open a dialog
instead of saving) and it is yours to make. The two options, if you want it:
add !e.shiftKey to the save branch plus a shiftKey && key === 's' branch
calling saveContentAs, or leave Save As menu-only as it is now.

2. The Reload-from-Disk tooltip said Ctrl+F5; the binding is plain F5.
Because showTooltip prefixed the modifier onto whatever suffix it was handed,
'F5' became Ctrl+F5 — while the menu row two lines below it correctly said
F5. Same fact, two copies, disagreeing. Fixed by making the parameter a
complete chord from the registry.

The registry

src/lib/utils/shortcuts.ts — 34 entries. Each carries a stable id (the Monaco
action 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: Mod renders as Cmd/Ctrl, and a literal Ctrl stays
Ctrl — which is what tab cycling actually binds on macOS, since Cmd+Tab is
the system switcher.

Where the panel lives

A sixth Settings category, argued rather than assumed:

  • Settings is already reachable (Mod+,, kebab menu) and the category
    sidebar exists, so it costs no new entry point.
  • It needs no shortcut of its own. A new global chord would have to be
    cleared against Monaco's standalone defaults, monaco-vim's keymap and both
    app layers — the exact cost the read-only design exists to avoid.
  • Precedent: Obsidian files Hotkeys under Settings. VS Code's dedicated
    editor (Ctrl+K Ctrl+S) is a remapping UI; this is not one, so borrowing
    its entry point would promise editing.

i18nCoverage.test.ts asserts the set of panes containing labelled controls is
appearance/editor/files/preview; a read-only pane adds no <label for>, so
that stays true.

i18n — one new string

I checked what is enforced before choosing. i18nCoverage.test.ts gates on
English only ("Per-locale completeness is REPORTED, never asserted"); the
26-locale bar in editorContextMenuI18n.test.ts applies to a hand-maintained
list, 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). One
new key total — settings.shortcuts, for the nav item — English only, following
the 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:

key locales missing
menu.moveToWindow, menu.window 23
menu.reloadFromDisk, menu.find 22
menu.openFileLocation, menu.back, menu.forward 21

Every 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 exactly
as the menu does. They are pinned in PARTIALLY_TRANSLATED so a new
under-translated label fails. That table is a ready-made translation worklist.

Tests

scripts/shortcutRegistry.test.ts, 16 tests. The contract is every chord the
panel shows is the chord that actually fires
.

The #480 harness moved to scripts/keymapHarness.ts and 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.ts
keeps all 10 of its tests, unchanged in substance.

One harness fix, found by writing the new tests: documentKeymap recorded
only calls, and Mod+, and the zoom chords do their work by assigning
(showSettings = true, zoomLevel = …). They looked like dead keys. The scope
proxy's set trap discarded the write; it now records it, which is the
empty-iteration failure one level down. Math also had to be handed in
explicitly — has: () => true meant the scope claimed it too, so Math.min
returned a recording stub instead of a number.

Falsification

Every assertion was broken, the failure read, and the change reverted.

broke failing test message
registry chord ≠ editor binding (fmt-quoteMod+Shift+Q) chord the editor really registers macOS: the panel would show Cmd+Shift+Q for fmt-quote, but the editor binds it to Shift+Meta+.
the real binding (Editor.svelte Shift+EShift+G) macOS: the panel would show Cmd+Shift+E for fmt-inline-code, but the editor binds it to Shift+Meta+G
registry chord that fires nothing (app-findMod+G) runs the command it names macOS: the panel would show Cmd+G for app-find, but that chord does nothing outside the editor
re-added Save As at Mod+Shift+S macOS: Shift+Meta+S is advertised as file-save-as (saveContentAs) but runs saveContent
swapped zoom +/ in the handler zoom in raises the level Meta+= zooms in on macOS
a literal back in TitleBar menu prints no literal every menu chord comes from shortcutLabel()
a literal back in editorToolbar.ts toolbar declares no chord a chord literal is back in editorToolbar.ts; it belongs in shortcuts.ts
a row naming no implementation no entry is unverifiable made-up names no implementation, so nothing here can confirm its chord fires
two rows on one chord no two entries advertise the same chord macOS: Meta+1 is advertised by fmt-heading-1 and fmt-heading-2
wrong native accelerator native accelerator the menu claims app-exit defers to the native CmdOrCtrl+X, which the menu does not claim
deleted the fmt-bold row (binding kept) every keybinding is advertised or consciously not listed fmt-bold as unexplained
emptied the registry the registry was actually read checking 4 label keys — the vacuity guards fire
bogus labelKey the repo's own i18nCoverage menu.boldish <- src/lib/utils/shortcuts.ts

The last one confirms the existing labelKey: '…' harvest already reaches the
registry, 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.ts derives from
shortcuts.ts, both sides are the same copy. It is replaced by a check that no
chord literal remains in that file; the toolbar-vs-reality link stays where it
is real, in formatShortcutKeymap.test.ts, which compares the rendered hint
against the keybinding Editor.svelte registers.

What I migrated, and what I did not

Migrated: all 14 TitleBar chord literals, all 5 tooltip suffixes, and every
editorToolbar.ts hint. No shortcut literal remains in either file, and two
tests keep it that way.

Left behind, deliberately:

  • TabList.svelte's hard-coded (Ctrl+T) tooltip, which says Ctrl on macOS
    too. 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.
  • The clipboard chords (Mod+C/Mod+V). OS conventions, and Mod+V is a
    bare addCommand with no id, so listing Copy without Paste is a worse
    half-answer. Recorded in NOT_ADVERTISED with that reason, and the test fails
    if the binding disappears.
  • view-toggle-split's second binding (OEM_102, the extra backslash key on
    ISO 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. carryActiveTabToNextWindow moves the tab to the next window, or detaches
it to a new one when there is only one — so menu.moveToNewWindow would be
actively 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 master baseline (7aa2aa9), not #480's numbers:

                  baseline        this branch
npm run check     0 errors        0 errors, 652 files
npm test          746 tests       762 tests, 762 pass
cargo test        125 passed      125 passed   (no Rust touched)
npm run build     clean           clean
npm audit         0 vulns         0 vulns

+16 tests is exactly shortcutRegistry.test.ts. formatShortcutKeymap.test.ts
stays at 10 across the harness extraction, so nothing was lost in the move.

Not verified

  • I did not press any of these keys. Everything above is the keymap the app
    declares — the actions Editor.svelte registers and the branches
    handleKeyDown takes, both executed rather than pattern-matched — plus the
    native accelerators in lib.rs. What a running WebView does with these chords,
    how monaco-vim shadows them, and Monaco's own resolution order are reasoned
    from source, not observed.
  • I worked on macOS. Windows and Linux were not exercised at all; the
    per-platform rows come from Monaco's decodeKeybinding run once per
    OperatingSystem.
  • The panel was never rendered. It type-checks and builds, and
    shortcutSections() output was inspected as text, but no screenshot exists —
    layout, the kbd styling in each theme, and whether 34 rows scroll comfortably
    in the 420px settings modal are unconfirmed.
  • Chord display conventions are not localised: macOS users see Cmd+Shift+E
    rather than ⇧⌘E. That matches what the app menu already prints.

Based on master (7aa2aa9), which already contains #480, #479 and #481. My
brief 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

PathGao and others added 2 commits August 6, 2026 11:03
"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
PathGao merged commit 10483d2 into master Aug 6, 2026
4 checks passed
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>
@PathGao PathGao mentioned this pull request Aug 6, 2026
@PathGao
PathGao deleted the feat/shortcuts-reference branch August 6, 2026 04:09
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