Visual Studio エディターの機能拡張を使用する

Visual Studio エディターでは、その機能に追加する拡張機能がサポートされています。 たとえば、既存の言語でコードを挿入および変更する拡張機能があります。

新しい Visual Studio 機能拡張モデルの最初のリリースでは、次の機能のみがサポートされています。

  • 開いているテキスト ビューと閉じているテキスト ビューをリッスンしています。
  • テキスト ビュー (エディター) の状態変更をリッスンしています。
  • ドキュメントのテキストと選択/キャレットの場所を読み取ります。
  • テキストの編集と選択/キャレットの変更の実行。
  • 新しいドキュメントの種類の定義。
  • 新しいテキスト ビューの余白を使用してテキスト ビューを拡張する。

Visual Studio エディターは、通常、任意の種類のテキスト ファイル (ドキュメントと呼ばれます) を編集する機能を指します。 個々のファイルを編集用に開き、開いているエディター ウィンドウを TextView.

エディター オブジェクト モデルについては、エディターの概念を 参照してください

作業の開始

拡張機能コードは、さまざまなエントリ ポイント (ユーザーが Visual Studio と対話するときに発生する状況) に応答して実行するように構成できます。 現在、エディターの機能拡張では、リスナー、EditorExtensibility サービス オブジェクト、コマンドの 3 つのエントリ ポイントがサポートされています。

イベント リスナーは、エディター ウィンドウで特定のアクションが発生したときにトリガーされます。これは、コード TextViewで表されます。 たとえば、ユーザーがエディターに何かを入力すると、 TextViewChanged イベントが発生します。 エディター ウィンドウを開いたり閉じたりするとTextViewClosedTextViewOpenedイベントが発生します。

エディター サービス オブジェクトは、テキスト編集の EditorExtensibility 実行など、リアルタイムのエディター機能を公開するクラスのインスタンスです。

コマンド は、メニュー、コンテキスト メニュー、またはツール バーに配置できる項目をクリックすることで、ユーザーによって開始されます。

テキスト ビュー リスナーを追加する

リスナーには、ITextViewChangedListener と ITextViewOpenClosedListener の 2 種類があります。 これらのリスナーを一緒に使用して、テキスト エディターの開いている、閉じる、変更を観察できます。

次に、ExtensionPart 基本クラスと ITextViewChangedListenerITextViewOpenClosedListenerまたはその両方を実装する新しいクラスを作成し、VisualStudioContribution 属性を追加します。

次に、ITextViewChangedListener と ITextViewOpenClosedListener必要に応じて TextViewExtensionConfiguration プロパティを実装し、C# ファイルの編集時にリスナーを適用します。

public TextViewExtensionConfiguration TextViewExtensionConfiguration => new()
{
    AppliesTo = new[] { DocumentFilter.FromDocumentType("CSharp") },
};

他のプログラミング言語とファイルの種類に使用できるドキュメントの種類については、この記事の後半で説明します。また、必要に応じてカスタム ファイルの種類を定義することもできます。

両方のリスナーを実装することにした場合、完成したクラス宣言は次のようになります。

  [VisualStudioContribution]                
  public sealed class TextViewOperationListener :
      ExtensionPart, // This is the extension part base class containing infrastructure necessary to use VS services.
      ITextViewOpenClosedListener, // Indicates this part listens for text view lifetime events.
      ITextViewChangedListener // Indicates this part listens to text view changes.
  {
      public TextViewExtensionConfiguration TextViewExtensionConfiguration => new()
      {
          // Indicates this part should only light up in C# files.
          AppliesTo = new[] { DocumentFilter.FromDocumentType("CSharp") },
      };
      ...

ITextViewOpenClosedListener と ITextViewChangedListener の両方で TextViewExtensionConfiguration プロパティが宣言されているため、構成は両方のリスナーに適用されます。

拡張機能を実行すると、次の内容が表示されます。

これらの各メソッドには、ユーザーがアクションを 呼び出したときのテキスト ビューとテキスト ドキュメントの状態を含む ITextViewSnapshot と、IDE が保留中のアクションを取り消す場合の CancellationToken が渡されます IsCancellationRequested == true

拡張機能が関連するタイミングを定義する

通常、拡張機能はサポートされている特定のドキュメントの種類とシナリオにのみ関連するため、その適用性を明確に定義することが重要です。 AppliesTo 構成) を使用して、拡張機能の適用性を明確に定義できます。 拡張子でサポートされているコード言語などのファイルの種類を指定するか、ファイル名またはパスに基づいてパターンに一致することで、拡張機能の適用性をさらに調整できます。

AppliesTo 構成でプログラミング言語を指定する

