解决 corfu tab-and-go 与 eglot 一起使用时,选择候选词后 snippet 总是展开的问题

问题背景

tab-and-go 模式,即在显示候选词时,按“下一个候选词”键(经常是tab)时,同时选择输入下一个候选词。corfu 默认就打开了这个功能,只是没设置 tab 的快捷键,它的开关是 corfu-preview-current。 corfu 在它的文档中讲述了其 tab-and-go 模式的注意点:

请注意,进一步的输入不会展开 snippet 或 template,······ 为了强制展开 snippet ,请使用 RET 确认选择候选词。

这个描述与我习惯的,nvim 中的模式一致。但是 corfu 在实际使用时的行为,却不是这样。

问题描述

当我将 corfu 与 eglot 一起使用时,选择候选词(方法名等)后,继续输入括号时,snippet 竟然自动展开了。

截图 2023-05-11 14-17-37

截图 2023-05-11 14-18-02

一开始我认为这是什么新的 feature,但在阅读了一些 issue 和源码后,发现并不是,这就是一个 bug。并且不是什么无法解决的 bug,是拒绝修改的 bug。

问题源于 corfu 的作者与 eglot 的作者对于 capf 中 :exit-function 的 exact 状态理解的不同。

corfu 的作者认为 exact 就是什么都不做,保持当前的状态。然而在 eglot 的 eglot-completion-at-point 方法中,excat 和 finish(正常的结束的状态,corfu 中为按回车)在 :exit-function 中视为同一个状态,干同一件事,也就是会自动展开 snippet。

具体参见这个 Issue 的二楼:

解决方案

修改 corfu.elcorfu--prepare 方法最后一行

(corfu--insert 'exact)

(corfu--insert nil)

并且推荐设置 corfu-on-exact-match 为 nil 或者 quit,来让自己输入完整候选词后不自动展开

(setq corfu-on-exact-match nil)

这样处理后 corfu 的行为就与文档中描述的一致了。

或者,如果不需要 eglot 的 snippet 功能,也可以直接关闭

(setq eglot-stay-out-of '(yasnippet))
4 个赞

这个确实坑。

我也用的 corfu Tab-and-Go + eglot,在 python 3.11 + pyright 时,输入完 __init__ 按空格,就会自动展开,设置了 (setq eglot-stay-out-of '(yasnippet)) 也还是会展开。不知道是哪个包的问题?

展开前:

展开后:

(use-package corfu
  :custom
  (corfu-cycle t)
  (corfu-auto t)
  (corfu-auto-prefix 1)
  (corfu-auto-delay 0.1)
  (corfu-preselect 'prompt)
  (corfu-on-exact-match nil)
  :bind (:map corfu-map
              ([tab] . corfu-next)
              ([backtab] . corfu-previous)
              ("S-<return>" . corfu-insert)
              ("RET" . nil)
              ([remap move-end-of-line] . nil))
  :hook (eshell-mode . (lambda () (setq-local corfu-auto nil)))
  :init
  (global-corfu-mode)
  (corfu-popupinfo-mode))

PS: Company 也是同样。

试了一下,确实如此。

__init__ 展开的这一段应该不是 snippet,所以设置了 stay-out-of yasnippet 也无效。
但原理上是一样的,corfu 发送 exact 信号,eglot 调用 :exit-function 帮你补全了这一段不需要的代码 :joy:

这种展开即使不用 tng,选中后按回车也会感觉挺迷惑的。。。
研究这种问题费时费力啊,最简单的解决方法还是自己手打__init__:joy:

补充:
开 VSCode 试了一下,展开来也是一样的,那这就不是 Emacs 这几个插件的锅了

确实是的。看来要看看 Pyright 的选项了。

我之前试图讨论问过两边,失败了,主要我对 lsp 一窍不通

仔细看了下讨论,感觉主要是服务器端的问题。

我在 vscode 上也试过了,是同样的情况。不折腾这个了,要输入 __init__ 的时候全部手打就好了。

主要是 pyright 的维护者也很固执,很难交流,万人血书加上忽略下划线开头的变量到现在还是东扯西扯2333

实在不喜欢就换后端😄。

python 除了 pyright,还有好2个在维护的 language server,哪个更好点?

  1. python-lsp-server, Python 实现,目前Spyder IDE 团队和社区还在积极维护中。

  2. jedi-language-server ,不知道有什么特点。