Braindump

lsp-mode と corfu を同時に使う際の注意点

July 31, 2021
emacs

tl;dr #

  • lsp-modecorfu をデフォルト設定のまま同時に使うと, corfucompany-mode の補完窓が同時に出てきてしまい,後者にカーソルが奪われる
  • 原因は lsp-mode 内部で company-mode が起動されるから
  • これを阻止するためには lsp-completion-provider:none を指定すれば良い

環境 #

背景 #

corfucompany-mode のように補完機能を強化してくれるパッケージ.Emacs が備える補完機構 (dabbrev-completionCapfs (completion-at-point-functions)) に準拠しており,補完 UI を提供するのみというミニマルな設計思想であるため,リッチな機能を備える company-mode と比べて軽量で高速.

lsp-mode は language server から情報を拾ってきてくれるパッケージ.python を例に挙げると, numpy をインポートするとその内部の情報を拾ってきてくれる.その情報を補完機構に渡すことで,補完候補にいい感じにメソッドなどが現れてくれる.

/images/2021-07-numpy.png

lsp-modecorfu を同時に使ったところ補完窓が2つ出てくるという問題が発生したため,その原因と解決策をまとめる.

問題と原因 #

lsp-modecorfu をデフォルト設定のまま使うと補完窓が2つ出てきてしまう.カーソルは裏の窓に持っていかれるので放置するわけにもいかない.

/images/2021-07-corfu-problem.png

裏に出ているのは起動していないはずの 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-modecorfu の最小設定は以下のとおり.(lsp の起動自体は lsp-pyrightlsp-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 を指定する必要がある設計は難しすぎる・・・