AppliesTo 構成は、拡張機能をアクティブ化する必要があるプログラミング言語シナリオを示します。 AppliesTo = new[] { DocumentFilter.FromDocumentType("CSharp") }これは、ドキュメントの種類が Visual Studio に組み込まれている言語の既知の名前、または Visual Studio 拡張機能で定義されたカスタムである場合に記述されます。

一部の既知のドキュメントの種類を次の表に示します。

DocumentType 説明
"CSharp" C#
"C/C++" C、C++、ヘッダー、IDL
"TypeScript" TypeScript と JavaScript の型言語。
"HTML" HTML
"JSON" JSON
"text" "text" から派生する "code" の階層的子孫を含むテキスト ファイル。
"code" C、C++、C# など。

DocumentTypes は階層型です。 つまり、C# と C++ はどちらも "コード" から派生するため、"コード" を宣言すると、すべてのコード言語、C#、C、C++ などの拡張機能がアクティブになります。

新しいドキュメントの種類を定義する

拡張プロジェクト内の任意のクラスに静的 な DocumentTypeConfiguration プロパティを追加し、プロパティに属性を付けることで、カスタム コード言語をサポートするなど、新しいドキュメントの種類を VisualStudioContribution 定義できます。

DocumentTypeConfiguration では、新しいドキュメントの種類を定義し、1 つ以上の他のドキュメントの種類を継承することを指定し、ファイルの種類を識別するために使用される 1 つ以上のファイル拡張子を指定できます。

using Microsoft.VisualStudio.Extensibility.Editor;

internal static class MyDocumentTypes
{
    [VisualStudioContribution]
    internal static DocumentTypeConfiguration MarkdownDocumentType => new("markdown")
    {
        FileExtensions = new[] { ".md", ".mdk", ".markdown" },
        BaseDocumentType = DocumentType.KnownValues.Text,
    };
}

ドキュメントの種類の定義は、従来の Visual Studio 拡張機能によって提供されるコンテンツ タイプ定義とマージされます。これにより、追加のファイル拡張子を既存のドキュメントの種類にマップできます。

ドキュメント セレクター

DocumentFilter.FromDocumentType加えて、DocumentFilter.FromGlobPattern では、ドキュメントのファイル パスが glob (wildカード) パターンと一致する場合にのみ拡張機能をアクティブにすることで、拡張機能の適用可能性をさらに制限できます。

