lsp-mode と corfu を同時に使う際の注意点
July 31, 2021
tl;dr #
lsp-mode
とcorfu
をデフォルト設定のまま同時に使うと,corfu
とcompany-mode
の補完窓が同時に出てきてしまい,後者にカーソルが奪われる- 原因は
lsp-mode
内部でcompany-mode
が起動されるから - これを阻止するためには
lsp-completion-provider
に:none
を指定すれば良い
環境 #
背景 #
corfu
は company-mode
のように補完機能を強化してくれるパッケージ.Emacs が備える補完機構 (dabbrev-completion
や Capfs
(completion-at-point-functions
)) に準拠しており,補完 UI を提供するのみというミニマルな設計思想であるため,リッチな機能を備える company-mode
と比べて軽量で高速.
lsp-mode
は language server から情報を拾ってきてくれるパッケージ.python を例に挙げると, numpy
をインポートするとその内部の情報を拾ってきてくれる.その情報を補完機構に渡すことで,補完候補にいい感じにメソッドなどが現れてくれる.
lsp-mode
と corfu
を同時に使ったところ補完窓が2つ出てくるという問題が発生したため,その原因と解決策をまとめる.
問題と原因 #
lsp-mode
と corfu
をデフォルト設定のまま使うと補完窓が2つ出てきてしまう.カーソルは裏の窓に持っていかれるので放置するわけにもいかない.
裏に出ているのは起動していないはずの company-mode
の補完窓である.じゃあ誰が起動してるのかと言うと,
lsp-completion.el 内の lsp-completion-mode
の真ん中辺りに答えはあった. lsp-completion-provider
がデフォルトでは :capf
になっており,このとき company-mode
が起動されてしまう.
(define-minor-mode lsp-completion-mode
"Toggle LSP completion support."
:group 'lsp-completion
:global nil
:lighter ""
(let ((completion-started-fn (lambda (&rest _)
(setq-local lsp-inhibit-lsp-hooks t)))
(after-completion-fn (lambda (result)
(when (stringp result)
(lsp-completion--clear-cache))
(setq-local lsp-inhibit-lsp-hooks nil))))
(cond
(lsp-completion-mode
(setq-local completion-at-point-functions nil)
(add-hook 'completion-at-point-functions #'lsp-completion-at-point nil t)
(setq-local completion-category-defaults
(add-to-list 'completion-category-defaults '(lsp-capf (styles basic))))
(cond
((equal lsp-completion-provider :none))
;; デフォルトではここに入る
;; 過去に company をインストールしたことがあれば company-mode がオンになる
((and (not (equal lsp-completion-provider :none))
(fboundp 'company-mode))
(setq-local company-abort-on-unique-match nil)
(company-mode 1)
(setq-local company-backends (cl-adjoin 'company-capf company-backends :test #'equal)))
(t
(lsp--warn "Unable to autoconfigure company-mode.")))
(when (bound-and-true-p company-mode)
(add-hook 'company-completion-started-hook
completion-started-fn
nil
t)
(add-hook 'company-after-completion-hook
after-completion-fn
nil
t))
(add-hook 'lsp-unconfigure-hook #'lsp-completion--disable nil t))
(t
(remove-hook 'completion-at-point-functions #'lsp-completion-at-point t)
(setq-local completion-category-defaults
(cl-remove 'lsp-capf completion-category-defaults :key #'cl-first))
(remove-hook 'lsp-unconfigure-hook #'lsp-completion--disable t)
(when (featurep 'company)
(remove-hook 'company-completion-started-hook
completion-started-fn
t)
(remove-hook 'company-after-completion-hook
after-completion-fn
t))))))
解決策 #
lsp-completion-provider
に :none
を指定すれば company-mode
の起動を阻止できる.
(custom-set-variables '(lsp-completion-provider :none))
直感的には,デフォルトの :capf
から :none
に変更すると Capf
に lsp からの情報が渡されなくなりそうに感じる.しかし,capf 関連の設定は lsp-completion-provider
の条件分岐の直前で行われるため問題ない.
leaf.el における lsp-mode
と corfu
の最小設定は以下のとおり.(lsp
の起動自体は lsp-pyright
や lsp-latex
など,個別に設定しておく必要はある.)
(leaf lsp-mode
:emacs>= 25.1
:ensure t
:custom (lsp-completion-provider . :none))
(leaf corfu
:ensure t
:global-minor-mode corfu-global-mode)
余談 #
lsp-completion-provider
を用いた条件分岐でデフォルトの :capf
を名前指定している箇所を lsp-mode
全体で探したところ,驚くことに一つも見つからなかった.名前指定の条件分岐は :none
のみであった.であれば, :capf
という紛らわしい名前ではなく :company-capf
とするのが妥当であると思われる.単に capf に lsp の情報を渡したいのに,そのためには :capf
ではなく :none
を指定する必要がある設計は難しすぎる・・・