CodeMirror 6 编辑器迁移规划
返回文档导航
1. 状态与结论
- 编制日期:2026-09-13。
- 状态:已接入 CodeMirror 正文运行路径,实现与本机自动化验证已完成,跨平台人工验收及性能门槛仍待完成;原规划保留在下文,实际实施与验证结果见第 11 节。
- 当前实现:Electron、原生 JavaScript、多标签 CodeMirror 6 编辑器。
- 推荐目标:CodeMirror 6,保留现有 Markdown 文件格式、文档模型和桌面工作区。
- 技术可行性高,迁移复杂度及行为回归风险中高。
- 主要价值:统一输入、选区、撤销和装饰层的实现,降低手工维护编辑器附加 DOM 的成本,为源码语法高亮等能力提供基础。
- 先完成基线、最小原型和适配层,再逐步迁移产品交互。
- 本文作为迁移工作的主要规划入口;当前已交付行为仍以功能说明及既有专题文档为准。
2. 范围与行为约束
2.1 本次迁移范围
- 普通 Markdown 编辑页、空编辑页及只读搜索结果和失效链接报告页。
- 编辑、选区、撤销/重做、缩进、列表切换和整行复制/剪切。
- 本地图片、剪贴板图片和跨文档粘贴时的图片相对路径调整。
- 行号、当前行、标题区间、查找命中及文件差异行的展示。
- 当前文件查找替换、源码链接点击、跳转到行、大纲和搜索结果定位。
- 命令面板、Quick Open、应用内对话框与编辑器之间的焦点恢复。
- 双向预览滚动同步、导航历史、标签切换和恢复会话。
- 开发构建、分发打包、测试访问接口和相关文档。
2.2 保持的产品语义
- 正文仍是普通 Markdown;文件服务继续负责 UTF-8、BOM 和 LF/CRLF 读写策略。
- 预览继续使用现有
markdown-it、清洗及 Mermaid 渲染链路。
- Preview、Normal、Pinned、Dirty 标签规则继续由
DocumentStore 管理。
- 标签切换保留各自正文、撤销历史、选区方向及横纵滚动位置。
- Dirty 状态根据正文与最近成功保存基线比较,不根据撤销栈是否为空判断。
- 编辑器保持不自动折行、Tab 两空格和当前列表命令语义。
- 编辑器语法配色针对深色背景定义,Markdown 结构符号与链接目标不沿用面向浅色背景的默认颜色。
- 一次列表切换、块缩进、图片插入或替换命令构成一个独立撤销单元。
- 快捷键设置、命令 ID、平台差异和原生菜单入口继续有效。
- 搜索报告可选择、复制和跳转,但不能通过键盘、粘贴或命令修改正文。
- 应用内对话框继续使用现有组件,不引入原生
alert、confirm、prompt。
- Electron 的上下文隔离、沙箱和禁用 Node 集成配置保持现状。
2.3 后续独立评估
- 富文本所见即所得、编辑区内图片预览、折叠、自动折行和多光标产品行为。
- Markdown 自动续列表、自动闭合、补全和 Vim/Emacs 键位。
- 预览区代码高亮、数学公式、脚注和导出。
- 用 CodeMirror merge 组件替换现有完整文件差异视图。
- React/Vue 改造、同步协议升级、移动端和协同编辑。
- 快捷键 JSON 设置编辑器的替换;该控件不属于本轮正文编辑器范围。
3. 当前实现与改动地图
| 模块 |
当前职责 |
计划处理 |
src/renderer/editor/pages.mjs |
长期存活的 textarea、页面注册和自建补丁历史 |
保留页面注册职责,委托适配器管理编辑状态和视图 |
src/renderer/document/store.mjs |
标签、正文、保存基线、Dirty 和恢复快照 |
保留业务模型,通过明确的更新入口同步正文 |
src/renderer/runtime/core.js |
激活页面、状态捕获、行号及当前区间 |
提取编辑器相关调用,使用适配接口和装饰层 |
src/renderer/runtime/editor/events.js |
输入、剪贴板、局部按键和页面事件 |
用编辑器更新回调及 DOM handlers 接入既有业务 |
src/renderer/runtime/editor/view.js |
预览、源码定位、范围替换及滚动同步 |
原生文本域操作迁入适配器,保留预览调度 |
src/renderer/find/controller.mjs |
编辑与预览共用查找控制器 |
保留工具栏和搜索规则,分离两侧显示与定位实现 |
src/renderer/runtime/editor/links.js |
源码链接镜像命中及导航 |
用编辑器坐标查询替代镜像测量,保留链接解析和路径规则 |
src/renderer/editor/diff-highlights.mjs |
手工差异行覆盖层和相邻差异计算 |
展示迁入 decorations,保留差异块导航语义 |
src/renderer/runtime/editor/differences.js |
差异基线、整页只读 diff 和导航 |
保留基线及共享 diff 算法,适配编辑器模式与位置 |
src/renderer/search/editor.mjs |
报告文本、跳转映射和命中覆盖层 |
复用文本生成及位置映射,替换覆盖层 |
src/renderer/runtime/navigation.js、src/renderer/preview/scroll.mjs |
位置恢复、源码锚点及滚动插值 |
保留锚点解析,改造编辑器坐标来源 |
src/renderer/runtime/keyboard.js |
命令上下文及快捷键协调 |
用编辑器焦点、选区和只读接口替代 DOM 等值判断 |
src/renderer/commands/palette.mjs、src/renderer/runtime/workspace.js |
命令面板和 Quick Open 的焦点捕获及恢复 |
接入可恢复的编辑器位置 |
src/renderer/dialogs/alert.mjs、src/renderer/runtime/dialogs.js |
提示、确认及焦点恢复 |
同时支持普通输入控件和编辑器位置快照 |
src/index.html、src/renderer/styles、src/renderer/layout.mjs |
页面模板、布局、缩放和字体 |
改为挂载容器及 CodeMirror 主题,保留 Flexbox 布局 |
src/renderer.js、scripts/renderer、package.json |
加载、检查及分发 |
新增编辑器构建入口,并接入开发、测试和打包流程 |
test/paste-undo-smoke.js、test/smoke/renderer |
原生编辑器及产品行为回归 |
引入编辑器测试访问层,保留真实输入与剪贴板验证 |
- 上表是主要影响范围,不是穷尽清单。
- 实施前继续搜索
selectionStart、setSelectionRange、getComputedStyle、activeElement、#editor 和 data-editor,覆盖辅助模块及测试中的间接依赖。
- 当前
runtime/core.js 已超过 600 行;迁移时优先提取编辑器职责,不能继续向该文件集中逻辑。
- 代码提取保留现有注释、调试代码和日志;停用旧实现与删除开发材料分别处理,删除遵循项目授权规则。
4. 目标结构与内部契约
4.1 模块组织
EditorPages 负责文档 ID 到编辑器页面的映射、激活及销毁。
EditorAdapter 作为项目内部契约,不模拟完整 HTMLTextAreaElement。
- 初期由 textarea 实现契约,随后接入 CodeMirror 实现。
- 正式交付目标只维护 CodeMirror 正文编辑实现,不新增长期双引擎用户设置。
- 拟新增
editor/adapter.mjs、editor/textarea-adapter.mjs 和 editor/codemirror-adapter.mjs。
- 拟按职责组织
editor/codemirror/entry.mjs、theme.mjs、decorations.mjs 和 keymap.mjs。
- 具体文件边界在实施时调整,人工维护文件控制在 600 行以内。
4.2 适配器最小接口
| 接口组 |
建议接口 |
契约 |
| 正文 |
getText()、getLength()、getLine()、lineAt() |
使用逻辑行及源码偏移,读取不触发业务更新 |
| 变更 |
replaceRange()、replaceDocument() |
标明来源、撤销分组和是否重置历史 |
| 选区 |
getSelection()、setSelection() |
使用 anchor/head,保留反向选区 |
| 历史 |
undo()、redo() |
仅影响所属文档,不依赖浏览器全局历史 |
| 焦点 |
focus()、hasTextFocus()、containsFocus() |
区分正文焦点和编辑器附属控件焦点 |
| 视图 |
captureViewState()、restoreViewState()、revealRange() |
位置恢复与主动跳转采用不同滚动策略 |
| 坐标 |
positionAtCoords()、lineBlockAt()、getScrollMetrics() |
统一文档坐标和视口坐标的边界 |
| 展示 |
setHighlights()、setReadOnly()、setFontSize() |
仅改变配置或展示,不制造正文编辑 |
| 生命周期 |
onUpdate()、destroy() |
回调包含文档身份及变化类型,销毁时释放监听和视图 |
- CodeMirror 适配器内部使用
EditorState、EditorView 和 transaction 更新。
- 更新回调区分
docChanged、选区、视口和焦点变化,避免任意光标移动都触发预览。
- 初期只启用一个主选区;多选区需要另行定义状态栏、剪贴板和会话语义。
- 项目内部源码偏移沿用 JavaScript UTF-16 计数。
- 对外行号从 1 开始;现有预览源码锚点从 0 开始,在适配边界集中转换。
- 恢复选区前按当前正文长度裁剪偏移,测试中文、emoji、空行及文件末尾。
CodeMirror 的状态与 transaction 模型参见官方系统指南。
4.3 正文与业务状态同步
- 用户输入和编辑命令先生成编辑器 transaction。
- 编辑器变化通过单一桥接入口,按文档 ID 同步到
DocumentStore。
- 桥接入口必须核对目标文档;不能把异步结果写入“当前活动文档”作为默认行为。
- 当前
updateActiveContent() 的使用需要限制在身份已验证的活动页,或提取等价的按 ID 更新入口。
- 同一正文变化只执行一次 Dirty 更新、标签状态更新、恢复稿调度和预览调度。
DocumentStore 继续负责保存基线、持久化、外部修改和标签生命周期。
- 外部磁盘重载、历史恢复及搜索报告刷新通过
replaceDocument() 进入视图,并标记为外部来源,避免重复业务回调。
- 保持现有外部整体替换会清空该页撤销历史的行为;普通保存和标签切换不能重置历史。
addToHistory: false 只用于控制某次 transaction 是否入栈,不能代替“清空旧历史”的状态重建。
- 配置和样式更新保留文档状态,不通过重建
EditorState 实现。
- 全文字符串先在正文变化时生成并复用;不得在每次选区或滚动回调中反复
toString()。
- 首轮允许保留当前全文业务更新链路,后续性能优化由基线证据决定。
4.4 多标签、恢复与异步操作
- 第一版沿用每个打开文档一个长期存活页面,页面内持有独立
EditorView 和历史。
- 标签激活不重新设置正文;关闭标签和替换 Preview 标签时调用
destroy()。
- 隐藏页重新显示、字体变化、缩放和面板调整后请求重新测量,再执行必要的滚动恢复。
- 初期不叠加“一个视图轮换多个状态”的内存优化;若多标签基准不达标,再单独评估。
- 持久化继续保存正文及现有源码偏移、选区方向和滚动字段,不把 CodeMirror 对象写入恢复文件。
- 应用重启不新增撤销历史持久化承诺;会话恢复与同一运行期的标签切换分别验证。
- 图片落盘、路径重写等异步工作记录目标文档 ID、发起时正文版本和选区。
- 返回时验证目标仍有效;已有取消或过期结果丢弃行为先保持一致。
- 若需要在持续输入后仍插入到原意图位置,应单独定义 transaction 位置映射规则,不能直接复用过期偏移。
5. 关键交互实施策略
5.1 输入、历史和快捷键
- CodeMirror 历史接管正文撤销,不再让原生 textarea 历史与新历史同时处理一次命令。
- 通过 transaction 的分组策略隔离批量操作,避免与前后连续输入合并。
- 沿用现有块编辑纯函数,先保留 Tab、Shift+Tab 和列表切换结果及选区规则。
- 保留整行复制/剪切的末行、末尾空行和空文档语义。
- 验证原生 Edit 菜单的撤销、重做、复制、剪切和粘贴;不能只验证键盘入口。
- 显式选择扩展与键位,避免直接引入整套
basicSetup 导致新快捷键或输入行为变化。
- 应用自定义快捷键优先;未处理的编辑操作再交给 CodeMirror。
- 同一次按键只能执行一次,处理完成时遵守
defaultPrevented 和现有组合键规则。
- 输入法组合期间保留 Enter、方向键和候选词操作,测试中文输入的撤销分组。
5.2 搜索、装饰与链接
- 保留当前编辑器/预览查找工具栏、命中计数、大小写及全字匹配语义。
- 初期复用
src/shared/search.mjs 的匹配规则,命中范围通过 decorations 显示。
- 当前结果与其他结果采用不同样式,并保留焦点回到正文后仍显示命中的行为。
- 替换及全部替换通过 transaction 提交,保持一次命令可撤销。
- 预览侧继续使用自己的 DOM 查找实现,不让正文迁移改变预览搜索。
- 行号和当前行采用 CodeMirror 扩展,当前行正文与行号区域使用明显高于章节底色的蓝色高亮;当前行样式通过更高优先级覆盖同一行上的章节 decoration,并显示左侧强调线。标题区间、差异行、报告命中使用项目自定义 decorations。
- 大范围装饰优先按可见范围生成,正文变化后更新范围映射;避免把旧全文镜像搬入新编辑器。
- 源码链接保留既有解析和路径安全规则,仅把鼠标坐标到源码位置的映射交给编辑器。
- 搜索结果和失效链接报告沿用生成文本及行目标映射,并验证高亮区的双击定位。
- CodeMirror 内部 DOM 不作为可直接改写的渲染模板。
装饰及扩展接口参见官方 API 参考。
5.3 预览同步、差异模式与焦点
- 保留预览块的
data-source-start、data-source-end 和现有双向插值思想。
- 编辑器一侧改用行块几何信息及
scrollDOM,不在业务层继续假设“行号乘固定行高”就是实际位置。
- 当前
buildScrollSyncPoints() 也接收固定行高,需同步调整其输入和测试,使其接受编辑器锚点坐标。
- 统一处理内容顶部偏移、padding、坐标原点、滚动边界和不可见行的估算位置。
- 通过测量后的校正处理长距离跳转,不依赖不可见行已经存在于 DOM。
- 保留程序化滚动事件抑制,防止两个面板互相反馈;隐藏页不能驱动共享预览。
- 预览异步渲染继续使用过期结果令牌;图片及 Mermaid 改变高度后更新映射。
- 当前整页差异模式继续使用共享 diff 输出,保留只读、临时布局和导航规则。
- 退出差异模式后,重新测量编辑器并恢复源码位置。
- 命令面板、Quick Open 和对话框捕获“文档身份、选区、滚动、焦点目标”,不能只缓存 contenteditable DOM 节点。
- 执行命令前先恢复编辑器位置;关闭面板且不执行命令时恢复原方向和视口。
- 普通 input/textarea 的原有焦点恢复仍有效;关闭期间文档已移除时安全跳过恢复。
6. 依赖与构建交付
- 建议增加独立的 Rollup 编辑器构建,输出本地 ESM 产物。
- 先仅打包 CodeMirror 入口及其依赖,沿用当前
renderer.js 的其余加载结构。
- 初选依赖包括
@codemirror/state、@codemirror/view、@codemirror/commands、@codemirror/language 和 @codemirror/lang-markdown。
@codemirror/search 是否引入取决于是否需要其查询能力;第一阶段可沿用项目匹配器。
- 代码块语言包按需加载,首轮不一次性引入全部语言。
- 安装时核实并锁定版本、传递依赖和许可证;本文不预设未经验证的具体版本组合。
- 本地构建文件和分块必须包含在安装包内,离线打开编辑器不依赖 CDN。
- 拟新增
build:editor 命令,并确保 start、dev、测试、pack 和 dist 都能从干净检出生成或消费最新产物。
- 打包先完成再启动 Electron;依赖变化后需重新构建并重启应用验证。
- 生成物目录明确加入忽略规则,人工源文件继续接受 ESLint 和文件长度检查。
- 若生成物置于
src 内,需要同时明确打包包含规则与生成物 lint/行数排除规则。
- CI 验证干净构建及打包后的离线加载,不能仅验证开发目录已有产物的情况。
- 是否延迟初始化编辑器由原型测量决定;仅在不改变初始化和恢复顺序时采用按需加载。
- 所有新增构建步骤保留既有 CSP,避免需要
eval 或远程脚本。
模块分发与打包方式参见CodeMirror 官方打包示例。
7. 分阶段任务与验收
| 阶段 |
估算 |
交付物 |
完成门槛 |
| P0:基线和最小原型 |
2–3 工程日 |
行为清单、性能数据、本地 CodeMirror 原型及打包验证 |
中文输入、选区、历史和只读原型可用;获得两种实现的同机对比 |
| P1:适配层 |
2–3 工程日 |
textarea 适配器、业务接口迁移和测试访问辅助层 |
既有行为回归通过;核心业务不再直接操作 textarea 选区和内容 |
| P2:正文编辑迁移 |
3–5 工程日 |
CodeMirror 页面、状态桥接、命令、剪贴板、恢复和构建 |
保存基线、标签历史、异步插入、焦点及会话验证通过 |
| P3:导航与展示迁移 |
3–5 工程日 |
查找、装饰、源码链接、预览同步及只读报告 |
缩放、长行、跨文档导航、差异和双向滚动回归通过 |
| P4:交付验证 |
2–4 工程日 |
性能对比、跨平台验证记录、文档和可安装制品 |
质量门槛满足;最终运行路径收敛;回退演练完成 |
- 合计约 12–20 工程日,假设一位熟悉项目的开发者执行,不包含额外 Windows/macOS 人工回归与发布观察时间。
- 估算来自代码耦合及交互范围,尚未通过原型校准;P0 完成后重新估算。
- 每阶段保留独立、可回归的小提交,不把正文切换、滚动算法和全部测试改造混成一个提交。
- P0 后若输入或可靠性不达标,保留数据并调整方案;未达到门槛不切换正式运行路径。
- P1 依旧使用当前引擎,为后续适配提供稳定基线。
- P2/P3 中未完成的 CodeMirror 运行路径仅用于开发验证,正式路径切换以完整验收为门槛。
8. 测试与性能门槛
8.1 功能回归矩阵
| 主题 |
必测场景 |
主要现有入口 |
| 编辑与历史 |
连续输入、IME、跨标签撤销、保存点、正反选区、空文档 |
test/paste-undo-smoke.js、文档模型测试 |
| 块命令与剪贴板 |
缩进、列表、整行操作、图片落盘、跨目录图片链接重写 |
paste 冒烟、test/smoke/cut-line.js、Markdown 剪贴板回归 |
| 搜索替换 |
多命中、全字/大小写、中文长行、替换撤销、只读保护、Escape |
工作区搜索及查找缩放回归 |
| 焦点与键盘 |
命令执行前选区恢复、Quick Open 取消、对话框返回、窗口聚焦 |
keyboard、quick-open、window-focus 冒烟 |
| 导航与滚动 |
大纲、链接、前进后退、水平滚动、隐藏面板、缩放 |
document-outline、navigation-history、layout 冒烟 |
| 文档生命周期 |
Preview 替换、Pinned、关闭重开、恢复稿、外部修改、历史恢复 |
workspace-session、恢复稿和标签回归 |
| 差异与报告 |
Git/时间基线、变化块边界、只读 diff、搜索/失效链接跳转 |
差异相关测试、workspace-search、broken-links 冒烟 |
| 分发 |
干净构建、离线安装包、资源分块加载及安全配置 |
Windows/macOS CI 和打包后人工验证 |
- 测试辅助层可提供正文、选区及状态读取,避免大量测试依赖编辑器内部 DOM。
- 输入法、剪贴板、原生菜单和键盘必须保留真实 Electron 路径,不用直接 dispatch 代替全部交互测试。
- Unicode、CRLF/BOM 文件往返及异步返回时已切换标签属于可靠性验收必测项。
- 输入、撤销、保存、恢复或目标文档错误均阻止交付。
8.2 性能基准
- 当前
npm run benchmark 只测 Markdown 渲染,不能作为编辑器输入延迟证据。
- P0 增加 Electron 内部基准,同时测 textarea 基线和 CodeMirror 原型。
- 数据集覆盖约 100 KiB、1 MiB、5 MiB 正文,按实际 UTF-8 字节数记录。
- 每个规模分别包含普通多行 Markdown、极长单行、中文/emoji、密集链接和代码块。
- 测试编辑单页、20 个普通标签、50 个普通标签及少量大文档标签;记录实际总字节数。
- 预览、文件差异和查找分别开启/关闭测量,区分编辑器开销与现有业务开销。
- 记录冷启动/首次打开、切换标签、输入到下一帧、粘贴、撤销、查找和跳转耗时。
- 延迟报告包含 p50/p95、样本数及暖机方式;同时记录渲染进程内存和关闭标签后的回收趋势。
- 输入到下一帧指标明确起止点和自动化限制,不能等同于系统键盘端到端延迟。
- 记录 Electron/依赖版本、硬件、操作系统、显示缩放和数据集,保证同机可比。
以下为拟定的工程门槛,P0 后结合测量噪声校准并固定:
- 100 KiB 常用场景输入 p95 目标不高于 32 ms。
- 若基线已超标,新方案至少不能明显回退,并需记录主因及后续处理决定。
- 输入、查找和切换的同机 p95 相比基线回退超过 20% 时复测并定位,未解释前不通过性能验收。
- 1 MiB、5 MiB 作为压力场景报告指标,不在缺少测量时承诺固定吞吐或延迟。
- 多标签关闭并在同等空闲/回收条件下重复开关,不应出现持续增长的残留视图或监听器。
- 内存绝对预算与启动预算在 P0 固定;CodeMirror 可能增加基础依赖和单标签开销,不能假定虚拟化等于更省内存。
8.3 性能解释边界
- CodeMirror 虚拟化改善编辑器视图的规模上限,但业务回调仍可能对全文做
split、搜索、大纲解析和 diff。
- 当前输入链路立即更新差异等内容,预览虽有 120 ms 去抖,仍可能在持续编辑时产生较高后续开销。
- 原型必须加入真实业务接线后的复测,不能只依据空白 CodeMirror 示例决定迁移收益。
- 如瓶颈在全文业务计算,先基于数据确定调度或增量更新任务,不把未经实施的优化计入迁移收益。
9. 风险、回退与兼容决策
| 风险 |
控制措施 |
通过依据 |
| 双向正文同步循环或写错标签 |
单一桥接入口、来源标记和文档身份校验 |
异步切页、撤销和外部重载测试 |
| IME 或历史分组变化 |
独立验收真实输入和命令边界 |
Windows/macOS 中文输入与菜单回归 |
| 全局与局部快捷键竞争 |
明确优先级、焦点上下文和事件处理规则 |
用户绑定、组合键、Tab 和只读场景 |
| 坐标漂移及滚动反馈 |
使用测量坐标、边界裁剪和程序化滚动抑制 |
缩放、长行、图表及隐藏面板回归 |
| Markdown 高亮与预览解析不同 |
首期高亮只提供视觉辅助,沿用既有大纲及链接业务解析 |
项目常见语法语料对照 |
| 隐藏页面测量失效或内存积累 |
激活后测量、关闭时销毁、反复开关检测 |
多标签及分栏基准 |
| 构建分块漏打包 |
干净构建并启动离线安装包 |
双平台制品验证 |
| 旧会话滚动值与新布局不同 |
维持字段并验证恢复效果,以源码位置辅助校正 |
未保存稿及导航历史恢复测试 |
- 不计划更改文件格式或同步协议,因此迁移不应要求服务端联动升级。
- 若实际遇到需要更改恢复格式、旧配置语义或其他兼容处理的问题,先按项目要求询问用户是否需要兼容支持,再实施相关工作。
- 不预先建设旧会话格式转换或长期双引擎支持。
- 正式切换前保留最后一个通过验证的提交及安装制品,记录其对应依赖锁文件。
- 回退前确认最新正文已保存,或按现有流程保留并验证恢复稿;不能直接销毁唯一持有未保存内容的编辑器。
- 通过新的回退提交或重新发布已验证制品恢复旧运行路径,不使用破坏性 Git 重置处理用户工作区。
- 回退验证包括普通文件、未保存恢复稿、标签位置和外部修改保护。
10. 完成定义与文档交付
- 所有范围内功能通过既有及新增的必要回归,Windows/macOS 人工验证有记录。
- 完成 P0 与最终实现的同机性能对比,记录未解决限制及是否满足阶段门槛。
- 核心业务通过适配接口访问正文编辑器,正式运行路径不再依赖 textarea 编辑实现。
- 已替代的展示和历史实现退出正文运行路径,现有注释、调试代码和日志按项目要求保留或经明确授权处理。
- 运行
npm run lint、npm run test 和 npm run pack,完成对应平台安装包离线启动验证。
- 修正生成物检查配置,不能用跳过人工源文件检查的方式规避 600 行要求。
- 更新
tech-arch.md、FEATURES.md、keyboard-mvp.md、navigation-history.md、phase-2-workspace.md 和开发构建说明中的实际变化。
- 当前文件查找与代码入口只在各自主要文档维护,其余位置通过链接引用。
- 每阶段提交仅包含对应工作,最终报告提交哈希、验证结果、限制及重启要求。
- 本规划文档本身的交付只需文档链接、编码/换行、行数和 Git 差异检查,无需启动 Electron。
11. 实施记录(2026-09-13)
11.1 已交付实现
- 正文编辑页统一使用
CodeMirrorAdapter,每个文档拥有独立的状态、选区、滚动位置和事务历史。
- 正文、选区、替换、只读状态和坐标访问已迁移至显式适配接口。
- DOM 仅承担事件、焦点与可访问性职责。
- 旧 smoke 的 textarea 属性桥接仅在测试中安装,不进入产品代码。
- 行号、当前行、章节、差异、查找和报告高亮改用 CodeMirror gutter/decorations。
- 文档桥接按文档 ID 回写;外部替换清除历史并作废待发事件;保存期间的后续输入保持脏状态。
- 查找替换、图片插入、链接定位、预览滚动、导航恢复、菜单及快捷键接入事务或实测坐标。
- Rollup 生成本地 ESM,并收集实际打包依赖的许可证;启动和打包命令自动构建。
- 旧正文展示与历史实现保存在
src/renderer/editor/legacy/,仅用于审计;旧测试断言保存在 test/smoke/legacy/。
- 实施与原规划的差异:直接接入 CodeMirror 适配器,未建立临时产品 textarea 适配器或双引擎开关;本次整体提交包含相互依赖的接线变更。
11.2 验证结果与边界
- 本机平台为 macOS,Electron 43.4.0。
- 新编辑器 smoke 通过原生输入/粘贴、Unicode 反向选区、撤销/重做、只读、外部重载、虚拟化跳转、视图恢复及销毁验证。
- 大文件语法保护以及跨阈值删除、撤销和重做也已覆盖。
- 粘贴回归和实际应用菜单的撤销、重做、复制、剪切验证通过。
- 键盘、工作区搜索、大纲、失效链接、导航历史、窗口焦点、工作区会话和目录标题专项回归通过。
- 与本次变更相关的单元回归 53/53 通过;变更范围 ESLint 检查通过。
npm run test 的单元部分为 356/357。
- Git HEAD 文件读取测试返回
git-unavailable;迁移前基线同样失败(354/355)。
- 因单元步骤失败,该命令未进入 smoke;上述 smoke 已单独执行。
- 完整 Electron 串行 smoke 尚未全绿。
- 已通过编辑、查找、链接、缩放、快速打开、工作区搜索等前段流程,随后在工作区会话流程出现空 DOM 点击。
- 全量驱动未创建仅在专项参数下生成的会话目录;后续路径复制调用还缺少参数。本次未扩大范围重构整套 smoke 驱动。
- 全量
npm run lint 仍受既有超长文件阻断;单独执行全仓 ESLint 及 runtime 拼接检查也存在原有错误。
- 超长文件包括两份既有设计文档、同步模块及原本已超长的完整 smoke 驱动。
- 未通过忽略人工源文件规避检查。
npm run pack 成功;macOS 打包应用的离线启动、正文编辑、预览、撤销/重做及 renderer 隔离检查通过。
- 尚未执行 Windows 制品验证、双平台真实中文输入法人工测试,以及完整输入→预览/差异/查找链路性能和反复开关后的内存趋势验收。
- 因上述项目及下节性能限制,不能将原规划 P0–P4 的所有验收门槛标记为通过。
11.3 性能测量与大文件保护
- 原始报告与复现方法见 基准记录。
- 初次 100 KiB 隔离编辑基准的 p95(textarea / CodeMirror,毫秒):
- 普通 Markdown:93.4 / 17.6。
- 单长行:17.1 / 17.6。
- 中文与 emoji:119.5 / 16.9。
- 密集链接:35.3 / 41.4。
- 代码块:100.0 / 18.5。
- 密集链接未达到 32 ms 目标;这些结果不代表完整应用输入延迟。
- 初次压力测试在 1 MiB CodeMirror 密集链接场景达到 120 秒运行上限,保留不完整报告以记录问题。
- 据此加入语法解析保护:正文超过 524,288 个 UTF-16 单元时禁用 Markdown 语法解析,缩小后自动恢复。
- 该阈值不是文件字节数;例如 1 MiB 中文文本可能仍低于阈值。
- 正文编辑、历史、业务装饰与 Markdown 预览保持可用;超过阈值时不显示源码语法配色。
- 启用保护后的 CodeMirror 压力复测完成全部五类 100 KiB、1 MiB、5 MiB 语料。
- 1 MiB / 5 MiB 密集链接的 p95 均约 17.8 ms;部分 Unicode 和代码块场景仍高于 32 ms。
- 多标签原型测量为 20 / 50 标签,总正文量 200,000 / 500,000 字节;切换耗时约 3.9 / 4.2 ms。
- 销毁后 DOM 中无残留编辑器;该断言不等于内存泄漏验收。
11.4 使用与回退
- 更新后重启正在运行的桌面应用;开发模式使用
npm run dev,自动先构建编辑器。
- 无需升级服务端或转换 Markdown 文件。
- 实施前代码基线为
540ced7c27f01371b26ef03c79367d7ce3263d32,依赖锁文件随该提交保留。
- 此基线也有上述测试失败,不能视作所有验证通过的发布制品。
- 回退须先保存正文或验证恢复稿,再通过新的回退提交恢复基线;不要直接重置含用户改动的工作区。
11.5 迁移后定位滚动修复
- 大纲点击、键盘导航、跳转到行、差异导航、搜索结果定位及源码链接跳转统一使用 CodeMirror 滚动事务。
- 目标位置按顶部留白对齐,并在虚拟化目标行完成测量后滚动。
- 显式定位覆盖设置选区产生的待处理滚动快照及先前排队的视图恢复,避免光标已到目标但视口回到原处。
- 查找仍保留原有就近或居中定位方式;隐藏编辑面板时的大纲跳转仍定位可见预览。
- 回归检查同时验证异步滚动后的实际目标坐标、大纲点击与键盘定位、搜索位置、标题链接和无片段链接回到文首。
- 本次验证结果:
- 编辑器、大纲与收藏、导航历史三组专项 smoke 通过,相关单元测试 13/13 通过。
- 适配器与编辑器 smoke 的 ESLint 检查、Git 差异空白检查通过。
- 全量 lint 仍被既有超长文件阻断;runtime 拼接检查仍报既有
resetFavoriteSortMode 未使用错误。
- 更新后需重启桌面应用;开发模式通过
npm run dev 重新构建并启动。
11.6 迁移后适配器边界复查
- 修复程序化正文输入的换行与偏移不一致。
- 初始化及全文重载的缓存正文使用 CodeMirror 规范化后的 LF 文本。
- 范围插入按规范化后的文本长度放置光标,避免 CRLF 在文末插入时造成选区越界。
- 仅换行表示不同、规范化后内容相同的重载不再清空撤销历史。
- 磁盘 BOM 和 LF/CRLF 保存策略仍由文件服务负责。
- 修复全文重载丢失占位提示的问题。
- 重建编辑状态时使用当前占位提示、只读和禁用配置。
- 正文发生外部替换时仍清空撤销历史。
- 新增 Electron 回归覆盖初始化与重载的 CRLF/CR、插入后的光标与撤销/重做、等价重载保留历史,以及占位提示和禁用配置保留。
- 修复前新增测试复现
Reload lost placeholder。
- 修复后编辑器专项 smoke 通过。
- 本次检查与验证结果:
- 改动前完整 smoke 中的编辑器、布局、粘贴步骤通过;综合流程在失效最近笔记库检查失败,表现为未移除记录且未显示通知。
- 改动后键盘专项 smoke 通过。
- 全量单元测试为 356/357;失败项仍为已知的 Git HEAD 读取
git-unavailable。
- 改动文件 ESLint 与 Git 差异空白检查通过。
- 全量 lint 仍被既有超长文件阻断;runtime 拼接检查仍报告
resetFavoriteSortMode 未使用。
- 本次未重新执行打包、跨平台人工输入法、完整链路性能或内存验收;这些边界仍按第 11.2 节记录。
- 更新后需重启桌面应用;开发模式运行
npm run dev 重新构建并启动。