如何解决 Corfu 和 YASnippet 的 TAB 按键冲突?

最近一段时间喜欢上了在 Corfu 中使用 TAB-and-Go 的按键方式,corfu-map 中通过 TABShift-TAB 进行候选的上下浏览,并自动预览。

不过当 Corfu 遇上 YASnippet 时,TAB 就被 yasnippet 抢占了。不知道有没有更加方便的方法进行设置,实现以下功能:

  1. 当没有 corfu 候选框时,TAB 按键由 yas-map 占用。
  2. 当 corfu 弹出候选框时,TAB 就交给 corfu-map
  3. 当 corfu 候选框关闭后,TAB 交回给 yas-map

目前我能想到的方案是通过 advice-add 实现,功能是正常的,但觉得写的有点丑,也许大佬们有更好的方案?

(advice-add #'corfu--make-frame :around
            (defun +corfu--make-frame-a (oldfun &rest args)
              (setq yas-keymap-disable-hook t)
              (apply oldfun args)))
(advice-add #'corfu--popup-hide :around
            (defun +corfu--popup-hide-a (oldfun &rest args)
              (setq yas-keymap-disable-hook nil)
              (apply oldfun args)))

这是我的 corfu 和 yasnippet 配置:

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

(use-package yasnippet
  :diminish yas-minor-mode
  :hook (after-init . yas-global-mode))

(use-package yasnippet-snippets)

感谢 @kigo64 的指点,目前我的 YASnippet 使用下面的设置,已经能够完美在 Corfu 中配合 tng (TAB-and-Go) 按键模式使用了。

3 个赞

比较好奇有没有 yas 的快捷键在 corfu 中补全的方法。

不好意思,我实在没看懂你说的是什么意思。

yas 的快捷键是指的哪些命令?yas-expand

是我没写清楚,应该是: snippet 的 key 进行补全 。 如 org-mode下 # key: dit_ 可以直接配合 corfu 进行补全。

你意思是把 yasnippet 的key加入 corfu 的候选列表中?

是的, 这个功能实现的话,两者冲突的概率应该就不大了。

你说的这个是可以实现的。最近有人写了 GitHub - elken/cape-yasnippet

结合 cape 就可以。比如下面的配置:

(defun yas-setup-capf ()
  (setq-local completion-at-point-functions
              (cons #'cape-yasnippet
                    completion-at-point-functions)))

(add-hook 'prog-mode-hook 'yas-setup-capf)
(add-hook 'text-mode-hook 'yas-setup-capf)

如果使用 eglot的话,还可以加上这个设置:

