源码、描述和 DOM 三层
源码 codec 读取语义;投影层根据已接受输入返回声明式描述;renderer 才创建 DOM 或框架组件。不能在 build() 中创建图表、发网络请求或调用编辑器提交事务。
| 输出 | 典型用途 | 限制 |
|---|---|---|
mark | 高亮、拼写标记 | 装饰范围使用规范源码坐标 |
line | 原生段落/容器行样式 | 锚定行起点 |
widget | 控件、提示 | 组件不可接管正文输入 |
replace + text | 源码中立的字面呈现 | 文本不会按 HTML 解释 |
replace + view | 图表和复杂预览 | 跨行结构需要 block 层 |
inline 和 block 不能混用
placement: 'inline' 在有界可见范围中工作,不应返回跨行结构性替换。placement: 'block' 的结果在 CodeMirror 计算视口之前提供,可参与多行高度计算。
普通节点源码 renderer 的 placement: 'replace' | 'before' 是行内视图入口,不能把它等同于任意整块编辑容器。图片可用特定的 measureHeight 协议;大型围栏预览使用 block 层,见 图表完整示例。
声明依赖和稳定身份
addProjectionLayers() {
const name = this.name
return [{
key: `${name}.highlight`,
placement: 'inline',
build(input) {
return input.query(name).matches.map(match => ({
type: 'mark', from: match.from, to: match.to, class: 'my-source-mark',
}))
},
}]
}
层的 key 全局唯一;视图条目的 key 在层内稳定。不要在 build() 中创建新的 renderer 函数,避免每次状态改变都卸载组件。示例保留 renderer 模块级常量,但实例资源仍由组件作用域管理。
读取 input.selection 的构建器必须设置 selection: true;读取状态槽时把槽列入 states。普通源码显露已由运行时处理,不需要因此让每次光标移动都重建全部图表。
modes 默认 live / locked。suppressDuringComposition 默认 true:Markdown 编辑中,仅与输入法选区相交的描述暂时回退源码;替换描述以 revealRange 判断完整语义范围,行描述以该行判断。其他块保留呈现与组件身份。隐藏标记的文档编辑保持投影,原生正文仍由 CodeMirror 接受组合输入。
组合输入开始/结束只重新物化已有描述,不要求重建整篇层。预览仍在页面上时,“编辑源码”等操作也必须检查 readSourcePresentation(editor.state).composing,避免抢走输入法焦点。不要为视觉稳定取消输入保护。冲突替换可能回退源码,可查看 editor.work.projectionConflicts 定位所有权争用。
普通输入时只更新受影响块
当输出只依赖当前语法块时,可以声明 incremental: true。块级层会收到变更后的完整语法块,未受影响的描述映射到新源码位置并保留组件身份;内联层仍按可见范围查询,并复用映射后的组件身份。省略该选项时继续完整构建,以兼容依赖全文的插件。
启用前需满足以下约定:
- 使用
input.query(name)或本次范围内的源码,输出及revealRange属于所查询的块。不要在每次构建中扫描全文、给全文编号或生成跨块目录。 - 引用链接依赖其他位置的定义时声明
references: true,新增、删除或修改定义都会重新构建该层。其他全局依赖仍应使用保守构建。 selection和states仍须声明;权限、模式或这些依赖变化时,运行时会重新评估完整范围。key在一次构建中标识唯一源码位置,renderer 保持稳定。组件身份由运行时沿源码变化映射,整块覆盖或删除会终止旧身份。
如果组件属性带有源码位置,用 componentView 的第三个参数显式映射。下面是放在 input.query(name).matches.map(...) 中的视图片段,renderPreview 是模块级 renderer:
view: componentView(renderPreview, {
source: input.document.slice(match.from, match.to),
range: { from: match.from, to: match.to },
}, (props, offset) => ({
...props,
range: {
from: props.range.from + offset,
to: props.range.to + offset,
},
}))
映射函数只返回新属性,不创建 DOM、请求资源或提交事务。renderer 的 update(next) 必须保存最新属性,点击操作不能闭包捕获第一次挂载的位置;预览内容没有变化时,应保留现有图表实例。这样在前方输入后,即使预览稍后才滚入视口,其“编辑源码”操作也会定位到正确块。
局部更新不等于只解析光标所在行。修改代码围栏、列表/容器边界、表格分隔行或引用定义,可能合法地影响前后块;运行时以解析器确认的结构变化扩大更新范围。可对比 editor.work 中的 syntaxDiffBuilds、syntaxDiffChunks、syntaxDiffComparisons、layerVisitedNodes 与 blockBuilds,并验证 DOM/资源是否复用。当前仍需映射既有描述并生成装饰,不保证所有输入都是恒定成本。
ComponentView 与 NodeView
| 协议 | 输入 | 场景 |
|---|---|---|
| NodeView | nodeView.attrs、updateAttributes、getRange 等 | 与源码字段绑定的控件 |
| ComponentView | 自定义 view 属性 | 搜索、折叠按钮、图表等通用组件 |
Vue 使用 VueNodeViewRenderer / VueComponentViewRenderer,React 使用对应适配器。两类视图都需要原来的 Provider 树中的 Host;不要给每个节点创建独立应用。
原生 ComponentViewRenderer<Props> 取得 dom、props、host、scope 和 requestMeasure,返回 update、destroy,可选 ignoreEvent。ignoreEvent 默认 true;只有明确希望 CodeMirror 消费的事件才返回 false。
高度与卸载
挂载容器归运行时所有。renderer 管理自己的子 DOM、监听器、观察器和第三方实例,不改写 .cm-content。异步图片或图表尺寸变化后调用 requestMeasure(),它合并测量请求。
视图更新应复用资源;源码或关键渲染参数变化时取消旧请求。destroy() 移除框架入口,scope 结束时释放资源。二者都要能正确处理未完成的加载。完整模式见 资源生命周期。
需要验证的交互
检查鼠标拖选经过预览、键盘进入源码、搜索命中、只读复制、IME、滚动卸载、快速更新及换文档。先比较工作计数变化和资源存活数量,再检查最终画面;光标移动不应触发全文解析或所有图表重新初始化。协议见 渲染 API。