🐈

Emacs での LSP と tree-sitter と lint

2021/12/03に公開

概要

  • eglot を試してみたところ LSP はうまく動いていて、かなり使えるレベルになっている。特に TypeScript の言語サーバはおすすめできる。何が良いのかはこの記事の本文で列挙した。
  • LSP はシンタックスハイライトをより精細にする機能を持っているが、eglot はそれに対応していない。代わりに emacs-tree-sitter を使えばほぼ同等の効果が得られる。動作もかなり速いのでおすすめ。
  • LSP と lint ツールを協調させるのは難しい。今は妥協して別々に動かすしかなさそう。

はじめに

エディタの機能からプログラミング言語の解析に関わる機能を切り出し、エディタに依存しない「言語サーバ」を作るというアイデアがある。これを整備して、エディタと言語サーバが通信するルールを決めたものが LSP である。LSP を使うことで良質な開発体験ができる、という意見をしばしば耳にするようになってきた。RubyKaigi 2021 でも、TypeProf for IDE: Enrich Dev-Experience without Annotations で Ruby3 の言語サーバを使えばエディタでこんな事ができる、と説明していた。

Emacs でも LSP に従ったクライアントとして振る舞うためのパッケージは存在している。しかし、私が数年前に試した頃は、いまいち使えない印象だった。パフォーマンスは良くないし、精度もよくない。仕事でよく使う Ruby の言語サーバーの性能が良くなかったせいかもしれない。しかし、VSCode の盛り上がりを見るに、近年ではそれも変化してきたような印象を受ける。そこで、あらためて Emacs に LSP を導入してみることにした。

TypeScript の LSP を試す

typescript-mode に上乗せして TypeScript の言語サーバを使ってみよう。Emacs の LSP クライアントとしては lsp-modeeglot がある。どうやら lsp-mode より eglot のほうが少ない設定で導入できるようなので、eglot をインストールしてみることにした。use-package を使っているので設定ファイルに下記の行を追加して実行する。

(use-package eglot)

とりあえずこれだけでも動くらしい。適当なファイルを開いて M-x eglot で eglot を有効にしてみた。言語サーバをインストールしてないので警告が出てしまった。

[eglot] I guess you want to run 'javascript-typescript-stdio', but I can't find 'javascript-typescript-stdio' in PATH! Enter program to execute (or <host>:<port>):

nvm でバージョン管理しているので、プロジェクトの nodejs のバージョンに対して typescript-language-server をインストールした。再び M-x eglot で今度は言語サーバの実行コマンドを与えてみる。成功メッセージが表示された。

[eglot] Connected! Server `EGLOT (my-project/typescript-mode)' now managing `typescript-mode' buffers in project `my-project'.

毎回 M-x eglot を実行して言語サーバを指定するのが面倒なので、設定ファイルに書くことにした。(言語サーバーのパスはもっと見直したほうが良さそうだ)

(use-package eglot
  :config
  (add-to-list 'eglot-server-programs '(typescript-mode . ("/Users/eggc/.nvm/versions/node/v12.19.0/bin/typescript-language-server" "--stdio")))
  :hook
  (typescript-mode . eglot-ensure))

eglot で紹介されている機能をいくつか試してみた。

  • Completion: 補完機能。 company-mode は文法を考慮しないが eglot を有効にすると補完候補がより賢いものになる。たとえば this. とタイプして補完をかけると、そのオブジェクトのメソッドやフィールドだけが補完候補になる。
  • Diagnostics: 構文チェック機能。 flycheck を使えば lint したり構文エラーを発見したりできるが、LSP ではそのような設定無しに構文エラーを見つけることができる。Emacs 組み込みの flymake でマークされるので基本的に追加設定は不要。カーソルを当てるとミニバッファにエラー内容が表示される。
  • Hover on symbol: ヒント機能。関数呼び出しや変数を参照しているコードにカーソルを当てると、ミニバッファにインターフェース(関数なら引数型と戻値型、変数なら型定義)が表示される。
  • Rename: リネーム機能。 wgrep を使えばファイルを横断した検索置換をできるが、LSP では M-x eglot-rename でクラスやメソッドなどのリネームができる。これは文脈を踏まえたリネームなので偶然同じ名前が使われているシンボルがリネームに巻き込まれたりすることがない。
  • Find definition: 定義ジャンプ機能。dumb-jump を使えば言語サーバなしで定義ジャンプできるが LSP は dumb-jump で対応できない外部ライブラリの定義ジャンプもできる。Emacs 組み込みの定義ジャンプコマンド xref-find-definitions に、自動でヒント情報を与えるので特別な設定は不要。
  • Code Actions: M-x eglot-code-actions で言語サーバが持っている code action を実行できるらしい。私が試したところでは、未使用変数に下線が表示され、そこをクリックすると、変数を削除するかアンダースコアをつけるといった自動修正が実行できた。

