# 命令与原子事务

调用、组合、查询和编写命令，正确处理失败、只读与撤销。

## 三种调用方式

以下调用假设实例安装了 `StarterKit`。命令的 TypeScript 类型可见，不代表运行时功能已经安装。

~~~ts
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` 返回布尔结果；`can()` 只试算，不发布事务，也不更新 `lastCommandResult`。命令链在一个草稿上顺序执行，最后 `run()` 一次提交；任何一步失败都会放弃整条链。它不能包含异步步骤。

## 写一个会参与命令链的业务命令

~~~ts
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()}`)
      },
    }
  },
})
~~~

安装 `Signature` 后即可调用 `editor.can().insertSignature('Ada')`。若业务需要 Markdown 字面值，应使用正确的字段编码或限制允许字符，不要误把用户文本当作可信语法模板。

## 草稿状态和嵌套命令

命令工厂接收业务参数，返回同步纯函数。读取 `props.state` 或 `tr.state`，它们包含链中前面步骤的结果。读 `props.editor.state` 会读到尚未提交的旧状态。

使用 `props.commands`、`props.chain()`、`tr.replace()` 等继续同一草稿。命令内部不能调用 `editor.commands` 提交另一笔修改，不能手动调用 `dispatch`，不能发请求、打开弹窗或修改 `storage`。试算同样会调用你的代码。

`tr.replace(from, to, insert)` 的坐标属于当前草稿；前一步改变长度后，后续步骤需要重新计算或映射坐标。`tr.apply()` 是高级 CodeMirror 入口，同样必须遵守该坐标规则。

## 已有规划函数

~~~ts
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?)`，它返回 `MsfeResult`。返回 `null` 表示不适用；不要直接改写 DOM 来代替事务。

## 失败需要处理

| 原因 | 含义 | 宿主动作 |
| --- | --- | --- |
| `not-installed` | 类型存在但功能没安装 | 检查预设是否禁用或漏装 |
| `not-applicable` | 当前语法或选区不接受此动作 | 禁用按钮或保留原内容 |
| `readonly` | 显式只读或 locked | 关闭写操作 |
| `stale-target` | 目标的实例、文档或版本已变 | 重新确认意图，不能直接重放旧偏移 |
| `invalid-change` | 修改范围或事务无效 | 检查坐标与字段编码 |
| `composing` | 组合输入正在拥有编辑过程 | 等待本次输入结束，不抢焦点 |
| `destroyed` / `not-mounted` | 生命周期无效 | 结束动作并释放资源 |

## 快捷键、撤销与测试

结构性回车使用扩展的 `addBlockInput()`，由块声明 `enter`（Enter）、`lineBreak`（Shift-Enter）和 `exit`（Mod-Enter）。普通业务快捷键继续使用 `addKeyboardShortcuts()`。

处理器根据当前源码、选区和块边界决定行为，不累计按键次数。返回一个 `TransactionSpec` 表示整笔处理，`'parent'` 表示委托外层块，`'native'` 表示回到既有快捷键链；后两者也能直接作为规则值。未声明的意图委托父级，未注册的语法 Block 阻止外层默认规则接管。块内普通段落应显式委托，让列表、引用或自定义容器获得处理机会。

公共层先检查模式、只读、组合输入和已解析范围，再沿每个选区端点的祖先查找。所有端点必须解析到同一个处理函数才调用一次，否则整笔回退。处理器接收的 `targets` 是端点及其所有者，同一非空选区有两个端点；实际修改仍使用 `state.selection`。处理器必须纯粹，不得调用 `dispatch`、`editor.commands` 或修改 storage，返回的编辑由公共层一次提交和撤销。

每个 parser 节点只允许一个声明所有者，通过原扩展的 `.extend()` 替换规则，不要安装两个扩展争用节点。下面的最小示例让顶层二级标题末尾回车进入新段落；在 `StarterKit.configure({ heading: SectionHeading })` 中安装它。产品 MD Editor 已为段落、标题、列表、引用、容器、代码和表格配置完整默认规则。

~~~ts
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',
    }]
  },
})
~~~

在扩展的 `addKeyboardShortcuts()` 返回 `{ 'Mod-Shift-s': () => this.editor.commands.insertSignature('Ada') }`。`false` 允许其他绑定继续处理；避免覆盖浏览器或输入法行为而不给出替代入口。

用户可感知的一次动作尽量通过一条命令链提交；连续原生输入仍遵循编辑历史的分组规则。测试至少覆盖：单次和链中调用、链中失败不留半截修改、`can()` 不改变状态、只读拒绝、撤销恢复原始拼写，以及多选区的处理策略。

需要跨弹窗或异步请求保留位置时，先阅读 [选区与异步目标](/guide/selection)。签名见 [命令 API](/api/commands)。