[VisualStudioContribution]                
public sealed class TextViewOperationListener
    : ExtensionPart, ITextViewOpenClosedListener, ITextViewChangedListener
{
    public TextViewExtensionConfiguration TextViewExtensionConfiguration => new()
    {
        AppliesTo = new[]
        {
            DocumentFilter.FromDocumentType("CSharp"),
            DocumentFilter.FromGlobPattern("**/tests/*.cs"),
        },
    };
[VisualStudioContribution]                
public sealed class TextViewOperationListener
    : ExtensionPart, ITextViewOpenClosedListener, ITextViewChangedListener
{
    public TextViewExtensionConfiguration TextViewExtensionConfiguration => new()
    {
        AppliesTo = new[]
        {
            DocumentFilter.FromDocumentType(MyDocumentTypes.MarkdownDocumentType),
            DocumentFilter.FromGlobPattern("docs/*.md", relativePath: true),
        },
    };

このパラメーターは pattern 、ドキュメントの絶対パスで照合される glob パターンを表します。

Glob パターンには、次の構文を使用できます。

  • * パス セグメント内の 0 個以上の文字に一致する場合
  • ? パス セグメント内の 1 文字で一致する場合
  • ** 任意の数のパス セグメント (なしを含む) に一致する場合
  • {} 条件をグループ化する (たとえば、 **​/*.{ts,js} すべての TypeScript ファイルと JavaScript ファイルに一致する)
  • []パス セグメント内で一致する文字の範囲を宣言する場合 (たとえば、example.[0-9]一致する文字の範囲 、example.0example.1...)
  • [!...]パス セグメント内で一致する文字の範囲を否定する場合 (たとえば、example.[!0-9]一致するexample.a場合は 、 example.bexample.0

円記号 (\) は glob パターン内では有効ではありません。 glob パターンを作成するときは、円記号をスラッシュに変換してください。

アクセス エディターの機能

エディター拡張機能クラスは ExtensionPart から 継承します。 クラスは ExtensionPart Extensibility プロパティを公開します。 このプロパティを使用すると、EditorExtensibility オブジェクトのインスタンスを要求できます。 このオブジェクトを使用して、編集などのリアルタイム エディター機能にアクセスできます。

EditorExtensibility editorService = this.Extensibility.Editor();

コマンド内のエディターの状態にアクセスする

ExecuteCommandAsync()では、コマンドCommandが呼び出されたときIClientContextの IDE の状態のスナップショットを含むオブジェクトが渡されます。 非同期メソッドGetActiveTextViewAsyncを呼び出すことによってオブジェクトからEditorExtensibility取得するインターフェイスを使用ITextViewSnapshotして、アクティブなドキュメントにアクセスできます。

using ITextViewSnapshot textView = await this.Extensibility.Editor().GetActiveTextViewAsync(clientContext, cancellationToken);

作成したら ITextViewSnapshot、エディターの状態にアクセスできます。 ITextViewSnapshotは、ある時点でのエディターの状態を変更できないビューであるため、エディター オブジェクト モデルの他のインターフェイスを使用して編集を行う必要があります。

拡張機能からテキスト ドキュメントを変更する

編集、つまり、Visual Studio エディターで開いているテキスト ドキュメントに対する変更は、ユーザーの操作、言語サービスなどの Visual Studio のスレッド、その他の拡張機能によって発生する可能性があります。 リアルタイムで発生するドキュメント テキストの変更に対処するには、拡張機能を準備する必要があります。

非同期デザイン パターンを使用して Visual Studio IDE プロセスと通信する、メイン Visual Studio IDE プロセスの外部で実行されている拡張機能。 これは、C# のキーワード (keyword)によって示され、メソッド名のサフィックスによってasync強化された非同期メソッド呼び出しの使用をAsync意味します。 非同期性は、ユーザーアクションに対する応答性が期待されるエディターのコンテキストにおいて大きな利点です。 従来の同期 API 呼び出しでは、予想以上に時間がかかる場合、ユーザー入力への応答が停止し、API 呼び出しが完了するまで続く UI フリーズが作成されます。 最新の対話型アプリケーションに対するユーザーの期待は、テキスト エディターは常に再メイン応答性が高く、動作を妨げないということです。 そのため、拡張機能を非同期にすることは、ユーザーの期待に応えるために不可欠です。

非同期プログラミングと await を使用した非同期プログラミングの詳細について説明します。

新しい Visual Studio 拡張機能モデルでは、拡張機能はユーザーに対する 2 番目のクラスです。エディターやテキスト ドキュメントを直接変更することはできません。 すべての状態変更は非同期で協調的であり、Visual Studio IDE は拡張機能に代わって要求された変更を実行します。 拡張機能は、ドキュメントまたはテキスト ビューの特定のバージョンに対して 1 つ以上の変更を要求できますが、ドキュメントのその領域が変更された場合など、拡張機能からの変更が拒否される可能性があります。

編集はメソッドをEditAsync()使用して要求されます。EditorExtensibility

従来の Visual Studio 拡張機能に慣れている場合は、 ITextDocumentEditor ITextBuffer と ITextDocument からの状態変更メソッドとほぼ同じであり、ほとんどの同じ機能をサポートしています。

MutationResult result = await this.Extensibility.Editor().EditAsync(
batch =>
{
    var editor = document.AsEditable(batch);
    editor.Replace(textView.Selection.Extent, newGuidString);
},
cancellationToken);

間違った編集を回避するために、エディター拡張機能からの編集は次のように適用されます。

  1. 拡張機能は、ドキュメントの最新バージョンに基づいて編集を要求します。
  2. その要求には、1 つ以上のテキスト編集、キャレット位置の変更などが含まれる場合があります。 実装するIEditable任意の型は、ITextViewSnapshotITextDocumentSnapshot1 つのEditAsync()要求で変更できます。 編集はエディターによって行われます。これは、特定のクラス AsEditable()で要求できます。
  3. 編集要求は Visual Studio IDE に送信され、変更されているオブジェクトが変更されていない場合にのみ成功します。これは、要求が行われたバージョン以降です。 ドキュメントが変更された場合、変更が拒否される可能性があり、拡張機能は新しいバージョンで再試行する必要があります。 変異操作の結果は、に格納されます result.
  4. 編集はアトミックに適用されます。つまり、他の実行中のスレッドからの中断はありません。 ベスト プラクティスは、狭い時間枠内で発生するすべての変更を 1 回 EditAsync() の呼び出しにして、ユーザーの編集によって予期しない動作が発生する可能性を減らすか、編集の間に発生する言語サービス アクションを減らすことです (たとえば、拡張機能の編集が Roslyn C# とインターリーブされてキャレットを移動します)。

非同期実行

ITextViewSnapshot.GetTextDocumentAsync は、Visual Studio 拡張機能でテキスト ドキュメントのコピーを開きます。 拡張機能は個別のプロセスで実行されるため、すべての拡張機能の対話は非同期で協調的であり、いくつかの注意事項があります。

注意

GetTextDocumentAsync 古い ITextDocumentファイルで呼び出されると失敗する可能性があります。これは、ユーザーが作成されてから多くの変更を行った場合、Visual Studio クライアントによってキャッシュされなくなった可能性があるためです。 このため、後でドキュメントにアクセスするためのドキュメントを保存 ITextView する予定で、失敗を許容できない場合は、すぐに呼び出 GetTextDocumentAsync すのが良い場合があります。 これにより、そのバージョンのドキュメントのテキスト コンテンツが拡張機能にフェッチされ、そのバージョンのコピーが期限切れになる前に拡張機能に送信されます。

注意

GetTextDocumentAsync または MutateAsync 、ユーザーがドキュメントを閉じた場合に失敗する可能性があります。

同時実行

⚠️ エディター拡張機能が同時に実行される場合がある

初期リリースには、エディター拡張機能コードが同時に実行される可能性がある既知の問題があります。 各非同期メソッドは正しい順序で呼び出されることは保証されますが、最初 await のメソッドの後の継続はインターリーブされる可能性があります。 拡張機能が実行順序に依存している場合は、この問題が修正されるまで、受信要求のキューを保持して順序を保持することを検討メイン。

詳細については、「StreamJsonRpc の既定の順序付けとコンカレンシー」を参照してください

新しい余白を使用して Visual Studio エディターを拡張する

拡張機能は、Visual Studio エディターに新しいテキスト ビューの余白を提供できます。 テキスト ビューの余白は、四辺のいずれかのテキスト ビューにアタッチされた四角形の UI コントロールです。

テキスト ビューの余白は余白コンテナーに配置され (ContainerMarginPlacement.KnownValues を参照)、他の余白の前後に並べ替えられます (MarginPlacement.KnownValues を参照)。

テキスト ビューの余白プロバイダーは、ITextViewMarginProvider インターフェイスを実装し、TextViewMarginProviderConfiguration を実装して提供する余白を構成し、アクティブ化されると、CreateVisualElementAsync を介して余白でホストされる UI コントロールを提供します。

VisualStudio.Extensibility の拡張機能は Visual Studio からアウトプロセスになる可能性があるため、テキスト ビューの余白のコンテンツのプレゼンテーション レイヤーとして WPF を直接使用することはできません。 代わりに、テキスト ビューの余白にコンテンツを提供するには、RemoteUserControl とそのコントロールの対応するデータ テンプレートを作成する必要があります。 以下に簡単な例がいくつかありますが、テキスト ビューの余白 UI コンテンツを 作成する場合は、リモート UI のドキュメント を参照することをお勧めします。

/// <summary>
/// Configures the margin to be placed to the left of built-in Visual Studio line number margin.
/// </summary>
public TextViewMarginProviderConfiguration TextViewMarginProviderConfiguration => new(marginContainer: ContainerMarginPlacement.KnownValues.BottomRightCorner)
{
    Before = new[] { MarginPlacement.KnownValues.RowMargin },
};

/// <summary>
/// Creates a remotable visual element representing the content of the margin.
/// </summary>
public async Task<IRemoteUserControl> CreateVisualElementAsync(ITextViewSnapshot textView, CancellationToken cancellationToken)
{
    var documentSnapshot = await textView.GetTextDocumentAsync(cancellationToken);
    var dataModel = new WordCountData();
    dataModel.WordCount = CountWords(documentSnapshot);
    this.dataModels[textView.Uri] = dataModel;
    return new MyMarginContent(dataModel);
}

テキスト ビューの余白プロバイダーは、余白の配置を構成するだけでなく、GridCellLength プロパティと GridUnitType プロパティを使用して余白を配置するグリッド セルのサイズを構成することもできます。

テキスト ビューの余白は通常、テキスト ビューに関連するデータ (現在の行番号やエラーの数など) を視覚化するため、ほとんどのテキスト ビューの余白プロバイダーは、テキスト ビューの開き、閉じる、ユーザー入力に対応するためにテキスト ビュー イベントをリッスンする必要もあります。

Visual Studio では、ユーザーが開く適用可能なテキスト ビューの数に関係なく、テキスト ビューの余白プロバイダーのインスタンスが 1 つだけ作成されるため、余白にステートフル データが表示される場合は、プロバイダーが現在開いているテキスト ビューの状態を維持する必要があります。

詳細については、「ワード カウントの余白のサンプル」を参照してください

テキスト ビューの行に合わせてコンテンツを配置する必要がある縦書きテキスト ビューの余白はまだサポートされていません。

エディターの概念で エディターのインターフェイスと種類について説明します

単純なエディター ベースの拡張機能のサンプル コードを確認します。

上級ユーザーは、エディター RPC の サポートについて知りたい場合があります。