Korzystanie z rozszerzalności edytora programu Visual Studio

Edytor programu Visual Studio obsługuje rozszerzenia, które dodają do swoich możliwości. Przykłady obejmują rozszerzenia, które wstawią i modyfikują kod w istniejącym języku.

W przypadku początkowej wersji nowego modelu rozszerzalności programu Visual Studio obsługiwane są tylko następujące funkcje:

  • Nasłuchiwanie widoków tekstowych otwieranych i zamkniętych.
  • Nasłuchiwanie zmian stanu widoku tekstu (edytora).
  • Odczytywanie tekstu dokumentu i lokalizacji zaznaczeń/karetek.
  • Wykonywanie edycji tekstu i zmian zaznaczenia/karetki.
  • Definiowanie nowych typów dokumentów.
  • Rozszerzanie widoków tekstu przy użyciu nowych marginesów widoku tekstu.

Edytor programu Visual Studio zazwyczaj odnosi się do funkcji edytowania plików tekstowych, znanych jako dokumenty dowolnego typu. Poszczególne pliki mogą być otwierane do edycji, a otwarte okno edytora TextViewjest określane jako .

Model obiektów edytora jest opisany w temacie Pojęcia edytora.

Rozpocznij

Kod rozszerzenia można skonfigurować tak, aby był uruchamiany w odpowiedzi na różne punkty wejścia (sytuacje, w których użytkownik wchodzi w interakcję z programem Visual Studio). Rozszerzalność edytora obsługuje obecnie trzy punkty wejścia: odbiorniki, obiekt usługi EditorExtensibility i polecenia.

Odbiorniki zdarzeń są wyzwalane, gdy w oknie edytora wystąpią określone akcje reprezentowane w kodzie przez element TextView. Na przykład gdy użytkownik wpisze coś w edytorze, TextViewChanged wystąpi zdarzenie. Po otwarciu lub zamknięciu okna edytora wystąpią TextViewOpenedTextViewClosed zdarzenia.

Obiekt usługi edytora jest wystąpieniem EditorExtensibility klasy, która uwidacznia funkcje edytora w czasie rzeczywistym, takie jak wykonywanie edycji tekstu.

Polecenia są inicjowane przez użytkownika, klikając element, który można umieścić w menu, menu kontekstowym lub pasku narzędzi.

Dodawanie odbiornika widoku tekstu

Istnieją dwa typy odbiorników: ITextViewChangedListener i ITextViewOpenClosedListener. Razem te odbiorniki mogą służyć do obserwowania otwartych, bliskich i modyfikacji edytorów tekstu.

Następnie utwórz nową klasę, implementuj klasę bazową ExtensionPart i ITextViewChangedListener, ITextViewOpenClosedListenerlub zarówno , jak i dodaj atrybut VisualStudioContribution .

Następnie zaimplementuj właściwość TextViewExtensionConfiguration zgodnie z wymaganiami elementów ITextViewChangedListener i ITextViewOpenClosedListener, co spowoduje zastosowanie odbiornika podczas edytowania plików języka C#:

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

Dostępne typy dokumentów dla innych języków programowania i typów plików są wymienione w dalszej części tego artykułu, a niestandardowe typy plików mogą być również zdefiniowane w razie potrzeby.

Zakładając, że zdecydujesz się zaimplementować oba odbiorniki, zakończona deklaracja klasy powinna wyglądać następująco:

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

Ponieważ zarówno ITextViewOpenClosedListener , jak i ITextViewChangedListener deklarują właściwość TextViewExtensionConfiguration , konfiguracja ma zastosowanie do obu odbiorników.

Po uruchomieniu rozszerzenia powinny zostać wyświetlone następujące elementy:

Każda z tych metod jest przekazywana element ITextViewSnapshot zawierający stan widoku tekstowego i dokumentu tekstowego w momencie wywołania akcji przez użytkownika i tokenu CancellationToken, który będzie miał IsCancellationRequested == true , gdy środowisko IDE chce anulować oczekującą akcję.

Definiowanie, kiedy rozszerzenie jest istotne

Rozszerzenie jest zwykle istotne tylko dla niektórych obsługiwanych typów i scenariuszy dokumentów, dlatego ważne jest, aby jasno zdefiniować jego zastosowanie. Możesz użyć opcji ZastosujDo konfiguracji) na kilka sposobów, aby jasno zdefiniować możliwość zastosowania rozszerzenia. Można określić, jakie typy plików, takie jak języki kodu obsługiwane przez rozszerzenie, i/lub dodatkowo uściślić zastosowanie rozszerzenia, pasując do wzorca na podstawie nazwy pliku lub ścieżki.

