Zed 插件开发实战教程_Rust Extension 从零开发 Zed 编辑器扩展_zed extension rust
最近我做了一个 Zed 插件,最初目标很简单:
在编辑器里选中一段文本,快速看翻译结果,必要时直接替换成更适合代码里的命名格式。
一开始我以为这会是个“很轻”的插件:
做个右键菜单,弹出翻译结果,再加几个替换选项,应该就差不多了。
但真正开始实现后,我很快发现,问题根本不在“翻译接口怎么调”,而在于:
- Zed 公开扩展能力里,哪种交互入口最适合这件事
- “查看翻译”和“替换文本”其实是两种不同操作
- 自然语言翻译和工程命名不是一回事
- 插件本体和语言服务器应该怎么拆
- 插件怎么保证在任意项目里都能正常启动
最后我做出来的,不是一个单纯的“翻译弹窗插件”,而是一个更贴近代码编辑场景的方案:
slash command用来做主动翻译code action用来做选区翻译与替换Rust Extension + Rust LSP两部分协作- 翻译结果再转成多种命名格式,直接服务代码改写
这篇文章我不准备按功能列表讲,而是从插件作者视角,把这个插件的实现思路和取舍过程讲清楚。
一、我想做的不是“翻译一下”,而是“在写代码时真的好用”
如果只是做翻译,其实有太多方式了:
- 切浏览器
- 打开词典应用
- 用系统翻译工具
- 在编辑器外处理
但这些方式有一个共同问题:
它们都不够贴近写代码的上下文。
在真实编码场景里,我更常遇到的是这些需求:
- 选中中文字段,想快速翻成英文命名
- 选中英文短语,想知道中文意思
- 看完翻译后,希望直接替换成
camelCase、snake_case之类 - 不想切面板,不想离开当前编辑流
- 不想只是“查词”,而是直接产出可以落进代码里的命名结果
所以我做这个插件时,核心目标不是“把翻译做进 Zed”,而是:
把翻译、命名和替换三件事,收敛进编辑器内的一个顺手工作流里。
二、这个插件实际上分成两部分,而不是一个单体
这一点是我一开始就明确下来的。
因为如果把所有能力都塞进一个扩展入口里,后面无论是交互还是能力扩展,都会变得很别扭。
2.1 第一部分:Zed Dev Extension(Rust → wasm)
这一层主要负责和 Zed 本体对接,包括:
- 向 Zed 注册扩展
- 提供 slash command
- 启动语言服务器(LSP)
对应文件:
/Users/magegojo/Demo/youdao/src/lib.rs
它更像一个“桥接层”,职责不是做复杂业务,而是把插件能力挂进 Zed 的扩展体系里。
2.2 第二部分:Rust LSP
这一层才是核心业务层,主要负责:
- 接收 Zed 发来的 LSP 请求
- 读取当前选区文本
- 调用翻译能力
- 生成 code actions
- 返回预览卡片和替换动作
对应文件:
/Users/magegojo/Demo/youdao/lsp/src/main.rs
也就是说,这个插件并不是“wasm 里把一切做完”,而是:
Zed Extension 负责接入
Rust LSP 负责交互与业务处理
这套拆法的好处很明显:
- 扩展接入层职责更清楚
- LSP 逻辑更容易独立调试
- 后续如果要扩能力,不容易把入口层写乱
建议配图 1:插件整体架构图
放在这里最合适。
图里可以画成:Zed Editor -> Extension(wasm) -> Rust LSP -> 翻译能力 -> Code Action / Slash Command 返回
这张图的作用是先把“两层结构”讲清楚。
三、为什么我最后选择了 LSP,而不是做一个“右键翻译面板”
这部分其实是整个插件设计里最关键的转折点。
3.1 一开始最自然的想法,确实是右键翻译
我一开始想做的是:
- 选中文本
- 右键
- 看到“翻译”菜单
- 点一下弹结果
- 再决定是否替换
这很符合直觉,也很像很多传统编辑器插件的做法。
但问题在于,Zed 当前公开的扩展能力里,并没有给出那种完全自由的自定义右键 UI 注入方式。
3.2 与其强行做非原生入口,不如贴近 Zed 已有工作流
我后面想清楚了一件事:
做插件,不一定要追求“最像我脑中的理想 UI”,而应该优先贴近编辑器已经稳定提供的原生入口。
对 Zed 来说,当时最合适的能力其实是:
slash commandLSPtextDocument/codeAction
所以我最后采用的是:
- Rust 写 Zed 扩展
- Rust 写 LSP
- 通过
textDocument/codeAction做“选中即出翻译动作”
3.3 这套方案最大的好处,不只是能实现,而是交互更自然
最终这条路线有几个明显优势:
-
更接近编辑器原生工作流
用户本来就会用 code action,不需要新学一个特殊交互。 -
不需要切到别的面板
翻译、预览、替换都在当前编辑流里完成。 -
天然适合分成“预览”和“替换”两类动作
这点对翻译场景尤其重要。 -
更容易和后续命名替换结合
code action 本来就适合“给出一组可执行动作”。
所以这一步的关键不只是“LSP 能做”,而是:
LSP 恰好是当下 Zed 里最适合承载这种编辑器内交互的方式。
四、插件具体是怎么实现的
接下来我按功能入口拆开讲。
4.1 Slash Command:适合主动输入式翻译
插件里注册了一个命令:
/youdao_translate
用户输入文本后,插件会做两件事:
- 如果是中文,翻成英文
- 如果是英文,翻成中文
但我没有把它停在“返回一句翻译”这一步。
因为对开发者来说,更有价值的是“翻译后直接生成命名格式”。
所以基于英文结果,我还会继续推导:
- Title Words
- PascalCase
- camelCase
- snake_case
- kebab-case
- CONSTANT_CASE
这意味着 slash command 不只是“翻译工具”,它同时也是一个命名辅助工具。
4.2 LSP Code Action:适合选区翻译与代码替换
LSP 这边实现了这些能力:
initializedidOpendidChangedidClosecodeActionexecuteCommand
其中最关键的是 codeAction 和 executeCommand。
工作过程大概是这样:
- 用户选中一段文本
- 打开 code action
- Zed 把当前文档和选区发给 LSP
- LSP 从选区中提取文本
- 调用翻译能力
- 生成预览动作和替换动作
- 返回给 Zed 展示
这一步是整个插件最核心的交互入口。
建议配图 2:Code Action 工作流程图
放在这里。
图内容建议:选中文本 -> Zed 发送 codeAction 请求 -> LSP 提取选区 -> 翻译 -> 生成动作 -> 返回动作列表
这张图能把“为什么选中后就能出翻译动作”讲得很直观。
五、为什么我要把动作拆成“预览”和“替换”两层
这是我在实现过程中非常明确的一次取舍。
5.1 翻译场景里,用户其实有两类需求
第一类需求是:
- 我想先看看这段话什么意思
- 我想确认翻译结果对不对
- 我还不想直接改文本
第二类需求是:
- 我已经知道意思了
- 我希望快速替换成适合代码里的写法
- 我不想再手动复制粘贴
如果只提供替换动作,体验会太“硬”;
如果只提供预览,又会不够高效。
所以我最后的做法是把动作拆成两层。
5.2 第一类动作:预览翻译卡片
比如会生成这样一个动作:
有道翻译:查看翻译卡片 → User Name
点开后:
- 不替换文本
- 只显示整理后的翻译结果卡片
这个动作的作用更像“解释”和“确认”。
5.3 第二类动作:直接替换
后面再给出一组可执行替换动作,例如:
有道翻译:替换为 Username
有道翻译:替换为 UserName(PascalCase)
有道翻译:替换为 userName(camelCase)
有道翻译:替换为 user_name(snake_case)
有道翻译:替换为 user-name(kebab-case)
有道翻译:替换为 USER_NAME(CONSTANT_CASE)
这些动作点击后,会直接改写选中的文本。
从编辑器体验上看,我觉得这种设计比“点一下立刻替换”要稳得多,也更符合开发场景。
六、翻译预览卡片为什么不能只显示一句结果
如果预览卡片只展示一条翻译,其实意义不大。
因为开发者真正关心的,通常不只是“它是什么意思”,还包括:
- 更自然的表达是什么
- 更紧凑的英文写法是什么
- 适合代码命名的版本是什么
- 各种命名格式分别长什么样
所以我在预览卡片里整理的是一组结构化内容,而不是一句孤立的翻译。
6.1 预览卡片里我会放这些内容
- 原文
- 主翻译结果
- 紧凑英文 / 完整英文
- 各种命名格式
这样点开第一个动作时,体验就更接近“看一张说明卡片”,而不是立刻执行替换。
建议配图 3:翻译预览卡片示意图
放在这一节后面最合适。
图中可以展示一段选中文本,比如“用户名”,然后右侧是卡片内容:原文、主翻译、Title Words、PascalCase、camelCase、snake_case 等。
这张图非常适合做文章里的“核心效果图”。
七、真正让这个插件对开发有帮助的,不是翻译,而是命名格式生成
我在做的过程中越来越明确:
只做翻译,工具价值其实有限;翻译后能直接生成工程命名,价值才会明显提高。
7.1 自然语言翻译和工程命名不是一回事
比如“用户名”翻译出来,词典结果可能是完整短语;
但代码里需要的,往往不是一段自然语言,而是可落地的命名格式。
再比如下面这些输入:
- 用户名
- user name
- Username
- user_name
它们表面不同,但实际都可以规整到同一个词序上。
7.2 我做了一层词语拆分和格式统一
这一层的目标不是“翻译得更文学”,而是“统一成适合程序命名的词序”。
最终我会输出:
User NameUsernameUserNameuserNameuser_nameuser-nameUSER_NAME
这样做的关键在于,它把翻译结果从“可读文本”变成了“可直接写进代码的名字”。
八、为什么这个插件尽量挂到更大范围的语言集上
这个能力本质上和具体编程语言关系不大。
它不是 Rust 专用,也不是 TypeScript 专用。
只要用户在编辑器里选中的是一段文本,就可能需要这个能力:
- 变量命名
- 字段命名
- 配置项命名
- 注释翻译
- 文档片段翻译
所以我最终在 extension.toml 里做的是:
- 给 language server 配比较大的覆盖语言集
- 尽可能兼容常见代码文件、配置文件、标记语言、脚本语言
这样插件就不会只局限在个别语言里,而是能在更广泛的文件类型中工作。
从插件设计上看,这一步的意义是:
把能力定义成“编辑器级文本处理工具”,而不是“某门语言的翻译插件”。
九、实现过程中,我主要踩了哪些坑
这一部分我觉得比功能介绍更有价值。
因为很多问题不是“会不会写代码”,而是“编辑器能力边界到底在哪”。
9.1 Zed 不能直接做任意右键 UI
问题:
一开始我希望做一个完全自定义的右键翻译菜单或浮层,但 Zed 当前公开扩展能力并没有给出这种自由度很高的 UI 注入方式。
解决:
改走 LSP code action,利用编辑器已有入口完成交互。
经验:
做 Zed 插件时,优先贴近原生入口,比强行做自定义 UI 更稳。
9.2 “预览”和“替换”是两种不同交互
问题:
用户有时只是想先看翻译,有时则想直接替换。如果只给一类动作,体验会失衡。
解决:
把动作拆成两层:
- 第一个动作:查看翻译卡片
- 后续动作:直接替换成不同命名格式
经验:
编辑器里的“理解型操作”和“修改型操作”最好分开。
9.3 Hover 并不适合做主入口
问题:
我一开始也尝试过 hover 风格的方案,但 hover 是基于光标位置,而不是基于用户明确选区。对中文尤其不稳定,容易拿到过长文本或错误边界。
解决:
放弃把 hover 当主交互,改成:
选中 -> code action -> 第一个动作预览卡片
经验:
中文场景下,选区比 hover 更可靠。
9.4 多语言服务器并存时会有能力冲突
问题:
比如 Rust 文件里本来就有 rust-analyzer,它自己也会提供 hover、code action、diagnostics。
如果翻译插件也强依赖这些入口,就可能出现优先级和展示混杂问题。
解决:
我没有把主流程压在 hover 上,而是把核心入口固定在“选区后的 code action”。
经验:
和已有语言服务器共存时,要尽量选一个稳定、不会和主 LSP 强冲突的入口。
9.5 LSP 启动路径不能依赖当前项目
问题:
如果语言服务器启动逻辑写成“从当前项目目录找 LSP”,那么 Zed 打开别的工程时就会出错,甚至把插件 LSP 错当成当前项目 workspace 的一部分。
解决:
改成始终从插件自身目录启动 LSP,而不是依赖用户当前打开的项目目录。
经验:
做编辑器插件时,必须明确区分:
- 插件自己的安装 / 构建目录
- 用户当前工程目录
这一点如果不分清,后面很容易出莫名其妙的问题。
9.6 “看起来像翻译”,但实际上要服务代码命名
问题:
自然语言翻译结果往往偏完整表达,但代码命名更需要紧凑、规范、便于拼接。
解决:
我把结果分成三层组织:
- 更自然的主翻译
- 更适合命名的紧凑写法
- 各种工程命名格式输出
经验:
开发者工具不能只停留在“懂意思”,还要往“能直接用到代码里”再走一步。
9.7 开发阶段和交付阶段的产物管理不同
问题:
开发阶段可以依赖 cargo run 或 target/...,但实际安装 dev extension 时,产物路径和可执行文件必须明确。
解决:
我会保留并验证这些关键产物:
extension.wasmtarget/release/youdao-translate-lsp
同时确认:
- wasm 扩展能编译
- LSP release 二进制能编译
- release 版本能响应 LSP 请求
经验:
插件能编译,不等于插件能交付。
真正可安装、可加载、可运行,才算完成闭环。
十、最后形成的稳定交互方案是什么
经过几轮取舍后,我现在比较满意的交互是这样的:
10.1 选区场景
例如用户选中:
用户名
然后打开 Code Actions。
10.2 第一个动作:先看翻译卡片
例如:
有道翻译:查看翻译卡片 → User Name
点击后先确认翻译结果和命名候选。
10.3 后续动作:需要时直接替换
例如继续选择:
UsernameUserNameuserNameuser_nameuser-nameUSER_NAME
这样整个流程的优点是:
- 比 hover 稳
- 比单纯 slash command 快
- 比直接替换更安全
- 更符合代码编辑语境



