feat(shortcuts): Mod+K inserts a link, and the table verbs move off the chord - #652
Merged
Conversation
`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>
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>
#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>
PathGao
added a commit
that referenced
this pull request
Aug 12, 2026
…660) * docs: bring the release runbook and the syntax reference up to 2.7.4 Four claims in the docs describe behaviour that PRs since 2.7.3 changed, and two features shipped without reaching the file that documents what Markpad can do. RELEASING.md - snapcraft.yaml no longer builds the app; it unpacks the release's own .deb (#579), so the `npm ci` note and the `rust-deps` troubleshooting row both described a file that no longer exists in that shape (#577). - The .deb/.rpm coverage note still said *Check for Updates…* offers an update and then fails to install it. #573 asks `self_update_supported` first, so it says where updates come from instead. - Adds the AppImage checks (#584, #658) as their own troubleshooting row, and notes that release builds are serialized (#612). README, README.zh-CN - The same pre-#573 claim, in both languages. The Chinese one also told .deb/.rpm users to update "through their distribution channels", which is the sentence #566 removed from the English one — there is no apt or dnf repository. snapcraft.yaml - The comment pointed at .github/workflows/test_snap.yml, deleted in #601. samples/markdown-syntax{,.zh-CN}.md - Lists: Enter continues the marker, Tab changes level, Enter on an empty item leaves the list (#636), and the three list chords (#652). - Tables: cell, row and column keys, why deleting a row has no chord (#645, #653). - Links: Ctrl/Cmd+K (#652). npm test 904 pass, vitest 365 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(syntax): drop two design arguments that belong in the PRs, not here The syntax reference carries no issue numbers, no history and no defence of a decision anywhere in its 500 lines; where it does give a reason -- the three deliberate incompatibilities -- the reason tells the reader what to write instead. Two sentences I added argued for the design at the reader: that four other editors use Mod+K, and why deleting a row has no chord while deleting a column does. Neither changes anything the reader would type. What is left of the second is the part that was actually useful: to delete a row, delete its line. * docs(readme): drop the only issue number a reader ever sees #570 was the last issue number in either README, and it was one I added a few days ago. It is a closed bug report, and after #573 the sentence it was attached to describes ordinary behaviour -- so a reader asking "how does my .deb update" now clicks through to a fault that no longer exists. The half-sentence in front of it went for the same reason: "rather than offering one it cannot install" describes what the app used to do. What is left is what the reader needs -- .deb and .rpm are one-time installs, there is no apt or dnf repository, and Check for Updates says so. Both READMEs keep the Report a Bug link, which is the only issue link either of them had before. * docs(releasing): cut the post-mortems back out of the runbook This file is what you read on the day you cut a release. Over three days in August I turned it into a runbook with post-mortems threaded through it: which run pushed Chocolatey 2.7.2 at 15:37, that the snap served 2.6.11 for three months and six versions, which two of the three v2.7.2 attempts the AppImage strip failed in, how the 2.7.2/2.7.3 lock skew was found. All true, none of it changes what you do next, and it sits between you and step 5 while a build is running. Test applied to each one: can it change an action taken on release day. Gone: the "Why package managers publish after the release" section entirely -- its one operational sentence (workflow_dispatch works against a published tag, the only way to exercise snapcraft.yaml) moves into step 7, where you would need it. The failure histories in three troubleshooting rows, keeping the symptom and the fix. The placeholder-pubkey era. Two design defences. Kept: reasons that stop you doing the wrong thing -- why not to create anything at alecdotdev/Markpad, why the Cargo.lock bump is the one that gets forgotten. A prohibition without its reason gets deleted as superstition by whoever comes next. 1957 -> 1563 words. The version before I started was 883. --------- 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.
Mod+Kwas a chord prefix holding two table verbs, and the Link button — the single function four independent editors document the same chord for — had no keyboard shortcut at all. Hand-testing settled it: the chord was too awkward to press, which is what started this.Mod+KMod+K TMod+Alt+TMod+Shift+DMod+Alt+ZMod+Shift+7Mod+Shift+8Mod+Shift+9The registry holds
Alt; #651'sformatChordrenders itOpton macOS, so the panel showsCmd+Opt+Tthere andCtrl+Alt+Telsewhere. The panel, the native menu and the toolbar tooltips all readshortcutLabel(), so they picked up every change without being touched.Mod+Shift+9has no precedent anywhere — GitHub documents no task-list chord, and neither do Typora or Obsidian. Both the comment and the registry label it arbitrary-but-adjacent rather than implying a source it does not have.Insert Column did not get a chord
Mod+Alt+Cis occupied. On macOSAlt+Meta+Cis Monaco'stoggleFindCaseSensitive,kbExpr: EditorContextKeys.focus— live whenever the editor has focus, not gated on the find widget being open.What rules it out is not the collision but the recovery: it is a
registerEditorCommand, not aregisterEditorAction, so it has no command palette entry. Taking it would leave find-case-sensitivity reachable only by clicking theAabutton, which breaks the contract everyDELIBERATE_OVERRIDESrow in this repo rests on — the displaced command is still reachable.Insert Column is therefore palette-only, recorded in
TABLE_VERBS_WITHOUT_A_CHORDwith that reasoning, alongside the delete verbs that are palette-only for their own reason. FreeMod+Altletters on all three platforms, for whoever picks this up: A B D E G H I J K M N O Q S U V X Y.The guardrail could not see the largest change in the file
monacoChordOwnership.spec.tskeys Monaco's defaults by the full chord. Monaco has no bareMeta+K— it hasMeta+K Meta+C,Meta+K Meta+0and so on — somonaco.get('Meta+K')wasundefinedand the ownership rulecontinued.It waved the biggest keymap change in the app straight through. A rule that only compares whole chords cannot see a chord being claimed as a complete key when Monaco uses it as a prefix, which is precisely the shape of this change.
Added a second rule for exactly that, plus a
namespace: truerow whose staleness check looks behind the prefix — so the row goes stale if Monaco stops usingMod+Kas a prefix, not merely if it stops bindingMod+Kitself.The cost, enumerated rather than summarised
Taking plain
Mod+Kends Monaco'sMod+K …namespace: 31 chord sequences carrying 30 commands, 20 of them folding, identical on all three platforms. TheDELIBERATE_OVERRIDESrow names all thirty.That is larger than the audit that proposed this change estimated (22 commands, 14 folding), and the row says so.
Why it is still acceptable, verified rather than assumed: editor folding does almost nothing for Markdown here. There is no
registerFoldingRangeProvideranywhere insrc/, and Monaco'sbasic-languages/markdown/markdown.jsdeclares folding only through<!-- #region -->markers, which nobody writes in Markdown. What remains is indentation folding. The app's useful folding is its own preview-side system (foldState.ts) — click-driven, untouched by any of this, and reached through the fold icons rather than a key. Every displaced command remains in the command palette.Also carried:
Mod+Shift+Kno longer deletes a lineA commit that was verified, then never pushed, so it missed #645 and is not in master —
Mod+Shift+Kstill deletes a line there today. It is Monaco'sdeleteLines, and it was reported mis-firing.Refolding it conflicted: #648 deleted
MONACO_DEFAULTS, and the conflict region swallowed the new test with it. Resolved by keeping master's comment and re-adding the test intact; its only dependency on the deleted table was prose. Delete Line keeps its palette entry.Falsification
Four red-first failures, one per change, each quoted in the commit that fixes it. Two more after rebasing onto #651: a literal
Cmd+Alt+T, and an assertion that the Cmd rendering is the Ctrl rendering with one word swapped — itself a copy of the assumption #651 removed. The second now proves only that the two renderings are the same chord, and leaves per-platform naming to the test that owns it.npm test900 → 903 ·vitest356 → 357 ·check0 errors.🤖 Generated with Claude Code