Skip to content

feat(shortcuts): Mod+K inserts a link, and the table verbs move off the chord - #652

Merged
PathGao merged 3 commits into
masterfrom
feat/cmd-k-inserts-a-link
Aug 12, 2026
Merged

feat(shortcuts): Mod+K inserts a link, and the table verbs move off the chord#652
PathGao merged 3 commits into
masterfrom
feat/cmd-k-inserts-a-link

Conversation

@PathGao

@PathGao PathGao commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Mod+K was 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.

command from to source
Insert Link (none) Mod+K Typora, Bear, iA Writer, GitHub
Insert Table Mod+K T Mod+Alt+T Typora and Bear's macOS chord
Zen Mode Mod+Shift+D Mod+Alt+Z
Numbered List (none) Mod+Shift+7 GitHub
Bullet List (none) Mod+Shift+8 GitHub
Checklist (none) Mod+Shift+9 none — arbitrary, and labelled as such

The registry holds Alt; #651's formatChord renders it Opt on macOS, so the panel shows Cmd+Opt+T there and Ctrl+Alt+T elsewhere. The panel, the native menu and the toolbar tooltips all read shortcutLabel(), so they picked up every change without being touched.

Mod+Shift+9 has 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+C is occupied. On macOS Alt+Meta+C is Monaco's toggleFindCaseSensitive, 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 a registerEditorAction, so it has no command palette entry. Taking it would leave find-case-sensitivity reachable only by clicking the Aa button, which breaks the contract every DELIBERATE_OVERRIDES row in this repo rests on — the displaced command is still reachable.

Insert Column is therefore palette-only, recorded in TABLE_VERBS_WITHOUT_A_CHORD with that reasoning, alongside the delete verbs that are palette-only for their own reason. Free Mod+Alt letters 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.ts keys Monaco's defaults by the full chord. Monaco has no bare Meta+K — it has Meta+K Meta+C, Meta+K Meta+0 and so on — so monaco.get('Meta+K') was undefined and the ownership rule continued.

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: true row whose staleness check looks behind the prefix — so the row goes stale if Monaco stops using Mod+K as a prefix, not merely if it stops binding Mod+K itself.

The cost, enumerated rather than summarised

Taking plain Mod+K ends Monaco's Mod+K … namespace: 31 chord sequences carrying 30 commands, 20 of them folding, identical on all three platforms. The DELIBERATE_OVERRIDES row 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 registerFoldingRangeProvider anywhere in src/, and Monaco's basic-languages/markdown/markdown.js declares 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+K no longer deletes a line

A commit that was verified, then never pushed, so it missed #645 and is not in master — Mod+Shift+K still deletes a line there today. It is Monaco's deleteLines, 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 test 900 → 903 · vitest 356 → 357 · check 0 errors.

🤖 Generated with Claude Code

PathGao and others added 3 commits August 13, 2026 00:39
`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
PathGao merged commit 9dd419e into master Aug 12, 2026
4 checks passed
@PathGao
PathGao deleted the feat/cmd-k-inserts-a-link branch August 12, 2026 16:54
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>
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