Określanie języków programowania przy użyciu konfiguracji AppliesTo

Konfiguracja AppliesTo wskazuje scenariusze języka programowania, w których rozszerzenie powinno zostać aktywowane. Jest on zapisywany jako AppliesTo = new[] { DocumentFilter.FromDocumentType("CSharp") }, gdzie typ dokumentu jest dobrze znaną nazwą języka wbudowanego w program Visual Studio lub niestandardowym zdefiniowanym w rozszerzeniu programu Visual Studio.

Niektóre dobrze znane typy dokumentów przedstawiono w poniższej tabeli:

DocumentType opis
"CSharp" C#
"C/C++" C, C++, nagłówki i IDL
"TypeScript" Języki typów TypeScript i JavaScript.
"HTML" Kod HTML
"JSON" JSON
"tekst" Pliki tekstowe, w tym hierarchiczne elementy potomne "kodu", które pochodzą z "tekstu".
"kod" C, C++, C# itd.

Typy dokumentów są hierarchiczne. Oznacza to, że język C# i C++ pochodzą z "kodu", dlatego deklarowanie "kodu" powoduje aktywowanie rozszerzenia dla wszystkich języków kodu, C#, C, C++itd.

Definiowanie nowego typu dokumentu

Można zdefiniować nowy typ dokumentu, na przykład w celu obsługi niestandardowego języka kodu, dodając statyczną właściwość DocumentTypeConfiguration do dowolnej klasy w projekcie rozszerzenia i oznaczając właściwość atrybutem VisualStudioContribution .

DocumentTypeConfiguration Umożliwia zdefiniowanie nowego typu dokumentu, określenie, że dziedziczy jeden lub więcej innych typów dokumentów, i określ jedno lub więcej rozszerzeń plików, które są używane do identyfikowania typu pliku:

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

Definicje typów dokumentów są scalane z definicjami typów zawartości udostępnianymi przez starszą rozszerzalność programu Visual Studio, co umożliwia mapowanie dodatkowych rozszerzeń plików na istniejące typy dokumentów.

Selektory dokumentów

Oprócz parametru DocumentFilter.FromDocumentType, documentFilter.FromGlobPattern umożliwia dalsze ograniczenie stosowania rozszerzenia przez aktywowanie go tylko wtedy, gdy ścieżka pliku dokumentu jest zgodna ze wzorcem symbolu wieloznakowego:

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

Parametr pattern reprezentuje wzorzec globu zgodny ze ścieżką bezwzględną dokumentu.