十一、这次做 Zed 插件,我总结出的几条核心经验
这部分基本是我做完以后最想留下来的东西。
11.1 优先贴近 Zed 原生入口
不要先想“我要做一个自定义浮层”或“我要接管右键 UI”,
先想这些问题:
- 能不能放进 code action
- 能不能放进 slash command
- 能不能走 LSP 标准能力
这样实现成功率更高,也更不容易和编辑器本体打架。
11.2 “预览”和“执行”最好拆开
如果一个插件既承担“帮助理解内容”的职责,又承担“直接修改代码”的职责,最好分两步:
- 先预览
- 再替换
这样会明显更稳,也更符合编辑器操作习惯。
11.3 中文文本场景下,选区比 hover 更可靠
hover 很适合英文符号说明,
但涉及中文短语、自然语言边界时,选区往往更稳定、更可控。
11.4 插件路径和项目路径必须分清
这一点是编辑器插件里非常容易踩坑的地方。
只要牵涉语言服务器启动,就必须明确:
- 插件在哪里
- LSP 从哪里启动
- 当前打开项目在哪里
这三者不能混。
11.5 工具型插件真正的价值,在于能落到代码改写上
只做翻译,价值有限;
翻译 + 命名 + 替换,才真正能帮开发者省掉编辑器里的重复动作。
十二、总结
这个 Zed 插件做下来,我最大的感受是:
编辑器插件的难点,很多时候不在“功能能不能写出来”,而在“能不能嵌进原生工作流里”。
如果只是做一个翻译结果弹窗,其实很容易停留在“能看,但不够顺手”。
而我这次更想做的是:
- 用
slash command支持主动翻译 - 用
LSP code action支持选区交互 - 用预览卡片承接“先理解”
- 用命名格式替换承接“再执行”
- 让翻译这件事真正进入代码编辑流
从结果看,这个插件最终更像一个开发命名辅助工具,而不只是一个“有道翻译入口”。
如果后面我继续做 Zed 插件,这次最大的经验大概就是一句话:
先贴近编辑器原生能力,再去设计自己的交互层;先解决开发者真正会执行的动作,再考虑展示形式。
更多推荐


所有评论(0)