Zed: 用 Rust 实现终极编辑器之梦
“本文并非 Zed 编辑器的广告,而是想探索 Zed 团队背后的商业动机与技术栈。不要因为本文而引起编辑器之争!
Zed 编辑器介绍
Zed[1] 是一款由 Atom[2] 和 Tree-sitter[3] 的创造者开发的高性能多人代码编辑器。
“Atom,是 GitHub 出品的一款文本编辑器。Tree-sitter 是 Rust 实现的一款增量式解析器。
Zed 的口号是 「以思维的速度编写代码」,这句口号至少传达出了下面几个理念:
快速响应:这个口号暗示 Zed 编辑器具有极高的性能和响应速度,能够与开发者的思考速度保持同步。在实际使用中,这意味着无论代码编辑、导航还是其他操作都能迅速、流畅地完成。 提升效率:口号强调了 Zed 旨在提高编码效率。通过优化的用户界面和智能功能,Zed 允许开发者更快地实现想法,减少等待和冗余操作的时间。 强调直觉性和易用性:“思考的速度”这一表述还暗示了 Zed 的易用性和直觉性设计。这意味着用户界面直观易懂,功能布局合理,使开发者能够本能地进行操作,无需花费大量时间学习和适应。 提升创造力:此口号还表明 Zed 编辑器支持和激发开发者的创造力。通过提供高效的工具和流畅的编程体验,Zed 帮助开发者更专注于创意和解决问题,而不是被琐碎的操作所困扰。 适应现代编程需求:在当今快节奏的软件开发环境中,Zed通过这个口号展现了它对于满足现代开发者需要快速、高效工具的理解和承诺。
Zed 目前的特点就是性能好,无论是插入延迟、启动时间还是内存占用,都是同类编译器和 IDE 中占用时空资源最少的。
Zed 提供了一些其他编辑器目前还没有的现代化开发理念:
团队协作支持(软件团队的虚拟办公室)。Zed 通过Channel 和 CRDTs 来支持多人协作开发,意味着,开发团队可以同时多人修改同一个文件,并可以同时进行交流讨论。 集成了 AI 辅助编码。支持 GitHub Copilot 和 GPT。
Zed 也支持 Vim 模式、定制快捷键等。
总的来说,Zed 目前的给我体验和该口号比较吻合,突出了其旨在提供无缝、高效编程体验的目标。Zed 在经历三年的开发之后,于昨日开源,并且已经发布了稳定版本供下载。
Zed 的研发历程
始于 Xray
Zed 的基本理念始于 2018 年 GitHub 内部的一个名为 Xray 的项目。
Xray[4] 是 GitHub Atom 团队之前立项开发的一个基于 Electron 的文本编辑器,因为 Atom 编辑器推出后性能方面一直被社区和用户诟病,其中在加载大文件的情况下,性能问题尤为明显,因此 Atom 开发团队希望通过 WebGL 将界面这块进行重新实现。但是他们不希望抛弃 Electron,因为他们相信 Electron 还是开发跨平台可扩展界面最优秀的技术平台。并且 Xray 开始选择 Rust 来实现。
然而 Xray 在 2018 年底就因为复杂性太高而无法持续,项目停滞。
Xray 的两位主创 Nathan 和 Antonio 在 2019 年到 2020 年之间就开始重新实现他们的新的编辑器,就是 Zed 和 GPUI(Zed 内部使用的 GUI 库) 的雏形。
2020 年 Nathan 收到了 Warp[5] (Rust 实现的终端) 的一份工作邀请,但是他在 Warp 待了三个月就离职了,因为他始终无法放弃构建终极编辑器的梦想。
因此,他又集结了Antonio 和另一位开发者 Max ,三人一起从 2021 年开始全职进行 Zed 开发。
到 2022 年 3 月,Zed 团队终止闭门造车,开始建立 Alpha 公测社区,逐步扩大用户圈,为公测做准备。这个策略现在看来还是不错的,这也是开源商业社区值得学习的一种策略。不是等产品完全开发完成再面向用户,而是让用户的试用来促进开发。现在 Zed 编辑器开源了,Zed 社区又将引入新的一批用户。
Zed 编辑器的开源许可证说明:
共享版权许可:Zed 的代码使用了共享版权(或称“copyleft”)的许可方式。这种许可方式的特点是,任何基于此代码的衍生作品都必须在相同或兼容的许可证下发布。这样做的目的是保证任何对原始代码的改进或扩展都能惠及整个社区,而不是被封闭或私有化。 编辑器使用GPL许可:Zed编辑器本身采用了GNU 通用公共许可证(GPL)。GPL 是一种广泛使用的共享版权许可证,它要求任何发布基于 GPL 许可代码的衍生作品时,也必须使用 GPL 许可。这意味着,如果其他开发者修改了 Zed 编辑器的代码,他们必须在 GPL 许可下发布这些修改。 服务器端组件使用 AGPL 许可:Zed 的服务器端组件使用的是 GNU Affero 通用公共许可证(AGPL)。AGPL 与 GPL 类似,但有一个关键区别:如果 AGPL 许可的软件作为网络服务运行,那么提供该服务的人必须向用户提供源代码的访问权限,即使他们没有修改过代码。这个许可证的选择意味着,任何基于 Zed 服务器端组件的网络服务都需要向其用户提供源代码。
Zed Channel : Fireside Hacks
Zed 使用 Channels 来运行一个名为 Fireside Hacks 的新项目,在这个项目中,全球的开发者都可以在一个公共频道中与出现的任何人一起实时开发 Zed。
私以为,Zed Channel 可能为开源社区贡献带来一种全新的方式:开源项目的维护者可以通过 Zen 编辑器与新的项目贡献者实时交流与指导。
Zed 的开源商业模式
也许和创始人在 Wrap 公司工作经历有关,Zed 的商业模式和 Wrap 类似:个人免费和增量付费,团队收费。将来也可能会推出企业专用产品。
Zed 的 Channel 功能将来会对个人也收费(测试期间是完全免费的),但我期望他们的费用不要太贵,因为它可能为开源社区贡献带来一种全新的方式。不要因为付费太贵而断送了这个可能性。
Zed 也提供 AI 功能,这个也是增量付费的一种方式。悄悄说:Wrap AI 是免费的。
Zed 也承诺永远不会在用户的代码编辑器中显示横幅广告。
这些都是 Zed 团队当前对商业模式的设想,希望 Zed 可以做到更好,毕竟用爱发电是不可能长久的。
Zed 技术栈一瞥
Zed 性能强劲和支持多人协作的秘密就是它使用下面技术栈:
Rust 异步并发加持 CRDTs 增量解析器 Tree-sitter GPUI
Rust 异步并发加持
利用 Rust 支持的安全并发,支持在多核上并行处理工作,而不会影响应用程序的稳定性。基于 Rust 异步,将 CPU 密集型任务从主线程中移出,从而实现了在单线程编辑器中无法实现的响应性。
CRDTs
CRDTs(Conflict-Free Replicated Data Types,无冲突复制数据类型)是一种特殊的数据结构,用于在分布式系统中同步和存储数据。在这样的系统中,多个副本可以独立更新,随后这些更新会被合并,以确保所有副本最终一致。CRDTs 的关键特点是它们的设计允许在不同节点上并行和独立地进行操作,而不需要立即进行中央协调或锁定。
CRDTs 的核心特征:
自动冲突解决:CRDTs能够自动解决多个副本之间的数据冲突,无需复杂的冲突检测和解决机制。 强最终一致性:尽管各个副本可以独立更新,但 CRDTs 保证所有副本最终会达成一致状态。 离线操作和延迟容忍:CRDTs适合于网络条件不稳定的环境,可以支持离线操作,之后再将更改同步回集群。 无需中央协调:与传统的需求中央服务器进行协调和锁定的数据结构不同,CRDTs允许多个副本独立进行更新。
CRDTs 主要分为两类:
CvRDTs(Convergent Replicated Data Types):通过同步整个状态来保证最终一致性。适用于带宽不是问题,但延迟容忍度较高的场景。 CmRDTs(Commutative Replicated Data Types):通过传播操作(而非状态)来实现一致性。适合于网络带宽有限,但需要更快速应答的场景。
CRDTs 的应用场景
分布式系统:在多个节点间同步数据时,CRDTs 提供了一种有效的方法来处理网络延迟和分区。 协作编辑:CRDTs非常适合于实时协作编辑工具,如在线文档编辑器,它们可以确保不同用户的更改不会相互冲突。 离线应用:在需要支持离线工作的应用中,CRDTs 允许用户在没有网络连接时进行操作,随后在重新连接时同步更改。
在 Zed 编辑器中,每一个缓冲区(编辑器用户打开文件内容的内存区域)就是一个 CRDTs 结构。使用 CRDTs 是为了支持多用户实时协作编辑功能。通过 CRDTs,编辑器能够确保即使在网络条件不稳定的情况下,多个用户的更改也可以无冲突地被合并,保持文档的一致性和完整性。这使得团队成员可以同时在同一个文档上工作,无需担心编辑冲突或数据丢失。
增量解析器 Tree-sitter
Tree-sitter 是 Zed 另一位联合创始人的作品,也是开发了很多年。它是专门为编写编辑器而实现的解析器。因为源代码经常变化,需要快速重新解析,并且正在编写中的代码也可能是不完整的,但仍然需要解析其中正确的部分。其他解析器在这种场景下的性能不足以支持 Zed 的理想。
Tree-sitter 使用上下文无关文法的精确形式来解析代码,采用了一种称为广义 LR 解析(或 GLR)的算法,几乎支持任何编程语言的文法。并且支持增量解析来允许在编辑后进行高效的重新解析,并且具有一种新颖的错误恢复技术,即使文件处于无效状态,解析器也能产生有用的结果。
延伸知识:
Zed 中 使用 Sum Tree 来存储解析后产生的抽象语法树(AST)。在 Zed 中也广泛使用它来存储有序集合。
"Sum tree"(和树)是一种特殊类型的数据结构,具有 B-tree(B树)和copy-on-write(写时复制)的特性。这种数据结构通常用于高效地管理可变数据,特别是在需要优化读取和写入性能的场景中。
B-tree 特性:B 树是一种自平衡树数据结构,它能够保持数据有序,适合于大量数据的存储和检索。B 树通过在每个节点保持多个键和子节点链接来工作。这使得 B 树在数据库和文件系统中非常高效,尤其是在处理大型数据集时。 Copy-on-Write 特性:写时复制是一种优化策略,其中系统只在必要时才复制对象。当对象被修改时,不是直接在原始对象上进行更改,而是创建一个新的副本并在副本上进行更改。这种方法有助于减少不必要的数据复制,提高了修改操作的效率。
Sum Tree 工作原理:
维护聚合信息:Sum tree的一个关键特性是在每个节点维护某种形式的聚合信息(例如,子树的总和、最大值或最小值等)。这使得可以快速查询整个树或树的部分区域的聚合信息。 优化读写操作:结合 B 树的有序性和写时复制的优化,sum tree 能够在保持高效读取的同时,减少因修改操作而引起的性能损耗。
应用场景
版本控制:在需要频繁进行读取和偶尔进行修改的系统中,如版本控制系统,sum tree可以提供高效的数据访问和更改记录。 数据库索引:数据库中的索引可以利用sum tree来快速计算范围查询的结果,例如求和或者找到特定范围内的最大值。
GPUI
现代显示器的刷新率范围从 60 到 120 帧每秒,这意味着一个应用程序每帧只有8.33毫秒的时间将像素推送到屏幕。这包括更新应用程序状态,布局UI元素,最后将数据写入帧缓冲区。
对于编辑器的性能来说,占领这 8.33 毫秒就意味着带给用户丝滑般的响应。因此,Zed 决定自己研发 GUI 框架,整体思路是,将需要渲染 Zed 的用户界面的每个特定图形原语编写自定义着色器。通过在 CPU 上以数据驱动的方式描述每个原语的属性,将所有繁重的工作委托给 GPU,在那里并行绘制 UI 元素。
当前开源的 Zed GPUI 已经是经过了一次大的重构的第二版。在第一版 GPUI 中使用的是类似 Flutter 的布局。而重构以后使用的是更加灵活的 Flexbox 弹性布局。使用 的是 Rust 布局库 DioxusLabs/taffy[6] 。
目前 Zed 仅支持 Mac 的版的原因是,当前底层 GPU 平台只支持 Metal 平台,基于 gfx-rs/metal-rs[7] 的库来操作 Metal 3D Graphics API。但是从源码实现来看,Zed 在架构上已经做好了跨平台抽象,但是估计 Windows 和 Linux 相关的支持不会很快,因为具体平台实现的代码还没有开始写。
Zed 团队在编写 GPUI 的过程中最大的挑战就是 Rust 的所有权。如何用 Rust 的特性来动态性来表达真实世界的图形界面,是 Zed 团队的一大挑战。
Zed 团队当前探索出来的方法:在 GPUI 中,应用程序中的每个模型或视图实际上都由一个称为 AppContext
的顶层对象拥有。当创建一个新的模型或视图(统称为实体)时,将应用程序的所有权交给它,以使其能够参与各种应用服务并与其他实体进行交互。
use gpui::{prelude::*, App, AppContext, Model};
struct Counter {
count: usize,
}
fn main() {
App::new().run(|cx: &mut AppContext| {
let counter: Model<Counter> = cx.new_model(|_cx| Counter { count: 0 });
// Call `update` to access the model's state.
counter.update(
cx,
|counter: &mut Counter, cx: &mut ModelContext<Counter>|
{
counter.count += 1;
}
);
// ...
});
}
为了说明,考虑上面的简单示例代码。通过调用 run
来启动应用程序,并传递一个回调函数(闭包),该函数接收一个对 AppContext
的引用,该 AppContext
拥有应用程序的所有状态。这个 AppContext
是访问所有应用程序级别服务的入口,比如打开窗口、显示对话框等。它还有一个 new_model
方法,在下面调用它来创建一个模型并将其所有权交给应用程序。
调用 new_model
返回一个模型句柄,该句柄携带基于所引用对象类型的类型参数。单独使用这个 Model<Counter>
句柄无法访问模型的状态。它只是一个惰性的标识符加上一个编译时类型标签,并且它维护着对由应用程序拥有的底层 Counter
对象的引用计数。
为了更新计数器,调用 update
,传递上下文引用和回调函数。回调函数被提供了对计数器的可变引用,就可以使用它来操作状态。
GPUI 在内部也实现了观察者模式和订阅模式来进行状态更改和事件分发。关于更多GPUI 内部状态管理的内容可以查看最新的官方博客:gpui-ownership[8] 。后续官方团队还会有更多实现细节披露。
用 Rust 实现的其他编辑器
https://xi-editor.io/ 100[9] https://helix-editor.com/ 92[10] https://kakoune.org/ 63[11] https://lapce.dev/ 70[12] https://amp.rs/ 63[13] https://zee.rs/ 109[14] pepper | A simple and opinionated modal code editor for your terminal 55[15] GitHub - neovide/neovide: No Nonsense Neovim Client in Rust 38[16]
这里就不比较优劣了。
后记
通过了解 Zed 编辑器背后的研发历程和技术栈来看,可以理解 Zed 的团队真的在为他们心目中的终极编辑器努力打磨。并且 Zed 编辑器提供了 Channel 功能让开发团队之间可以在代码层面实时交流,并且这也是他们商业模式的核心,就是服务好开发团队。
那么,你看好 Zed 编辑器吗?感谢阅读!
Zed: https://github.com/zed-industries/zed
[2]Atom: https://github.com/atom/atom
[3]Tree-sitter: https://github.com/tree-sitter/tree-sitter
[4]Xray: https://github.com/atom-archive/xray
[5]Warp: https://www.warp.dev/
[6]DioxusLabs/taffy: https://github.com/DioxusLabs/taffy
[7]gfx-rs/metal-rs: https://github.com/gfx-rs/metal-rs
[8]gpui-ownership: https://zed.dev/blog/gpui-ownership
[9]https://xi-editor.io/ 100: https://xi-editor.io/
[10]https://helix-editor.com/ 92: https://helix-editor.com/
[11]https://kakoune.org/ 63: https://kakoune.org/
[12]https://lapce.dev/ 70: https://lapce.dev/
[13]https://amp.rs/ 63: https://amp.rs/
[14]https://zee.rs/ 109: https://zee.rs/
[15]pepper | A simple and opinionated modal code editor for your terminal 55: https://vamolessa.github.io/pepper/
[16]GitHub - neovide/neovide: No Nonsense Neovim Client in Rust 38: https://github.com/neovide/neovide