Open10

haskell-language-server読んだメモ

ararkarark

まずはトップレベルのhaskell-language-server.cabalstack.yamlを読みにいく。

stack.yamlにはプラグイン一覧が書いてあるだけ。

haskell-language-server.cabalを読むと、haskell-language-server-wrapperhaskell-language-serverというexecutableが定義されている。つまりhlsを起動するとこのexeが呼ばれている。

ararkarark

haskell-language-server-wrapperの実装はexe/Wrapper.hsにある。main関数を読んでくとコマンドライン引数のパースとかロガーの設定とかやってる。launchHaskellLanguageServerという関数で実際の起動をやっている。

launchHaskellLanguageServerではプロジェクトで使ってるstackとかcabalの設定(これをCradleと呼んでいる)を読み込む。CradleからGHCのバージョンを特定して、それに対応したHLSのバイナリを探して起動する。

この後の処理はhaskell-language-serverに移る。

ararkarark

haskell-language-serverの実装はexe/Main.hsにある。main関数みてみるとなんとかRecorderみたいな処理がいっぱいあるが、これは要するにloggerの抽象。Priorityというログの詳細度フラグ?みたいなものにしたがって、対応したloggerを作ってる。個人的にそういうのはReaderモナドでやらんかったんかと思わなくもないが、何か理由があるんだろう。つまりrecorderがなんちゃらみたいな処理は全部飛ばしていい。

実際にHLSを起動してるのはdefaultMain関数になる。これはロガーと動作モードとプラグイン一式をうけとって、HLSを起動する。
動作モード(型としてはArguments)について補足する。HLSはバージョンの表示とか、設定したCradleとか、VsCodeのスキーマとかを生成することもできるので、その動作の切り替えを指定する。

プラグインはビルド時に指定する感じで、プラグインの実態はsrc/HlsPlugins.hsidePlugins関数で実装している。idePluginsはビルドオプションで読みづらいが要するに長い長いプラグインのリストになっている。

ちなみにHLS自体はコア機能だけを提供していて、実際のlspの機能はPluginとして開発されている。なので自分で機能開発する場合は実際はPluginの開発をすることになる。

ararkarark

defaultMainを読んでいく。場所はsrc/Ide/Main.hs。HLSを起動するのはArgumentsGhcideのとき。runLspModeでHLSを起動する。

runLspModeはやはりロガーと設定とプラグイン一覧を受け取る。ここではロガーの設定(何回やるんだよ)とはCWDの変更とかをやるだけで、HLSの実態はIDEMain.defaultMainに移る。

ararkarark

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を走らせるやつらしい。

ararkarark

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の起動が完了する。

ararkarark

ということで、HLSの実態は結局Language.LSP.Serverだということがわかった。

HLSが提供するプラグインは巡り巡ってrunLanguageServer の最後の引数のsetupに含まれている。これをいい感じにさらに変形してLanguage.LSP.Server.runServerWithHandlesに渡している。

これ以上はLSPの仕様とLanguage.LSPを真面目に理解してないと読み進められなさそうなので一旦お休み。

ararkarark

Pluginの実装を見ていく。

プラグインはPluginDescriptor IdeStateという型で表される。

基本的にPluginはdescriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeStateという関数を公開して、HlsPluginからPluginIdをもらう。

PluginDescriptorはhls-plugin-apiで定義されていて、プラグインがどうやってlspを実装するのかが定義されている。