Usare l'estendibilità dell'editor di Visual Studio

L'editor di Visual Studio supporta le estensioni che aggiungono alle relative funzionalità. Gli esempi includono estensioni che inseriscono e modificano il codice in un linguaggio esistente.

Per la versione iniziale del nuovo modello di estendibilità di Visual Studio, sono supportate solo le funzionalità seguenti:

  • Ascolto delle visualizzazioni di testo aperte e chiuse.
  • In ascolto delle modifiche dello stato della visualizzazione testo (editor).
  • Lettura del testo del documento e delle selezioni/punti di inserimento.
  • Esecuzione di modifiche di testo e selezione/cursore.
  • Definizione di nuovi tipi di documento.
  • Estensione delle visualizzazioni di testo con nuovi margini di visualizzazione testo.

L'editor di Visual Studio si riferisce in genere alla funzionalità di modifica di file di testo, noti come documenti, di qualsiasi tipo. È possibile aprire singoli file per la modifica e la finestra dell'editor aperto viene definita TextView.

Il modello a oggetti dell'editor è descritto in Concetti dell'editor.

Attività iniziali

Il codice di estensione può essere configurato per l'esecuzione in risposta a vari punti di ingresso (situazioni che si verificano quando un utente interagisce con Visual Studio). L'estendibilità dell'editor supporta attualmente tre punti di ingresso: listener, oggetto servizio EditorExtensibility e comandi.

I listener di eventi vengono attivati quando si verificano determinate azioni in una finestra dell'editor, rappresentate nel codice da un oggetto TextView. Ad esempio, quando un utente digita qualcosa nell'editor, si verifica un TextViewChanged evento. Quando una finestra dell'editor viene aperta o chiusa e TextViewOpenedTextViewClosed si verificano eventi.

L'oggetto servizio editor è un'istanza della EditorExtensibility classe , che espone funzionalità dell'editor in tempo reale, ad esempio l'esecuzione di modifiche di testo.

I comandi vengono avviati dall'utente facendo clic su un elemento, che è possibile posizionare su un menu, un menu di scelta rapida o una barra degli strumenti.

Aggiungere un listener di visualizzazione testo

Esistono due tipi di listener, ITextViewChangedListener e ITextViewOpenClosedListener. Insieme, questi listener possono essere usati per osservare l'apertura, la chiusura e la modifica degli editor di testo.

Creare quindi una nuova classe, implementare la classe di base ExtensionPart e ITextViewChangedListener, ITextViewOpenClosedListenero entrambi, e aggiungere un attributo VisualStudioContribution .

Implementare quindi la proprietà TextViewExtensionConfiguration , come richiesto da ITextViewChangedListener e ITextViewOpenClosedListener, rendendo il listener applicabile durante la modifica dei file C#:

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

I tipi di documento disponibili per altri linguaggi di programmazione e tipi di file sono elencati più avanti in questo articolo e i tipi di file personalizzati possono essere definiti anche quando necessario.

Supponendo di decidere di implementare entrambi i listener, la dichiarazione di classe completata dovrebbe essere simile alla seguente:

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

Poiché sia ITextViewOpenClosedListener che ITextViewChangedListener dichiarano la proprietà TextViewExtensionConfiguration , la configurazione si applica a entrambi i listener.

Quando si esegue l'estensione, verrà visualizzato quanto illustrato di seguito:

Ognuno di questi metodi viene passato a un ITextViewSnapshot contenente lo stato della visualizzazione testo e del documento di testo al momento in cui l'utente ha richiamato l'azione e un CancellationToken che avrà IsCancellationRequested == true quando l'IDE desidera annullare un'azione in sospeso.

Definire quando l'estensione è pertinente

L'estensione è in genere rilevante solo per determinati tipi e scenari di documento supportati ed è quindi importante definirne chiaramente l'applicabilità. È possibile usare la configurazione AppliesTo) in diversi modi per definire chiaramente l'applicabilità di un'estensione. È possibile specificare i tipi di file, ad esempio i linguaggi di codice supportati dall'estensione, e/o perfezionare ulteriormente l'applicabilità di un'estensione associando un criterio in base al nome file o al percorso.

Specificare i linguaggi di programmazione con la configurazione AppliesTo

