(今のところ前後編に分ける予定ですが、追記したり構成が変更になったりするかもしれません。予定は未定。)
2016年6月に、Microsoftがlanguage server protocolという仕様を公開しました。
本稿では、このlanguage server protocolの存在意義や具体的な実現方法について解説します。
language server protocolの存在意義
language serverとは、IDEが必要とするプログラムのプロジェクト ソースを解析して情報を提供する機能を、サービスとして実現するものです。language serverがサポートされたIDEでは、型やメンバーの自動補完、変数やメンバーの定義参照、変数やメンバーの利用箇所の検索、コードの自動フォーマット、コードのエラー分析や修正案の提示といった、さまざまな機能を実現できます。
Microsoftの技術で言えば、Roslynが最も近い存在であると言えます。あるいは、TypeScriptは、最初からlanguage serverの実現を意図しながら開発された言語でした。
language server protocolとは、このlanguage serverとそのクライアントを接続する仕組みを、プロトコルとして規定したものです。サーバーの実体は主にプログラミング言語環境、クライアントの実体は主にテキストエディタやIDEということになります。プロトコルはJSON-RPCで規定されています。(これが2000年代だったらSOAPで規定されていたかもしれませんね…!)
今回Microsoftがこのlanguage server protocolで提案したのは、language serverの機能というものは、プログラミング言語のいずれかを問わず、コンパイラー インフラストラクチャーの類にある程度は共通しているので、それを取り決めてしまおう、というものです。これが実現すれば、どのIDEやテキストエディタでも、この仕様で定めるプロトコルのクライアントを実装すれば、その接続先にあるサーバーがいかなる言語を取り扱っていても処理できるようになります。逆に、どのプログラミング言語でも、この仕様が定めるプロトコルのサーバーを実装すれば、どんなクライアントが接続してきても、画一的に対応できるようになります。
Microsoftの中でこのイニシアティブの原動力となったのは、Visual Studio Codeです。VSCodeは本質的にはelectronを使用した「単なるエディタ」を拡張する(アドイン・エクステンションを取り込む)ことで実現していくスタイルのIDEです。
(本稿におけるテキストエディタとは、単なるメモ帳のようなものではなく、拡張機能のサポートを前提としたものであり、この意味では、IDEとはテキストエディタにプロジェクトモデルやコンパイラー ツールチェインを統合したものであると考えることができます。以降も、「(テキスト)エディタ」と「IDE」という語句を使って解説していきますが、本稿においては、これらの間に本質的な違いはありません。)
通常、IDEのアドイン開発は、それぞれのIDEの内部構造に密結合する、strongly-typedな機構に基づいています。MEFを活用したVisual Studio SDK、Mono.Addinsを活用したMonoDevelop Addin、Eclipse Platform、
NetBeans Platformといった仕組みでもあり、emacs, vim, atom, vscodeといったエディタでも、それぞれ独自のプラグイン機構が存在しています。これらにおいて、サポートされていない言語環境を新たにサポートすることになった場合は、それぞれの環境に合わせたアドインを、それぞれのエディタのユーザーが開発します。すなわち、エディタの数だけ、言語の数だけ、それらを繋ぐアドインの開発が必要になっていたということです。
vscodeに限らず、IDEでエクステンション開発を促進するには、その基盤が確立されていることが重要です。前述の通り、通常、IDEのアドイン機構は強い型付けに基づいており、IDE別のAPIを使用して実装します。そして、これはIDEに限った話ではありませんが、アドイン機構で安定したAPIを提供するというのは、なかなか実現しない夢のような目標です。自分が使っていたFirefoxのアドインやChrome拡張が、ブラウザをバージョンアップしたら機能しなくなった、という経験のある人は多いのではないでしょうか。そして、APIが安定しているということは、むしろ発展が止まったということでもあります。
また、IDEごとに実装言語が異なるため、複数のIDEをひとつの言語でサポートするというのは現実的ではありません。
このような場面では、強い型付けによるAPIに基づいてアドインを実装するより、実装言語を問わずに実現できるプロトコルとして規定する、というのは、かしこい選択であると言えるでしょう。
language server protocolの内容
language server protocolは具体的に定義されたプロトコルです。
このプロトコルの内容が大まかに分かると、このプロトコルを実装したエディタや言語環境でどんな機能が実現できるのか、が分かるようになるでしょう。また、自分が普段使っているIDEにどんな機能があるのか(あるいはどんなことが出来る可能性があるのか)を知る、いい機会となるでしょう。
これはmarkdownで1600行にもわたる長大なドキュメントですが、大半はコードによる例示であり、実際に読むべきところは多くありません。前半部分はJSONメッセージの構造を説明しており、これだけを読んでも何ら得られるものはありません。後半になると、具体的なリクエストとレスポンスを規定する内容になっており、これらを俯瞰すれば、このプロトコルで何が実現できるか見えてきます。"Actual Protocol"というセクション以降が主な内容です。
(本稿では、2016年6月に公開された「バージョン2.0」に基づいて解説しています。これより後のバージョンでは、説明が当てはまらない部分もあるかもしれません。)
このプロトコルのメッセージには、クライアントからリクエストを送ってレスポンスを受け取るタイプのものと、サーバーからメッセージを通知のかたちで送るタイプのものがあります。
クライアントから送信するリクエストの中には、解析対象ソースコードの追加やエディタ上で編集内容を送る、といった、language serverが機能するために必要だけど面白みのないものと、自動補完や定義参照などlanguage serverと呼ぶにふさわしい機能を実現するものとがあります。ここでは面白みのある部分を取り上げて眺めてみましょう。language server protocolでは、以下のようなリクエストが定義されています(JSON-RPCにおけるmethodの名前を取り上げます)。
リクエスト | 説明 |
---|---|
textDocument/completion | 自動補完 |
completionItem/resolve | 自動補完候補の選択 |
textDocument/hover | ヒント(ホバー)の表示 |
textDocument/signatureHelp | メンバー定義(signature)候補の表示 |
textDocument/definition | シンボルの定義の位置を取得 |
textDocument/references | シンボルの利用(参照)位置のリスト取得 |
textDocument/documentHighlight | ハイライト対象シンボルの利用(参照)のリスト取得 |
textDocument/documentSymbol | ドキュメント中で定義されている全シンボルの取得 |
workspace/symbol | ワークスペース全体からクエリ条件に合致するシンボルの取得 |
textDocument/codeAction | コード アクションのリストの取得 |
textDocument/codeLens | code lensのリストの取得 |
codeLens/resolve | code lensの処理の実行 |
textDocument/formatting | ドキュメントの整形 |
textDocument/rangeFormatting | 選択範囲の整形 |
textDocument/onTypeFormatting | タイプ時の整形 |
textDocument/rename | 識別子の変更 |
以下、それぞれのリクエストについて、少しずつ補足します。(各種機能の具体例としてスクリーンショットをいくつか挙げますが、このlanguage server protocolを実装したものには限定しません。)
コーディング支援
自動補完は、典型的には . (dot)などをタイプした時に、その左辺オブジェクトのメンバーの一覧を表示する機能です。textDocument/completionが補完候補を表示するために送信するリクエスト、completionItem/resolveが補完候補を(カーソルキーなどでリストなどから)選択している時に送信するリクエストです。
テキストエディタは、ユーザー入力の1文字1文字について、completionリクエストを送信したりすることはありません(completionの処理はそれほど軽いものではなく、必要が無い場面で呼ぶべきものではありません)。completionのトリガーとなる文字は、(まだ説明していない)initializeリクエストのレスポンスの中で、サーバーから提示されます。クライアントとなるエディタは、その結果に基づいて、エディタ上のキー入力がトリガー文字であったらcompletionを送る、という処理を行えば良いことになります。
(自動補完をトリガーする文字はlanguage server側が指定しますが、クライアントがそれに従ってリクエストを送信するかどうかは分かりません。VSCodeでは、ユーザー設定によって、トリガー文字による自動補完を無効にするオプションがあるので、それが設定されていたら、リクエストは送信されないことになるでしょう。)
completionItem/resolveについては仕様書に補足説明があり、textDocument/completionがリストを生成している時は、各項目の詳細情報(documentationプロパティなど)はスキップされるかもしれないが、その後エディタ上で選択肢をカーソル等で選んでいる時には、resolveを要求することで、その詳細情報を受け取って表示することができる、という使用例が挙げられています。
textDocument/signatureHelpの典型的な用途は、メソッド呼び出しの引数を入力する場面です。ドキュメント上の現在のカーソル位置にある、ひとつのメソッド名について、複数のオーバーロードがある場合には、それらが全て言語サーバーから返されて、IDE上でカーソルキーなどで選択できるようなUIが多いでしょう。そして、現在の引数が何番目のものかという情報も、併せて返されることになります。
ドキュメント構造の提示
textDocument/documentSymbolは、そのドキュメント上で定義されているシンボルを列挙するもので、その典型的な用途は、エディタの上部などにメンバーを選択して移動できるドロップダウンリストです。
別のアプローチとしては、ソースコードのドキュメント構造の表示にも活用できます。
コードの検索
textDocument/referencesは、現在カーソルが指しているシンボルが使用されている、他の場所のリストを返します。find usagesと言えば意味が分かるかもしれません。一般的には検索結果リストの表示というかたちでユーザーが利用します。
textDocument/documentHighlightは、同じシンボルの使用箇所をエディタ上でハイライトする場合にも使用できます。
referencesとdocumentHighlightの目的はほぼ同じなのですが、language server protocolの仕様では、documentHighlightはもっと曖昧な検索結果もハイライティング対象として返すことがあると想定されています。
textDocument/documentSymbolと似たような目的に見えるworkspace/symbolは、実際の用途は全く異なり、プロジェクト ワークスペース全体に対する検索結果を返すもので、これは検索キーワードを指定することが想定されています。モダンなIDEには、グローバル検索ボックスが付いていて(MacOSのSpotlightのようなものを想像すると良いでしょう)、そこからファイル名、型名、メンバー名など、検索条件に該当すれば何でも探して移動することが出来るものが多く、このリクエストはそのような検索機能を実現するときに必要になります。
コード アクションとCode Lens
textDocument/codeActionは、現在のエディタ上のカーソル位置をサーバーに送信して、実行できる補助アクションのリストをレスポンスとして受け取るものです。典型的な実現方法は、カーソル付近にヒントのアイコンを表示して、クリックするとドロップダウンリストでアクションを表示したり(Visual Studioスタイル)。あるいは、コンテキストメニューからコード アクションのサブメニューを表示して、そこからドロップダウンリストで選択してもらったりする(MonoDevelopスタイル)といったものでしょう。
Code Lensという機能(この名前は具体性を欠き、一般的に通用するものではないので、筆者は適切ではないと考えていますが)は、Visual Studioに由来するもので、あるコードドキュメントを渡すと、その中に含まれる型やメンバーの定義などのそれぞれについて、参照数などの統計的なデータを表示したり、テスト実行などのコマンドが適用できる場合にはその情報を渡したりといった、包括的な情報(Lens)を形成して、そのリストを返したりするものです。
このCode Lensのそれぞれについて、さらにcodeLens/resolveを実行すると、サーバーがその引数lensの内容に基づいて、所定の処理を実行することになります。
コード アクションとCode Lensは、目的はほぼ重複しそうですが、コード アクションがコンテキスト位置に依存するのに対して、Code Lensはドキュメント上に情報を表示するためにも用いられれば、そこからアクションを実行するためにも用いられる、といった実用上の違いがあると言えます。
ドキュメントとワークスペースの管理
ここまで説明してきたリクエストは、プログラムのソースを分析した結果や、それに対する変更の要求など、language server「らしい」機能が中心でした。この節で説明するのは、それらの機能を可能にするためにエディタが行う必要がある、地味なメッセージのやり取りです。たとえば、ソースを分析するためには、そのソースファイルを解析対象としてサーバーに登録しなければなりません。
それらの処理を不足なく行うために、このlanguage server protocolでは、以下のリクエストや通知(サーバーからクライアントに送られるメッセージ)が定義されています。
リクエスト | 説明 |
---|---|
workspace/didChangeConfiguration | ワークスペースの設定変更 |
textDocument/didOpen | ドキュメントを開いたという通知 |
textDocument/didChange | ドキュメントの内容を変更したという通知 |
textDocument/didClose | ドキュメントを閉じたという通知 |
textDocument/didSave | ドキュメントを保存したという通知 |
workspace/didChangeWatchedFiles | 監視していたファイルへの変更を検出したという通知 |
textDocument/publishDiagnostics | ドキュメントに対する検証処理の結果通知 |
publishDiagnosticsがサーバーからクライアントへの通知というスタイルになっているのは、検証処理に時間がかかる可能性を考慮して(実際ソース編集中に自動的に行われるエラーチェックは時間がかかるでしょう)、完了したらクライアントに通知するようにするためでしょう。
その他の一般的なリクエストと通知
最後に落ち穂拾いとして簡潔に一覧にしますが、このlanguage server protocolでは、キャンセル処理や汎用的なメッセージ表示など、一般的なリクエストや通知も規定されています。
リクエスト | 説明 |
---|---|
$/cancelRequest | リクエストのキャンセル |
exit | 終了 |
window/showMessage | メッセージ表示の要求 |
window/showMessageRequest | 応答要求を含むメッセージ表示の要求 |
window/logMessage | メッセージのログ記録要求 |
telemetry/event | 各種telemetryイベントの発生 |
telemetryというのは各種の使用統計に使える情報として送信されるもので、内容の具体的なフォーマットは規定されていません。
language server protocolの実装
さて、ここまで説明してきたことの振り返りになりますが、language server protocolは、任意のIDEやエディタから、任意の言語をサポートできるようにするための仕組みであり、JSON-RPCに基づくプロトコルを実装できる環境であれば、開発言語は何でも実現可能です。公式リポジトリのwikiでは、既知の実装のリストが公開されています。
ここには、(2016年8月時点で)実装を3カテゴリーに分けて紹介しています。
- 言語サーバー: C++、 C#、JSON、CSS/LESS/SASSなど、既にいくつかの言語サポートがリストアップされています。注意すべきは、これらは、VSCodeでサポートされている言語のリストではないということです。VSCodeでサポートされている言語には、このlanguage server protocolの成立以前に実現していた言語サポートがいくつか存在しています。分かりやすい例がTypeScriptやGoです。これらは、VSCodeではlanguage server protocolを経由するよりも直接的に、それぞれの実装APIを使用してサポートされています。
- エディタ/IDE: 言語クライアントがリストアップされています。
- SDK: このプロトコルを実装した各種言語のライブラリが列挙されています。上記2カテゴリーのいずれかで使用されているものと考えてよいでしょう。vscodeの名前で公開されているリポジトリも、vscodeのAPIに
依存せずに実装されている部分は、vscode以外でも利用できるでしょう。
language service clientの実装例: MonoDevelop
Microsoftがlanguage-server-protocolの仕様を提案・公開したのを受けて、dotnetconf 2016では、秋頃を目指してXamarin Studioに(すなわちmonodevelopに)この仕様をサポートするべく開発していくという計画が発表されました。
2016年7月の時点で、MonoDevelopハッカーの1人が、このlanguage server protocolを試験的に利用して、vscodeが利用しているJSONのlanguage serverに接続するかたちで、JSON編集の機能を強化するアドインを公開しています。
https://github.com/mrward/monodevelop-json-addin
これは内部的にnode.jsを利用するもので、node.jsのインストールされていない環境では動作しません。vscodeがインストールされている必要はありません。実行に必要なJavaScriptコードはアドイン パッケージにバンドルされています。
このアドインは、vscode-languageserver-nodeのTypeScriptソースをJavaScriptにコンパイルして、MonoDevelopのアドイン拡張点として定義されたMonoDevelop.JsonBinding.JsonTextEditorExtensionオブジェクトの初期化時に、language serverのクライアントであるMonoDevelop.LanguageServices.LanguageServiceClientクラスのオブジェクトを生成し、nodeで実装されたJSONのlanguage serverを立ち上げます。サーバーはnodeアプリケーションですが、クライアントはJSON-RPCをC#で(自前で)実装しています。アドイン全体として見れば、クライアントとサーバーの両方を含んでいることになりますが、サーバーとクライアントの実装は全く異なるものです。(1つのアドインがクライアントとサーバーの両方を含むというのは、vscodeのサンプルにも共通している、典型的なlanguage serverの設計なのですが、それについてはvscodeのサンプルを解説する際に改めて見ていきます。)
Microsoftはvscode-languageserver-nodeのC#実装を開発中であると表明していますが、language server clientについては、MonoDevelopはこのアドインで既に実装していることになります。コードの名前空間もMonoDevelop.LanguageServicesとなっており、いずれMonoDevelop本体に組み込む前提で開発していると考えても良いでしょう。
参考実装: omnisharp-vscode
vscodeでlanguage server protocolを使用している言語はいくつか存在します。ただし、その機能をフルに提供している言語は多くありません。typescriptなどは、ここで列挙されている機能の大半をカバーしていますが、(前述の通り)typescriptのエクステンションはこのプロトコルの登場以前から存在しており、これを使用していないのです。
そのような現状で、おそらくこのプロトコルを最大限活用していると言えるのは、C#サポートのエクステンションに使用されているomnisharp-vscodeではないでしょうか。参考までに、code lensを実装しているコードを紹介しておきます。
このコードの中では、出現した型とメンバーについて、quick fixアクションの追加と、テスト実行のアクションの追加、参照回数の取得と表示が実装されています。
なお、同じように複雑かつ高度な機能を実現する可能性のあるC++サポートのエクステンションは、(なぜか)オープンソースで公開されていないので、参考にはなりませんでした。
(後編に続く - vscodeでの実装や本プロトコルの批判的検討などを予定)