🎅

Eglot for JavaScript

2022/12/16に公開

今まで emacs 内で LSP を利用する時は lsp-mode を利用していたのですが、
Emacs 29 からEglotが標準パッケージに入るとのことなので、Eglot を利用してみることにしました。
普段 JavaScript を書くことが多いので、備忘も兼ねて Eglot の JS 向けの設定を紹介したいと思います。

TypeScript Language Server を利用する場合の設定

Eglot は lsp-mode とは違い、自分で lsp server をインストールする必要があります。
TypeScript Language Serverをインストールしましょう。
eglot-server-programs の初期値としてすでに登録されているので、js-mode や typescript-mode を利用する場合、設定は特に必要ないです。

npm install -g typescript-language-server typescript

deno lsp を利用する場合の設定

Deno をインストールすれば、標準で lsp server が入ります。deno lspというコマンドで lsp server を起動できます。便利。
Deno のドキュメントに設定内容が書かれています。
ですが、これだといくつか微妙な部分があるので設定を入れていきます。

TypeScript Language Server と併用する際に辛い問題

Deno だけ書く人は Deno のドキュメント通りに設定すれば問題ないと思いますが、
自分は node.js なども書くので typescript-mode に deno lsp が 固定されてしまうと困ります。
なので、こちら を参考に deno なのか node なのかで lsp を使い分ける分岐を入れるようにしました。
分岐を入れたものを eglot-server-programs に登録します。

(defun deno-project-p ()
  "Predicate for determining if the open project is a Deno one."
  (let ((p-root (cdr (project-current))))
    (file-exists-p (concat p-root "deno.json"))))

(defun node-project-p ()
  "Predicate for determining if the open project is a Node one."
  (let ((p-root (cdr (project-current))))
    (file-exists-p (concat p-root "package.json"))))

(defun es-server-program (_)
    "Decide which server to use for ECMA Script based on project characteristics."
    (cond ((deno-project-p) '("deno" "lsp" :initializationOptions (:enable t :lint t)))
          ((node-project-p) '("typescript-language-server" "--stdio"))
          (t                nil)))

(add-to-list 'eglot-server-programs '((js-mode typescript-mode) . es-server-program))

標準ライブラリへの定義ジャンプができない問題

Deno のドキュメント通りに設定しても大体の機能は利用できるのですが、標準ライブラリや import したパッケージへのジャンプができず不便です。
deno lsp はそのような標準ライブラリへの uri を deno:/ から始まる uri で返すようで、そのまま xref に渡してもジャンプできないようです。
実際に Eglot のログを見ても deno の lsp server はそのような uri を返していることがわかります。

console.log に定義ジャンプしようとした場合
[server-reply] (id:31) Fri Dec 16 11:01:10 2022:
(:jsonrpc "2.0" :result
	  [(:targetUri "deno:/asset/lib.deno.console.d.ts" :targetRange
		       (:start
			(:line 8 :character 0)
			:end
			(:line 37 :character 1))
		       :targetSelectionRange
		       (:start
			(:line 8 :character 18)
			:end
			(:line 8 :character 25)))]
	  :id 31)

調べてみると、このような deno:/の uri 場合は deno/virtualTextDocument API をコールする必要があるようだったので、それに対する設定を入れました。

(defun advice-eglot--xref-make-match (old-fn name uri range)
    (cond
     ((string-prefix-p "deno:/" uri)
      (let ((contents (jsonrpc-request (eglot--current-server-or-lose)
                                       :deno/virtualTextDocument
                                       (list :textDocument (list :uri uri))))
            (filepath (concat (temporary-file-directory)
                              (replace-regexp-in-string "^deno:/\\(.*\\)$" "\\1" (url-unhex-string uri)))))
        (unless (file-exists-p filepath)
          (make-empty-file filepath 't)
          (write-region contents nil filepath nil 'silent nil nil))
        (apply old-fn (list name filepath range))))
     (t
      (apply old-fn (list name uri range)))))

(advice-add 'eglot--xref-make-match :around #'advice-eglot--xref-make-match)

Eglot の中身を見てみるとと、 jsonrpc-request をそのまま利用してリクエストを投げているようだったので、そのまま流用しました。

Volar(Vue.js)を利用する場合の設定

Vue.js で lsp server を利用する場合はvetur(vls)か Volar だと思いますが、Vue 3 をメインで書くことが多いので volar を入れていきます。
まずVolarをインストールしましょう。

npm i -g @volar/vue-language-server

TypeScript を利用したい場合は typescript.tsdk に TypeScript へのパスを設定する必要があるので、initializationOpionで設定を入れつつ、eglot-server-programs に登録します。

(add-to-list 'eglot-server-programs '(vue-mode . ("vue-language-server" "--stdio"
                                                    :initializationOptions
                                                    (:typescript
                                                     (:tsdk "node_modules/typescript/lib")
                                                     :vue
                                                     (:hybridMode :json-false)
                                                     :serverMode 0
                                                     :diagnosticModel 1
                                                     :textDocumentSync 2))))

まとめ

Eglot で JS 向けの設定をいくつか紹介しました
メリークリスマス✨🎅🎄

Eglot と lsp-mode の比較(雑)

  • Eglot だと major-mode を hook して lsp を決定するため、JS のように major-mode から一意に lsp を決定し辛い言語(言語 対 lsp というより フレームワーク 対 lsp が多い)だとややハックが必要
    (lsp-mode は priority や.dir-locals.el で管理できた)
  • Eglot の方が早い(気がする)
  • Eglot の方がソースを読みやすい(気がする)
  • lsp-mode の方が利用しやすい(lsp のインストール、設定など不要、lsp-uiとか便利)(気がする)

Discussion