Open14

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を実装するのかが定義されている。

ararkarark

1年ぶりくらいに解読を再開してみる。lspパッケージを解読する。
lspパッケージはここ数か月で結構破壊的変更が入っているようで、参照するバージョンに注意。ここでは2.3.0.0をみる。
lspでLSを起動する関数はrunServer :: forall config. ServerDefinition config -> IO Intで、このServerDefinition config引数でLSPとしての動作を定義する。config型が型パラメータになっているのは、開発者がサーバーの設定というか状態を自由に定義できるから。Stateモナドの状態的な。とりあえず最初は()でよい。

ararkarark

ServerDefinitionには色々プロパティがあるが、よくイメージされるようなLSPのRequestとResponseをハンドリングする処理を定義するのはstaticHandlers :: ClientCapabilities -> Handlers mぽい。これはそのままClient Capabilitiesを受け取って、初期化?されたLSPのハンドリング処理群Handlers mを返す。

Handlers mの作り方は結構複雑。
まずHandlersのコンストラクタをそのまま使ってはいけない。requestHandlernotificationHandlerで作ったHandlers mmconcatで合成していく感じになる。

ararkarark

requestHandlerの使い方を見る。型定義は以下のようになっている。

requestHandler :: forall (m :: Method ClientToServer Request) f. SMethod m -> Handler f m -> Handlers f

型引数のm :: Method ClientToServer RequestはHoverとかGotoとかのLSPで定義されたメソッドを型レベルで区別するもの。第一引数のSMethod mも似た感じで、LSPメソッドの種類を実引数として与えてる。幽霊型みたいなことがやりたいのかな?PureScriptだったらvisible type applicationで指定できるんだけどな。

で、第二引数のHandler f mがハンドラ定義の実態になるんだけど、Handler f mはType Familyなので型から型への対応を表す。具体的にはmのなかに型レベルでRequestかNothificationかの情報が入っているので、それを見てハンドラの型を分岐させている。

RequestならハンドラはTRequestMessage m -> (Either ResponseError (MessageResult m) -> f ()) -> f ()になる。第一引数のTRequestMessage mはパースされたLSPのリクエストメッセージ。第一引数にLSPで定義されたparamsとかが入ってる。第二引数はresponderと呼ばれているやつで、このresponderにLSとしての応答を表すEither ResponseError (MessageResult m)を渡すことで実際の応答を行ってくれる。

NothificationならハンドラはTNotificationMessage m -> f ()になる。Notificationなので応答することはないためこれだけになる。

クライアントからサーバーへのNotificationにはファイルのリネームとかプログレスバーでのキャンセル押下があるらしい。Methodにこういった型が全部定義されているのがうれしい。

ararkarark

fとかmなんなのか問題

さっきからHandlers mmとかHandler f mfがやたら出てきたが、実際これはなんなのかみていく。以降この謎の型を”謎型”とよぶことにする。
”謎型”はHandlers mからHandler f mfへと渡っている。非常にわかりづらいが、Handlers -> Handlerの過程で、型名がm -> fにつけかわっている。Handler f mfが謎型で、mは多分Methodのmだと思う。

Handlerで定義された謎型のカインドをみると、Type -> Typeになっている。これはモナドと同じカインドになる。

結局謎型が定義されているのはServerDefinitionなのでそこまでもどる。謎型はmとしてServerDefinitionの

onConfigChange :: config -> m ()
staticHandlers :: ClientCapabilities -> Handlers m
interpretHandler :: a -> m <~> IO

に現れる。interpretHandlerの <~>は同型(isomorphic)を表すらしい。つまり謎型mはIOと同型な型になる。

interpretHandlerのドキュメントによると、このパッケージではIOと同型な任意のモナドでサーバーを走らせることができるらしく、

interpretHandler = env -> Iso
    (runLspT env) -- how to convert from IO ~> m
    liftIO        -- how to convert from m ~> IO

とすることが想定されている。このようにするとIOと同型なモナドmとして

type LspT a = ReaderT (LanguageContextEnv config) IO a

が使用される。