feat(editor): shortcuts for the six unbound formatting commands, and one meaning for Ctrl+T - #480
Merged
Merged
Conversation
`fmt-inline-code`, `fmt-code-block`, `fmt-quote` and the three headings have been in the toolbar and the command palette since v2.6.12 and answered no key. The report (#121) is from a QWERTZ user, for whom the backtick is a dead key: typing a code fence needs three dead-key escapes, which is why the request is for a shortcut and not for the command. Bold, Italic and Underline already had Ctrl/Cmd+B/I/U. These six now have: | action | Windows/Linux | macOS | why that chord | |--------------------|----------------|--------------|---------------------------------| | `fmt-heading-1..3` | Ctrl+1/2/3 | Cmd+1/2/3 | Typora, Mark Text | | `fmt-inline-code` | Ctrl+Shift+E | ⇧⌘E | GitHub's Ctrl+E, plus Shift | | `fmt-code-block` | Ctrl+Shift+F | ⇧⌘F | no precedent survives Monaco | | `fmt-quote` | Ctrl+Shift+. | ⇧⌘. | GitHub | Every one of them was checked against standalone Monaco's OWN defaults, dumped from the installed monaco-editor 0.55.1 rather than read off VS Code's documentation — the two keymaps are not the same, and `editor.addAction` registers at weight 1000, above every Monaco default, so a clash does not error: our action simply wins and Monaco's loses its key. That check rejected the obvious candidates: - Ctrl/Cmd+E for inline code (GitHub's chord) is `view-toggle-edit` here already — and Ctrl+E for edit/read is itself the mainstream reading, in Obsidian and Mark Text. So inline code takes the same letter with Shift. - Ctrl/Cmd+Shift+K for a code block (Typora, Mark Text) is `editor.action.deleteLines` on BOTH platforms, not just on Windows. Their macOS alternative, Cmd+Option+C, is `toggleFindCaseSensitive`. No mainstream code-fence chord is free, so this one is a deviation and says so. - Ctrl/Cmd+Shift+Q for a blockquote (Typora, Mark Text) is the macOS system Log Out on ⇧⌘Q, which is exactly why both of them use Cmd+Option+Q there. GitHub's Ctrl+Shift+. is the same on both platforms, and Shift+. is `>`. Quote is the one binding that takes a chord away from Monaco: `editor.action.inPlaceReplace.down`, which has no menu or toolbar presence here, means nothing in Markdown, and stays in the command palette. Headings stop at 3 because the app has three heading actions. 4, 5 and 6 are left unbound rather than given to something else, so completing Typora's range later moves nothing. The toolbar hint is the second copy of each of these facts, so it is filled in too, and the test holds the two together — which it did not for Bold, Italic and Underline either. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Ctrl/Cmd+T meant two things (#392). Monaco resolves its keybindings on the editor's container node and calls stopPropagation() when it consumes one, so the document-level handler in MarkdownViewer.svelte only ever runs for keystrokes the editor did not claim. Inside the editor, `file-new` answered Ctrl+T and opened an Untitled buffer; outside it, the handler opened a Home tab. Same key, different document, decided by where the caret happened to be. The issue reported three paths. There are now two: the macOS native menu was cut back to Settings and Quit by #281 and no longer takes Cmd+T at all, and this asserts that, so a fourth meaning cannot be added above the other two without failing. The app's own labels had already picked a side. The tab strip's + button is titled "New Tab (Ctrl+T)" and calls `addNewTab`; the app menu prints Ctrl/Cmd+T beside "New File". The branch was the odd one out, not the labels, which is what @alecdotdev's "new file seems the common reading" says in #392. `file-new` also binds Ctrl/Cmd+N, and the document handler knew about only one of the two — so Ctrl+N did nothing at all outside the editor. Both keys are handled here now, and the test asserts the two layers agree rather than asserting either one in isolation. ## The Home tab `addHomeTab()` had exactly one caller in the app, and it was this branch, so unifying Ctrl+T does remove the only way to open the home screen AS A TAB. What it does not remove is the home screen: the app menu's Home item toggles `showHome`, which renders the same `<HomePage>` with the same recent files, pinned tags and callbacks. The tab form was already the weaker of the two — it is dropped on session restore (`hasRealFilePath` is false for the sentinel) and closed when windows are merged, so it never survived a restart anyway. `addHomeTab` and the `isHomePath` special cases stay. The sentinel still has to be recognised for snapshots written by older builds, and #429's render gate is tested against it. Removing the now-callerless constructor is a separate cleanup and is deliberately not bundled here. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 6, 2026
PathGao
added a commit
that referenced
this pull request
Aug 6, 2026
…hind it (#488) * feat(shortcuts): one registry behind the panel, the menu and the toolbar "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> * refactor(shortcuts): drop an unused LanguageCode re-export 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> --------- Co-authored-by: PathGao <gaoyanbo@gaoyanbodeMacBook-Air.local> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 6, 2026
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.
Closes #121. Closes #392.
Two keyboard items that needed the same thing first — the whole keymap — so
they share one PR and one test file. @alecdotdev approved the Ctrl+T direction
in #392
("proposed solutions sounds good to me"); #121 is from @kago76, on a German
QWERTZ keyboard where the backtick is a dead key.
The keymap
Standalone Monaco is not VS Code. Its defaults were dumped from the
installed
monaco-editor@0.55.1— loadmonaco-editorunder a DOM shim andprint
KeybindingsRegistry.getDefaultKeybindings()once perprocess.platform— rather than read off VS Code's keybinding docs, because two of the four
chords proposed for this change survive the VS Code reading and die on the real
one. It matters that a clash is silent:
editor.addActionregisters dynamickeybindings at weight 1000, above
EditorContrib(100) andEditorCore(0), so Markpad always wins the chord and the Monaco command just stops
answering its key.
Editor layer — every action
Editor.svelteregisters with a keybindingNew in this PR marked ★.
custom-copyCtrl+C⌘Ceditor.action.clipboardCopyAction(deliberate — goes via the Rust clipboard)addCommand)Ctrl+V⌘Veditor.action.clipboardPasteAction(deliberate)toggle-tabsCtrl+Shift+B⇧⌘Btoggle-zen-modeCtrl+Shift+Z⇧⌘Zredofmt-boldCtrl+B⌘Bfmt-italicCtrl+I⌘Ieditor.action.triggerSuggest(+2 suggest-widget commands)fmt-underlineCtrl+U⌘UcursorUndofmt-inline-codeCtrl+Shift+E⇧⌘Efmt-code-blockCtrl+Shift+F⇧⌘Ffmt-quoteCtrl+Shift+.⇧⌘.editor.action.inPlaceReplace.downfmt-heading-1Ctrl+1⌘1fmt-heading-2Ctrl+2⌘2fmt-heading-3Ctrl+3⌘3insert-table-simpleCtrl+KT⌘KTCtrl+Kchords all takeCtrlon the second key)file-newCtrl+N,Ctrl+T⌘N,⌘Tfile-openCtrl+O⌘Ofile-saveCtrl+S⌘Sfile-closeCtrl+W⌘Wfile-revealCtrl+Shift+R⇧⌘Reditor.action.refactor(Windows/Linux only)view-toggle-editCtrl+E⌘Eactions.findWithSelection(macOS only)view-toggle-liveCtrl+L⌘LexpandLineSelectionview-toggle-splitCtrl+\,Ctrl+OEM_102⌘\,⌘OEM_102tab-next/tab-prevCtrl+Tab/Ctrl+Shift+TabCtrl+Tab/Ctrl+Shift+Tab(real Ctrl —⌘Tabis the system switcher)tab-undo-closeCtrl+Shift+T⇧⌘Tapp-command-paletteCtrl+P⌘PRegistered with no keybinding, unchanged:
toggle-minimap,toggle-word-wrap,toggle-line-numbers,toggle-vim-mode,toggle-status-bar,toggle-word-count,toggle-line-highlight,toggle-occurrences-highlight,toggle-whitespace,fmt-bullet-list,fmt-numbered-list,fmt-checklist,fmt-link.Document layer —
MarkdownViewer.svelte's<svelte:document onkeydown>This layer runs only when Monaco did not claim the key: the standalone
keybinding service listens on the editor's container node and calls
stopPropagation()on every chord it resolves, so a keystroke reaches thedocument handler only from outside the editor.
Ctrl+W/F4close · ★Ctrl+TandCtrl+Nnew file ·Ctrl+Oopen ·Ctrl+Ssave ·Ctrl+Etoggle edit ·Ctrl+\split ·Ctrl+Shift+Tundoclose tab ·
Ctrl+Shift+Mmove tab to next window ·Ctrl+Tab/Ctrl+PageUp/Down/⌘⌥←→cycle tabs ·Alt+←/→file history ·Ctrl+=/-/0zoom ·
Ctrl+,settings ·Ctrl+Ffind ·Ctrl+Alt+[/]preview width ·F5reload ·Ctrl+Qclose window (returns early on macOS, where the nativemenu owns it).
Native menu (macOS only)
⌘,Settings and⌘QQuit. That is the whole menu since #281 — the thirdCtrl+T path #392 described is already gone, and there is now a test that keeps
it gone.
Vim mode
monaco-vimhookseditor.onKeyDown, which fires at the textarea before thekeybinding service's container listener, and calls
stopPropagation()when itskeymap matches — so a chord in vim's default keymap shadows the app's action
while vim is on. Its keymap contains
<C-a> <C-b> <C-c> <C-d> <C-e> <C-f> <C-i> <C-n> <C-o> <C-p> <C-q> <C-r> <C-t> <C-u> <C-v> <C-w> <C-x> <C-y> <C-Space> <C-BS> <C-[>and no digits and no Ctrl+Shift combinations.monacoToCmKeyspells ⌘ asMeta-, which vim never matches, so macOS isunaffected throughout. All six new chords are therefore vim-safe on every
platform; several existing ones (
Ctrl+B,Ctrl+I,Ctrl+U,Ctrl+E,Ctrl+N,Ctrl+O,Ctrl+P,Ctrl+T,Ctrl+W) are not, on Windows/Linux.That is pre-existing and not touched here.
#121 — the six bindings, and what each of them displaced
Each proposal was checked against the table above; three of the four obvious
candidates failed.
Ctrl/⌘+1/2/3Ctrl/Cmd+1..6; Mark Text bindsCmd+1..6on macOS. Free in standalone Monaco on both platforms, free in Markpad, absent frommonaco-vim.Ctrl/⌘+Shift+ECtrl/Cmd+E— but plainCtrl/⌘Eis alreadyview-toggle-edithere, and that is the mainstream reading (ObsidianCtrl+Eedit↔read, Mark TextCtrl+Esource mode). Same letter, plus Shift. Typora'sCtrl+Shift+`and Mark Text'sCtrl+`were rejected on purpose: #121 is about the backtick dead key.Ctrl/⌘+Shift+FCtrl+Shift+Kon Windows/Linux —editor.action.deleteLinesin standalone Monaco on both platforms — and both fall back toCmd+Option+Con macOS, which istoggleFindCaseSensitive.Fis for the fences ("Code Fences" is Typora's own name for the command). This one is a deviation, not a precedent.Ctrl/⌘+Shift+.Shift+.is>. Typora and Mark Text useCtrl+Shift+Q, which macOS cannot deliver —⇧⌘Qis the system Log Out — which is exactly why both of them useCmd+Option+Qthere.Corrections to what was proposed to me, since they are the interesting part:
Ctrl+Efor inline code is wrong here. It is GitHub's chord, but it isMarkpad's
view-toggle-edit, and on macOS it is also Monaco'sactions.findWithSelection.Ctrl+Shift+Qfor blockquote is wrong on macOS. Typora's blockquote isCtrl+Shift+Q(Win/Linux) /Cmd+Option+Q(macOS) — notCtrl+Q, andthe macOS half exists because
⇧⌘Qis taken by the OS.Ctrl+Shift+K→deleteLineswas right, and it is worse than described:it collides on macOS too, and Typora's macOS alternative collides as well.
Ctrl+Einserts inlinecode, not a fenced block ("Inserts Markdown formatting for a code
one-liner"). Obsidian's official docs document no formatting hotkeys beyond
macOS
Cmd+B/Cmd+I.Headings 1–6? No — 1–3 only. The app has three heading actions;
LINE_MARKERS, the toolbar and the context menu all stop at H3, andeditorContextMenuI18n.test.tsrequires every formatting label to betranslated in all 26 locales, so H4–H6 is three actions, three toolbar entries
and 78 translations — a feature, not a keybinding.
Ctrl+4/5/6are leftunbound rather than given to something else, so completing Typora's range later
moves nothing.
editorToolbar.tscarries the second copy of each of these facts (the buttontooltip), so it is filled in too, and a new test holds the two together — which
nothing did for Bold, Italic and Underline either.
#392 — Ctrl+T
Verified before changing anything: it is now two paths, not three. Monaco's
file-newanswersCtrl+Tinside the editor and opens an Untitled buffer; thedocument handler answered it outside the editor and opened a Home tab. The
macOS native menu no longer takes it — #281 cut the menu to Settings and Quit —
so the issue's third path is already gone.
Both of the app's own labels had already picked "new file": the tab strip's
+button is titledNew Tab (Ctrl+T)and callsaddNewTab, and the appmenu prints
Ctrl/⌘+Tbeside New File. The branch was the odd one out.file-newalso bindsCtrl/⌘+N, and the document handler knew about only oneof the two — so
Ctrl+Ndid nothing at all outside the editor. Both arehandled now, and the test asserts the two layers agree rather than asserting
either in isolation.
How does a user open the Home tab now? — the question the issue never asked
addHomeTab()had exactly one caller in the app, and it was this branch.Unifying
Ctrl+Ttherefore does remove the only way to open the home screenas a tab. It does not remove the home screen:
Ctrl/⌘+Toutside the editortabManager.addHomeTab)showHome, the same<HomePage>, same recent files / pinned tags / callbacks)+buttonThe two render the identical component through the same
{#if}— the tab formwas already the weaker of the two. It is dropped on session restore
(
hasRealFilePathis false for theHOMEsentinel) and closed when windows aremerged, so a Home tab never survived a restart. I am calling that a
simplification rather than a regression, but it is a judgement call and it is
here rather than buried in the diff.
addHomeTaband theisHomePathspecial cases stay: the sentinel still hasto be recognised for snapshots written by older builds, and #429's render gate
is tested against it. Removing the now-callerless constructor is a separate
cleanup, deliberately not bundled here.
Scope
Ctrl+Estaysview-toggle-edit.fmt-bullet-list/fmt-numbered-list/fmt-checklist/fmt-link. GitHub has chords for the lists (Ctrl+Shift+7/8) andCtrl+Kfor links; none of them were asked for and
Ctrl+Kis a chord prefix here.TabList.svelte's hard-coded(Ctrl+T)tooltip, which saysCtrl on macOS too. Now merely cosmetic, and it was wrong before this PR as
well.
(
Ctrl+I→ suggest,Ctrl+U→ cursorUndo,Ctrl+Shift+Z→ redo).tab-next/tab-prev, whose two layers use differentmodifiers on macOS by design. The new test names that divergence in
KNOWN_LAYER_DIVERGENCESso a new one fails instead of joining it.Editor.sveltedeclares anonhomeprop thatnothing in the component uses.
Ctrl/⌘+Shift+Cfor "copy documentas Markdown". Nothing here takes it — that is part of why the code block is
not on
Ctrl+Shift+C.Tests
scripts/formatShortcutKeymap.test.ts, 10 tests. It does not match eithercomponent as text.
registerLocalizedActionsandhandleKeyDownare extracted by name(
functionSource, not an anchor pair that widens as neighbours are added),transpiled, and run inside a
withblock over a Proxy scope that answersfor every free identifier with a recording stub. Renaming a local or
reordering the actions changes nothing; changing a key does.
KeyMod/KeyCodeand are turnedback into chords by Monaco's real
decodeKeybinding, once per operatingsystem, so
CtrlCmd→ Meta/Ctrl is resolved rather than assumed.fired for every chord in a bounded space (a–z, 0–9, Tab, PageUp/Down, arrows,
F4/F5, the brackets,
\,.,,,-,=, × Ctrl/Meta × Shift × Alt) andthe app functions that ran are recorded. That is why "Ctrl+T does not open a
Home tab" is an assertion about behaviour.
run()and its call recorded, sofmt-heading-2carryingheading 3's body fails.
and any chord both layers answer must mean the same thing in both — the
second one is the shape of the Ctrl+T means three different things, and the Home tab it opens renders blank #392 bug, and it catches the next person's
addition.
refactor that makes the extraction silently return nothing fails rather than
passing vacuously.
Weaker assertions, named:
MONACO_DEFAULTSis a snapshot of thestandalone defaults the new chords were checked against, not a reading of them.
Enumerating them for real needs Monaco's browser-side contribution graph, which
needs a DOM; a shim big enough to load it would be a second, synthetic source of
truth for exactly the thing being checked. The regeneration recipe is in the
file. Layout-dependent
keyvalues are modelled, not reproduced: the syntheticevents carry the unshifted character, which is what the two punctuation branches
(
Ctrl+,,Ctrl+=/-) actually compare against.Falsification
Fix reverted, tests kept.
Item #121 —
Editor.svelteandeditorToolbar.tsback to master:Item #392 —
MarkdownViewer.svelteback to master:The third one is the
Ctrl+Ngap, found by the test rather than by me.each new shortcut runs the command it is labelled withstays green under bothreverts, correctly: the
runbodies already existed, it is the keys that didnot.
Verification
Not verified: I did not run the app and press these keys. Everything above
is the keymap the app declares, plus Monaco's declared defaults; what a
running WebView does with
⇧⌘.on a German layout, whether⇧⌘Qreally neverreaches the app on macOS, and how the vim adapter behaves in practice are all
reasoned from source and vendor documentation, not observed. Windows and Linux
were not exercised at all — I am on macOS. If someone can try
Ctrl+Shift+Fand
Ctrl+Shift+.on a real Windows build before this lands, that is the gapworth closing.
Rebased on
e98da36, which already contains #471 and #474. Merge order maystill require a rebase — #231 touches the neighbouring lines of both files.