色々なパッケージを組み合わせて何とかしていた部分を、何もせずとも LSP がまとめているのがありがたい。私は試していないが eglot は他にも LSP で定められている機能のいくつかを実装しているようだ。

  • Snippet completion: yasnippet の内容を補完できるらしい。
  • Find references: xref-find-references により関数や変数の利用箇所を検索できる。

シンタックスハイライトと LSP

Emacs でのシンタックスハイライト(コードの色付け処理)は正規表現を使って行われている。たとえば ruby-mode のソースコードを見るとこのような大量の正規表現を使っている。しかし、正規表現は文脈を持たないのに対して、ソースコードは文脈を持つため細やかな色付けができない。

正規表現によるシンタックスハイライトに対して、ソースコードを意味的に解釈して色付けを行う方法をセマンティックハイライトと呼ぶらしい。セマンティックハイライトは文脈を解釈するので正規表現によるハイライトよりも精密な色付けが可能である。

LSP 3.6 から、セマンティックハイライトのための機能 Color Presentation Request が追加されたらしい。これを使うとこの記事で見られるように、変数と関数の引数を区別して色分けしたりできる。

Emacs でも lsp-mode を使えばセマンティックハイライトを利用できるらしい。ただし、もうひとつの LSP クライアントである eglot はセマンティックハイライトに対応しない方針のようだ。理由として、LSPを通じたセマンティックハイライトは、Emacs 組み込みのハイライトよりも低速であると添えている。

LSP を使わずにセマンティックハイライトを利用したい場合は tree-sitter を使う方法もある。tree-sitter はソースコードを解析して高速に構文木を作成するライブラリ。一度作成した構文木は保持していて、ソースコードの変更に追従して内部的な構文木を変化させることができるらしい。

ソースコードを解析するという点においては tree-sitter と LSP は似ているが、tree-sitter はあくまでライブラリとして最小限の機能しか定めていないのに対して LSP はコード補完やドキュメント参照など幅広い機能を定めている。

tree-sitter は、実行ファイルなどは提供してなくて純粋なC言語のライブラリとして提供されている。Emacs から呼び出す場合は dynamic module という機能を使って tree-sitter を呼び出さなければならない。dynamic module についてはこの記事が詳しい。dynamic module を使った tree-sitter の呼び出し実装が emacs-tree-sitter である。

emacs-tree-sitter の使い方はごく簡単で、インストールガイドに書いてあるとおりにすれば良い。実際に動かしてみると、たしかにコードの色付けが見やすくなっていると感じる。動作もかなり速いのでおすすめできる。

ただデフォルトでは tsx を色付けできないので、設定が必要。この記事が参考になる。

lint と LSP

プロジェクトでコードスタイルに一貫性をもたせるために eslint を使っているが言語サーバーは lint 機能は持っていない。そのため普段は flycheck を使って eslint を実行するようにしていたが、LSP の構文チェックと lint が別々に動いているのは気持ちが悪い。どうにか設定をまとめられないかと考えた。下記のツールが目についた。

どちらも lint のアダプタになり、外向きには言語サーバとして働く。言語サーバが存在しない markdown などに対して、lint を言語サーバ代わりに使うという考えに基づいているようだ。

試しに efm-langserver をインストールして下のような設定を追加してみた。

tools:
  typescript-eslint: &javascript-eslint
    lint-command: 'yarn eslint -f visualstudio --stdin --stdin-filename ${INPUT}'
    lint-ignore-exit-code: true
    lint-stdin: true
    lint-formats:
      - "%f(%l,%c): %tarning %m"
      - "%f(%l,%c): %rror %m"
languages:
  typescript:
    - <<: *typescript-eslint

さらに emacs に下記の設定を追記する。

(add-to-list 'eglot-server-programs '(typescript-mode . ("efm-langserver")))

とりあえず動作させることはできたが typescript-language-server と共存できていない。ひとつのプログラミング言語に対して2つの言語サーバを動かすということはできないようだ。冷静に考えると、2つの言語サーバと json で会話するとしたら、ほとんど無駄だから、できなくてあたりまえだ。加えて、efm-langserver は code action にも対応していないので lint の自動修正はできない。flycheck を外すのは諦めることにした。

さいごに

LSP を導入して何が良いのか、何ができるのかを確かめながら実験した。結論としてはなかなか便利になっているということがわかった。今後の LSP と言語サーバにも期待したい。

Discussion