Lambdaカクテル

京都在住Webエンジニアの日記です

EmacsによるScala開発 2022年版 (補完編)

最近ちまちまEmacsでScalaを書けるようにしているのですが、そのメモ的な記事です。特に、補完の話に注目して書きます。

補完

補完を行うためには、まずは言語を認識して適切な解析を行うメカニズムが必要になります。この仕組みはデファクトスタンダードが確立しています。それはLSP (Language Server Protocol) です。LSPはエディタと解析ツールとの間の共通語を定義して、どんなエディタでも等しく補完や解析の恩恵に与れるようにするものです。ScalaのデファクトスタンダードなLSPバックエンドはMetalsです。MetalsはLSPの上で各種エディタにScalaの解析メカニズムやリファクタ機能等を提供します。

次に、エディタがLSPに対応する必要があります。Emacsは標準状態ではLSPに対応していないので、プラグインが必要です。Emacsの場合はlsp-modeが必要です。これもデファクトスタンダードです。

Emacs上でmetalsを使うために、lsp-metalsも必要です。

また、UI上の問題として補完フレームワークというものが必要になります。ユーザがTABなどを入力したときに補完候補を表示したり、それを選択して入力したりする存在です。これはデファクト的なものはなく、自由なものを選べば良いと思いますが、自分はcompanyを使っています。company自体はインターフェイスのみを提供し、具体的な補完メカニズムはcompany用バックエンドとしてモジュラーに提供されます。companyのLSPバックエンドは最近自動的にインストールされるようになりました。これを使うとcompanyを使ってLSPを用いた補完ができるようになります。

これで、基本的なScalaの補完は使えるようになるはずです。軽くまとめると、

  • LSPというエディタのための標準的なプロトコルが存在する。
  • Scalaでは、MetalsがLSPを実装している。
  • Emacsは標準ではLSPをサポートしていないので、lsp-modeとlsp-metalsで対応させる。
  • 補完用のUIとしてcompanyを採用する。
  • すると、TABキーなどで補完メニューが出るようになる。

という感じです。

ちなみに、metalsとそれ用のコンパイラであるbloopのインストールは、scala-bootstrap.elにより自動化できます。

追記: 最近はscalabootstrapいらないとのことです:

しばらく前まではcompany-lspというcompany用のLSPバックエンドがありましたが、古くなってメンテナンスされなくなりました。lsp-modeがcompanyをサポートするようになったので、特にインストールしなくても良くなったようです。

じゃあどうすればいいの

  • scala-mode入れて
  • lsp-mode入れて
  • lsp-metals入れて
  • company入れて
  • ~scala-bootstrap入れて~

↑をやって最小限のelispを書く:

(require 'company)
(require 'scala-mode)
(require 'scala-bootstrap)
(require 'lsp)
(require 'lsp-mode)
(require 'lsp-metals)

(setq company-require-match nil)
(setq company-dabbrev-downcase nil)

(setf lsp-prefer-capf t)

(setq scala-bootstrap:metals-scala-version "2.13")
(setq scala-bootstrap:metals-version nil) ;; force latest metals version (nil corresponds for latest)
(add-hook 'scala-mode-hook
          '(lambda ()
              (lsp)
              (company-mode-on)))

たぶんこれで動作するはず!!! 動かなかったら教育して叱ってくれ

相性の悪いプラグイン

EmacsでLSP+companyを使うにあたって、相性が悪くて併用しないほうが良さそうなプラグインがあります。

  • elscreen

elscreenを入れていると、補完が起動しませんでした。削除したところ動作しはじめました。直接の関連性は不明ですが、使わないことをおすすめします。

  • company-fuzzy

あいまいマッチによる入力を行うことができるプラグインですが、有効になっていると正しくパッケージ名などをフィルインできなくなります。

(setq company-fuzzy-passthrough-backends '(company-capf)) するとパススルーできるようになるみたいです。

参考elisp

参考までに、設定ファイルは以下のような感じで仕上げています。上掲していないプラグインもいくつか含まれています。

01_company.el

まずcompanyまわりの設定です。company-quickhelpにより、補完中にもそのメソッドのシグネチャ等を確認できるようにしています。company-fuzzyによってあいまい補完にも対応させています。だいぶ秘伝のタレっぽくなっています。

(require 'company)
(require 'company-quickhelp)
(require 'company-fuzzy)

(define-key company-active-map (kbd "M-n") nil)
(define-key company-active-map (kbd "M-p") nil)
(define-key company-active-map (kbd "C-n") 'company-select-next)
(define-key company-active-map (kbd "C-p") 'company-select-previous)
;;; C-hがデフォルトでドキュメント表示にmapされているので、文字を消せるようにmapを外す
(define-key company-active-map (kbd "C-h") nil)
;;; 1つしか候補がなかったらtabで補完、複数候補があればtabで次の候補へ行くように
(define-key company-active-map (kbd "<tab>") 'company-complete-common-or-cycle)
;;; ドキュメント表示
(define-key company-active-map (kbd "M-d") 'company-show-doc-buffer)

(setq company-minimum-prefix-length 3) ;; 3文字入力で補完されるように
 ;;; 候補の一番上でselect-previousしたら一番下に、一番下でselect-nextしたら一番上に行くように
(setq company-selection-wrap-around t)
(setq company-idle-delay 2)

;; 候補にあるものしか入力できなくするか
(setq company-require-match nil)
;; tooltipを右に揃える
(setq company-tooltip-align-annotations t)

;; backends
(custom-set-variables
 '(company-backends
   (quote
    (company-tide company-capf company-semantic company-clang company-files
                  (company-dabbrev-code company-gtags company-etags company-keywords)
                  company-oddmuse company-dabbrev))))

(set-face-attribute 'company-tooltip nil
                    :foreground "black"
                    :background "lightgray")
(set-face-attribute 'company-preview-common nil
                    :foreground "dark gray"
                    :background "black"
                    :underline t)
(set-face-attribute 'company-tooltip-selection nil
                    :background "steelblue"
                    :foreground "white")
(set-face-attribute 'company-tooltip-common nil
                    :foreground "black"
                    :underline t)
(set-face-attribute 'company-tooltip-common-selection nil
                    :foreground "white"
                    :background "steelblue"
                    :underline t)
(set-face-attribute 'company-tooltip-annotation nil
                    :foreground "red")
(setq company-quickhelp-color-background "#3c3836")

;; 勝手に小文字に展開しない
(setq company-dabbrev-downcase nil)
;; auto saveさせない
(setq company-eclim-auto-save nil)

(setq company-fuzzy-sorting-backend 'flx)

;; company-transformers  ;;  is  nil
;; company-fuzzy-all-other-backends
;; company-backends
;; company-fuzzy--backends

;; Enable downcase only when completing the completion.
(defun jcs--company-complete-selection--advice-around (fn)
  "Advice execute around `company-complete-selection' command."
  (let ((company-dabbrev-downcase t))
    (call-interactively fn)))
(advice-add 'company-complete-selection :around #'jcs--company-complete-selection--advice-around)

(global-company-mode 1)
;;  it  has  bug.
(global-company-fuzzy-mode t) ; 神の機能だ!!!!!

(company-quickhelp-mode)

(eval-after-load 'company
  '(define-key company-active-map (kbd "C-c h") #'company-quickhelp-manual-begin))
;; (set-face-attribute 'company-quickhelp-color-background nil
;;                     :background "steelblue")
(setq company-quickhelp-color-background "steel blue")

;;; 01_company.el ends here

02_lsp.el

LSPまわりの設定ですが、require以外のことはあまりやっていません。(setf lsp-prefer-capf t)すると、自動的にcompanyがLSP対応します。

(require 'lsp)
(require 'lsp-ui)

;; use capf(completion-at-point) as completion backend for lsp
(setf lsp-prefer-capf t)

03_scala.el

scalaファイルを開いたら自動的にmetals/bloopをインストールして起動するようにしています。

(require 'scala-bootstrap)
(require 'lsp-mode)
(require 'scala-mode)
(require 'lsp-metals)
(require 'lsp-ui)

(setq scala-bootstrap:metals-scala-version "2.13")
(setq scala-bootstrap:metals-version nil) ;; force latest metals version (nil corresponds for latest)
(add-hook 'scala-mode-hook
          '(lambda ()
              (scala-bootstrap:with-metals-installed
               (scala-bootstrap:with-bloop-server-started
                (lsp)
                ))))

(setq lsp-response-timeout 20)
(setq lsp-headerline-breadcrumb-enable t)

(setq lsp-ui-doc-enable t)
(setq lsp-ui-doc-header t)
(setq lsp-ui-doc-include-signature t)
(setq lsp-ui-doc-position 'top)
(setq lsp-ui-imenu-enable t)

(setq lsp-verify-signature nil)

(defun scala-mode-hooks ()
  (company-mode-on)
  (smartparens-mode t)
  (setq prettify-symbols-alist scala-prettify-symbols-alist))

(add-hook 'scala-mode-hook 'scala-mode-hooks)
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?