言語サーバー プロトコルとは
エディターや IDE でソース コードのオートコンプリートやプログラミング言語の 定義に移動 などの豊富な編集機能をサポートすることは、従来は非常に困難で時間がかかります。 通常、エディターまたは IDE のプログラミング言語でドメイン モデル (スキャナー、パーサー、タイプ チェッカー、ビルダーなど) を記述する必要があります。 たとえば、Eclipse IDE で C/C++ をサポートする Eclipse CDT プラグインは、Eclipse IDE 自体が Java で記述されているため、Java で記述されます。 このアプローチに従うと、TypeScript for Visual Studio Code で C/C++ ドメイン モデルを実装し、C# for Visual Studio で別のドメイン モデルを実装することになります。
また、開発ツールで既存の言語固有のライブラリを再利用できる場合は、言語固有のドメイン モデルを作成する方がはるかに簡単です。 ただし、これらのライブラリは通常、プログラミング言語自体で実装されます (たとえば、C/C++ ドメイン モデルは C/C++ で実装されます)。 C/C++ ライブラリを TypeScript で記述されたエディターに統合することは技術的には可能ですが、行うのは困難です。
言語サーバー
もう 1 つの方法は、ライブラリを独自のプロセスで実行し、プロセス間通信を使用してライブラリと通信することです。 送受信されるメッセージはプロトコルを形成します。 言語サーバー プロトコル (LSP) は、開発ツールと言語サーバー プロセスの間で交換されるメッセージを標準化する製品です。 言語サーバーや悪魔を使用することは、新しいアイデアや新しいアイデアではありません。 Vim や Emacs などのエディターは、セマンティックオートコンプリートのサポートを提供するためにしばらくの間これを行ってきました。 LSP の目的は、このような統合を簡素化し、言語機能をさまざまなツールに公開するための便利なフレームワークを提供することでした。
共通のプロトコルを使用すると、言語のドメイン モデルの既存の実装を再利用することで、プログラミング言語機能を最小限の手間で開発ツールに統合できます。 言語サーバーバックエンドは PHP、Python、または Java で記述でき、LSP を使用すると、さまざまなツールに簡単に統合できます。 このプロトコルは一般的な抽象化レベルで動作するため、ツールは、基になるドメイン モデルに固有の微妙な違いを完全に理解しなくても、豊富な言語サービスを提供できます。
LSP での作業の開始方法
LSP は時間の経過と同時に進化し、現在はバージョン 3.0 にあります。 言語サーバーの概念が OmniSharp によって取り上げられ、C# の豊富な編集機能が提供されたのが始まりです。 当初、OmniSharp は JSON ペイロードで HTTP プロトコルを使用し、 Visual Studio Code を含む複数のエディターに統合されています。
同じ頃、Microsoft は、Emacs や Sublime Text などのエディターで TypeScript をサポートするという考えで、TypeScript 言語サーバーで作業を開始しました。 この実装では、エディターは stdin/stdout を介して TypeScript サーバー プロセスと通信し、要求と応答に V8 デバッガー プロトコルから着想を得た JSON ペイロードを使用します。 TypeScript サーバーは、TypeScript Sublime プラグインと VS Code に統合され、豊富な TypeScript 編集が可能です。
2 つの異なる言語サーバーを統合した後、VS Code チームはエディターと IDE 用の共通言語サーバー プロトコルの調査を開始しました。 共通プロトコルを使用すると、言語プロバイダーは、異なる IDE で使用できる単一の言語サーバーを作成できます。 言語サーバー コンシューマーは、プロトコルのクライアント側を 1 回実装するだけで済みます。 これにより、言語プロバイダーと言語コンシューマーの両方に win-win の状況が発生します。
言語サーバー プロトコルは、TypeScript サーバーで使用されるプロトコルで開始され、VS Code 言語 API から着想を得たより多くの言語機能で拡張されました。 プロトコルは、そのシンプルさと既存のライブラリのために、リモート呼び出し用の JSON-RPC でサポートされています。
VS Code チームは、ファイルの lint (スキャン) 要求に応答し、検出された一連の警告とエラーを返す複数のリンター言語サーバーを実装することで、プロトコルのプロトタイプを作成しました。 目標は、ユーザーがドキュメント内で編集する際にファイルをリントすることでした。つまり、エディター セッション中に多くの linting 要求が発生します。 ユーザーの編集ごとに新しいリンティング プロセスを開始する必要がないように、サーバーを稼働させ続けることは理にかなっています。 VS Code の ESLint 拡張機能や TSLint 拡張機能など、いくつかのリンター サーバーが実装されました。 これら 2 つのリンター サーバーはどちらも TypeScript/JavaScript で実装され、Node.jsで実行されます。 プロトコルのクライアントとサーバーの部分を実装するライブラリを共有します。
LSP のしくみ
言語サーバーは独自のプロセスで実行され、Visual Studio や VS Code などのツールは JSON-RPC 経由で言語プロトコルを使用してサーバーと通信します。 専用プロセスで動作する言語サーバーのもう 1 つの利点は、1 つのプロセス モデルに関連するパフォーマンスの問題が回避される点です。 クライアントとサーバーの両方が Node.jsで書き込まれる場合、実際のトランスポート チャネルは stdio、sockets、named pipes、または node ipc のいずれかになります。
ツールと言語サーバーが日常的な編集セッション中に通信する方法の例を次に示します。
ユーザーがツールでファイル (ドキュメントと呼ばれます) を開きます。ツールは、ドキュメントが開かれていることを言語サーバーに通知します ('textDocument/didOpen')。 今後は、ドキュメントの内容に関する真実はファイル システム上にありませんが、ツールによってメモリに保持されます。
ユーザーが編集を行う: ツールは、ドキュメントの変更 ('textDocument/didChange') についてサーバーに通知し、プログラムのセマンティック情報が言語サーバーによって更新されます。 その場合、言語サーバーはこの情報を分析し、検出されたエラーと警告 ('textDocument/publishDiagnostics') をツールに通知します。
ユーザーがエディター内のシンボルに対して "定義に移動" を実行します。ツールは、(1) ドキュメント URI と (2) 定義への移動要求が開始された場所からサーバーへのテキスト位置の 2 つのパラメーターを含む 'textDocument/definition' 要求を送信します。 サーバーは、ドキュメント URI と、ドキュメント内のシンボルの定義の位置で応答します。
ユーザーはドキュメント (ファイル) を閉じます。'textDocument/didClose' 通知がツールから送信され、ドキュメントがメモリ内になくなったことを言語サーバーに通知し、現在の内容がファイル システムで最新であることを通知します。
この例では、"定義に移動"、"すべての参照の検索" などのエディター機能のレベルで、プロトコルが言語サーバーと通信する方法を示します。 プロトコルで使用されるデータ型は、現在開いているテキスト ドキュメントやカーソルの位置などのエディターまたは IDE の "データ型" です。 データ型は、通常は抽象構文ツリーとコンパイラ シンボル (解決された型、名前空間、...) を提供するプログラミング言語ドメイン モデルのレベルではありません。これにより、プロトコルが大幅に簡略化されます。
次に、'textDocument/definition' 要求を詳しく見てみましょう。 C++ ドキュメントの "定義に移動" 要求のクライアント ツールと言語サーバーの間を移動するペイロードを次に示します。
これは要求です。
{
"jsonrpc": "2.0",
"id" : 1,
"method": "textDocument/definition",
"params": {
"textDocument": {
"uri": "file:///p%3A/mseng/VSCode/Playgrounds/cpp/use.cpp"
},
"position": {
"line": 3,
"character": 12
}
}
}
これは応答です。
{
"jsonrpc": "2.0",
"id": "1",
"result": {
"uri": "file:///p%3A/mseng/VSCode/Playgrounds/cpp/provide.cpp",
"range": {
"start": {
"line": 0,
"character": 4
},
"end": {
"line": 0,
"character": 11
}
}
}
}
振り返ると、プログラミング言語モデルのレベルではなくエディターのレベルでデータ型を記述することが、言語サーバー プロトコルが成功する理由の 1 つです。 さまざまなプログラミング言語で抽象構文ツリーとコンパイラ シンボルを標準化する場合と比較して、テキスト ドキュメント URI またはカーソル位置を標準化する方がはるかに簡単です。
ユーザーが異なる言語で作業している場合、VS Code は通常、プログラミング言語ごとに言語サーバーを起動します。 次の例は、ユーザーが Java ファイルと SASS ファイルで作業するセッションを示しています。
能力
すべての言語サーバーがプロトコルによって定義されているすべての機能をサポートできるわけではありません。 そのため、クライアントとサーバーは、それぞれのサポート機能セットを「ケイパビリティ」として発表します。 たとえば、サーバーは 'textDocument/definition' 要求を処理できることを読み上げ、'workspace/symbol' 要求を処理しない可能性があります。 同様に、クライアントは、ドキュメントが保存される前に "保存中" の通知を提供できることを通知できます。これにより、サーバーはテキスト編集を計算して、編集したドキュメントの書式を自動的に設定できます。
言語サーバーの統合
特定のツールへの言語サーバーの実際の統合は、言語サーバー プロトコルによって定義されておらず、ツールの実装者に任されています。 一部のツールは、あらゆる種類の言語サーバーを起動して通信できる拡張機能を備えることで、言語サーバーを一般的に統合します。 VS Code などの他のユーザーは、言語サーバーごとにカスタム拡張機能を作成して、拡張機能でいくつかのカスタム言語機能を引き続き提供できるようにします。
言語サーバーとクライアントの実装を簡略化するために、クライアントおよびサーバー パーツ用のライブラリまたは SDK があります。 これらのライブラリは、さまざまな言語で提供されています。 たとえば、言語サーバーの VS Code 拡張機能への統合を容易にする 言語クライアント npm モジュール と、Node.jsを使用して言語サーバーを記述する別 の言語サーバー npm モジュール があります。 これは、サポート ライブラリの現在の 一覧 です。
Visual Studio での言語サーバー プロトコルの使用
- 言語サーバー プロトコル拡張機能の追加 - 言語サーバーを Visual Studio に統合する方法について説明します。