言語サーバー プロトコル

言語サーバー プロトコルとは

ソースコード オートコンプリートやエディターまたは IDE 内のプログラミング言語での [定義へ移動] など、豊富な編集機能をサポートすることは、通常、非常に困難で時間のかかる作業です。 一般に、エディターまたは IDE のプログラミング言語でドメイン モデル (スキャナー、パーサー、型チェッカー、ビルダーなど) を記述する必要があります。 たとえば、Eclipse IDE 自体が Java で記述されているため、Eclipse IDE で C/C++ のサポートを提供する Eclipse CDT プラグインは Java で記述されます。 このアプローチに従うと、TypeScript for Visual Studio Code で C/C++ ドメイン モデルを実装し、C# for Visual Studio で別のドメイン モデルを実装することになります。

開発ツールで既存の言語固有のライブラリを再利用できる場合、言語固有のドメイン モデルの作成がはるかに簡単になります。 ただし、これらのライブラリは通常プログラミング言語自体で実装されます (たとえば、適切な C/C++ ドメイン モデルは C/C++ で実装されます)。 TypeScript で記述されたエディターに C/C++ ライブラリを統合することは、技術的には可能ですが、実行するのは困難です。

言語サーバー

別のアプローチは、ライブラリを独自のプロセスで実行し、プロセス間通信を使用して通信する方法です。 送受信されるメッセージによってプロトコルが形成されます。 言語サーバー プロトコル (LSP) は、開発ツールと言語サーバー プロセスの間で交換されるメッセージを標準化するための製品です。 言語サーバーまたはデーモンを使用することは、新たなアイデアではありません。 Vim や Emacs などのエディターは、セマンティック オートコンプリート サポートを提供するために、しばらく前からこれを行っています。 LSP の目標は、これらの種類の統合を簡略化し、さまざまなツールに言語機能を公開するための便利なフレームワークを提供することでした。

共通のプロトコルがあることで、言語のドメイン モデルの既存の実装を再利用することにより、面倒な作業を最小限に抑えてプログラミング言語の機能を開発ツールに統合できます。 言語サーバー バックエンドは PHP、Python、または Java で記述でき、LSP を使用すると、さまざまなツールに簡単に統合できます。 プロトコルは共通の抽象化レベルで機能します。これにより、ツールでは、基になるドメイン モデルに固有の微妙な差異を完全に理解する必要なく、豊富な言語サービスを提供できます。

LSP での作業の開始方法

LSP は時間の経過と共に進化し、現在はバージョン 3.0 になっています。 C# 向けの豊富な編集機能を提供するために OmniSharp によって言語サーバーの概念が取り入れられたときに始まりました。 当初、OmniSharp では JSON ペイロードを含む HTTP プロトコルを使用され、Visual Studio Code などの複数のエディターに統合されてきました。

同時期に、Microsoft では、Emacs や Sublime Text などのエディターで TypeScript をサポートすることを念頭に置いて、TypeScript 言語サーバーに取り組み始めました。 この実装では、エディターは TypeScript サーバー プロセスと stdin/stdout を介して通信し、要求と応答には V8 デバッガー プロトコルから着想を得た JSON ペイロードを使用します。 Typescript サーバーは、豊富な TypeScript 編集のために Typescript Sublime プラグインおよび VS Code に統合されています。

2 つの異なる言語サーバーを統合した後、VS Code チームは、エディターと IDE 用の共通言語サーバー プロトコルの調査を開始しました。 共通プロトコルを使用すると、言語プロバイダーは、異なる IDE で使用できる単一の言語サーバーを作成できます。 言語サーバー コンシューマーは、プロトコルのクライアント側を 1 回だけ実装する必要があります。 その結果、言語プロバイダーと言語コンシューマーの両方にとってメリットが生まれます。

言語サーバー プロトコルは、TypeScript サーバーによって使用されるプロトコルから始まり、VS Code 言語 API から着想を得たその他の言語機能を使用して拡張されました。 プロトコルは、シンプルさと既存のライブラリにより、リモート呼び出し用の JSON-RPC で支持されています。

