最近的几个月里,我一直在寻找一种合适的方式来构建底层应用。可能是 Web、App 应用已经缺乏了一些新鲜感;也可能是受受国际局势的影响,我开始思考构建底层应用架构的能力。
于是,在我学习了一段时间的 Rust 之后,我便不断地往系统的底层探索。在那之前,另外一门合适的语言大抵是 Golang,不过我偏向于认为 Golang 是一个 Web 应用或者普通应用的开发语言,而非一个系统编程语言。
这其中的一个话题就是:编辑器以及 IDE。毕竟,讨论 Emacs 和 Vim 哪一个更好,已经不能满足我的需求。程序员的乐趣应该在于如此去写一个 Emcas,便实现一个 Vim —— 毕竟 Emacs 操作系统太复杂了。
1. 无 UI 式:命令行编辑器
作为过程的第一步,我开始寻找一些合适的编辑器(PS:主要是简单),以作为我的编辑器和 IDE 生涯的第一步。然而,这并不是一件容易的事,毕竟我先前构建 Client 端的经验,都是使用现成的 UI 组件,如 WebView 的 Textarea、Android 框架中的 EditText 组件。
于是乎,我模仿 复制 Iota 项目的一部分代码,形成了一份最小可工作的代码,以了解命令行编辑器如何运作的整体原理:
字符移动
对于终端编程来说,并不存在组件可以使用,所以我们所要做的事情是:在特定的位置显示特定的字符,如:
代码语言:javascript复制rustbox.print_char(1, 1, RustBoxStyle::empty(), Color::White, Color::Black, 'A')
这样一来,它就在屏幕上的 1,1 的位置画了一个 A,它的前景是白色,背景是黑色。嗯,没错,这种体验就是我大学玩的嵌入式编程。
于是,第一步我们要做的就是读取文本,然后渲染。这里用的库是 RustBox,它封装了 C 语言下的 Termbox。对于一些人来说,更为熟悉的名字可能是 curses,又或者是 GNU 里的 ncurses。在另外的一个 Rust 编写的编辑器 [amp] 中,使用的是 termion。
快捷键识别随后,我们可以启动起编辑器,而后做各种事件轮询,等待用户的交互,如快捷键。同样的,这个功能也是由底层的 Termbox 提供了支持,我们只需要创建行为与快捷键的绑定即可。
状态栏 命令模式。有了上面的基础之后,这个也不会遇到什么困难。
语法高亮。这里我就被 iota 这个项目坑了,项目的截图上是有语法高亮的,但是代码上已经删了。回溯了一下过程,发现这部分的功能删了,因为原先的设计并不合理。合理的方式应该是使用 syntect 这种现成的方案,它使用了 Sublime Text 的语法定义格式。
理解了原理,快速画了个瓢之后,我就转向 UI 式的文件编辑器。
GitHub: https://github.com/rapilab/rinput
2. UI 式:进程分离
随后,我在 Awesome-rust 项目中,物色到了第二个可以项目:xi-editor。Xi Editor 是 Google 员工开源的一款用Rust 语言编写的文本编辑器。从之前的新闻来看,像是火了一段时间,但是好像已经没有那么活跃了。
它最主要的特点有:
前后端分离。编辑器分为两部分,后端和前端。后端(即核心部分)负责保存文件缓冲区,并负责所有潜在的昂贵编辑操作。你可以将等价为前后端分离应用的关系,又或者是 Electron 应用中:Electron 的 Node.js 和 WebView 部分的区别。
JSON RPC。xi-editor 提供了 JSON 形式的 RPC(远程过程调用)用于前后端之间进行通讯。采取 JSON 的主要原因是减少开发插件的成本,更好的扩大生态。如,我从 UI 上修改编辑器的主题,将通过 RPC 的方式通知后端,并将对应的配置存储到系统中。并且诸如于 IDEA 的索引模式,它应该也会在后台运行,而不占用 UI 进程,影响用户体验。如此一来,我们所面临的卡顿问题,会进一步得到缓解。
不限 UI。因为 xi-editor 本身只提供 core 模块,所以,我们可以看到有各种各样的 xi-editor 的前端,如原生 macOS 实现、基于 Electron 实现等等。
所以,我尝试基于这种架构模式,开发了一个基于这种架构模式的系统状态应用 Stadal。有了这样的模式,我们就可以分离 UI 进程,提供更好的用户体验。
顺便一题,在这种模式之后,编辑器的模型都统一由后端管理(PS:这一点与 Web 应用是相似的,笑~)。
Emacs 架构:M-V-C 架构
这样一类比的情况下,Emcas 的架构就好似一个大单体一样。毕竟这是 M-V-C 架构(源自《架构之美》:
- 模型。程序所操作数据的底层描述,如文本属性、缓冲区等等;并与系统进行交互。
- 视图。面向用户展示数据的方法,如对于窗口增量显示更新逻辑等。
- 控制器。负责实现用户与视图的交互(如按键、鼠标事件等),并对模型进行更新(采用 Lisp 作为支撑)
至于插件部分则是由 Lisp 脚本来实现,至于是插件好还是脚本好就是另外一个问题了。
GitHub :https://github.com/phodal/stadal
IDE:IDEA 的插件化
因为偶然地原因,我分析了一段时间的 Intellij IDEA 社区版 Android Studio 的源码之后,我有了一些新的感受 —— 这个系统架构有点复杂,哈哈。当然,也发现了一些相似的模式。
隔离层:独立二进制
对于工具的制造者来说,开发者并不希望工具被捆绑在某一个开发工具上。因此,对于开发者而言,优先做的是提供一个可独立运行的程序,而后再封装一个针对于该工具的实现。
于是乎,这个运行的程序,它可能是:
- C/C 编写的二进制应用。通过 daemon 的方式来运行,并能通过解析输出来进行错误处理。
- Gradle 开发的插件。并借助于 Gradle Tooling API 来实现插件的调用。
- Java 编写的应用。通过直接集成的方式进行。
- ……
这样一来,我们就在 IDE 中集成了这样的能力,并引入到我们的系统中使用。
语言扩展
IDEA 本身的插件体系已经设计得很完善了,如我们可以快速添加一门语言,只需要:
- 注册文件类型
- 实现 Lexer(词法分析)
- 实施 Parser(语法分析) 和 PSI(程序结构接口)
- 语法高亮显示和错误高亮显示
- 代码补全
- 查找用法
- 重构:重命名、安全删除
- 代码格式化程序
- ……
这样一来,我们就能快速地具备一个语言 IDE 应该有的能力。比如 IDEA 的 Rust 插件就是这样一个不错的示例。
总结
一个好的编辑器/IDE 应该能:
- 滋长的特性:通过插件化支撑
- 可维护性:具备良好的可读性
- 进程分离
- 速度
讨论哪个编辑器/IDE 是一件没意义的事。
只有自己挖的坑才是好的。
我行我上。