La configurazione AppliesTo indica gli scenari del linguaggio di programmazione in cui deve essere attivata l'estensione. Viene scritto come AppliesTo = new[] { DocumentFilter.FromDocumentType("CSharp") }, dove il tipo di documento è un nome noto di un linguaggio incorporato in Visual Studio o personalizzato definito in un'estensione di Visual Studio.

Nella tabella seguente sono illustrati alcuni tipi di documenti noti:

DocumentType Descrizione
"CSharp" C#
"C/C++" C, C++, intestazioni e IDL
"TypeScript" Linguaggi di tipo TypeScript e JavaScript.
"HTML" HTML
"JSON" JSON
"text" File di testo, inclusi discendenti gerarchici di "codice", che derivano da "text".
"codice" C, C++, C# e così via.

I DocumentType sono gerarchici. Ovvero, C# e C++ derivano entrambi da "codice", quindi la dichiarazione di "codice" fa sì che l'estensione venga attivata per tutti i linguaggi di codice, C#, C, C++ e così via.

Definire un nuovo tipo di documento

È possibile definire un nuovo tipo di documento, ad esempio per supportare un linguaggio di codice personalizzato, aggiungendo una proprietà DocumentTypeConfiguration statica a qualsiasi classe nel progetto di estensione e contrassegnando la proprietà con l'attributo VisualStudioContribution .

DocumentTypeConfiguration consente di definire un nuovo tipo di documento, specificare che eredita uno o più tipi di documento e specificare una o più estensioni di file utilizzate per identificare il tipo di file:

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

Le definizioni dei tipi di documento vengono unite con le definizioni dei tipi di contenuto fornite dall'estendibilità legacy di Visual Studio, che consente di eseguire il mapping di estensioni di file aggiuntive ai tipi di documento esistenti.

Selettori di documenti

Oltre a DocumentFilter.FromDocumentType, DocumentFilter.FromGlobPattern consente di limitare ulteriormente l'applicabilità dell'estensione attivandola solo quando il percorso del file del documento corrisponde a un modello GLOB (jolly):

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

Il pattern parametro rappresenta un modello GLOB corrispondente al percorso assoluto del documento.

