Braindump

rust 実装のLSPクライアント lsp-proxy が凄く早くて良い

December 7, 2025
emacs, lsp

本記事は Emacs Advent Calendar 2025 の7日目です。

はじめに #

emacs から LSP を使うための client 実装はいくつかある。

  • lsp-mode: 最も使われているであろう代表的なLSPクライアント
  • eglot: emacs 29から標準パッケージに入ったLSPクライアント
  • lsp-bridge: こちらの記事 で紹介されているように、「最速」を謳うLSPクライアント

今回紹介する lsp-proxy は lsp-bridge inspired な LSP クライアント。 lsp-bridge 同様に Emacs と language server の通信を仲介して非同期に通信することで高速化を図っている。

私は元々 lsp-bridge を使っていたが、lsp-proxy を試したところ高速だし設定もしやすく使いやすかったので乗り換えた。

lsp-bridge と lsp-proxy の主な違い #

lsp-bridgelsp-proxy
仲介サーバー言語PythonRust
補完UI独自実装の補完UIcapf に関数を登録するだけで補完UIは無し

capf (completion-at-point-functions) とは、カーソル位置 (ポイント) で何らかのテキストを補完する際に、どの関数を呼び出すかを定義・管理するための変数である。

ref: https://www.gnu.org/software/emacs/manual/html_node/elisp/Completion-in-Buffers.html

lsp-proxy は capf に補完テーブルを提供する関数を登録するのみで、lsp-bridge と異なり補完UIは提供しない。 その代わり、補完UIは corfucompany のような capf を活用できる好みのものを利用できる。

インストール #

  • emacs 30.1 or 29
  • emacs-lsp-proxy binary を以下いずれかの方法で用意し、emacs から呼びだせるようにする
    • npm install する
    • lsp-proxy を clone し、cargo build する
    • releases からビルド済み binary をダウンロードする

設定 #

lsp-proxy は melpa などに登録されていないので、 git clone してきてパスを通す必要がある。

`use-package` を使う場合、README にあるように :load-path でパスを通し、 lsp-proxy を起動したい mode の hook に登録すれば良い。

(use-package lsp-proxy
  :load-path "/path/to/lsp-proxy"
  :config
  (add-hook 'tsx-ts-mode-hook #'lsp-proxy-mode)
  (add-hook 'js-ts-mode-hook #'lsp-proxy-mode)
  (add-hook 'typescript-mode-hook #'lsp-proxy-mode)
  (add-hook 'typescript-ts-mode-hook #'lsp-proxy-mode))

私は leafstraight を組み合わせて使っているので以下のように設定している。

