Markra 2.0.0 发布了。
距离 1.0.0 发布,只过去了不到一个月。这个版本跨度并不在原来的计划里,而是一次真实的编辑问题把 Markra 很快推到了必须重写内核的节点。
这是 Markra 目前最大的一次版本升级:编辑器内核从 Milkdown / ProseMirror 完整迁移到了 CodeMirror 6,所见即所得编辑也在新的内核上重新实现。
先说 2.0.0 带来了什么
除了重写编辑器内核,2.0.0 还带来了这些变化:
- 所见即所得编辑建立在真实 Markdown 上,链接、图片、表格、代码块、Mermaid、公式、HTML 和 frontmatter 都可以在预览与源码之间自然切换。
- 新增打字机模式和 Vim 模式,分别服务于沉浸式写作与键盘操作。
- 支持开关自定义主题、配置
Alt组合快捷键和折叠文件列表。 - 新增未使用图片安全清理、文件树键盘删除、历史版本预览恢复和移动端 Web 文件抽屉。
- 改进长时间流式 AI 请求,避免已经建立的响应被统一总超时提前中断。
这些功能让 2.0.0 不只是“换了内核还能继续用”,也开始利用新的底层扩展写作体验。不过,这次重构真正的起点,是一个看上去并不起眼、却一直无法稳定修好的中文输入法问题——它最终变成了 Markra 的 Issue #419。
一个比崩溃更难处理的问题
问题出现在所见即所得模式的格式边界。
例如下面这句话:
这是**测试内容**为格式文本的编辑。把光标放在粗体文字的结尾,继续使用中文输入法输入,候选文字有时不会落在预期位置。它可能跑到粗体范围之外,也可能继承错误的格式。更麻烦的是,这个问题并不是每次都出现,在不同系统、不同 WebView 和不同输入法上的表现也不完全一致。
英文输入通常正常,中文输入却会出错;普通文本正常,粗体、斜体等格式边界却更容易触发;浏览器里可能正常,Tauri 桌面端又能复现。
这种问题不会让应用直接崩溃,也不会留下清晰的错误堆栈,但它会破坏编辑器最核心的可信度:用户无法确定接下来输入的文字究竟会出现在哪里。
对写作工具来说,这比某个次要功能失效更严重。
一开始,我以为它只是一个边界判断
最初的判断很自然:既然问题发生在粗体结尾,那就在格式边界附近修正光标或选区。
Markra 当时的所见即所得编辑器建立在 Milkdown 和 ProseMirror 上。Markdown 会先被解析成 ProseMirror 的结构化文档,** 这样的 Markdown 定界符不再是编辑器里的普通文本,而是被表达为文档模型中的 mark。浏览器最终看到的是一组可编辑 DOM,ProseMirror 再负责在 DOM 选区和自己的文档位置之间同步。
对于常规富文本编辑,这套模型非常成熟。问题在于,Markdown 编辑器还需要额外维持另一层语义:保存到磁盘上的内容必须仍然是原始 Markdown。
一次输入因此至少穿过了几层状态:
磁盘中的 Markdown
↓ 解析 / 序列化
ProseMirror 文档与 mark
↓ DOM 映射
浏览器 Selection / Range
↓ 原生输入系统
IME composition光标落在普通文字中间时,这几层通常没有分歧。但当光标刚好位于一个 mark 的结尾时,“当前位置”并不只有一种解释:
- 它可以属于前面的粗体;
- 也可以属于后面的普通文本;
- DOM 光标可能落在
<strong>内部的末尾; - 也可能落在
<strong>后面的相邻文本节点; - 输入法在 composition 开始后,还会继续记住自己的输入目标。
应用层看到的选区位置看似没有变化,原生输入法实际绑定的 DOM 位置却可能已经不同。
为什么补丁总是差一点
围绕这个问题,我尝试过修正格式边界的 selection affinity、在 composition 开始前保存格式状态、在输入完成后重新对齐光标等方案。
其中一些补丁在特定环境下确实有效,但很难同时覆盖 macOS 和 Windows 的系统 WebView。修好光标位置后,可能又出现选区异常;修好选区后,撤销栈或 stored marks 又产生新的边界行为;如果在 composition 过程中主动更新 DOM,还可能直接打断输入法正在生成的候选文字。
Issue #419 中间经历过一次“看起来已经修复”,随后又在 Windows 11 上被重新确认。之后在原生 macOS 和 Windows WebView 中测试,问题依然可以复现:中文输入法提交的文本仍可能落到预期格式范围之外。
这时我逐渐意识到,问题并不只是少了一个 if。
继续修补并非完全没有可能,但应用层能够控制的只是整个输入链条的一部分。每次修复都需要同时猜测 ProseMirror、DOM、系统 WebView 和输入法的行为,稳定性很难得到保证。
真正需要改变的,是 Markra 的编辑模型。
为什么最后选择 CodeMirror 6
CodeMirror 也不是天然免疫 IME 问题。它同样运行在浏览器和 WebView 中,也需要处理 composition、DOM 选区和 decoration 边界。
它真正吸引我的地方,是能够让 Markra 重新把 Markdown 源文本作为唯一编辑状态。
新的模型更接近这样:
Markdown 源文本(唯一状态)
↓ syntax tree / range index
Decoration、Widget 与视觉样式
↓
用户看到的所见即所得界面在 CodeMirror 中,下面这段文字里的星号始终真实存在:
这是**测试内容**为格式文本的编辑。非编辑状态下,Markra 可以通过 decoration 把 ** 隐藏或弱化,把中间的文字显示为粗体;当光标进入粗体范围时,再把完整源码展开出来。
无论视觉层怎么变化,文档位置始终是 Markdown 字符串中的 offset。AI 编辑、搜索替换、链接定位、撤销重做和文件保存,都可以基于同一套位置系统工作。
这并没有消灭所有边界问题,但它减少了一次关键转换:Markra 不再需要在 Markdown 字符位置和另一套富文本文档位置之间来回同步。
更重要的是,应用层终于可以直接决定:
- 哪些 Markdown 标记应该显示;
- 光标进入什么范围时展开源码;
- composition 期间哪些 decoration 不应该重建;
- 哪些预览组件必须保持尺寸稳定;
- 一次格式操作究竟替换了哪些源文本;
- 选区应该包含正文,还是排除左侧控件和 Widget。
搜索、AI 编辑、格式命令和插件扩展也可以共享同一套 Markdown offset 与编辑器接口。
我选择 CodeMirror,不是因为它替我完成了所见即所得,而是因为它允许我自己控制所见即所得。
这不是“换一个依赖”
真正开始迁移后,我很快确认了最初的判断:这几乎等于重新实现一遍编辑器。
从行内格式、块级预览到拖拽、拼写检查和 AI 选区编辑,原有能力过去都依附在 Milkdown / ProseMirror 的 schema、plugin、node view 和 transaction 上;换成 CodeMirror 后,它们需要重新建立在 Markdown 范围、transaction、decoration 和 widget 上。
第一轮替换完成时,新编辑器能打开、能输入,也能保存,但离真正可用还很远。
后续的 alpha 版本几乎都在处理交互细节:表格点击后不能突然退回源码;链接需要在普通点击时展开、在 Cmd/Ctrl + Click 时打开;代码块要能选择文字,又不能因为拖动鼠标而自动收起;标题、粗体、删除线需要在光标进入时完整显示定界符;图片预览不能在中文 composition 的每次更新中隐藏和重建。
它们看起来是很多零散 Bug,实际上都指向同一个新问题:如何让“真实源码”和“视觉预览”在编辑过程中保持稳定关系。
IME 又一次成为了验收标准
迁移 CodeMirror 后,我没有把“能输入中文”当作完成标准。
真正需要验证的是:
- 在粗体开头、内部和结尾分别输入;
- 在斜体、删除线、链接和行内代码边界输入;
- 输入法候选状态下移动选区或点击其他位置;
- composition 期间让下方图片、表格和代码块保持稳定;
- 在软换行、段落边界和 Widget 附近提交文字;
- 分别在 macOS、Windows 和 Web 版中测试。
这个过程也让我认识到,IME 并不是一串特殊的键盘事件。
用户输入一个汉字时,浏览器经历的是一段持续的 composition 会话。候选文字会反复更新,最终才提交为文档内容。如果编辑器把每次 compositionupdate 都当作普通文本修改,并立即重新解析、折叠语法或重建大块预览,光标、选区和页面布局都可能变化。
因此 Markra 2.0.0 的一条重要原则是:输入法合成期间,视觉层应该尽量保持安静。
源码可以更新,但无关的预览不应反复销毁;与 composition 相交的格式不能突然折叠;图片、Mermaid、HTML 等块级 Widget 需要稳定复用;依赖选区的浮动工具栏也不能抢走焦点。
这部分工作并不比最初替换内核轻松,但不同之处在于,它已经处于 Markra 可以测试和控制的范围内。
写在最后
Markra 2.0.0 表面上增加了不少功能,但对我来说,最重要的更新仍然是重新获得了编辑器的控制权。
Issue #419 最初只是在确认一个旧问题到底有没有修好。
如果当时只看问题表面,它可能会继续变成又一个边界补丁。但沿着中文输入法、格式 mark、DOM 选区和系统 WebView 一层层追下去,我最终发现,真正限制 Markra 的不是某段错误代码,而是应用希望提供的 Markdown 编辑体验,已经超出了原有编辑模型适合承载的边界。
所以 Markra 2.0.0 并不是一次为了“更现代”而做的技术升级。
它更像是一次架构止损:与其继续在无法完全控制的映射边界上打补丁,不如接受重写的成本,把 Markdown、光标、选区和视觉预览重新放回同一套可控模型里。
一个中文输入法 Bug,最后推动我重写了整个编辑器内核。
这大概也是开发编辑器最真实的一面:最小的输入细节,往往连接着最深的系统边界。
如果你正在使用 Typora、Obsidian 或 VS Code 编写 Markdown,又希望保留本地文件、所见即所得和内联 AI,可以试试 Markra 2.0.0。
- Web 版:editor.markra.app
- GitHub:markrahq/markra
- 桌面版下载:GitHub Releases
2.0.0 是一次大规模重写,也一定还会存在没有覆盖到的边界场景。如果你遇到中文输入、格式编辑、表格、代码块或其他问题,欢迎直接在 GitHub 提交 Issue。真实文档和真实输入习惯,仍然是 Markra 继续完善最重要的测试。
