言語サーバー プロトコル拡張機能を追加する
言語サーバー プロトコル (LSP) は、さまざまなコード エディターに言語サービス機能を提供するために使用される、JSON RPC v2.0 の形式の一般的なプロトコルです。 このプロトコルを使用すると、開発者は 1 つの言語サーバーを記述して、IntelliSense、エラー診断、すべての参照の検索などの言語サービス機能を、LSP をサポートするさまざまなコード エディターに提供できます。 従来、Visual Studio の言語サービスは、TextMate 文法ファイルを使用して構文の強調表示などの基本的な機能を提供するか、Visual Studio 拡張機能 API の完全なセットを使用してより豊富なデータを提供するカスタム言語サービスを記述することによって追加できます。 Visual Studio で LSP がサポートされている場合は、3 つ目のオプションがあります。
Visual Studio
可能な限り最適なユーザー エクスペリエンスを確保するには、多くの同じ操作のローカル処理を提供する 言語構成の実装も検討してください。そのため、LSP でサポートされている多くの言語固有のエディター操作のパフォーマンスを向上させることができます。
言語サーバー プロトコル
この記事では、LSP ベースの言語サーバーを使用する Visual Studio 拡張機能を作成する方法について説明します。 LSP ベースの言語サーバーを既に開発しており、Visual Studio に統合したいだけであることを前提としています。
Visual Studio 内でのサポートのために、言語サーバーは、次に例を示す任意のストリーム ベースの転送メカニズムを介してクライアント (Visual Studio) と通信できます。
- 標準の入力/出力ストリーム
- 名前付きパイプ
- ソケット (TCP のみ)
Visual Studio での LSP とそのサポートの目的は、Visual Studio 製品に含まれていない言語サービスをオンボードすることです。 Visual Studio で既存の言語サービス (C#など) を拡張するためのものではありません。 既存の言語を拡張するには、言語サービスの機能拡張ガイド (たとえば、"Roslyn" .NET コンパイラ プラットフォーム) を参照するか、「エディターと言語サービスを拡張する」を参照してください。
プロトコル自体の詳細については、こちらドキュメントを参照してください。
サンプル言語サーバーを作成する方法、または既存の言語サーバーを Visual Studio Code に統合する方法の詳細については、こちらドキュメントを参照してください。
言語サーバー プロトコルでサポートされている機能
次の表は、Visual Studio でサポートされている LSP 機能を示しています。
メッセージ | Visual Studio でサポートを受ける |
---|---|
初期化する | はい |
initialized | はい |
shutdown | はい |
exit | はい |
$/cancelRequest | はい |
ウィンドウ/メッセージを表示 | はい |
window/showMessageRequest | はい |
window/logMessage | はい |
telemetry/event | |
client/registerCapability | |
client/unregisterCapability | |
workspace/didChangeConfiguration | はい |
workspace/didChangeWatchedFiles | はい |
ワークスペース/シンボル | はい |
workspace/executeCommand | はい |
workspace/applyEdit | はい |
textDocument/publishDiagnostics | はい |
textDocument/didOpen | はい |
textDocument/didChange | はい |
textDocument/willSave | |
textDocument/willSaveWaitUntil | |
textDocument/didSave | はい |
textDocument/didClose | はい |
textDocument/completion | はい |
完了/解決 | はい |
textDocument/hover | はい |
textDocument/signatureHelp | はい |
テキストドキュメント/リファレンス (textDocument/references) | はい |
textDocument/documentHighlight | はい |
textDocument/documentSymbol | はい |
テキストドキュメント/書式設定 | はい |
textDocument/rangeFormatting | はい |
textDocument/onTypeFormatting | |
textDocument/definition | はい |
textDocument/codeAction | はい |
textDocument/codeLens | |
codeLens/resolve | |
textDocument/documentLink | |
documentLink/resolve | |
textDocument/rename | はい |
作業の開始
手記
Visual Studio 2017 バージョン 15.8 以降では、共通言語サーバー プロトコルのサポートが Visual Studio に組み込まれています。 プレビュー Language Server Client VSIX バージョンを使用して LSP 拡張機能をビルドした場合、バージョン 15.8 以降にアップグレードすると、機能が停止します。 LSP 拡張機能を再び動作させるためには、次の操作を行う必要があります。
Microsoft Visual Studio Language Server Protocol Preview VSIX をアンインストールします。
バージョン 15.8 以降では、Visual Studio でアップグレードを実行するたびに、プレビュー VSIX が自動的に検出され、削除されます。
Nuget 参照を、LSP パッケージのプレビュー版以外の最新バージョンに更新します。
VSIX マニフェストの Microsoft Visual Studio Language Server Protocol Preview VSIX への依存関係を削除します。
VSIX でインストール ターゲットの下限として Visual Studio 2017 バージョン 15.8 Preview 3 が指定されていることを確認します。
再構築して再デプロイします。
VSIX プロジェクトを作成する
LSP ベースの言語サーバーを使用して言語サービス拡張機能を作成するには、まず、VS のインスタンス用に Visual Studio 拡張機能開発 Workload がインストールされていることを確認します。
次に、ファイル>新しいプロジェクト>Visual C#>拡張>VSIX プロジェクトに移動して、新しい VSIX プロジェクトを作成します。
vsix プロジェクト
言語サーバーとランタイムのインストール
既定では、Visual Studio で LSP ベースの言語サーバーをサポートするために作成された拡張機能には、言語サーバー自体や実行に必要なランタイムは含まれません。 拡張機能開発者は、言語サーバーと必要なランタイムを配布する責任があります。 これを行うには、いくつかの方法があります。
- 言語サーバーは、コンテンツ ファイルとして VSIX に埋め込むことができます。
- MSI を作成して、言語サーバーや必要なランタイムをインストールします。
- Marketplace で、ランタイムと言語サーバーを取得する方法をユーザーに通知する手順を提供します。
TextMate 文法ファイル
LSP には、言語のテキストの色分け方法に関する仕様は含まれていません。 Visual Studio で言語のカスタム色付けを提供するために、拡張機能開発者は TextMate 文法ファイルを使用できます。 カスタム TextMate 文法またはテーマ ファイルを追加するには、次の手順に従います。
拡張機能内に "Grammars" という名前のフォルダーを作成します (または、任意の名前にすることができます)。
Grammars フォルダー内に、*.tmlanguage、*.plist、*.tmtheme、または *.json ファイルを追加し、カスタムカラー化を提供するものを含めます。
ヒント
.tmtheme ファイルは、スコープを Visual Studio 分類 (名前付きカラー キー) にマップする方法を定義します。 ガイダンスについては、%ProgramFiles(x86)%\Microsoft Visual Studio\<バージョン>\<SKU>\Common7\IDE\CommonExtensions\Microsoft\TextMate\Starterkit\Themesg ディレクトリのグローバル .tmtheme ファイルを参照できます。
.pkgdef ファイルを作成し、次のような行を追加します。
[$RootKey$\TextMate\Repositories] "MyLang"="$PackageFolder$\Grammars"
ファイルを右クリックし、[プロパティ] 選択します。 [ビルド] アクションを [コンテンツ] に変更し、[VSIX に含める] プロパティを [true] に変更します。
前の手順を完了すると、Grammars フォルダーがパッケージのインストール ディレクトリに 'MyLang' という名前のリポジトリ ソースとして追加されます ('MyLang' はあいまいさを解消するための名前であり、任意の一意の文字列にすることができます)。 このディレクトリ内のすべての文法 (.tmlanguage ファイル) とテーマ ファイル (.tmtheme ファイル) が候補として取得され、TextMate で提供される組み込みの文法よりも優先されます。 文法ファイルの宣言された拡張子が、開いているファイルの拡張子と一致する場合、TextMate はステップ インします。
単純な言語クライアントを作成する
メイン インターフェイス - ILanguageClient
VSIX プロジェクトを作成したら、次の NuGet パッケージをプロジェクトに追加します。
手記
前の手順を完了した後に NuGet パッケージに依存すると、Newtonsoft.Json パッケージと StreamJsonRpc パッケージもプロジェクトに追加されます。 拡張機能がを対象とする Visual Studio のバージョンに新しいバージョンがインストールされる場合を除き、これらのパッケージを更新しないでください。 アセンブリは VSIX に含まれません。代わりに、Visual Studio のインストール ディレクトリから取得されます。 ユーザーのコンピューターにインストールされているアセンブリよりも新しいバージョンのアセンブリを参照している場合、拡張機能は機能しません。
その後、ILanguageClient インターフェイスを実装する新しいクラスを作成できます。これは、LSP ベースの言語サーバーに接続する言語クライアントに必要なメイン インターフェイスです。
次に例を示します。
namespace MockLanguageExtension
{
[ContentType("bar")]
[Export(typeof(ILanguageClient))]
public class BarLanguageClient : ILanguageClient
{
public string Name => "Bar Language Extension";
public IEnumerable<string> ConfigurationSections => null;
public object InitializationOptions => null;
public IEnumerable<string> FilesToWatch => null;
public event AsyncEventHandler<EventArgs> StartAsync;
public event AsyncEventHandler<EventArgs> StopAsync;
public async Task<Connection> ActivateAsync(CancellationToken token)
{
await Task.Yield();
ProcessStartInfo info = new ProcessStartInfo();
info.FileName = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Server", @"MockLanguageServer.exe");
info.Arguments = "bar";
info.RedirectStandardInput = true;
info.RedirectStandardOutput = true;
info.UseShellExecute = false;
info.CreateNoWindow = true;
Process process = new Process();
process.StartInfo = info;
if (process.Start())
{
return new Connection(process.StandardOutput.BaseStream, process.StandardInput.BaseStream);
}
return null;
}
public async Task OnLoadedAsync()
{
await StartAsync.InvokeAsync(this, EventArgs.Empty);
}
public Task OnServerInitializeFailedAsync(Exception e)
{
return Task.CompletedTask;
}
public Task OnServerInitializedAsync()
{
return Task.CompletedTask;
}
}
}
実装する必要がある主なメソッドは、OnLoadedAsync と ActivateAsyncです。 OnLoadedAsync は、Visual Studio が拡張機能を読み込み、言語サーバーを起動する準備ができたときに呼び出されます。 このメソッドでは、StartAsync デリゲートをすぐに呼び出して、言語サーバーを起動する必要があることを通知するか、追加のロジックを実行して後で StartAsync 呼び出すことができます。 言語サーバーをアクティブにするには、ある時点で StartAsync を呼び出す必要があります。
ActivateAsync は、StartAsync デリゲートを呼び出すことによって最終的に呼び出されるメソッドです。 これには、言語サーバーを起動し、そのサーバーへの接続を確立するためのロジックが含まれています。 サーバーに書き込み、サーバーから読み取るためのストリームを含む接続オブジェクトを返す必要があります。 発生した例外はすべてキャッチされ、Visual Studio のインフォバー メッセージによってユーザーに表示されます。
アクティベーション
言語クライアント クラスを実装したら、Visual Studio に読み込んでアクティブ化する方法を定義するために、それに対して 2 つの属性を定義する必要があります。
[Export(typeof(ILanguageClient))]
[ContentType("bar")]
MEF
Visual Studio では、MEF (Managed Extensibility Framework) を使用してその機能拡張ポイントを管理します。 Export 属性は、このクラスを拡張ポイントとして取得し、適切なタイミングで読み込む必要があることを Visual Studio に示します。
MEF を使用するには、VSIX マニフェストで MEF を資産として定義する必要もあります。
VSIX マニフェスト デザイナーを開き、アセット タブに移動します。
MEF アセット
「新規」をクリックして、新しい資産を作成します。
MEF 資産
- 型: Microsoft.VisualStudio.MefComponent
- ソース: 現在のソリューション内のプロジェクト
- プロジェクト: [あなたのプロジェクト]
コンテンツ タイプの定義
現時点では、LSP ベースの言語サーバー拡張機能を読み込む唯一の方法は、ファイル コンテンツの種類です。 つまり、言語クライアント クラス (ILanguageClient 実装) を定義する場合は、開いたときに拡張機能が読み込まれるファイルの種類を定義する必要があります。 定義されたコンテンツ タイプに一致するファイルが開かなければ、拡張機能は読み込まれません。
これを行うには、1 つ以上の ContentTypeDefinition
クラスを定義します。
namespace MockLanguageExtension
{
public class BarContentDefinition
{
[Export]
[Name("bar")]
[BaseDefinition(CodeRemoteContentDefinition.CodeRemoteContentTypeName)]
internal static ContentTypeDefinition BarContentTypeDefinition;
[Export]
[FileExtension(".bar")]
[ContentType("bar")]
internal static FileExtensionToContentTypeDefinition BarFileExtensionDefinition;
}
}
前の例で、.bar ファイル拡張子で終わるファイルに対して、コンテンツ タイプ定義が作成されています。 コンテンツ タイプ定義には "bar" という名前が付けられ、CodeRemoteContentTypeNameから派生しなければなりません。
コンテンツ タイプ定義を追加した後、言語クライアント クラスで言語クライアント拡張機能を読み込むタイミングを定義できます。
[ContentType("bar")]
[Export(typeof(ILanguageClient))]
public class BarLanguageClient : ILanguageClient
{
}
LSP 言語サーバーのサポートを追加する場合、Visual Studio で独自のプロジェクト システムを実装する必要はありません。 お客様は、Visual Studio で 1 つのファイルまたはフォルダーを開いて、言語サービスの使用を開始できます。 実際、LSP 言語サーバーのサポートは、開いているフォルダー/ファイルのシナリオでのみ機能するように設計されています。 カスタム プロジェクト システムが実装されている場合、一部の機能 (設定など) は機能しません。
高度な機能
設定
カスタム言語サーバー固有の設定のサポートは利用できますが、まだ改善中です。 設定は言語サーバーがサポートする内容に固有であり、通常は言語サーバーがデータを出力する方法を制御します。 たとえば、言語サーバーには、報告されるエラーの最大数の設定がある場合があります。 拡張機能の作成者は、特定のプロジェクトのユーザーが変更できる既定値を定義します。
以下の手順に従って、LSP 言語サービス拡張機能に設定のサポートを追加します。
設定とその既定値を含む JSON ファイル (MockLanguageExtensionSettings.jsonなど) をプロジェクトに追加します。 例えば:
{ "foo.maxNumberOfProblems": -1 }
JSON ファイルを右クリックし、[プロパティ] 選択します。 Build アクションを「Content」に、"Include in VSIX" プロパティを trueに変更します。
ConfigurationSection を実装し、JSON ファイルで定義されている設定のプレフィックスの一覧を返します (Visual Studio Code では、これは package.jsonの構成セクション名にマップされます)。
public IEnumerable<string> ConfigurationSections { get { yield return "foo"; } }
プロジェクトに .pkgdef ファイルを追加します (新しいテキスト ファイルを追加し、ファイル拡張子を .pkgdef に変更します)。 pkgdef ファイルには、次の情報が含まれている必要があります。
[$RootKey$\OpenFolder\Settings\VSWorkspaceSettings\[settings-name]] @="$PackageFolder$\[settings-file-name].json"
サンプル:
[$RootKey$\OpenFolder\Settings\VSWorkspaceSettings\MockLanguageExtension] @="$PackageFolder$\MockLanguageExtensionSettings.json"
.pkgdef ファイルを右クリックし、[プロパティ] 選択します。 [ビルド] アクションを Content に、[VSIX に含める] プロパティを true に変更します。
source.extension.vsixmanifest ファイルを開き、アセット タブにアセットを追加します。
vspackage アセット
- [種類]: Microsoft.VisualStudio.VsPackage
- ソース: ファイルシステム内のファイル
- [パス]: [.pkgdef ファイルへのパス]
ワークスペースの設定のユーザー編集
ユーザーは、サーバーが所有するファイルを含むワークスペースを開きます。
ユーザーは、.vs フォルダーに VSWorkspaceSettings.jsonという名前のファイルを追加します。
ユーザーは、サーバーが提供する設定の VSWorkspaceSettings.json ファイルに行を追加します。 例えば:
{ "foo.maxNumberOfProblems": 10 }
診断トレースを有効にする
診断トレースを有効にして、クライアントとサーバーの間のすべてのメッセージを出力できます。これは、問題をデバッグするときに役立ちます。 診断トレースを有効にするには、次の操作を行います。
- ワークスペース設定ファイル VSWorkspaceSettings.json を開くか作成します (「ワークスペースの設定のユーザー編集」を参照)。
- 設定 json ファイルに次の行を追加します。
{
"foo.trace.server": "Off"
}
トレースの詳細さには次の 3 つの値を使用できます。
- "オフ": トレースが完全にオフになっている
- "Messages": トレースは有効になっていますが、トレースされるのはメソッド名と応答 ID のみです。
- "Verbose": トレースが有効になっています。rpc メッセージ全体がトレースされます。
トレースを有効にすると、コンテンツは %temp%\VisualStudio\LSP ディレクトリ内のファイルに書き込まれます。 このログは、[LanguageClientName]-[Datetime Stamp].log 名前付け形式に従います。 現時点では、開いているフォルダーのシナリオでのみトレースを有効にできます。 言語サーバーをアクティブ化するために 1 つのファイルを開くと、診断トレースはサポートされません。
カスタム メッセージ
標準の言語サーバー プロトコルに含まれていない言語サーバーとの間でメッセージを渡したり、メッセージを受信したりできるようにするための API が用意されています。 カスタム メッセージを処理するには、言語クライアント クラス ILanguageClientCustomMessage2 インターフェイスを実装します。 VS-StreamJsonRpc ライブラリは、言語クライアントと言語サーバーの間でカスタム メッセージを送信するために使用されます。 LSP 言語クライアント拡張機能は他の Visual Studio 拡張機能と同じであるため、カスタム メッセージを使用して拡張機能の Visual Studio に追加機能 (LSP でサポートされていない機能) を追加できます (他の Visual Studio API を使用)。
カスタム メッセージを受信する
言語サーバーからカスタム メッセージを受信するには、ILanguageClientCustomMessage2 に [CustomMessageTarget]((/dotnet/api/microsoft.visualstudio.languageserver.client.ilanguageclientcustommessage.custommessagetarget) プロパティを実装し、カスタム メッセージの処理方法を知っているオブジェクトを返します。 次の例:
ILanguageClientCustomMessage2 に (/dotnet/api/microsoft.visualstudio.languageserver.client.ilanguageclientcustommessage.custommessagetarget) プロパティを実装し、カスタム メッセージの処理方法を把握しているオブジェクトを返します。 次の例:
internal class MockCustomLanguageClient : MockLanguageClient, ILanguageClientCustomMessage2
{
private JsonRpc customMessageRpc;
public MockCustomLanguageClient() : base()
{
CustomMessageTarget = new CustomTarget();
}
public object CustomMessageTarget
{
get;
set;
}
public class CustomTarget
{
public void OnCustomNotification(JToken arg)
{
// Provide logic on what happens OnCustomNotification is called from the language server
}
public string OnCustomRequest(string test)
{
// Provide logic on what happens OnCustomRequest is called from the language server
}
}
}
カスタム メッセージを送信する
言語サーバーにカスタム メッセージを送信するには、ILanguageClientCustomMessage2に AttachForCustomMessageAsync メソッドを実装します。 このメソッドは、言語サーバーが起動し、メッセージを受信する準備ができたときに呼び出されます。 JsonRpc オブジェクトはパラメーターとして渡され、保持しておくことができ、その後、VS-StreamJsonRpc API を使用して言語サーバーにメッセージを 送信できます。 次の例:
internal class MockCustomLanguageClient : MockLanguageClient, ILanguageClientCustomMessage2
{
private JsonRpc customMessageRpc;
public MockCustomLanguageClient() : base()
{
CustomMessageTarget = new CustomTarget();
}
public async Task AttachForCustomMessageAsync(JsonRpc rpc)
{
await Task.Yield();
this.customMessageRpc = rpc;
}
public async Task SendServerCustomNotification(object arg)
{
await this.customMessageRpc.NotifyWithParameterObjectAsync("OnCustomNotification", arg);
}
public async Task<string> SendServerCustomMessage(string test)
{
return await this.customMessageRpc.InvokeAsync<string>("OnCustomRequest", test);
}
}
中間層
拡張機能開発者が、言語サーバーとの間で送受信される LSP メッセージをインターセプトしたい場合があります。 たとえば、拡張機能の開発者は、特定の LSP メッセージに対して送信されるメッセージパラメーターを変更したり、言語サーバーから返される LSP 機能(たとえば、入力候補)の結果を修正したりすることができます。 これが必要な場合、拡張機能開発者は MiddleLayer API を使用して LSP メッセージをインターセプトできます。
特定のメッセージをインターセプトするには、ILanguageClientMiddleLayer インターフェイスを実装するクラスを作成します。 次に、ILanguageClientCustomMessage2 インターフェイスを言語クライアント クラスに実装し、MiddleLayer プロパティ内のオブジェクトのインスタンスを返します。 次の例:
public class MockLanguageClient : ILanguageClient, ILanguageClientCustomMessage2
{
public object MiddleLayer => DiagnosticsFilterMiddleLayer.Instance;
private class DiagnosticsFilterMiddleLayer : ILanguageClientMiddleLayer
{
internal readonly static DiagnosticsFilterMiddleLayer Instance = new DiagnosticsFilterMiddleLayer();
private DiagnosticsFilterMiddleLayer() { }
public bool CanHandle(string methodName)
{
return methodName == "textDocument/publishDiagnostics";
}
public async Task HandleNotificationAsync(string methodName, JToken methodParam, Func<JToken, Task> sendNotification)
{
if (methodName == "textDocument/publishDiagnostics")
{
var diagnosticsToFilter = (JArray)methodParam["diagnostics"];
// ony show diagnostics of severity 1 (error)
methodParam["diagnostics"] = new JArray(diagnosticsToFilter.Where(diagnostic => diagnostic.Value<int?>("severity") == 1));
}
await sendNotification(methodParam);
}
public async Task<JToken> HandleRequestAsync(string methodName, JToken methodParam, Func<JToken, Task<JToken>> sendRequest)
{
return await sendRequest(methodParam);
}
}
}
中間層機能はまだ開発中であり、まだ包括的ではありません。
LSP 言語サーバー拡張機能のサンプル
Visual Studio で LSP クライアント API を使用してサンプル拡張機能のソース コードを確認するには、VSSDK-Extensibility-Samples LSP サンプルを参照してください。
FAQ
Visual Studio で豊富な機能サポートを提供するために、LSP 言語サーバーを補完するカスタム プロジェクト システムを構築したい場合は、どうすればよいでしょうか。
Visual Studio での LSP ベースの言語サーバーのサポートは、開いているフォルダー機能 に依存し、カスタム プロジェクト システムを必要としないように設計されています。 ここで 手順に従って独自のカスタム プロジェクト システムを構築できますが、設定などの一部の機能が機能しない場合があります。 LSP 言語サーバーの既定の初期化ロジックは、現在開いているフォルダーのルート フォルダーの場所を渡すことです。そのため、カスタム プロジェクト システムを使用する場合は、初期化中にカスタム ロジックを指定して、言語サーバーを適切に起動できるようにする必要があります。
デバッガーのサポートを追加する方法
今後のリリースで 一般的なデバッグ プロトコルのサポートを提供する予定です。
VS でサポートされている言語サービス (JavaScript など) が既にインストールされている場合でも、追加機能 (linting など) を提供する LSP 言語サーバー拡張機能をインストールできますか。
はい。ただし、すべての機能が正しく機能するわけではありません。 LSP 言語サーバー拡張機能の最終的な目標は、Visual Studio でネイティブにサポートされていない言語サービスを有効にすることです。 LSP 言語サーバーを使用して追加のサポートを提供する拡張機能を作成できますが、一部の機能 (IntelliSense など) はスムーズなエクスペリエンスではありません。 一般に、LSP 言語サーバー拡張機能は、既存の言語エクスペリエンスを拡張せずに、新しい言語エクスペリエンスを提供するために使用することをお勧めします。
完成した LSP 言語サーバー VSIX はどこで発行できますか?
Marketplace の手順 については、こちらを参照してください。