Commands and atomic transactions
Call, compose, query and author commands with failure and history semantics.
Three ways to invoke a command
These calls assume StarterKit is installed. A visible TypeScript command does not prove that its runtime feature is installed.
const applied = editor.commands.toggleBold()
const enabled = editor.can().toggleBold()
const committed = editor.chain().insertText('A').insertText('B').run()
if (!committed) console.warn(editor.lastCommandResult)
Commands return booleans. can() runs a disposable draft without publishing or changing lastCommandResult. A chain executes sequentially on one draft and publishes once at run(). Any failed step discards the complete chain. Chains cannot contain asynchronous steps.
Author a chainable business command
import { Extension } from '@quirekit/msfe-core'
declare module '@quirekit/msfe-core' {
interface Commands<ReturnType> {
/** Commands installed by the signature feature. */
signature: {
/** @param name - Literal author name. @returns Command outcome. */
insertSignature(name: string): ReturnType
}
}
}
export const Signature = Extension.create({
name: 'signature',
addCommands() {
return {
insertSignature: (name: string) => ({ tr }) => {
if (!name.trim() || /[\r\n]/.test(name)) return false
return tr.insertText(`— ${name.trim()}`)
},
}
},
})
Install Signature before calling editor.can().insertSignature('Ada'). If user text must remain literal Markdown content, encode the relevant field or constrain allowed characters; do not confuse text with a trusted syntax template.
Draft state and nested commands
A command factory receives business arguments and returns a synchronous pure function. Read props.state or tr.state, which includes preceding chain steps. props.editor.state still represents the mounted state before publication.
Continue the draft through props.commands, props.chain() and tr.replace(). Never call editor.commands to dispatch another mutation from a command, invoke dispatch yourself, make requests, open dialogs or mutate storage. Applicability queries execute your code too.
tr.replace(from, to, insert) takes coordinates in the current draft. Recompute or map offsets after preceding steps change the length. The advanced tr.apply() CodeMirror entry follows the same coordinate rule.
Adapt an existing planner
import { sourceCommand } from '@quirekit/msfe-core'
editor.chain().command(sourceCommand(state => ({
changes: { from: state.selection.main.from, to: state.selection.main.to, insert: 'approved' },
}))).run()
editor.run(planner, target?) runs a pure planner directly and returns MsfeResult. Return null when inapplicable. DOM mutations are not a substitute for transactions.
Handle failures
| Reason | Meaning | Host response |
|---|---|---|
not-installed | The runtime feature is absent | Check preset exclusions and installation |
not-applicable | Syntax or selection does not admit the action | Disable the control or preserve content |
readonly | Explicit readonly or locked mode | Disable writing |
stale-target | Instance, document or revision changed | Reconfirm intent; never replay old offsets blindly |
invalid-change | Invalid range or transaction | Check coordinates and field encoding |
composing | Native composition owns input | Wait for composition; do not steal focus |
destroyed / not-mounted | Invalid lifetime | Stop and release resources |
Shortcuts, history and verification
Declare structural Enter behavior through addBlockInput(): enter maps to Enter, lineBreak to Shift-Enter, and exit to Mod-Enter. Keep ordinary business shortcuts in addKeyboardShortcuts().
Handlers decide from current source, selection and block boundaries, without counting key presses. Return one TransactionSpec to handle the whole gesture, 'parent' to delegate to an enclosing block, or 'native' to resume the existing keymap chain. The latter two can also be constant rule values. Omitted intents delegate; unregistered syntax Block nodes prevent outer defaults from taking ownership. Explicitly delegate ordinary paragraph leaves so lists, quotes and custom containers can handle their contents.
The shared router checks mode, readonly, composition and accepted syntax before following each selection endpoint's ancestors. It invokes a handler once only when every endpoint resolves to the same function; otherwise the whole gesture falls through. targets contains endpoints and their owners, with two endpoints for each nonempty range. Plan edits from state.selection. Handlers must be pure: never dispatch, call editor.commands or mutate storage. The shared layer commits and undoes the edit atomically.
Each parser node has one declared owner. Override it with the existing extension's .extend() instead of installing competing owners. This minimal example starts a paragraph after a top-level level-two heading; install it with StarterKit.configure({ heading: SectionHeading }). MD Editor already provides complete defaults for paragraphs, headings, lists, quotes, containers, code and tables.
import { Heading } from '@quirekit/msfe-heading'
export const SectionHeading = Heading.extend({
/** @returns A pure, source-owned heading boundary behavior. */
addBlockInput() {
return [{
nodes: ['ATXHeading2'],
enter: ({ state, targets }) => {
const range = state.selection.main
if (state.selection.ranges.length !== 1 || !range.empty
|| !targets[0]!.node.parent?.type.isTop
|| range.head !== targets[0]!.node.to) return 'native'
return {
changes: { from: range.head, insert: '\n\n' },
selection: { anchor: range.head + 2 },
scrollIntoView: true,
userEvent: 'input',
}
},
lineBreak: 'native',
}]
},
})
An extension may return { 'Mod-Shift-s': () => this.editor.commands.insertSignature('Ada') } from addKeyboardShortcuts(). Returning false lets other bindings handle the key. Preserve usable browser and input-method behavior.
Submit one user action through one chain where practical. Native typing still follows editor history grouping. Verify standalone and chained calls, complete rollback on failure, side-effect-free can(), readonly rejection, exact spelling after undo and explicit multiple-selection behavior.
For asynchronous dialogs, read Selection and captured intent. Exact contracts are in Command API.