-
Notifications
You must be signed in to change notification settings - Fork 339
Description
FR: Pluggable keyboard hooks for Enter/Backspace/Tab so other extensions can cooperate—without rewriting keybindings.json or breaking IME
Summary
Request: Expose a small, opt-in API in Markdown All in One (MAIO) that lets other extensions register Enter/Backspace/Tab hooks. Hooks can intercept, pass through, or transform the edit, and then optionally hand control back to MAIO.
This avoids keybinding wars, enables richer behaviors (e.g., smart “exit quotes”, smart paragraph split), and prevents fragile hacks that rewrite the user’s keybindings.json or intercept raw typing (which breaks IMEs).
The problem today
MAIO rightfully owns Enter/Backspace/Tab for Markdown ergonomics (list continuation, block quotes, ordered renumbering, etc.).
Other writing/formatting extensions (ours: andrea-novel-helper) also need domain-specific logic on the same keys (Chinese quote/bracket smart-exit, paragraph splitting with configurable blank lines, etc.).
VS Code lacks a general “pre-key middleware”, so extensions must compete for keybindings. In practice:
-
Rewriting
keybindings.json(our current approach)
We inject entries such as:Why this is unsafe/brittle
- Breaks user expectations and profile hygiene; may conflict with Settings Sync, remote/containers, locked profiles, or enterprise policies.
- Precedence is opaque; changes in MAIO’s
whenclause can silently “win back” the key. - Risky to edit
vscode-userdata:URIs; merges can fail; users must often close/reopen the doc to re-load bindings. - Feels intrusive (an extension mutating user config to disable another extension’s keybinding).
-
Intercepting the
typepipeline / emulating keystrokes (we tried; rejected)
Hooking the generic text input path (e.g., callingtypeand injecting text) breaks IME composition (Chinese/Japanese/Korean), dead keys, and sometimes inline suggestions. We observed:- Duplicate quotes (e.g.,
”“), stuck composition candidates, or lost commits. - Conflicts with
acceptSuggestionOnEnter, inline completions, and other editor states.
In short: IME compatibility is fragile with any “catch-and-retype” approach.
- Duplicate quotes (e.g.,
Both workarounds are error-prone, unfriendly to users, and brittle across platforms and future MAIO updates.
Proposal: a small, opt-in delegate API
MAIO exports a tiny registration surface at activation:
export interface MarkdownInputDelegates {
registerEnterHook(hook: EnterHook, opts?: { priority?: number }): vscode.Disposable;
registerBackspaceHook(hook: BackspaceHook, opts?: { priority?: number }): vscode.Disposable;
registerTabHook(hook: TabHook, opts?: { priority?: number }): vscode.Disposable;
}
export interface InputContext {
editor: vscode.TextEditor;
document: vscode.TextDocument;
selections: readonly vscode.Selection[];
languageId: string; // 'markdown' | 'rmd' | 'quarto' | ...
modifiers?: 'ctrl' | 'shift' | 'ctrl+shift' | null;
}
export type HookResult =
| { kind: 'handled' } // stop, MAIO does nothing
| { kind: 'pass' } // MAIO continues as today
| { kind: 'transform',
apply: (edit: vscode.TextEditorEdit) => void,
reveal?: vscode.Selection | vscode.Selection[] }; // apply edit; optionally continue
export type EnterHook = (ctx: InputContext) => HookResult | Promise<HookResult>;
export type BackspaceHook = (ctx: InputContext) => HookResult | Promise<HookResult>;
export type TabHook = (ctx: InputContext) => HookResult | Promise<HookResult>;Router behavior inside MAIO (sketch):
-
Sort registered hooks by
priority(small number = earlier). -
For each hook:
- Run with a short timeout; ignore on error/timeout.
handled→ stop MAIO.transform→ apply edit; optionally continue with MAIO (configurable).pass→ try next hook; if none, run MAIO’s default logic.
-
If no hooks or the API is disabled, behavior is exactly current MAIO.
Safety toggles (new settings, default off):
"markdown.extension.input.allowExternal": false, // default: off → no behavior change
"markdown.extension.input.delegateAllowlist": [ // whitelist callers
"andrea-novel-helper"
],
"markdown.extension.input.delegateTimeoutMs": 12,
"markdown.extension.input.continueAfterTransform": trueWhy MAIO is the right place
- MAIO already centralizes Enter/Backspace/Tab logic; letting it route cooperative handlers preserves its guarantees (ordered lists, quote rules, etc.) while unlocking safe extensibility.
- Users avoid invasive keybinding edits and IME-breaking “retype” tricks.
- Behavior remains unchanged unless users explicitly opt in (and MAIO allowlists a delegate).
Minimal consumer example
export async function activate(ctx: vscode.ExtensionContext) {
const maio = vscode.extensions.getExtension('yzhang.markdown-all-in-one')
?.exports as import('markdown-all-in-one').MarkdownInputDelegates | undefined;
if (!maio) return;
// Smart “exit quotes/brackets”: move caret out; do nothing else.
const d = maio.registerEnterHook((c) => {
if (!/^markdown$|^plaintext$/.test(c.languageId)) return { kind: 'pass' };
const moved = tryExitQuotesOrBrackets(c.editor, c.document, c.selections);
return moved ? { kind: 'handled' } : { kind: 'pass' };
}, { priority: 100 });
ctx.subscriptions.push(d);
}Backward compatibility
- With
allowExternal=false(default), no changes in behavior or performance. - Hooks are opt-in + allowlisted; misbehaving hooks are time-boxed and isolated.
Alternatives considered
- Keep rewriting
keybindings.json: intrusive, race-y, profile-fragile, bad UX. - Keep intercepting
type: IME-unsafe, interacts poorly with suggestions and composition. - Wait for a VS Code “pre-key” API: uncertain timeline; a focused router in MAIO solves a real ecosystem pain now.
Acceptance criteria
- Allowlisted extension can register hooks for Enter/Backspace/Tab.
-
handledstops MAIO;transformapplies an edit then (config) continues or stops;passfalls through. - Default settings keep MAIO identical to today.
- Timeouts/errors in hooks do not affect MAIO’s normal flow.
Impact: Fewer keybinding conflicts, safer IME behavior, and a cleaner path for extensions to collaborate with MAIO—no more invasive user-config edits.
{ "command": "-markdown.extension.onEnterKey", "key": "enter" }, { "key": "enter", "command": "andrea.smartEnter", "when": "..." }