I modelli Glob possono avere la sintassi seguente:

  • * per trovare la corrispondenza con zero o più caratteri in un segmento di percorso
  • ? per trovare la corrispondenza su un carattere in un segmento di percorso
  • ** per trovare la corrispondenza con qualsiasi numero di segmenti di percorso, tra cui nessuno
  • {} per raggruppare le condizioni (ad esempio, **​/*.{ts,js} corrisponde a tutti i file TypeScript e JavaScript)
  • [] per dichiarare un intervallo di caratteri da trovare in un segmento di percorso (ad esempio, example.[0-9] per la corrispondenza in example.0, example.1, ...)
  • [!...] per negare un intervallo di caratteri da trovare in un segmento di percorso (ad esempio, example.[!0-9] per la corrispondenza in example.a, example.bma non example.0)

Una barra rovesciata (\) non è valida all'interno di un criterio GLOB. Assicurarsi di convertire qualsiasi barra rovesciata in barra durante la creazione del modello GLOB.

Funzionalità dell'editor di accesso

Le classi di estensioni dell'editor ereditano da ExtensionPart. La ExtensionPart classe espone la proprietà Extensibility . Usando questa proprietà, è possibile richiedere un'istanza dell'oggetto EditorExtensibility . È possibile usare questo oggetto per accedere alle funzionalità dell'editor in tempo reale, ad esempio l'esecuzione di modifiche.

EditorExtensibility editorService = this.Extensibility.Editor();

Accedere allo stato dell'editor all'interno di un comando

ExecuteCommandAsync() in ogni Command viene passato un oggetto IClientContext che contiene uno snapshot dello stato dell'IDE al momento in cui è stato richiamato il comando. È possibile accedere al documento attivo tramite l'interfaccia ITextViewSnapshot , ottenuta dall'oggetto EditorExtensibility chiamando il metodo GetActiveTextViewAsyncasincrono :

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

Dopo aver ottenuto ITextViewSnapshot, è possibile accedere allo stato dell'editor. ITextViewSnapshotè una visualizzazione non modificabile dello stato dell'editor in un momento specifico, pertanto è necessario usare le altre interfacce nel modello a oggetti dell'editor per apportare modifiche.

Apportare modifiche in un documento di testo da un'estensione

Le modifiche apportate a un documento di testo aperto nell'editor di Visual Studio possono derivare da interazioni utente, thread in Visual Studio, ad esempio servizi di linguaggio e altre estensioni. L'estensione deve essere preparata per gestire le modifiche apportate al testo del documento in tempo reale.

Estensioni in esecuzione all'esterno del processo IDE principale di Visual Studio che usano modelli di progettazione asincroni per comunicare con il processo dell'IDE di Visual Studio. Ciò significa che l'uso delle chiamate asincrone ai metodi, come indicato dalla async parola chiave in C# e rafforzato dal suffisso sui nomi dei Async metodi. Asynchronicity è un vantaggio significativo nel contesto di un editor che dovrebbe essere reattivo alle azioni dell'utente. Una chiamata API sincrona tradizionale, se richiede più tempo del previsto, smetterà di rispondere all'input dell'utente, creando un blocco dell'interfaccia utente che dura fino al completamento della chiamata API. Le aspettative degli utenti delle applicazioni interattive moderne sono che gli editor di testo rimangono sempre reattivi e non li impediscono mai di funzionare. La presenza di estensioni asincrone è quindi essenziale per soddisfare le aspettative degli utenti.

Altre informazioni sulla programmazione asincrona sono disponibili in Programmazione asincrona con async e await.

Nel nuovo modello di estendibilità di Visual Studio, l'estensione è la seconda classe relativa all'utente: non può modificare direttamente l'editor o il documento di testo. Tutte le modifiche di stato sono asincrone e cooperative, con l'IDE di Visual Studio che esegue la modifica richiesta per conto dell'estensione. L'estensione può richiedere una o più modifiche in una versione specifica del documento o della visualizzazione testo, ma le modifiche apportate da un'estensione possono essere rifiutate, ad esempio se tale area del documento è stata modificata.

Le modifiche vengono richieste usando il EditAsync() metodo in EditorExtensibility.

Se si ha familiarità con le estensioni legacy di Visual Studio, ITextDocumentEditor è quasi identico a quello dei metodi di modifica dello stato da ITextBuffer e ITextDocument e supporta la maggior parte delle stesse funzionalità.

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

Per evitare modifiche non posizionate, le modifiche dalle estensioni dell'editor vengono applicate come segue:

  1. L'estensione richiede una modifica, in base alla versione più recente del documento.
  2. Tale richiesta può contenere una o più modifiche di testo, modifiche alla posizione del cursore e così via. Qualsiasi tipo di implementazione IEditable può essere modificato in una singola EditAsync() richiesta, inclusi ITextViewSnapshot e ITextDocumentSnapshot. Le modifiche vengono eseguite dall'editor, che può essere richiesto in una classe specifica tramite AsEditable().
  3. Le richieste di modifica vengono inviate all'IDE di Visual Studio, in cui ha esito positivo solo se l'oggetto modificato non è stato modificato dopo la versione in cui è stata effettuata la richiesta. Se il documento è stato modificato, la modifica potrebbe essere rifiutata, richiedendo l'estensione per riprovare alla versione più recente. Il risultato dell'operazione di mutazione viene archiviato in result.
  4. Le modifiche vengono applicate in modo atomico, ovvero senza interruzioni da altri thread in esecuzione. La procedura consigliata consiste nell'eseguire tutte le modifiche che devono verificarsi entro un intervallo di tempo ristretto in una singola EditAsync() chiamata, per ridurre la probabilità di comportamenti imprevisti derivanti dalle modifiche dell'utente o dalle azioni del servizio di linguaggio che si verificano tra le modifiche ( ad esempio, le modifiche delle estensioni vengono interleaved con Roslyn C# che spostano il cursore).

Esecuzione asincrona

ITextViewSnapshot.GetTextDocumentAsync apre una copia del documento di testo nell'estensione di Visual Studio. Poiché le estensioni vengono eseguite in un processo separato, tutte le interazioni di estensione sono asincrone, cooperative e presentano alcune avvertenze:

Attenzione

GetTextDocumentAsync potrebbe non riuscire se viene chiamato in una versione precedente ITextDocument, perché potrebbe non essere più memorizzato nella cache dal client di Visual Studio, se l'utente ha apportato molte modifiche dopo la creazione. Per questo motivo, se si prevede di archiviare un ITextView oggetto per accedere al documento in un secondo momento e non può tollerare un errore, potrebbe essere consigliabile chiamare GetTextDocumentAsync immediatamente. In questo modo recupera il contenuto di testo per tale versione del documento nell'estensione, assicurandosi che una copia di tale versione venga inviata all'estensione prima della scadenza.

Attenzione

GetTextDocumentAsync o MutateAsync potrebbe non riuscire se l'utente chiude il documento.

Esecuzione simultanea

⚠️ Le estensioni dell'editor possono talvolta essere eseguite contemporaneamente

La versione iniziale presenta un problema noto che può comportare l'esecuzione simultanea del codice di estensione dell'editor. Ogni metodo asincrono è garantito che venga chiamato nell'ordine corretto, ma le continuazioni dopo il primo await possono essere interleaved. Se l'estensione si basa sull'ordine di esecuzione, è consigliabile mantenere una coda di richieste in ingresso per mantenere l'ordine, fino a quando questo problema non viene risolto.

Per altre informazioni, vedere Ordinamento predefinito e concorrenza di StreamJsonRpc.

Estensione dell'editor di Visual Studio con un nuovo margine

Le estensioni possono contribuire ai nuovi margini della visualizzazione testo nell'editor di Visual Studio. Un margine di visualizzazione testo è un controllo interfaccia utente rettangolare collegato a una visualizzazione di testo su uno dei quattro lati.

I margini di visualizzazione del testo vengono inseriti in un contenitore di margini (vedere ContainerMarginPlacement.KnownValues) e ordinati prima o dopo relativamente ad altri margini (vedere MarginPlacement.KnownValues).

I provider di margini di visualizzazione del testo implementano l'interfaccia ITextViewMarginProvider , configurano il margine fornito implementando TextViewMarginProviderConfiguration e, quando attivato, forniscono il controllo dell'interfaccia utente da ospitare nel margine tramite CreateVisualElementAsync.

Poiché le estensioni in VisualStudio.Extensibility potrebbero essere out-of-process da Visual Studio, non è possibile usare direttamente WPF come livello di presentazione per il contenuto dei margini della visualizzazione testo. Per fornire un contenuto a un margine di visualizzazione testo è invece necessario creare un controllo RemoteUserControl e il modello di dati corrispondente per tale controllo. Anche se di seguito sono riportati alcuni semplici esempi, è consigliabile leggere la documentazione dell'interfaccia utente remota durante la creazione di contenuto dell'interfaccia utente del margine di visualizzazione testo.

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

Oltre a configurare la posizione dei margini, i provider di margini di visualizzazione testo possono anche configurare le dimensioni della cella della griglia in cui deve essere posizionato il margine usando le proprietà GridCellLength e GridUnitType .

I margini della visualizzazione testo visualizzano in genere alcuni dati correlati alla visualizzazione testo (ad esempio, il numero di riga corrente o il conteggio degli errori), quindi la maggior parte dei provider di margini di visualizzazione testo vuole anche ascoltare gli eventi di visualizzazione testo per reagire all'apertura, alla chiusura delle visualizzazioni di testo e alla digitazione dell'utente.

Visual Studio crea solo un'istanza del provider di margini di visualizzazione testo indipendentemente dal numero di visualizzazioni di testo applicabili che un utente apre, quindi se il margine visualizza alcuni dati con stato, il provider deve mantenere lo stato delle visualizzazioni di testo attualmente aperte.

Per altre informazioni, vedere Esempio di margine conteggio parole.

I margini della visualizzazione testo verticale i cui contenuti devono essere allineati con le righe di visualizzazione testo non sono ancora supportati.

Informazioni sulle interfacce e i tipi dell'editor in Concetti dell'editor.

Esaminare il codice di esempio per una semplice estensione basata su editor:

Gli utenti avanzati potrebbero voler ottenere informazioni sul supporto RPC dell'editor.