Nutzen der Erweiterbarkeit des Visual Studio-Editors

Der Visual Studio-Editor unterstützt Erweiterungen, die ihren Funktionen hinzugefügt werden. Beispiele sind Erweiterungen, die Code in einer vorhandenen Sprache einfügen und ändern.

Für die erste Version des neuen Visual Studio-Erweiterbarkeitsmodells werden nur die folgenden Funktionen unterstützt:

  • Lauschen auf Textansichten, die geöffnet und geschlossen werden.
  • Überwachen von Statusänderungen in der Textansicht (Editor).
  • Lesen des Texts des Dokuments und der Markierungs-/Caretsspeicherorte.
  • Durchführen von Textbearbeitungen und Auswahl-/Caretänderungen.
  • Definieren neuer Dokumenttypen.
  • Erweitern von Textansichten mit neuen Textansichtsrändern.

Der Visual Studio-Editor bezieht sich im Allgemeinen auf die Funktionalität der Bearbeitung von Textdateien, die als Dokumente bezeichnet werden, von jedem Typ. Einzelne Dateien können zur Bearbeitung geöffnet werden, und das geöffnete Editor-Fenster wird als ein TextView.

Das Editorobjektmodell wird unter Editor-Konzepten beschrieben.

Erste Schritte

Ihr Erweiterungscode kann so konfiguriert werden, dass er als Reaktion auf verschiedene Einstiegspunkte ausgeführt wird (Situationen, die auftreten, wenn ein Benutzer mit Visual Studio interagiert). Die Editorerweiterung unterstützt derzeit drei Einstiegspunkte: Listener, das EditorExtensibility-Dienstobjekt und Befehle.

Ereignislistener werden ausgelöst, wenn bestimmte Aktionen in einem Editorfenster auftreten, dargestellt in Code durch ein TextView. Wenn ein Benutzer beispielsweise etwas in den Editor eingibt, tritt ein TextViewChanged Ereignis auf. Wenn ein Editorfenster geöffnet oder geschlossen wird und TextViewOpenedTextViewClosed Ereignisse auftreten.

Das Editordienstobjekt ist eine Instanz der EditorExtensibility Klasse, die Echtzeit-Editorfunktionen verfügbar macht, z. B. das Ausführen von Textbearbeitungen.

Befehle werden vom Benutzer durch Klicken auf ein Element initiiert, das Sie in einem Menü, Kontextmenü oder einer Symbolleiste platzieren können.

Hinzufügen eines Textansicht-Listeners

Es gibt zwei Typen von Listenern, ITextViewChangedListener und ITextViewOpenClosedListener. Zusammen können diese Listener verwendet werden, um die offenen, schließenden und änderungen von Texteditoren zu beobachten.

Erstellen Sie dann eine neue Klasse, implementieren Sie die ExtensionPart-Basisklasse und ITextViewOpenClosedListenerITextViewChangedListener, oder beides, und fügen Sie ein VisualStudioContribution-Attribut hinzu.

Implementieren Sie dann die TextViewExtensionConfiguration-Eigenschaft, wie von ITextViewChangedListener und ITextViewOpenClosedListener erforderlich, sodass der Listener beim Bearbeiten von C#-Dateien angewendet wird:

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

Die verfügbaren Dokumenttypen für andere Programmiersprachen und Dateitypen werden weiter unten in diesem Artikel aufgeführt, und benutzerdefinierte Dateitypen können auch bei Bedarf definiert werden.

Wenn Sie sich für die Implementierung beider Listener entscheiden, sollte die fertige Klassendeklaration wie folgt aussehen:

  [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") },
      };
      ...

Da sowohl ITextViewOpenClosedListener als auch ITextViewChangedListener die TextViewExtensionConfiguration-Eigenschaft deklarieren, gilt die Konfiguration für beide Listener.

Wenn Sie Die Erweiterung ausführen, sollten Sie Folgendes sehen:

Jede dieser Methoden wird an einen ITextViewSnapshot übergeben, der den Status der Textansicht und des Textdokuments enthält, wenn der Benutzer die Aktion aufgerufen hat, und ein CancellationToken, das vorhanden IsCancellationRequested == true ist, wenn die IDE eine ausstehende Aktion abbrechen möchte.

Definieren, wann Ihre Erweiterung relevant ist

Ihre Erweiterung ist in der Regel nur für bestimmte unterstützte Dokumenttypen und Szenarien relevant, daher ist es wichtig, die Anwendbarkeit klar zu definieren. Sie können die AppliesTo-Konfiguration auf verschiedene Arten verwenden, um die Anwendbarkeit einer Erweiterung eindeutig zu definieren. Sie können entweder angeben, welche Dateitypen wie Codesprachen die Erweiterung unterstützt, und/oder die Anwendbarkeit einer Erweiterung weiter verfeinern, indem Sie auf einem Muster basierend auf dem Dateinamen oder Pfad übereinstimmen.

Angeben von Programmiersprachen mit der AppliesTo-Konfiguration

Die Konfiguration "AppliesTo " gibt die Programmierspracheszenarien an, in denen die Erweiterung aktiviert werden soll. Er wird geschrieben als AppliesTo = new[] { DocumentFilter.FromDocumentType("CSharp") }, wobei der Dokumenttyp ein bekannter Name einer in Visual Studio integrierten Sprache oder in einer Visual Studio-Erweiterung definiert ist.

Einige bekannte Dokumenttypen werden in der folgenden Tabelle angezeigt:

DocumentType Beschreibung
"CSharp" C#
"C/C++" C, C++, Header und IDL
"TypeScript" TypeScript- und JavaScript-Typsprachen.
"HTML" HTML
"JSON" JSON
"text" Textdateien, einschließlich hierarchischer Nachfolger von "Code", die von "Text" absteigen.
"Code" C, C++, C# usw.

DocumentTypes sind hierarchisch. Das heißt, C# und C++ gehen beide von "Code" ab, sodass durch das Deklarieren von "Code" die Erweiterung für alle Codesprachen, C#, C, C++ usw. aktiviert wird.

Definieren eines neuen Dokumenttyps

Sie können einen neuen Dokumenttyp definieren, z. B. um eine benutzerdefinierte Codesprache zu unterstützen, indem Sie einer beliebigen Klasse im Erweiterungsprojekt eine statische DocumentTypeConfiguration-Eigenschaft hinzufügen und die Eigenschaft mit dem VisualStudioContribution Attribut markieren.

DocumentTypeConfiguration ermöglicht es Ihnen, einen neuen Dokumenttyp zu definieren, anzugeben, dass er einen oder mehrere andere Dokumenttypen erbt, und eine oder mehrere Dateierweiterungen angeben, die zum Identifizieren des Dateityps verwendet werden:

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,
    };
}

Dokumenttypdefinitionen werden mit Inhaltstypdefinitionen zusammengeführt, die von der älteren Visual Studio-Erweiterbarkeit bereitgestellt werden, sodass Sie vorhandene Dokumenttypen zusätzliche Dateierweiterungen zuordnen können.

Dokumentauswahlen

Zusätzlich zu DocumentFilter.FromDocumentType können Sie DocumentFilter.FromGlobPattern die Anwendbarkeit der Erweiterung weiter einschränken, indem Sie sie nur aktivieren, wenn der Dateipfad des Dokuments mit einem Glob-Muster (wild Karte) übereinstimmt:

[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),
        },
    };

Der pattern Parameter stellt ein Globmuster dar, das auf dem absoluten Pfad des Dokuments übereinstimmt.

