不知道你有没有在Zed写代码的时候,被自动补全背刺过。
明明光标停在函数参数中间,手按Tab选中补全项,结果眼前的代码直接被改得面目全非。上一秒还好好的函数调用,下一秒原有变量名直接消失,留下一段语法不对的代码,自己还要回退撤销,重新敲一遍。这不是手速的问题,是编辑器的补全逻辑悄悄自作主张了。
这个坑的根源来自LSP协议本身。有些语言服务返回的补全项只有insertText和label,并不附带textEdit,也就是没有明确告诉编辑器应该插入还是替换哪一段文字。
按照LSP文档的说法,insertText交由客户端自行解释。这下难题就丢给Zed这边了。在本次修复之前,Zed会自作聪明地向后查找光标后面的单词,直接把整个现有单词当作待替换区间。
举个例子来说:
test(value1, ˇvalue2)
光标就在逗号后面、value2前面。LSP返回补全value2=,没有给出编辑范围。旧版Zed直接判定后面整个value2是要被替换掉的内容。一按Tab,直接变成:
test(value1, value2=)
原有value2直接不见了。很多人以为是LSP服务出bug,反复排查后端配置,殊不知是Zed本地推断编辑区间的逻辑闯了祸。
更让人头疼的是,此时你的completions.lsp_insert_mode设置直接形同虚设。不管你配置的是插入模式还是替换后缀模式,这一类缺失edit‑range的补全会直接无视该配置,强制走替换逻辑。用户的设置完全不起作用,相当于你的偏好被编辑器硬覆盖了。
这次 zed终于修好了这个bug。核心改动思路很简单:当LSP没有给出编辑范围的时候,不再盲目的往后吃掉后面完整单词;推断的编辑区间终止于光标位置。
这么一改之后,completions.lsp_insert_mode终于可以生效了。

拿默认配置replace_suffix举例。value2并不是value2=的后缀,编辑器就选择插入,最终结果:
test(value1, value2=value2)
看到这里有人会疑惑,怎么没有直接覆盖?这恰恰就是这套逻辑的本意。是否吃掉光标之后的文字,现在由后缀校验规则说了算;校验现在改用原始LSP的label,不再拿菜单上美化过后的显示文字或者filterText去做判断。
往后遇到LSP服务偷懒、不返回textEdit的补全项,编辑器不再擅自做主删除后面的标识符。补全行为回归用户配置。
不要小看这一处修复。日常写Go、Rust以外的第三方LSP时,不少语言服务器都习惯只返回insertText。之前在Zed写这类项目,时不时就要跟乱删代码的补全斗智斗勇。现在这一块终于理顺,以后按Tab补全的时候,心里总算踏实一点。