haskell-language-server読んだメモ
lsp触ってみたいのでよむ。
コミットはこれ。
用語集
- HLS: haskell-language-server
- LSP: language-server-protocol
- LS: languager-server
ここに公式の解説があるが、現在の実装はちょっと違う。
まずはトップレベルのhaskell-language-server.cabal
とstack.yaml
を読みにいく。
stack.yaml
にはプラグイン一覧が書いてあるだけ。
haskell-language-server.cabal
を読むと、haskell-language-server-wrapper
とhaskell-language-server
というexecutableが定義されている。つまりhlsを起動するとこのexeが呼ばれている。
haskell-language-server-wrapper
の実装はexe/Wrapper.hs
にある。main関数を読んでくとコマンドライン引数のパースとかロガーの設定とかやってる。launchHaskellLanguageServer
という関数で実際の起動をやっている。
launchHaskellLanguageServer
ではプロジェクトで使ってるstackとかcabalの設定(これをCradleと呼んでいる)を読み込む。CradleからGHCのバージョンを特定して、それに対応したHLSのバイナリを探して起動する。
この後の処理はhaskell-language-server
に移る。
haskell-language-server
の実装はexe/Main.hs
にある。main関数みてみるとなんとかRecorderみたいな処理がいっぱいあるが、これは要するにloggerの抽象。Priorityというログの詳細度フラグ?みたいなものにしたがって、対応したloggerを作ってる。個人的にそういうのはReaderモナドでやらんかったんかと思わなくもないが、何か理由があるんだろう。つまりrecorderがなんちゃらみたいな処理は全部飛ばしていい。
実際にHLSを起動してるのはdefaultMain
関数になる。これはロガーと動作モードとプラグイン一式をうけとって、HLSを起動する。
動作モード(型としてはArguments)について補足する。HLSはバージョンの表示とか、設定したCradleとか、VsCodeのスキーマとかを生成することもできるので、その動作の切り替えを指定する。
プラグインはビルド時に指定する感じで、プラグインの実態はsrc/HlsPlugins.hs
のidePlugins
関数で実装している。idePluginsはビルドオプションで読みづらいが要するに長い長いプラグインのリストになっている。
ちなみにHLS自体はコア機能だけを提供していて、実際のlspの機能はPluginとして開発されている。なので自分で機能開発する場合は実際はPluginの開発をすることになる。
defaultMain
を読んでいく。場所はsrc/Ide/Main.hs
。HLSを起動するのはArguments
がGhcide
のとき。runLspMode
でHLSを起動する。
runLspMode
はやはりロガーと設定とプラグイン一覧を受け取る。ここではロガーの設定(何回やるんだよ)とはCWDの変更とかをやるだけで、HLSの実態はIDEMain.defaultMain
に移る。
IDEMain.defaultMain
を読んでいく。場所はghcide/src/Development/IDE/Main.hs
。GhcIdeというのはhaskell-language-serverのpredecessorのこと。そのまま?持ってきてるらしい。
IDEMain.defaultMain
で大事なのはargCommands
で分岐しているところ。これはGhcIdeの動作モードを切り替えるやつで、argCommand == LSP
の時にrunLanguageServer
でLSが起動する。いつになったら起動するんだよ。
withNumCapabilities
というのはLSPでいうcapabilityではなく、システムのスレッド数分並列にIOを走らせるやつらしい。
runLanguageServer
はghcide/src/Development/IDE/LSP/LanguageServer.hs
にある。
コード内にやたらとLSP
というモジュールが出てくるが、これはlspモジュールのLanguage.LSP.Serverのこと。Language.LSP.Server
はHLSとは別に開発されていて、LSPをしゃべるサーバーとか、それに使うVirtual File Systemとかが実装されてる。ちなみにlsp-typesというのもあって、これはLSPでやり取りするメッセージをHaskellのデータ型で定義したものになっている。
このrunLanguageServer
の中でLanguage.LSP.Server.runServerWithHandles
を呼び出してようやくLSの起動が完了する。
ということで、HLSの実態は結局Language.LSP.Server
だということがわかった。
HLSが提供するプラグインは巡り巡ってrunLanguageServer
の最後の引数のsetup
に含まれている。これをいい感じにさらに変形してLanguage.LSP.Server.runServerWithHandles
に渡している。
これ以上はLSPの仕様とLanguage.LSP
を真面目に理解してないと読み進められなさそうなので一旦お休み。
Pluginの実装を見ていく。
プラグインはPluginDescriptor IdeState
という型で表される。
基本的にPluginはdescriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState
という関数を公開して、HlsPluginからPluginIdをもらう。
PluginDescriptor
はhls-plugin-apiで定義されていて、プラグインがどうやってlspを実装するのかが定義されている。