许愿式编程:我把 Typability 改成了能日常顶替 Typora 的样子
为什么要 fork
我用 Typora 挺长时间了,它确实好用,但有两个问题一直绕不过去
-
要花钱。网上那些"激活版"破解得不彻底——过一阵又弹试用提示,需要清除缓存,反复折腾。
-
改不动。用着用着总有些小地方不顺手(弹窗逻辑、窗口行为、菜单状态),但在闭源软件里只能忍着。
所以我找了开源基座:Typability 正好符合我的口味。它是一个基于 Milkdown 的所见即所得编辑器,Tauri 打包(体积小、内存占用低),Fluent UI 界面,还能用 Mica/亚克力窗口效果。上游最后的版本是 0.4.1,我从这里开始改——功能骨架都有了,但日常用起来到处是"小别扭"。
这里得先说句感谢的话:Typability 的底子是真的不错,感谢原作者 SimonShiki 。Milkdown 的 WYSIWYG 内核、Tauri 的打包链路、Fluent 的组件体系和 Nord/Tokyo 主题、多语言、查找替换、双栏编辑、LaTeX 与 Mermaid,上游都已经搭好了。我做的更多是"补齐细节,优化操作",而不是从零造一个 Markdown 编辑器。
于是 fork,然后一路改到 0.6.3。下面把这几个版本做了什么、踩了哪些坑、以及它现在到底能不能替代 Typora,完整记一遍。
版本一览
|
版本 |
主题 |
|---|---|
|
0.5.0 |
窗口记忆、保存/关闭语义、拖拽打开、标题栏路径、源码视图、检查更新、构建修复 |
|
0.6.1 |
双栏编辑器与源码视图互斥 |
|
0.6.2 |
链接气泡加宽 + OPEN 按钮、默认文档链接指向本仓库 |
|
0.6.3 |
查找替换高亮、匹配位置修正、自动聚焦 |
0.5.0:先把"每天都要碰"的别扭解决掉
1. 记住窗口大小和位置
原来每次启动都是固定的 540×680 居中窗口,每次都要手动拖大、挪到副屏。
现在首选项里有开关,勾选后:
-
关闭时由 Rust 侧同步读取窗口几何并落盘到应用配置目录(不放 localStorage,避免前端时序问题)
-
只记录"普通窗口"的尺寸:最大化/最小化时跳过,所以下次恢复的是你常用的那个窗口大小
-
加载时做了保护:如果记录的尺寸比所有显示器都大,就忽略尺寸用默认值;如果记录的位置在任何一个显示器上都不可见(比如拔掉了副屏),就忽略位置用默认居中
这一条本质上是"不要让我因为换了显示器就被锁在一个看不见的窗口里"。
2. 只有真的改过内容,才提示保存
这是最烦人的一个:打开文件什么都不改,关掉也弹"你还没有保存"。
根缘在编辑器而不是弹窗逻辑——打开文件时会调用 replaceAll 把内容灌进 Milkdown,这次程序化的文档替换会触发 markdownUpdated 事件,于是"刚打开的文件"被标记成已修改。
改法是两条:
-
记录"打开/保存时的原始内容",用
dirty = 当前内容 ≠ 原始内容判断,而不是用一个会被随意翻转的saved标志 -
编辑器里区分程序化替换和用户输入:
replaceAll期间置一个标记,它引起的那次 markdown 更新只用来同步基线,不当作修改
顺带把撤销回原样的情况也理顺了——改完又改回去,不算修改,不会弹窗。
同时统一了"取消"的语义:确认框只决定要不要保存,不决定要不要继续操作。关闭时点取消 = 不保存直接退出,打开/新建/拖拽时点取消 = 不保存但继续切换。否则会出现"不想保存就永远退不出软件"的死局。
3. 保存项的状态、另存为的弹窗
-
文件菜单的「保存」现在跟随改动状态:有改动才可用;从来没保存过的空白文档例外,始终可用(否则新建文档第一次没法保存)
-
「另存为」之前的实现是"先把当前路径置空 → 触发一次保存确认 → 再弹系统保存对话框",两个对话框叠在一起很荒谬。现在它走独立流程:只弹系统保存对话框,保存成功后切换到新路径
-
保存一个还没有路径的新文档后,路径会回写,不会每次按 Ctrl+S 都再问你存到哪里
4. 拖拽打开
支持直接把 md/txt 文件拖到窗口里就能打开。有未保存改动时先问是否保存,和文件菜单里的"打开"走同一条路径。
5. 标题栏显示当前文件名
编辑菜单右侧的空白区域显示当前文件名,悬停显示完整路径。宽度不够时中间省略(a-very-...e-name.md),比截断尾部更容易辨认。未保存的文档显示 Untitled-1、Untitled-2……每次新建递增。
6. 源代码视图(Typora 的 source mode)
编辑菜单最后加了分隔栏和「源代码视图」复选项,切换 WYSIWYG 和 Markdown 源码编辑。源码模式下是等宽字体的 textarea,直接读写同一份内容,所以保存、脏标记、自动保存都照旧生效。
几个细节:
-
和「双栏编辑器」互斥,勾一个自动取消另一个
-
打开或新建文档时都回到所见即所得(勾选状态自动取消)
-
源码模式下,依赖 Milkdown 实例的菜单项(查找/替换/插入表格、图表、公式)自动禁用,避免操作已销毁的实例
7. 「检查更新」从占位变成能用
关于页那个按钮原本是写死 disabled 的占位,项目里根本没启用 Tauri updater,也没有任何目标地址。现在它用默认浏览器打开本仓库的 GitHub Releases 页面——不做版本比较、不涉及 API 限流,够用了。
8. 一个构建问题
react-intl 的 @types/react 依赖范围包含 19,yarn 装出了 18.3.19 和 19.2.0 两份类型,导致 tsc 报 FormattedMessage 不能作为 JSX 组件。用 resolutions 统一到 18 解决。
0.6.1:双栏和源码视图归位
「双栏编辑器」菜单项原来夹在"插入表格"和"插入图表"之间,语义上它俩才是一对(都是视图切换),所以把它下移到「源代码视图」上方,并做互斥。打开/新建时两者都恢复未勾选。
这里还修了一个 Fluent UI 的细节:MenuItemCheckbox 默认 persistOnClick: true,点完菜单不收起,得显式关掉;另外勾选状态原来是组件内部维护的,外部重置时 UI 不会跟着变,改成了受控的 checkedValues。
0.6.2:链接气泡加一个 OPEN
点击文档里的链接会弹出编辑气泡,只有一个 APPLY 按钮——改完地址就完事,想真正访问这个链接只能先复制出来再粘到浏览器。
现在气泡加宽到 560px,APPLY 后面多了个同款 OPEN 按钮,用默认浏览器打开输入框里的地址;没写协议(比如 github.com)会自动补 https://。
这个气泡其实不在本项目源码里,而是 @milkdown/theme-pack-helper 提供的 input-chip——宽度和按钮文案都是主题里写死的。所以做法是:编辑器就绪后监听该节点,加宽、在 APPLY 后插入同款按钮(继承 .tooltip-input button 的样式)。图片链接气泡的 placeholder 不同,不会被加上 OPEN。
最后把默认文档里那个指向原作者仓库的链接改成了本仓库地址。
0.6.3:查找替换终于"看得见"了
匹配没有反显,却弹出了浮动气泡
现象很怪:Ctrl+F 搜索,明明提示"第 1 项,共 1 项",但编辑器里看不到选中的文字;反而弹出了选中文本才会出现的浮动工具窗口。
原因是两层叠加:
-
查找只是把匹配区间变成 ProseMirror 的选区(
setSelection),而 ProseMirror 只在编辑器获得焦点时才把内部选区同步到 DOM 选区。查找时焦点在搜索框里,所以浏览器根本不画高亮 -
Milkdown 的 tooltip 只看 editor state 的选择变化,不看焦点,所以选区一变它就先弹出来了
结果就是"状态里有选区,屏幕上看不见"的割裂。
改法:不依赖原生选区,改用 ProseMirror 的 Decoration 画高亮。带 PluginKey 的插件保存匹配区间,Decoration.inline 给所有匹配加 find-match、当前项加 find-match-current,doc 变化时清空、否则按 mapping 跟随。
高亮范围整体偏移
接上之后发现高亮错位:搜 Design,高亮的却是 esig 再往后两个字。
原来的 Finder 是递归累加 nodeSize 去算文本块的绝对位置,每进一层块级节点就漏算该节点自身的起始偏移。所以嵌套越深偏得越多——Fluent Design 在 bullet_list > list_item > paragraph 里,正好偏 2 个字符。
这个 bug 不只影响高亮:setSelection 用的是同一批位置,替换功能之前也会替换错位置。
改法:直接用 doc.descendants 拿每个文本块的真实位置(文本从 pos + 1 开始),顺手删掉了原来 50 多行递归代码。
打开时自动聚焦
Ctrl+F 打开查找条后焦点落在搜索框里,直接打字就行;已经在查找状态再按一次 Ctrl+F 也会把焦点拉回来。
几个值得记下来的坑
-
ProseMirror 的选区只在聚焦时同步到 DOM:所有"程序化选中"的场景(查找、定位、跳转)都要用 Decoration 来做视觉反馈
-
Milkdown 的主题包会生成 UI 但不在你项目里:链接气泡、工具栏这些都在
theme-pack-helper,要改只能拦截 DOM 或覆盖主题 feature -
Fluent 的 MenuItemCheckbox 默认点击后不关闭菜单,并且勾选状态需要受控才会跟随外部状态
-
Tauri v1 的 updater 默认关闭,一个写死
disabled的按钮看起来像"功能坏了",其实根本没实现 -
彩色 emoji 在 16 位色会话里会变成单色线稿:这个我排查了半天,最后发现同一个字符在 RDP 16 位色下渲染灰白记事本、32 位色下是黄色 Fluent emoji,跟代码无关
-
窗口状态别只靠前端存:移动/缩放事件 + 关闭时的异步写入会和窗口关闭赛跑,交给 Rust 在
CloseRequested里同步落盘更可靠
发布流水线
-
打
v*标签自动触发构建(也保留手动触发),三平台矩阵 -
发布前校验标签和
package.json版本一致,不一致直接失败 -
Windows 除了
setup.exe/msi,额外提供一个免安装的Typability_<版本>_x64_windows.zip(里面只有可执行文件) -
Linux 暂不提供官方构建,需要的同学自行编译(README 里有一行命令)
这一切是怎么写出来的
需要说明的是:这些改动全部是在 CodeBuddy + DeepSeek-4.1-flash 的"许愿式编程"下完成的(包括这篇文章的初稿)。
我做的事情基本只有两件:用中文把需求和现象描述清楚("关窗口时没改也弹窗,很烦"、"高亮的位置不对"),以及在编译出来的程序上点点看、把问题再反馈回去。代码本身——Rust 的窗口状态落盘与显示器越界保护、Tauri 配置和权限、ProseMirror 插件、React 组件、GitHub Actions 流水线——都是它写的。
我自己从来没写过 Rust。这次之前,我对 Rust 的认知仅限于比C++安全。但整个过程里,从环境搭建(rustup、国内镜像、MSVC 工具链)到 src-tauri/setup.rs 里的窗口恢复逻辑,再到为 release 工作流加 Windows 便携包,全都跑通了,而且每一步都能编译、能验证。
更"神奇"的是它能扛住排查——比如查找高亮偏移那个 bug,它能从现象(高亮偏了两个字符)一路推到根因(递归算位置时漏算块级节点自身偏移,嵌套两层正好偏 2),再直接改掉并解释为什么原来的实现会错。
对我这种"不会这门语言、但知道自己要什么"的用户,这种开发方式大概就是最好的形态:我负责许愿和验收,它负责实现和解释。
当然也得承认另一面:许愿式编程的前提是底子够好。上游把编辑器内核、打包、主题、i18n 这些"重活"都做完了,AI 才能把精力放在窗口状态、保存语义、查找高亮这类具体问题上。要是让我从零开始,光是把 Milkdown 跑起来大概就够喝一壶的。
它现在能替代 Typora 吗
就我自己的使用场景(写笔记、写文档、写博客草稿),能。日常高频的东西都覆盖了:
|
功能 |
状态 |
|---|---|
|
所见即所得编辑 |
✅ |
|
源代码视图切换 |
✅ 0.6.0 补上 |
|
标题/列表/引用/代码块/表格 |
✅ |
|
LaTeX 公式 |
✅ |
|
Mermaid 图表 |
✅ |
|
双栏编辑 |
✅ |
|
查找/替换 |
✅ 0.6.3 修好 |
|
自动保存 |
✅ |
|
拖拽文件打开 |
✅ |
|
深浅色主题、窗口毛玻璃 |
✅ 比 Typora 更好看 |
|
多语言界面 |
✅ |
|
HTML 导出 |
✅ |
还差的部分我也清楚:
|
功能 |
状态 |
|---|---|
|
大纲/文件树侧栏 |
❌ 最想补的一个 |
|
图片粘贴/图床 |
❌ 目前只能插链接 |
|
PDF / Word 导出 |
❌ |
|
多标签、多窗口 |
❌ 单文档 |
|
自动更新 |
⚠️ 只跳转 Releases 页 |
|
Linux 官方包 |
⚠️ 自行编译 |
但换个角度说:它的 90% 都比原来那个"破解不彻底的 Typora"更好用——不用再应付激活失效、不用忍受挡路的弹窗、窗口行为符合预期、出问题能自己改代码。对一个我每天要开十几次的工具来说,这比多几个导出格式重要得多。
下一步打算补大纲侧栏和图片插入,凑齐之后大概就可以彻底把 Typora 卸了。
*本文记录的改动都在 towerbit/Typability 上:从上游最后的 0.4.1 起,一路改到 0.6.3。
原文地址: https://www.cveoy.top/t/topic/qHSo 著作权归作者所有。请勿转载和采集!