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 TextView
jest 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ą TextViewOpened
TextViewClosed
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
, ITextViewOpenClosedListener
lub 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:
- ITextViewOpenClosedListener.TextViewOpenedAsync o nazwie anytime a text view is opened by the user.
- ITextViewOpenClosedListener.TextViewClosedAsync o nazwie anytime a text view is closed by the user.
- ITextViewChangedListener.TextViewChangedAsync nazywany za każdym razem, gdy użytkownik wprowadza zmianę tekstu w dokumencie tekstowym wyświetlanym przez widok tekstowy.
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 wexample.[0-9]
celu dopasowaniaexample.0
do , ,example.1
...)[!...]
aby negować zakres znaków, które mają być zgodne w segmencie ścieżki (na przykład wexample.[!0-9]
celu dopasowaniaexample.a
do wartości ,example.b
, ale nieexample.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ę GetActiveTextViewAsync
asynchroniczną :
using ITextViewSnapshot textView = await this.Extensibility.Editor().GetActiveTextViewAsync(clientContext, cancellationToken);
Po utworzeniu ITextViewSnapshot
elementu 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:
- Rozszerzenia żąda edycji na podstawie najnowszej wersji dokumentu.
- 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 jednymEditAsync()
żądaniu, w tymITextViewSnapshot
iITextDocumentSnapshot
. Edycje są wykonywane przez edytor, którego można zażądać w określonej klasie za pomocą poleceniaAsEditable()
. - Żą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
- 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 ITextDocument
elementu , 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.
Powiązana zawartość
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.