VS Code チームは、ファイルをリンティング (スキャン) する要求に応答し、検出された警告とエラーのセットを返す複数のリンター言語サーバーを実装することによって、プロトコルのプロトタイプを作成しました。 目標は、ユーザーがドキュメントで編集を行うときにファイルをリンティングすることでした。つまり、エディター セッション中に多数のリンティングが行われることになります。 サーバーの稼働を維持して、ユーザーが編集するたびに新しいリンティング プロセスを開始する必要がないようにすることは理にかなっています。 VS Code の ESLint および TSLint 拡張機能など、いくつかのリンター サーバーが実装されました。 これら 2 つのリンター サーバーは両方とも TypeScript/JavaScript に実装され、Node.js で実行されます。 プロトコルのクライアントおよびサーバー部分を実装するライブラリを共有します。

LSP の動作

言語サーバーは独自のプロセスで実行され、Visual Studio や VS Code などのツールは、言語プロトコルを使用して JSON-RPC を介してサーバーと通信します。 専用のプロセスで動作する言語サーバーのもう 1 つの利点は、単一プロセス モデルに関連するパフォーマンスの問題が回避されることです。 クライアントとサーバーの両方が Node.js で記述されている場合、実際のトランスポート チャネルには、stdio、ソケット、名前付きパイプ、またはノード ipc のいずれかを指定できます。

次に、定型的な編集セッション中にツールと言語サーバーが通信する方法の例を示します。

lsp flow diagram

  • ユーザーがツールでファイル (ドキュメントと呼ばれます) を開く: ツールでは、ドキュメントが開いていることを言語サーバーに通知します ('textdocument/didOpen')。 この時点から、ドキュメントの内容についての正確な情報はファイル システムに存在しなくなりますが、ツールによってメモリに保持されます。

  • ユーザーが編集を行う: ツールでは、ドキュメントの変更 ('textdocument/didChange') をサーバーに通知し、プログラムのセマンティック情報が言語サーバーによって更新されます。 このような状況が発生すると、言語サーバーではこの情報を分析し、検出されたエラーと警告をツールに通知します ('textDocument/publishDiagnostics')。

  • ユーザーがエディター内のシンボルに対して [定義へ移動] を実行する: ツールでは、次の 2 つのパラメーターを持つ 'textdocument/Definition' 要求を送信します。(1) ドキュメント URI、(2) サーバーに対して [定義へ移動] 要求が開始されたテキスト位置。 サーバーでは、ドキュメント 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 ファイルで作業するセッションを示します。

java and sass

機能

すべての言語サーバーで、プロトコルによって定義されたすべての機能がサポートされるとは限りません。 そのため、クライアントとサーバーは、サポートされている機能セットを 'capabilities' を通じて公表します。 たとえば、サーバーは 'textDocument/definition' 要求を処理できることを公表しますが、'workspace/symbol' 要求を処理できない可能性があります。 同様に、クライアントは、ドキュメントが保存される前に 'about to save' 通知を提供できると公表できます。これにより、サーバーはテキスト編集を計算して、編集済みのドキュメントを自動的に書式設定できます。

言語サーバーの統合

言語サーバーと特定のツールとの実際の統合は、言語サーバー プロトコルによって定義されず、ツールの実装者に任されています。 一部のツールでは、任意の種類の言語サーバーを起動して通信できる拡張機能を搭載することによって、言語サーバーを汎用的に統合します。 VS Code のように、言語サーバーごとにカスタム拡張機能を作成するツールもあります。これにより、拡張機能でもいくつかのカスタム言語機能を提供できます。

言語サーバーとクライアントの実装を簡略化するために、クライアントおよびサーバー パーツ用のライブラリまたは SDK があります。 これらのライブラリは、さまざまな言語で提供されています。 たとえば、言語サーバーと VS Code 拡張機能との統合を容易にする言語クライアント npm モジュールや、Node.js を使用して言語サーバーを記述するための別の言語サーバー npm モジュールがあります。 これはサポート ライブラリの現在の一覧です。

Visual Studio での言語サーバー プロトコルの使用