lsp-modeのLSPサーバーをプロジェクト別に切り替える方法

2021-06-15T15:47:15+0900
emacs

最近、Denoの開発をはじめたことにより、EmacsでTypeScriptの開発をするときに、lsp-modeで起動するLSPサーバーをプロジェクトによってNode.js(ts-ls)とDeno(deno-ls)で切り替えたいと思いました。

lsp-modeはすでにJavascript/Typescript (deno)をサポートしているので、おそらくできるはです。

公式回答はディレクトリローカル変数を使う #

調べてみたところ、公式ドキュメントのFAQに次の記載がありました。

I have multiple language servers for language FOO and I want to select the server per project, what can I do?

You may create dir-local for each of the projects and specify list of lsp-enabled-clients. This will narrow the list of the clients that are going to be tested for the project.

dir-localとはディレクトリローカル変数のことで、.dir-locals.elファイルに変数を記述しておけば、そのディレクトリ配下のファイルで有効化される仕組みです。

設定する変数は、無効化するLSPサーバーを指定するlsp-disabled-clientsか、逆に有効化するLSPサーバーを指定するlsp-enabled-clients(なぜかドキュメントがない)になります。

というわけで、これらを使えば簡単にいけるかなと思ったのですが、意外と手こずったので方法を共有しておきます。

メジャーモードフックでディレクトリローカル変数が機能しない #

僕の環境では、TypeScriptを編集するとき次のようにしてlsp-modeを起動していました。

(add-hook 'typescript-mode-hook #'lsp)

そして、.dir-locals.elファイルに次のように記述をしたのですが、ts-lsがLSPサーバーとして起動してしまいました。

((typescript-mode . ((lsp-enabled-clients . (deno-ls)))))

ファイル内でM-x describe-variable RET lsp-enabled-clients RETを調べてみても、次のように正しく値が反映されています。

lsp-enabled-clients is a variable defined in ‘lsp-mode.el’.

Its value is (deno-ls)
Local in buffer index.ts; global value is nil

  This variable’s value is directory-local, set

どうにも解決できないので、Emacs-jp Slackで相談したところdeno-lsが上手く起動したと報告を受けたので、もしかするとtypescript-mode-hookでlsp-modeを起動させているのが悪いのかもと思い、試しにフックをコメントアウトして手動でM-x lspを実行してみたところ、期待どおりdeno-lsが起動しました。

deno-lsが起動したときの*Messages*バッファの表示はこのようになりました。

LSP :: Client eslint is not in lsp-enabled-clients
LSP :: Client jsts-ls is not in lsp-enabled-clients
LSP :: Client ts-ls is not in lsp-enabled-clients
LSP :: Client eslint is not in lsp-enabled-clients
LSP :: Client jsts-ls is not in lsp-enabled-clients
LSP :: Client ts-ls is not in lsp-enabled-clients
LSP :: Connected to [deno-ls:22873/starting].

つまり、メジャーモードフックはディレクトリローカル変数がセットされるよりも先に実行されてしまうことがわかりました。

ディレクトリローカル変数をセットしてからメジャーモードフックを実行する #

原因がメジャーモードフックだとわかれば、だいぶ調べるのが楽になります。すぐに、Stack OverflowでHow can I access directory-local variables in my major mode hooks?という記事を見つけて、回答には次のように書かれていました。

This happens because normal-mode calls (set-auto-mode) and (hack-local-variables) in that order.

However hack-local-variables-hook is run after the local variables have been processed, which enables some solutions:

こちらの回答を元に、次のような設定をしてみました。

(defun run-local-vars-mode-hook ()
  "Run `major-mode' hook after the local variables have been processed."
  (run-hooks (intern (concat (symbol-name major-mode) "-local-vars-hook"))))
(add-hook 'hack-local-variables-hook 'run-local-vars-mode-hook)

(add-hook 'typescript-mode-local-vars-hook #'lsp)

ファイルを開き直すと、今度はフック実行前にディレクトリローカル変数のlsp-enabled-clientsがセットされてdeno-lsが起動してくれました。

deno-lsが起動したEmacs

なお、このハックはlsp-modeをメジャーモードフックで起動している人のみ必要となるものなので、手動で起動している人は必要ありません。

まとめ #

今後は個人的にDenoでTypeScriptを書く頻度が上がる可能性があり、なんとか解決しておきたい課題だったのですが、試しては放置してを繰り返していたのですが、Emacs-jpで相談することで1ヶ月ほど悩んでいた課題がすぐに解決できて、とても嬉しかったです。

Emacs-jp Slackで相談に乗ってくれた wi-fujitaさんありがとうございました。