Language Server開発で使うと便利なやつまとめ
Language Serverを開発する際に便利なフレームワーク・ツールをまとめました。
Volar.js
Volar.jsはTypeScriptで使えるLanguage Serverのフレームワークです。
もともとはVue.jsに特化していたものを他の言語でも扱えるようにしたようです。Vue.jsを扱っていたというだけあって、HTMLやCSSが入り乱れるようなある言語の中に別の言語があるというケースでも対応可能です。
もちろん単一の言語のLanguage Serverも開発できます。
フレームワークに乗っかってしまえば、リクエストやレスポンスの型を補完を使いながら実装できる点も魅力です。

VSCodeでの動作確認用の拡張機能も用意されており、Language Serverの再起動・初期パラメータの確認などがワンクリックで行えます。

※筆者はNeovim使いですが、VSCodeでも動作確認をしています。これはエディタの仕様に気づかずに詰まるのを防ぐためです。
他の言語でLanguage Serverのフレームワーク・SDKっぽいものが気になる人はSDKs for the LSPを見てみましょう。
AST Explorer
AST ExplorerはAST(abstract syntax tree・抽象構文木)をツリー表示で確認できるサイトです。

そもそもLanguage Serverでは次のような処理の流れがあります。
- コードを読み込む
- コードを解析する
- 解析した結果を使ってレスポンスを返す
「解析」は単純なものなら正規表現だけいいかもしれませんが、パーサーに任せることが多いでしょう。
AST Explorerでパーサーの結果(AST)を確認してみるとノードが示す位置とコードの範囲の対応が視覚されるため、分かりやすいです。
未対応のパーサーを追加したい人は別の記事AST explorerへ独自にパーサーを導入する手順を読んでください。
インデックス関係
Goto Definition(定義への移動)のように他のファイルが関わる場合、あらかじめワークスペース全体を解析しておく必要があります。
Language Serverが起動した時点で関係するファイルをすべて解析し、あとで取り出せるようにインデックスしておくのです。
インデックスのやり方
じゃあどうやってインデックスすればいいでしょうか。ASTの使い方がハッキリ決まっていれば独自の実装で問題ありません。
もし実装の基準となるようなものやインデクサーが欲しければ、次の2つがあります(それぞれ次の項目で説明します)。
ただ、Language Server開発初心者がいきなりLSIFやSCIPのようなものを扱おうとすると、それだけでかなりの時間を費やしてしまいます。
開発初期は独自の実装でインデックスするのがおすすめです。規模が大きくなったらLSIFやSCIPに則ったインデクサーを使い、既存のインデクサーが無ければ自作しましょう。
LSIF
LSIF(Language Server Index Format)はMicrosoftが提唱しているIndexの形式です。筆者はこちらを使って実装したことはないので概要だけ説明します。
LSIFではコードをグラフとしてインデックスするようです。次に示すのがLSIFでインデックスした結果の一部です。
// https://microsoft.github.io/language-server-protocol/overviews/lsif/overview/より引用// a vertex representing the document{ id: 1, type: "vertex", label: "document", uri: "file:///Users/username/sample.ts", languageId: "typescript" }// a vertex representing the range for the identifier bar{ id: 4, type: "vertex", label: "range", start: { line: 0, character: 9}, end: { line: 0, character: 12 } }// an edge saying that the document with id 1 contains the range with id 4{ id: 5, type: "edge", label: "contains", outV: 1, inV: 4}// a vertex representing the actual hover result{ id: 6, type: "vertex", label: "hoverResult", result: { contents: [ { language: "typescript", value: "function bar(): void" } ] }}// an edge linking the hover result to the range.{ id: 7, type: "edge", label: "textDocument/hover", outV: 4, inV: 6 }
LSIF.devにて、LSIFに則って各言語で実装されているインデクサーを確認できます。インデクサーを使えば「LSIFに則ってインデックする」という実装が省けます。
SCIP
SCIP(SCIP Code Intelligence Protocol←再帰的頭字語)はSourcegraphが提唱しているIndexの形式です。SCIPはLSIFでの問題点を意識して開発されました。実装の複雑さやパフォーマンス、扱いにくさなどを改善した、というようなことがブログに書かれています。
SCIPではProtobufで厳密に定義されたフォーマットに則ってインデックスします。次に示すのがSCIPでインデックスした結果の一部です。
documents { relative_path: "src/Domain/models/Book/Title/Title.ts" occurrences { range: 11 range: 21 range: 26 symbol: "scip-typescript npm foo 1.0.0 src/Domain/models/Book/Title/`Title.ts`/Title#validate().(value)" symbol_roles: 1 } symbols { symbol: "scip-typescript npm foo 1.0.0 src/Domain/models/Book/Title/`Title.ts`/Title#validate()." documentation: "```ts\n(method) validate(value: string): void\n```" relationships { symbol: "scip-typescript npm foo 1.0.0 src/Domain/models/shared/`ValueObject.ts`/ValueObject#validate()." is_reference: true is_implementation: true } }}
LSIFのグラフ構造とは違い、SCIPでは「どのファイルに何が定義されているのか」が一目で分かります。
Sourcegraph docsにて、SCIPに則って各言語で実装されているインデクサーを確認できます。インデクサーを使えば「SCIPに則ってインデックする」という実装が省けます。
まだ実装されていない言語や自分で実装したい場合はscip.protoを読みましょう。protoファイルにすべてが書いてあります。
NeovimでのLanguage Serverの開発
VSCodeだとログがOUTPUT画面へ出力されるため見やすいのですが、Neovimだとファイルへの出力であるため見づらいです。

そして、長い行のファイルをNeovimで開くと重くなってしまいます。
vim.lsp.log.set_format_func()
を使えば独自のフォーマットで保存できますが、自分で設定するのは大変です。
lnavのようなログビュアーを使うのもありですが、せっかくなら使い慣れたNeovimで操作したいところです。
そんなわけでlsp-dev.nvimというプラグインを開発しました。
:LspDev showLog
のようにコマンドを叩くと、パース&色付けされたログが表示されます。

ログが更新されればこのビュワーも更新され、カーソル行がログ末尾なら自動でスクロールします。
初めてのプラグインです。もしLanguage Server開発者の方がいれば使ってみてください。
じゃあ君は何を作ったんだい?
自分のブログなので筆者の話も書いておきます。2025年1月時点で筆者が作成したLanguage Serverは次の2つです。
- laravel-language-server(開発停止)
- markdown-language-server(普段使いしている)
どちらもVolar.jsを使って実装しました。
Laravelの方は、Laravelを使う機会が思ったよりも多くなかったため開発を止めました。こちらは「SCIP(をちょっと改造したもの)に則ってインデックスし、その情報から補完を出す」というところまでは開発できました。
せっかくなのでVolar.jsの知見を活かしつつ、普段使いできるLanguage Serverを開発したかったため、markdown-language-server
を作りました。こちらでは「Completion」「Goto Definition」「Code Action」「References」などを実装しました。
初めてモノレポ構成をやってみたのですが、pnpmのおかげで迷うことなく実装できました。
VSCodeではクライアントとなる拡張機能を用意しないといけないのですが、これはVolar.jsのガイドに載っているコードですぐに実装できました。
以上、Language Serverを開発する際に便利なツールまとめでした。