Glob-Muster können die folgende Syntax aufweisen:

  • * um null oder mehr Zeichen in einem Pfadsegment abzugleichen
  • ? übereinstimmung mit einem Zeichen in einem Pfadsegment
  • ** um einer beliebigen Anzahl von Pfadsegmenten zu entsprechen, einschließlich keiner
  • {} zum Gruppieren von Bedingungen (z **​/*.{ts,js} . B. entspricht allen TypeScript- und JavaScript-Dateien)
  • [] zum Deklarieren eines Zeichenbereichs, der in einem Pfadsegment übereinstimmen soll (z example.[0-9] . B. zum Abgleichen nach example.0, , example.1...)
  • [!...] um einen Bereich von Zeichen zu entfernen, der in einem Pfadsegment übereinstimmen soll (z. B. zum example.[!0-9] Abgleichen an example.a, example.baber nicht example.0)

Ein umgekehrter Schrägstrich (\) ist nicht innerhalb eines Globmusters gültig. Stellen Sie sicher, dass Sie beim Erstellen des Globmusters einen umgekehrten Schrägstrich in Schrägstrich konvertieren.

Access-Editor-Funktionalität

Ihre Editorerweiterungsklassen erben von ExtensionPart. Die ExtensionPart Klasse macht die Erweiterbarkeitseigenschaft verfügbar. Mit dieser Eigenschaft können Sie eine Instanz des EditorExtensibility-Objekts anfordern. Sie können dieses Objekt verwenden, um auf Echtzeit-Editorfunktionen zuzugreifen, z. B. zum Ausführen von Bearbeitungen.

EditorExtensibility editorService = this.Extensibility.Editor();

Access-Editorstatus in einem Befehl

ExecuteCommandAsync()in jedem Command wird ein IClientContext Momentaufnahme des Zustands der IDE zum Zeitpunkt des Aufrufs des Befehls übergeben. Sie können über die ITextViewSnapshot Schnittstelle auf das aktive Dokument zugreifen, das Sie über das EditorExtensibility Objekt abrufen, indem Sie die asynchrone Methode GetActiveTextViewAsyncaufrufen:

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

Sobald Sie haben ITextViewSnapshot, können Sie auf den Editorstatus zugreifen. ITextViewSnapshot ist eine unveränderliche Ansicht des Editorzustands zu einem Zeitpunkt, daher müssen Sie die anderen Schnittstellen im Editor-Objektmodell verwenden, um Bearbeitungen vorzunehmen.

Vornehmen von Änderungen in einem Textdokument aus einer Erweiterung

Bearbeitungen, d. h. Änderungen an einem textdokument, das im Visual Studio-Editor geöffnet ist, können sich aus Benutzerinteraktionen, Threads in Visual Studio wie Sprachdienste und anderen Erweiterungen ergeben. Ihre Erweiterung muss vorbereitet sein, um Änderungen am Dokumenttext in Echtzeit zu behandeln.

Erweiterungen, die außerhalb des Standard Visual Studio-IDE-Prozesses ausgeführt werden und asynchrone Entwurfsmuster für die Kommunikation mit dem Visual Studio-IDE-Prozess verwenden. Dies bedeutet die Verwendung asynchroner Methodenaufrufe, wie durch die async Schlüsselwort (keyword) in C# angegeben und durch das Async Suffix auf Methodennamen verstärkt. Asynchronität ist ein erheblicher Vorteil im Kontext eines Editors, der erwartet wird, dass er auf Benutzeraktionen reagiert. Ein herkömmlicher synchroner API-Aufruf, wenn er länger als erwartet dauert, reagiert nicht mehr auf Benutzereingaben, wodurch eine Benutzeroberfläche fixiert wird, die bis zum Abschluss des API-Aufrufs dauert. Benutzererwartungen an moderne interaktive Anwendungen sind, dass Texteditoren immer wieder reagieren Standard und sie niemals daran hindern, zu arbeiten. Die asynchrone Nutzung von Erweiterungen ist daher unerlässlich, um die Erwartungen der Benutzer zu erfüllen.

Erfahren Sie mehr über die asynchrone Programmierung bei der asynchronen Programmierung und erwarten Sie.

Im neuen Visual Studio-Erweiterbarkeitsmodell ist die Erweiterung eine zweite Klasse relativ zum Benutzer: Sie kann den Editor oder das Textdokument nicht direkt ändern. Alle Statusänderungen sind asynchron und kooperativ, wobei visual Studio IDE die angeforderte Änderung im Namen der Erweiterung ausführt. Die Erweiterung kann eine oder mehrere Änderungen an einer bestimmten Version des Dokuments oder der Textansicht anfordern, aber Änderungen aus einer Erweiterung können abgelehnt werden, z. B. wenn sich dieser Bereich des Dokuments geändert hat.

Bearbeitungen werden mithilfe der EditAsync() Methode für EditorExtensibilityangefordert.

Wenn Sie mit älteren Visual Studio-Erweiterungen vertraut sind, ITextDocumentEditor ist es fast identisch mit den Zustandsänderungsmethoden von ITextBuffer und ITextDocument und unterstützt die meisten der gleichen Funktionen.

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

Um falsch eingefügte Bearbeitungen zu vermeiden, werden Bearbeitungen aus Editorerweiterungen wie folgt angewendet:

  1. Erweiterungsanforderungen werden basierend auf der neuesten Version des Dokuments vorgenommen.
  2. Diese Anforderung kann eine oder mehrere Textbearbeitungen, Änderungen an der Caretposition usw. enthalten. Jede Implementierung des Typs IEditable kann in einer einzigen EditAsync() Anforderung geändert werden, einschließlich ITextViewSnapshot und ITextDocumentSnapshot. Bearbeitungen erfolgen über den Editor, der für eine bestimmte Klasse angefordert AsEditable()werden kann.
  3. Bearbeitungsanforderungen werden an Visual Studio-IDE gesendet, wobei sie nur erfolgreich ausgeführt wird, wenn sich das geänderte Objekt seit der Version der Anforderung nicht geändert hat. Wenn sich das Dokument geändert hat, kann die Änderung abgelehnt werden, sodass die Erweiterung für eine neuere Version erneut versucht werden muss. Das Ergebnis der Mutationsoperation wird gespeichert in result.
  4. Bearbeitungen werden atomar angewendet, d. h. ohne Unterbrechung von anderen ausgeführten Threads. Die bewährte Methode besteht darin, alle Änderungen durchzuführen, die innerhalb eines engen Zeitrahmens in einem einzelnen EditAsync() Aufruf auftreten sollten, um die Wahrscheinlichkeit eines unerwarteten Verhaltens zu verringern, das sich aus Benutzerbearbeitungen oder Sprachdienstaktionen ergibt, die zwischen Bearbeitungen auftreten (z. B. Erweiterungsbearbeitungen, die mit Roslyn C# interleaviert werden, um das Caret zu verschieben).

Asynchrone Ausführung

ITextViewSnapshot.GetTextDocumentAsync öffnet eine Kopie des Textdokuments in der Visual Studio-Erweiterung. Da Erweiterungen in einem separaten Prozess ausgeführt werden, sind alle Erweiterungsinteraktionen asynchron, kooperativ und weisen einige Einschränkungen auf:

Achtung

GetTextDocumentAsync kann fehlschlagen, wenn ein alter ITextDocumentAufruf ausgeführt wird, da er möglicherweise nicht mehr vom Visual Studio-Client zwischengespeichert wird, wenn der Benutzer seit der Erstellung viele Änderungen vorgenommen hat. Aus diesem Grund empfiehlt es sich, sofort aufzurufenGetTextDocumentAsync, wenn Sie beabsichtigen, ein ITextView Dokument zu speichern und einen Fehler nicht tolerieren zu können. Dadurch wird der Textinhalt für diese Version des Dokuments in Die Erweiterung abgerufen, um sicherzustellen, dass eine Kopie dieser Version vor ablaufen an Ihre Erweiterung gesendet wird.

Achtung

GetTextDocumentAsync oder MutateAsync schlägt fehl, wenn der Benutzer das Dokument schließt.

Gleichzeitige Ausführung

⚠️ Editorerweiterungen können manchmal gleichzeitig ausgeführt werden

Die erste Version hat ein bekanntes Problem, das zur gleichzeitigen Ausführung von Editorerweiterungscode führen kann. Jede asynchrone Methode wird garantiert in der richtigen Reihenfolge aufgerufen, aber Fortsetzungen nach dem ersten await können interleaviert werden. Wenn Ihre Erweiterung auf die Ausführungsreihenfolge angewiesen ist, sollten Sie erwägen, Standard eine Warteschlange eingehender Anforderungen beizubehalten, um die Reihenfolge beizubehalten, bis dieses Problem behoben ist.

Weitere Informationen finden Sie unter StreamJsonRpc Standardreihenfolge und Parallelität.

Erweitern des Visual Studio-Editors mit einem neuen Rand

Erweiterungen können neue Textansichtsränder zum Visual Studio-Editor beitragen. Ein Textansichtsrand ist ein rechteckiges UI-Steuerelement, das an einer Textansicht auf einer der vier Seiten angefügt ist.

Textansichtsränder werden in einem Randcontainer (siehe ContainerMarginPlacement.KnownValues) platziert und vor oder nach relativ zu anderen Seitenrändern sortiert (siehe MarginPlacement.KnownValues).

Textansichts-Randanbieter implementieren die ITextViewMarginProvider-Schnittstelle , konfigurieren den von ihnen bereitgestellten Rand durch Implementieren von TextViewMarginProviderConfiguration und bereitstellen, wenn aktiviert, ui-Steuerelement, das über CreateVisualElementAsync im Rand gehostet werden soll.

Da Erweiterungen in VisualStudio.Extensibility möglicherweise nicht von Visual Studio verarbeitet werden, können wir WPF nicht direkt als Präsentationsebene für Inhalte von Textansichtsrändern verwenden. Stattdessen muss für die Bereitstellung eines Inhalts an einem Textansichtsrand ein RemoteUserControl und die entsprechende Datenvorlage für dieses Steuerelement erstellt werden. Es gibt zwar einige einfache Beispiele unten, es wird jedoch empfohlen, die Dokumentation zur Remote-UI beim Erstellen von Ui-Inhalten für die Textansicht zu lesen.

/// <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);
}

Neben der Konfiguration der Seitenrandplatzierung können Anbieter von Textansichtsranden auch die Größe der Rasterzelle konfigurieren, in der der Rand mithilfe der GridCellLength - und GridUnitType-Eigenschaften platziert werden soll.

Textansichtsränder visualisieren in der Regel einige Daten im Zusammenhang mit der Textansicht (z. B. die aktuelle Zeilennummer oder die Anzahl der Fehler), sodass die meisten Textansichts-Randanbieter auch auf Textansichtsereignisse lauschen möchten, um auf das Öffnen, Schließen von Textansichten und Benutzereingaben zu reagieren.

Visual Studio erstellt nur eine Instanz Ihres Textansichtsrandanbieters, unabhängig davon, wie viele anwendbare Textansichten ein Benutzer öffnet. Wenn ihr Rand also einige zustandsbehaftete Daten anzeigt, muss Ihr Anbieter den Status der aktuell geöffneten Textansichten beibehalten.

Weitere Informationen finden Sie im Word Count Margin Sample.

Vertikale Textansichtsränder, deren Inhalt an Textansichtszeilen ausgerichtet werden muss, werden noch nicht unterstützt.

Erfahren Sie mehr über die Editorschnittstellen und -typen in Editor-Konzepten.

Überprüfen Sie den Beispielcode für eine einfache editorbasierte Erweiterung:

Erweiterte Benutzer möchten möglicherweise mehr über die Editor-RPC-Unterstützung erfahren.