Wzorce globu mogą mieć następującą składnię:

  • * aby dopasować zero lub więcej znaków w segmencie ścieżki
  • ? aby dopasować jeden znak w segmencie ścieżki
  • ** aby dopasować dowolną liczbę segmentów ścieżki, w tym brak
  • {} do grupowania warunków (na przykład **​/*.{ts,js} pasuje do wszystkich plików TypeScript i JavaScript)
  • [] aby zadeklarować zakres znaków do dopasowania w segmencie ścieżki (na przykład w example.[0-9] celu dopasowania example.0do , , example.1...)
  • [!...] aby negować zakres znaków, które mają być zgodne w segmencie ścieżki (na przykład w example.[!0-9] celu dopasowania example.ado wartości , example.b, ale nie example.0)

Ukośnik odwrotny (\) nie jest prawidłowy w deseniu globu. Pamiętaj, aby przekonwertować dowolny ukośnik odwrotny na ukośnik podczas tworzenia wzorca globu.

Dostęp do funkcji edytora

Klasy rozszerzeń edytora dziedziczą z rozszerzenia ExtensionPart. Klasa ExtensionPart uwidacznia właściwość Rozszerzalność . Za pomocą tej właściwości można zażądać wystąpienia obiektu EditorExtensibility . Za pomocą tego obiektu można uzyskać dostęp do funkcji edytora czasu rzeczywistego, takich jak wykonywanie edycji.

EditorExtensibility editorService = this.Extensibility.Editor();

Uzyskiwanie dostępu do stanu edytora w poleceniu

ExecuteCommandAsync() w każdym Command z nich jest przekazywany element IClientContext zawierający migawkę stanu środowiska IDE w momencie wywołania polecenia. Dostęp do aktywnego dokumentu można uzyskać za pośrednictwem interfejsu ITextViewSnapshot , który można uzyskać z EditorExtensibility obiektu, wywołując metodę GetActiveTextViewAsyncasynchroniczną :

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

Po utworzeniu ITextViewSnapshotelementu możesz uzyskać dostęp do stanu edytora. ITextViewSnapshot jest niezmiennym widokiem stanu edytora w danym momencie, dlatego należy użyć innych interfejsów w modelu obiektów edytora, aby dokonać edycji.

Wprowadzanie zmian w dokumencie tekstowym z rozszerzenia

Zmiany w dokumencie tekstowym otwieranym w edytorze programu Visual Studio mogą wynikać z interakcji użytkownika, wątków w programie Visual Studio, takich jak usługi językowe i inne rozszerzenia. Rozszerzenie musi być przygotowane do obsługi zmian w tekście dokumentu występujących w czasie rzeczywistym.

Rozszerzenia działające poza głównym procesem IDE programu Visual Studio, które używają asynchronicznych wzorców projektowych do komunikowania się z procesem IDE programu Visual Studio. Oznacza to użycie wywołań metod asynchronicznych, wskazywanych przez async słowo kluczowe w języku C# i wzmocnione przez Async sufiks nazw metod. Asynchroniność jest znaczącą zaletą w kontekście edytora, który ma odpowiadać na akcje użytkownika. Tradycyjne synchroniczne wywołanie interfejsu API, jeśli trwa dłużej niż oczekiwano, przestanie odpowiadać na dane wejściowe użytkownika, tworząc interfejs użytkownika zawieszający się, który trwa do momentu zakończenia wywołania interfejsu API. Oczekiwania użytkowników dotyczące nowoczesnych aplikacji interaktywnych są tym, że edytory tekstów zawsze pozostają dynamiczne i nigdy nie blokują ich działania. Posiadanie rozszerzeń jest asynchroniczne, dlatego niezbędne jest spełnienie oczekiwań użytkowników.

Dowiedz się więcej na temat programowania asynchronicznego w programowania asynchronicznego za pomocą asynchronicznego i await.

W nowym modelu rozszerzalności programu Visual Studio rozszerzenie jest drugą klasą względem użytkownika: nie może bezpośrednio modyfikować edytora ani dokumentu tekstowego. Wszystkie zmiany stanu są asynchroniczne i kooperatywne, a środowisko IDE programu Visual Studio wykonuje żądaną zmianę w imieniu rozszerzenia. Rozszerzenie może zażądać co najmniej jednej zmiany w określonej wersji dokumentu lub widoku tekstowego, ale zmiany z rozszerzenia mogą zostać odrzucone, na przykład jeśli ten obszar dokumentu uległ zmianie.

Żądania edycji są wymagane przy użyciu EditAsync() metody w pliku EditorExtensibility.

Jeśli znasz starsze rozszerzenia programu Visual Studio, ITextDocumentEditor jest prawie taka sama jak metody zmiany stanu z ITextBuffer i ITextDocument i obsługuje większość tych samych możliwości.

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

Aby uniknąć zagubionych edycji, zmiany z rozszerzeń edytora są stosowane w następujący sposób:

  1. Rozszerzenia żąda edycji na podstawie najnowszej wersji dokumentu.
  2. To żądanie może zawierać co najmniej jedną edycję tekstu, zmiany położenia karetki itd. Implementacja dowolnego typu IEditable może zostać zmieniona w jednym EditAsync() żądaniu, w tym ITextViewSnapshot i ITextDocumentSnapshot. Edycje są wykonywane przez edytor, którego można zażądać w określonej klasie za pomocą polecenia AsEditable().
  3. Żądania edycji są wysyłane do środowiska IDE programu Visual Studio, gdzie kończy się powodzeniem tylko wtedy, gdy obiekt, który jest zmutowany, nie zmienił się od momentu wysłania żądania. Jeśli dokument uległ zmianie, zmiana może zostać odrzucona, co wymaga ponownego ponawiania próby rozszerzenia w nowszej wersji. Wynik operacji mutacji jest przechowywany w .result
  4. Zmiany są stosowane niepodziealnie, co oznacza bez przerw w wykonywaniu innych wątków. Najlepszym rozwiązaniem jest wykonywanie wszystkich zmian, które powinny wystąpić w wąskim przedziale czasu w jednym EditAsync() wywołaniu, aby zmniejszyć prawdopodobieństwo nieoczekiwanego zachowania wynikającego z edycji użytkownika lub akcji usługi językowej, które występują między edycjami (na przykład zmiany rozszerzeń są przeplatane za pomocą języka Roslyn C#, przenosząc daszek).

Wykonywanie asynchroniczne

ITextViewSnapshot.GetTextDocumentAsync otwiera kopię dokumentu tekstowego w rozszerzeniu programu Visual Studio. Ponieważ rozszerzenia działają w osobnym procesie, wszystkie interakcje z rozszerzeniami są asynchroniczne, współpracujące i mają pewne zastrzeżenia:

Uwaga

GetTextDocumentAsync może zakończyć się niepowodzeniem w przypadku wywołania starego ITextDocumentelementu , ponieważ może już nie być buforowany przez klienta programu Visual Studio, jeśli użytkownik wprowadził wiele zmian od momentu jego utworzenia. Z tego powodu, jeśli planujesz przechowywać ITextView dokument w celu uzyskania dostępu do dokumentu później i nie można tolerować awarii, dobrym pomysłem może być natychmiastowe wywołanie GetTextDocumentAsync . W ten sposób pobiera zawartość tekstową dla tej wersji dokumentu do rozszerzenia, zapewniając, że kopia tej wersji jest wysyłana do rozszerzenia przed jego wygaśnięciem.

Uwaga

GetTextDocumentAsync lub MutateAsync może zakończyć się niepowodzeniem, jeśli użytkownik zamknie dokument.

Współbieżne wykonywanie

⚠️ Rozszerzenia edytora mogą być czasami uruchamiane współbieżnie

Wersja początkowa ma znany problem, który może spowodować współbieżne wykonanie kodu rozszerzenia edytora. Każda metoda asynchronizowana ma być wywoływana w prawidłowej kolejności, ale kontynuacje po pierwszym await mogą być przeplatane. Jeśli rozszerzenie opiera się na kolejności wykonywania, rozważ utrzymanie kolejki żądań przychodzących w celu zachowania kolejności, dopóki ten problem nie zostanie rozwiązany.

Aby uzyskać więcej informacji, zobacz StreamJsonRpc Default Ordering and Concurrency (Kolejność domyślna usługi StreamJsonRpc i współbieżność).

Rozszerzanie edytora programu Visual Studio przy użyciu nowego marginesu

Rozszerzenia mogą współtworzyć nowe marginesy widoku tekstu w edytorze programu Visual Studio. Margines widoku tekstu to prostokątna kontrolka interfejsu użytkownika dołączona do widoku tekstowego po jednej z czterech stron.

Marginesy widoku tekstu są umieszczane w kontenerze marginesu (zobacz ContainerMarginPlacement.KnownValues) i uporządkowane przed lub po stosunkowo innych marginesach (zobacz MarginPlacement.KnownValues).

Dostawcy marginesu widoku tekstu implementują interfejs ITextViewMarginProvider, konfigurują margines udostępniany przez implementację textViewMarginProviderConfiguration i po aktywowaniu zapewniają kontrolkę interfejsu użytkownika, która ma być hostowana na marginesie za pośrednictwem metody CreateVisualElementAsync.

Ponieważ rozszerzenia w programie VisualStudio.Extensibility mogą być poza procesem z poziomu programu Visual Studio, nie możemy bezpośrednio używać platformy WPF jako warstwy prezentacji dla zawartości marginesów widoku tekstu. Zamiast tego podanie zawartości do marginesu widoku tekstowego wymaga utworzenia kontrolki RemoteUserControl i odpowiedniego szablonu danych dla tej kontrolki. Chociaż poniżej przedstawiono kilka prostych przykładów, zalecamy przeczytanie dokumentacji interfejsu użytkownika zdalnego podczas tworzenia zawartości interfejsu użytkownika widoku tekstu.

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

Oprócz konfigurowania umieszczania marginesów dostawcy marginesu widoku tekstu mogą również skonfigurować rozmiar komórki siatki, w której należy umieścić margines przy użyciu właściwości GridCellLength i GridUnitType .

Marginesy widoku tekstu zwykle wizualizować niektóre dane związane z widokiem tekstowym (na przykład bieżący numer wiersza lub liczba błędów), więc większość dostawców marginesów widoku tekstu chce również nasłuchiwać zdarzeń widoku tekstu, aby reagować na otwieranie, zamykanie widoków tekstowych i wpisywanie przez użytkownika.

Program Visual Studio tworzy tylko jedno wystąpienie dostawcy marginesu widoku tekstu niezależnie od liczby widoków tekstu otwieranych przez użytkownika, więc jeśli margines wyświetla pewne dane stanowe, dostawca musi zachować stan aktualnie otwartych widoków tekstowych.

Aby uzyskać więcej informacji, zobacz Przykładowy margines liczby wyrazów.

Pionowe marginesy widoku tekstu, których zawartość musi być wyrównana do wierszy widoku tekstu, nie są jeszcze obsługiwane.

Dowiedz się więcej o interfejsach edytora i typach w sekcji Pojęcia dotyczące edytora.

Przejrzyj przykładowy kod dla prostego rozszerzenia opartego na edytorze:

Użytkownicy zaawansowani mogą chcieć dowiedzieć się więcej o obsłudze RPC edytora.