(leaf lsp-proxy
  :straight (lsp-proxy
             :type git
             :host github
             :repo "jadestrong/lsp-proxy"
             :files (:defaults))
  :hook (go-mode-hook
         go-ts-mode-hook
         vue-ts-mode-hook
         vue-mode-hook
         web-mode-hook
         tsx-ts-mode-hook
         typescript-mode-hook
         typescript-ts-mode-hook
         js-mode-hook
         emacs-lisp-mode-hook
         graphql-ts-mode-hook
         protobuf-ts-mode-hook)
  :custom `(lsp-proxy-user-languages-config . ,(expand-file-name "~/.emacs.d/etc/lsp-proxy/languages.toml"))
  :bind ((:lsp-proxy-mode-map
          :package lsp-proxy
          ("M-." . lsp-proxy-find-definition)
          ("M-," . xref-go-back)
          ("C-c C-d" . lsp-proxy-describe-thing-at-point)
          ("C-c C-r" . lsp-proxy-find-references)
          ("C-c l i" . lsp-proxy-find-implementations)
          ("C-c l r" . lsp-proxy-rename)
          ("C-c l c" . lsp-proxy-execute-command)
          ("C-c l s" . lsp-proxy-signature-activate)))
  )

使う上での注意点として、lsp-proxy が提供するのはあくまで language server と Emacs との仲介サーバーと、Emacs における LSP client だけである。 つまり、language server は自分で用意する必要がある。

必要なものを npm や brew、あるいはビルドするなりして用意する。

language server の追加・設定方法 #

デフォルトで使用可能な language server は公式が提供している 言語設定ファイル に記載してある。 ここに自分が使いたい language server の設定が無い場合、あるいは設定を変えたい場合は自分で書く必要がある。

とは言え、この設定は特に難しいものでもなく、設定できて拡張しやすいと考えれば良い点でもある。

言語設定ファイルは Helix editor と互換性のある TOML ファイルである。 設定は主に2つのパートからなる。

1つは language server を定義する部分、もう1つは language server を起動する部分。

# language server を定義する部分
[language-server.gql-ls]    # ← `gql-ls` がこれを呼び出すときの key となる
command = "graphql-lsp"
args = ["server", "--method", "stream"]

# language server を起動する部分
[[language]]
name = "graphql"                  # ← unique な ID として適当に名前を付ける
language-servers = ["gql-ls"]     # ← 定義済みの起動したい language server を記載
file-types = ["gql", "graphql"]   # ← どんな拡張子のファイルを開いたときに language-servers を起動させたいか
# ↓ workspace root をどこにするか (親ディレクトリを辿り roots で指定したファイルを探し、直近の親が root になる)
roots = ["graphql.config.ts", "graphql.config.js", ".graphqlrc", ".graphqlrc.yml", ".graphqlrc.js", ".graphqlrc.ts", ".graphqlrc.yaml", ".graphqlrc.json", ".graphqlrc.toml", "graphql.config.json", "graphql.config.toml"]

多くのケースでは基本的に上記の項目を設定すれば使える。 他にも、複数の language server を同時に起動したり、language server の config を設定することもできる。 .vue.tsx のように1ファイル内で複数の language server が必要なときも簡単に追加できる。

[language-server.gopls]
command = "gopls"

[language-server.gopls.config]
buildFlags = ["-tags=integration"]

[[language]]
name = "go"
file-types = ["go"]
roots = ["go.mod"]
language-servers = [ "gopls" ]

[language-server.oxlint-ls]
command = "oxlint"
args = ["--lsp"]

[language-server.tsgo-ls]
command = "/path/to/tsgo"   # ← 任意の実行ファイルを直接指定するときはフルパス
args = ["--lsp", "--stdio"]

[[language]]
name = "tsx"
language-id = "typescriptreact"
file-types = ["tsx"]
roots = ["package.json"]
language-servers = [
  # support-workspace は workspace root の検知方法を変更できる
  # ↓ これはモノレポ用
  { name = "tsgo-ls", support-workspace = ["package.json"] },
  # config-files を指定することで、root にそのファイルがあるときだけ起動させることもできる
  # プロジェクトによって使われる linter が違うときに便利
  { name = "oxlint-ls", support-workspace = true, config-files = [
    ".oxlintrc.json"
  ] },
  { name = "eslint", support-workspace = true, config-files = [
    ".eslintrc.js", ".eslintrc.cjs", ".eslintrc.yaml", ".eslintrc.yml", ".eslintrc", ".eslintrc.json", "eslint.config.js", "eslint.config.mjs", "eslint.config.cjs", "eslint.config.ts", "eslint.config.mts", "eslint.config.cts"
  ] },
  { name = "tailwindcss-ls", support-workspace = true, config-files = [
    "tailwind.config.js",
  ] },
]

その他、詳しい説明は README の Language Server Configuration を参照されたい。

うまく補完がでないときは #

言語設定ファイルで設定しても補完が思うように出ないときがある。 そんなときは (setq lsp-proxy-log-level 3) を実行し、 M-x lsp-proxy-open-log-file を実行しよう。 逆にこれをしないと情報がどこにもなくて設定がおかしいのか、どこに問題があるのか調査ができない。

その他の問題も、README の Troubleshooting を見ると参考になるかもしれない。

最後に #

lsp-proxy は初期の頃は機能が少なくバグも多かったが活発な開発により、ここ半年くらいは安定して非常に便利にもなってきている。 2025/12/07 に記事を書いている最中にもモノレポ対応が入って個人的に大歓喜している。

本記事で紹介できていないコマンドや設定項目もたくさんあるので、興味がある方は README に目を通すことを推奨する。 きっと試してみたくなると思う。