(defun my/eglot-capf ()
  (setq-local completion-at-point-functions
              (list (cape-super-capf
                     #'cape-yasnippet
                     #'eglot-completion-at-point))))

(add-hook 'eglot-managed-mode-hook #'my/eglot-capf)

javascript-ts-mode 中开启 eglot 的效果如下图:

我个人不喜欢把 snippet 放到补全列表中,比较喜欢手动用 TAB 触发。

但我的问题不是触发 yasnippet,是 yasnippet 展开后会抢占 TAB。

1 个赞

我用corfu同类插件company时把yasnippet从company-backends去掉了.用M-y触发yasnippet. 我的习惯是yasnippet用来输入略长一点的代码端.

用你的配置无法重现问题。

  1. 运行:
$ emacsq.sh -P use-package,yasnippet,corfu --eval "\
  (progn
    (use-package corfu
      :custom
      (corfu-cycle t)
      (corfu-auto t)
      (corfu-auto-prefix 1)
      (corfu-auto-delay 0.1)
      (corfu-preselect 'prompt)
      :bind (:map corfu-map
             ([tab] . corfu-next)
             ([backtab] . corfu-previous)
             (\"S-<return>\" . corfu-insert)
             (\"RET\" . nil))
      :hook (eshell-mode . (lambda () (setq-local corfu-auto nil)))
      :init
      (global-corfu-mode))

    (use-package yasnippet
      :diminish yas-minor-mode
      :hook (after-init . yas-global-mode))

    ;; +++
    (add-hook 'emacs-startup-hook
              (lambda ()
                (switch-to-buffer \"*scratch*\")
                (yas-minor-mode 1)
                (yas-define-snippets
                 'emacs-lisp-mode
                 '((\"glo\" \"yasnippet$0\" \"snippet\" nil nil nil nil nil nil))))))"
  1. 输入 glo
  2. 等待补全菜单出现
  3. TAB
  4. 结果
    • 期望:展开 glo
    • 实际:执行 corfu-next

在还没触发 yasnippet 展开时,这个结果我是能接受的。

是我描述的不清楚,应该是在 yasnippet 展开后和 corfu 有冲突。重现步骤如下:

  1. 除了安装 corfu 和 yasnippet,还应该安装一个 yasnippet-snippets
  2. scratch buffer 输入 lam
  3. 等待补全菜单出现
  4. TAB,执行的是 corfu-next,可以接受。
  5. 按 C-q 取消,或者直接按 yas-expand 对应的其他按键。
  6. TAB,光标在yasnippet 的编辑区跳转。
  7. 输入字符 a,等待补全菜单出现
  8. TAB
  9. 结果:
  • 期望:执行 corfu-next
  • 实际:跳转到下一个 yasnippet 编辑区。

:+1:我也是打算单独绑定一个按键用于 yas-expand,目前尝试用 M-*, 因为我用的原生按键, M-y 被占用了。

真赞 :+1:

这样应该可以?简单测试了一下,没有 corfu-frame 的时候,可以正常跳到下一个位置;弹出 corfu-frame 后,按 tab 执行了 corfu-next

  (defun my-corfu-frame-visible-h ()
    (and (frame-live-p corfu--frame) (frame-visible-p corfu--frame)))
  (add-hook 'yas-keymap-disable-hook #'my-corfu-frame-visible-h)
3 个赞

简单来说你的问题是希望在补全列表出现的时候用tab来循环选择是吧?

这个我从没用过,一开始是C-n C-p上下选择的,后来跟abo-abo抄了“列表出现的时候按1 2 3来选择某项(alt-1输入真的1)”这个做法。(见abo-abo的ora-company-number)基于这个,你的问题可以:

  1. 也用1 2 3来选择对应项
  2. 把1和2绑定到next/prev(alt-1输入真的1)

是的,我不想用 C-n C-p ,因为不想按太多的 Ctrl。用 TAB 感觉更流畅。

:+1: 感谢大佬,这就是我想要实现的。这样我就不用 advice-add

我基于你的代码做了下简化,(frame-live-p corfu--frame) 好像也没必要检查,只要检查弹出框是否可见就好了。

下面的配置就可以正常工作了。

(use-package yasnippet
  :diminish yas-minor-mode
  :bind ("M-*" . yas-expand)
  :custom (yas-keymap-disable-hook (lambda () (frame-visible-p corfu--frame)))
  :hook (after-init . yas-global-mode))

为了保险起见,还是 (frame-live-p corfu--frame) 检查一下,否则万一什么地方把窗口销毁了,或者窗口从来没显示过,变成 nil 就会出错。

1 个赞

确实像你说的,如果是刚启动 Emacs,没有弹出过 corfu 窗口,直接通过 M-x yas-insert-snippet 插入 sneppet 后,TAB 就没法跳到下一个选区了。

当存在补全框的时候按 C-y 会触发展开 yas, 当不存在补全框的时候就是按 tab 展开。一直都这么用,已经用习惯了。然后存在补全框的时候 tab 就是常规的 tng 模式。

:+1: 这样能省一个按键。

不过我用习惯了原生按键,C-y 是最常用的粘贴键,对我来说很难适应。

其实展开yasnippet 我更喜欢用 consult-yasnippet,可以快速查询 snippet 的名称和按键,还能实时预览。

类似的包,还有 ivy-yasnippet

更多的时候是使用 eglot 时,lsp-server 就会提供 snippet,补全时就会自动用 YASnippet 进行展开。