Zed 中的可扩展语言支持 - 第 1 部分 - Zed 博客

大约两个月前,@maxdeviant 和作者开始了使 Zed 可扩展的项目。项目第一阶段专注于可扩展的语言支持,希望人们能用任何编程语言在 Zed 中编码,而非少数几种。目前已达到第一个里程碑,现分享已完成的工作。

语言支持在 Zed 中

Zed 有两类可扩展的特定语言功能:

  1. 基于Tree-sitter对单个源文件进行基于进程的基于语法分析。这需要每种支持语言的 Tree-sitter 语法及描述如何使用语法树进行语法高亮、自动缩进等任务的 Tree-sitter 查询集。可参考之前的博客文章了解更多。
  2. 通过语言服务器协议提供语义理解的外部服务器。这需要指定如何运行给定的语言服务器、如何安装和升级该服务器以及如何调整其输出(补全和符号)以匹配 Zed 的风格。
    本帖先聚焦 Tree-sitter 部分,后续博客会描述如何处理可扩展语言服务器。

    打包解析器的挑战

    让扩展向 Zed 添加 Tree-sitter 解析器的难点在于 Tree-sitter 解析器以C 代码表达。语法用 JavaScript 编写,通过 Tree-sitter CLI 转换为 C 代码。Tree-sitter 这样设计有多种原因,简言之需要某种图灵完备语言,C 代码的有用特性是可通过 C 绑定从几乎任何高级语言使用。但 C 代码不是分发给最终用户最方便的产物。
    一种可能的分发扩展的方法是发送 C 代码本身,在用户安装扩展时用他们的 C 编译器在用户机器上编译,然后动态加载生成的共享库。Atom(使用 Node.js 打包工具)及 Neovim 和 Helix 等使用 Tree-sitter 的编辑器也使用此方法。但对于 Zed,想要平滑、安全的插件安装体验,不依赖用户的 C 编译器,且不能让扩展使 Zed 崩溃,因为语法作者可写包含任意逻辑的外部扫描器,已见过第三方外部扫描器的崩溃情况。

    显然,我们使用了 WebAssembly。但如何?

    解决方案涉及 WebAssembly 并不意外,已为 Tree-sitter 构建了WebAssembly 绑定,可通过 JavaScript API 在 Web 上运行 Tree-sitter 解析器。WebAssembly(简称 wasm)是分发解析器的好格式,跨平台且设计用于安全运行不受信任的代码。但如何在本地代码编辑器中使用 wasm 构建的解析器并不明显。
    运行 wasm 程序时,应用程序提供字节数组作为其线性内存,wasm 代码只能读取或写入这些字节。但在 Zed 中,运行解析器时需要交换大量数据,每次按键都需传入源代码,解析器需返回具体语法树,比相应文本大得多,增量解析时每个语法树与前一个语法树共享结构,Zed 经常将这些语法树发送到后台线程用于各种异步任务。如果完全通过 wasm 运行解析器,每次解析都需从 wasm 内存复制大量数据,简单将 Tree-sitter 编译为 wasm 是不够的。

    混合原生 + WebAssembly 系统

    决定利用 Tree-sitter 解析器是表驱动的事实,大部分生成的 C 代码由表示状态机的静态数组组成。Tree-sitter 的解析分为两部分,词法分析阶段逐个字符处理文本生成令牌,每个语法的词法分析器由一些自动生成的 C 函数和一些可选的手写函数实现,解析阶段更复杂,实际构建语法树,解析完全由静态数据驱动。
    这种两阶段的划分使我们能从 wasm 文件加载解析器,将 wasm 文件中的大部分静态数据复制到原生数据结构中,解析时需要运行词法分析函数时使用 WebAssembly 引擎,其余计算在原生环境中进行,与使用原生编译的 Tree-sitter 解析器方式相同。词法分析是解析过程中最不昂贵的部分,也是涉及自定义手写代码的唯一部分,这种混合原生 + wasm 设计在安全性和性能方面达到理想组合。

    扩展 Tree-sitter API

    为实现这种新的解析方法,向 Tree-sitter 库添加了一些新原语。Tree-sitter 核心库提供Parser类型用于解析源代码创建Tree对象,使用解析器需分配LanguageLanguage是从特定语法生成的不透明对象,在单独的库中提供。添加了新类型WasmStore,与Wasmtimewasm 引擎集成,可从 WASM 二进制文件创建Language实例,这些语言对象与普通原生语言对象相同,使用时解析器需分配WasmStore,因为 wasm 存储允许解析器在词法分析期间调用 wasm 函数。除了这一差异,从 WASM 加载的语言行为与原生编译的语言相同,生成的语法树也相同,且不与 wasm 存储耦合。

    实现亮点

    这些新 API 像 Tree-sitter 库的其余部分一样用 C 实现,使用 Wasmtime 的优秀 C API,可在这里找到完整实现。使用 wasm 时,对模块如何链接和加载的细节有低级控制,模块声明控制 wasm 线性内存布局的几个常量的导入,包括静态数据的地址、调用栈的基地址和堆的起始地址。模块还声明其依赖的所有函数的导入,Tree-sitter 语法可包含称为外部扫描器的手写源文件,这些文件常使用 C 标准库的函数,为处理此类导入,Tree-sitter 库嵌入一个包含来自libc子集函数的小 wasm 块供外部扫描器使用。提供自己的迷你libc的一个好处是不需要使用标准的内存分配函数mallocfree等,知道外部扫描器分配的内存仅在单个解析期间需要,实现了自己的小malloc库使用 bump-allocation,开销比通用malloc实现小得多,且外部扫描器不可能导致内存泄漏,可在每次解析开始时重置整个 wasm 堆。

    使用语言扩展

    该功能推出后,Zed 社区开始发布语言扩展,Zed 扩展商店中可用的语言扩展数量目前为 67 个且在不断增加,这些语言支持 Zed 的所有语法感知功能:语法感知选择、大纲视图、基于语法的自动缩进以及语法高亮。要浏览扩展,在应用程序菜单中点击Zed > 扩展。如果有使用但未被支持的语言,可在扩展仓库上提交 PR 或开问题请求帮助,可参考"开发扩展"文档开始。

    结束语

    使用 wasm 打包 Tree-sitter 语法对 Zed 很有效,Wasmtime 引擎使用起来很棒。Tree-sitter 语法只是 Zed 扩展系统的一部分,后续将讨论其他使 Zed 可扩展的方式。感谢阅读!

阅读 175
0 条评论