Loading

译:Zed 解码:为什么不直接嵌入 Neovim?

译:Zed 解码:为什么不直接嵌入 Neovim?
原文:https://zed.dev/blog/zed-decoded-vim
作者:Thorsten Ball, Conrad Irwin
译者:ChatGPT 4o
发布时间:06/13/24

Zed 解码:为什么不直接嵌入 Neovim?

通常,当我告诉别人我已经转用 Zed 作为主要编辑器后,他们的第一个问题是:你不想念 Vim 吗?然后我告诉他们:Zed 有 Vim 模式。如果没有这个模式,我认为我不会,也不可能切换。

然后,令人惊讶的是,后续问题经常是这样的:Vim 模式?你知道 Neovim 是可嵌入的吗?为什么 Zed 不直接嵌入 Neovim 呢?

所以在这个 Zed 解码篇中,让我们深入探讨 Zed 的 Vim 模式,并找到这些问题的答案。

伴侣视频: 为什么不直接嵌入 Neovim?

这篇文章配有一小时的伴侣视频,Thorsten 与 Conrad 进行了对话,后者在过去几个月里大力改进了 Zed 的 Vim 模式。Thorsten 和 Conrad 一起探讨了 Zed 的 Vim 模式,实施它的难点,为什么我们不嵌入 Neovim 以及我们如何使用 Neovim。

观看视频:点击这里

Zed 的 Vim 模式

首先:Zed 有 Vim 模式。你可以通过在 Zed 设置中添加以下内容来启用它:

{  "vim_mode": true}

一旦你添加并保存设置,你会看到你的光标变成一个块,这意味着你已经准备好探索 Zed 的 Vim 模式了:

  • h, j, k, l 已准备就绪。
  • 你可以使用 w, W, e, E, b, B, {, } 等移动命令。
  • 还有 ft 以及 ;,
  • 使用 v, Vctrl-v 进入不同的可视模式。
  • 有许多 g 命令,如 gg, gn, gx, gt

但 Zed 的 Vim 模式不仅支持标准的移动命令和操作符:

  • 基本的 vim-surround 功能已经可以使用:你可以用 cs"' 将周围的 " 改为 ',或用 ds[ 删除周围的 [。甚至连 vim-surround 的高阶组合 ysiw" 也能用。
  • 可以在普通模式下用 gcc 和在可视模式下用 gc 切换代码注释。
  • 许多 Vim 的“窗口管理”快捷键以 ctrl-w 开头的都有 Zed 的 Vim 模式等效命令
  • 一些更高级的 Vim 功能也有效。尝试用 * 搜索光标下的单词,用 cgn 改变下一个出现的内容,然后用 . 重复操作。
  • 缓冲区局部标记 ('a'z) 和一些内置标记 ('<, '>, '[, '], '{, '}, ^`) 都有效。
  • 对命名寄存器的基本支持 刚刚加入我们的 main 分支,并将出现在下一次 Zed 的预览版发布 中。

Zed 的 Vim 模式内容丰富。官方 Vim 模式文档页面 为你提供了可能实现的功能和可以配置的选项。你还可以阅读 默认 Vim 快捷键,了解支持哪些移动命令和操作符。

但还不完整。还没完成。还有一些大的东西缺失,比如寄存器和宏,还有一大堆操作符和移动命令需要添加。我们的跳转列表和更改列表版本也需要调整,以使它们更像 Vim 并保持一致。

好消息是,它正在不断改进。基本标记支持上个月已添加包围功能前一个月已加入gncgn 两个月前也合并了。仅 Conrad 一人就在过去两个月中将超过 20 个 PR 合并到 Zed 的 vim crate

哇,见证这些改进并参与其中真是令人开眼界。

操作符一个一个地实现,计数一个一个地添加

作为一个长期 Vim 用户,我已经对 Vim 了解颇多,作为一个新 Zed 开发者在其 Vim 模式上工作时,我半预料到需要实现一些神秘的 Vim 操作符和移动命令。我知道 Vim 里面有很多东西,但仍然被“哦,天啊,Vim 里面真的有很多东西”惊到——操作符、移动命令、修饰符和它们的组合——以及许多我以为很少用的功能实际上被广泛使用。

你知道吗,例如 gs 在 Vim 中代表“goto sleep”,让 Vim 休眠 N 秒?是的,当我们天真地以为 gs 还没被用作快捷键时,这在我们的问题跟踪器中出现了。

或者,也许你已经知道 z. 类似于 zz,将当前行居中。但你知道它接受一个计数吗?没错:你可以使用 5z. 将第 5 行居中。我不知道这一点。

你肯定知道 I,这个命令将 Vim 带入插入模式,可能是新 Vim 用户在 :q 之后学到的第二个命令。但你知道 I 也接受一个计数 吗?你可以使用 5ifoobar<esc> 插入 foobar 5 次。同样,a 也接受计数。

谈到计数:你是否曾坐下来思考过 5djd5j 之间的区别?我们在伴侣视频中讨论了这个问题,而 Conrad 在为更多操作符实现计数时做的不只是思考。

或者,让我问你:你会怀疑有人在日常 Vim 工作流程中使用 rR——都触发替换模式——吗?更不用说这些 Vim 用户会愿意为实现 rR 支付 $500。我当然不知道。我知道 R,但我以为,当然,没有人真正使用它。

或者 . 命令,每个 Vim 用户的宠儿。一个简单的命令,一个单独的 .,做一个简单的事情,对吧?它只是重复你上次所做的。然而,这并不完全是这样:它只重复最后一次对缓冲区的更改,不包括导航。但它也不会重复一个改变了缓冲区的完成动作。例如,当这样说时,这听起来很明显——“是的,嗯哼,那就是 . 所做的,我知道”——但当你试图构建一个通用的 . 命令时,这些细微的差别很容易让你踩到雷。

不同的基础

为什么我要告诉你这些令人惊讶的操作符和组合?分享一个认识:当你试图构建一个尽可能完整的 Vim 模式并不断碰到这些细微差别时,你会意识到 Vim 和 Zed 建立在不同的基础上。

例如,Vim 在缓冲区中定位字符。而 Zed 则定位字符之间的插槽。

这是 abc 中的光标位于 b 上(Vim)与位于 ab 之间(Zed)之间的区别。可以想象,这种不变量的涟漪效应会在五个抽象层次上形成波浪。

考虑两个编辑器如何处理换行:Vim 区分行尾和行中最后一个字符。实际上,这意味着你可以,例如,用 v$ 创建一个直到行尾的可视选择,然后通过按 l 另外选择换行符,因此用 d 删除时会删除整行,但看起来你只选择了第一行。

在非 Vim 模式的 Zed 中,你可以做类似

的事情,选择直到行尾。然而,只要你的光标停留在那一行,选择就不会包括换行符。只要你选择换行符,你的光标就会跳到下一行。

在 Zed 的 Vim 模式中,我们尽可能地解决(或:绕过)这些差异,使 Vim 模式尽可能像 Vim。引用我们关于 Vim 模式的文档

Zed 中的 Vim 模式主要是“做你期望的事”:它大多试图完全复制 Vim

这不仅是困难和棘手的工作(参见这个问题以了解涉及哪些边缘情况),还有一个限制,一个上限:我们不想抛弃 Zed 的基础。与之相关的有很多。

这就是为什么它没有嵌入

你看,如果你将 Neovim 嵌入 Zed,你最终会做的就是:你会抛弃 Zed 的基础,取而代之以 Neovim。

但这些基础——表示文本的数据结构CRDTs渲染管线定制的 Async Rust 运行时——是让 Zed 成为 Zed 的原因:一个高性能的、协作的文本编辑器。或者,用 CRDT 博客文章中的说法:CRDTs,Rope,SumTree,文本模型——这是 Zed 的 DNA。Zed 是基于“你不能仅仅在上面添加协作功能”的认识构建的,这需要从头开始内建。

如果我们将 Neovim 放入 Zed,我们必须在启用 Vim 模式时抛弃这种 DNA——这是我们不愿意做的——或者将 DNA 移植到 Neovim。(想象一下有人在做基因剪接,额头上汗水涔涔的动画。)这意味着,我们必须做很多事情两次:一次在 Zed 中,一次在嵌入的 Neovim 中。构建 CRDTs 两次,构建可以同时编辑多缓冲区的多个人两次,等等。

一次构建这些东西已经很难。要做很多工作并且难以做好。要在两个不同的代码库中构建两次……至少难度是两倍。

所以,这就是为什么我们不直接嵌入 Neovim 到 Zed 中。相反,我们在 Zed 内部构建了一个 Vim 模式。我个人认为,这比仅仅嵌入 Vim 更有趣。

Vim 和 Zed,融合

由于 Zed 的 Vim 模式建立在 Zed 的基础上,你得到的是两者的结合。当启用 Vim 模式时,你仍然可以使用几乎所有在非 Vim 模式 Zed 中可用的功能。

例如:你可以使用 gl 创建一个额外的光标,放置在当前单词的下一个出现位置上。或者用 gL 反向做同样的事。使用其中之一,然后按 <esc>,你会留下多个光标,但在 Vim 正常模式下,所有可用的移动和操作符都可用。

或者,试试这个:使用 ]x[x 选择一个 Treesitter 语法节点。你还可以将这些选择与多光标结合。因此,如果你选择了一个 Treesitter 节点,按 gl,它将在一个看起来相同的节点上创建另一个光标。

录制的视频展示了我在做的事情

或者按 :。这不仅打开 Zed 命令面板,还有一些常见命令的快捷方式和绑定,如 :w:E[xplore] 打开项目面板,:te[rm] 打开终端,等等。

g]g[ 在诊断错误之间导航,]c[c 在 git 更改之间导航。gs 打开当前文件中的符号大纲,gS 在整个项目中执行相同操作。g. 打开代码操作。查看 Vim 模式的 Zed 特定功能 了解在 Zed 的 Vim 模式中还能做什么。如果缺少某些东西,你可以随时回到 cmd- 快捷方式或打开命令面板,找到命令并创建自定义绑定。

例如,这里是我在 Zed keymaps.json 中的一些绑定,在 Vim 模式下使用:

[  {    "context": "EmptyPane || SharedScreen || vim_operator == none && !VimWaiting && vim_mode != insert",    "bindings": {      ", f b": "tab_switcher::Toggle",      ", f I": "file_finder::Toggle",      ", f o": "projects::OpenRecent",      ", r l": "task::Rerun",      ", r e": ["task::Rerun", { "reevaluate_context": true }],      "ctrl-s": "projects::OpenRecent"    }  },  {    "context": "Editor && VimControl && !VimWaiting && !menu",    "bindings": {      "g shift-r": "editor::FindAllReferences",      "g a": "editor::ToggleCodeActions",      "g r": "editor::Rename",      "space w": "workspace::Save",      ", g b": "editor::ToggleGitBlame"    }  }]

Neovim……最后

现在你知道为什么我们不嵌入 Neovim 以及这样做可能有哪些好处,让我送你一个可以和其他程序员朋友在喝酒时分享的有趣东西:我们在 Zed 中确实使用 Neovim,但我们是在我们的测试中使用它。

伴侣视频 中,Conrad 详细解释了它是如何工作的,这里是简短版本。

所有 Zed 的 Vim 模式都包含在一个单独的 crate 中,vim,在其中,一些测试 看起来像这样:

#[gpui::test]async fn test_visual_star_hash(cx: &mut gpui::TestAppContext) {    let mut cx = NeovimBackedTestContext::new(cx).await;     cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;    cx.simulate_shared_keystrokes("v 3 l *").await;    cx.shared_state().await.assert_eq("a.c. abcd ˇa.c. abcd");}

这里有趣的部分是 NeovimBackedTestContext::new():这使得测试运行一个无头的 Neovim 实例,发送初始状态,然后模拟按键。生成的最终状态将保存为一个 JSON 文件,我们根据该状态测试 Zed 的 Vim 实现。

换句话说:我们在测试中使用无头 Neovim 生成 "golden files",并根据这些文件检查 Zed 的 Vim 模式在相同按键下生成的内容。

相当整洁,对吧?

posted @ 2024-06-17 15:32  .net's  阅读(2)  评论(0编辑  收藏  举报