Utiliser l’extensibilité de l’éditeur Visual Studio

L’éditeur Visual Studio prend en charge les extensions qui s’ajoutent à ses fonctionnalités. Les exemples incluent des extensions qui insèrent et modifient du code dans un langage existant.

Pour la version initiale du nouveau modèle d’extensibilité Visual Studio, seules les fonctionnalités suivantes sont prises en charge :

  • Écoute des vues de texte ouvertes et fermées.
  • L’écoute des modifications d’état de l’affichage de texte (éditeur).
  • Lecture du texte du document et des emplacements des sélections/carets.
  • Exécution de modifications de texte et modifications de sélection/de caret.
  • Définition de nouveaux types de documents.
  • Extension des vues de texte avec de nouvelles marges d’affichage de texte.

L’éditeur Visual Studio fait généralement référence aux fonctionnalités de modification de fichiers texte, appelés documents, de n’importe quel type. Les fichiers individuels peuvent être ouverts pour modification, et la fenêtre d’éditeur ouverte est appelée TextView.

Le modèle objet de l’éditeur est décrit dans les concepts de l’éditeur.

Bien démarrer

Votre code d’extension peut être configuré pour s’exécuter en réponse à différents points d’entrée (situations qui se produisent lorsqu’un utilisateur interagit avec Visual Studio). L’extensibilité de l’éditeur prend actuellement en charge trois points d’entrée : les écouteurs, l’objet de service EditorExtensibility et les commandes.

Les écouteurs d’événements sont déclenchés lorsque certaines actions se produisent dans une fenêtre d’éditeur, représentées dans le code par un TextView. Par exemple, lorsqu’un utilisateur tape quelque chose dans l’éditeur, un TextViewChanged événement se produit. Lorsqu’une fenêtre d’éditeur est ouverte ou fermée, TextViewOpened et TextViewClosed que des événements se produisent.

L’objet du service d’éditeur est une instance de la EditorExtensibility classe, qui expose les fonctionnalités de l’éditeur en temps réel, telles que l’exécution de modifications de texte.

Les commandes sont lancées par l’utilisateur en cliquant sur un élément, que vous pouvez placer dans un menu, un menu contextuel ou une barre d’outils.

Ajouter un écouteur d’affichage texte

Il existe deux types d’écouteurs, ITextViewChangedListener et ITextViewOpenClosedListener. Ensemble, ces écouteurs peuvent être utilisés pour observer les éditeurs de texte ouverts, fermés et modifiés.

Ensuite, créez une classe, implémentant la classe de base ExtensionPart etITextViewOpenClosedListenerITextViewChangedListener, ou les deux, et ajoutez un attribut VisualStudioContribution.

Ensuite, implémentez la propriété TextViewExtensionConfiguration , comme requis par ITextViewChangedListener et ITextViewOpenClosedListener, ce qui rend l’écouteur applicable lors de la modification de fichiers C# :

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

Les types de documents disponibles pour d’autres langages de programmation et types de fichiers sont répertoriés plus loin dans cet article, et les types de fichiers personnalisés peuvent également être définis si nécessaire.

En supposant que vous décidez d’implémenter les deux écouteurs, la déclaration de classe terminée doit ressembler à ce qui suit :

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

Étant donné que ITextViewOpenClosedListener et ITextViewChangedListener déclarent la propriété TextViewExtensionConfiguration , la configuration s’applique aux deux écouteurs.

Lorsque vous exécutez votre extension, vous devez voir :

Chacune de ces méthodes est passée à un ITextViewSnapshot contenant l’état de la vue de texte et du document texte au moment où l’utilisateur a appelé l’action et un CancellationToken qui aura IsCancellationRequested == true lorsque l’IDE souhaite annuler une action en attente.

Définir quand votre extension est pertinente

Votre extension est généralement pertinente uniquement pour certains types de documents et scénarios pris en charge, et il est donc important de définir clairement son applicabilité. Vous pouvez utiliser la configuration AppliesTo) de plusieurs façons pour définir clairement l’applicabilité d’une extension. Vous pouvez spécifier les types de fichiers tels que les langages de code pris en charge par l’extension et/ou affiner davantage l’applicabilité d’une extension en fonction d’un modèle basé sur le nom de fichier ou le chemin d’accès.

Spécifier des langages de programmation avec la configuration AppliesTo

La configuration AppliesTo indique les scénarios de langage de programmation dans lesquels l’extension doit être activée. Il est écrit en tant que AppliesTo = new[] { DocumentFilter.FromDocumentType("CSharp") }, où le type de document est un nom bien connu d’un langage intégré à Visual Studio, ou personnalisé défini dans une extension Visual Studio.

Certains types de documents connus sont présentés dans le tableau suivant :

DocumentType ; Description
« CSharp » C#
« C/C++ » C, C++, en-têtes et IDL
« TypeScript » Langages de type TypeScript et JavaScript.
« HTML » HTML
« JSON » JSON
"text" Fichiers texte, y compris les descendants hiérarchiques de « code », qui descend de « text ».
« code » C, C++, C#, et ainsi de suite.

DocumentTypes sont hiérarchiques. Autrement dit, C# et C++ descendent tous les deux de « code », de sorte que la déclaration de « code » entraîne l’activation de votre extension pour tous les langages de code, C#, C, C++, et ainsi de suite.

Définir un nouveau type de document

Vous pouvez définir un nouveau type de document, par exemple pour prendre en charge un langage de code personnalisé, en ajoutant une propriété DocumentTypeConfiguration statique à n’importe quelle classe du projet d’extension et en marquant la propriété avec l’attributVisualStudioContribution.

DocumentTypeConfiguration vous permet de définir un nouveau type de document, de spécifier qu’il hérite d’un ou plusieurs autres types de documents et de spécifier une ou plusieurs extensions de fichier utilisées pour identifier le type de fichier :

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

Les définitions de type de document sont fusionnées avec les définitions de type de contenu fournies par l’extensibilité de Visual Studio héritée, ce qui vous permet de mapper des extensions de fichier supplémentaires aux types de documents existants.

Sélecteurs de documents

En plus de DocumentFilter.FromDocumentType, DocumentFilter.FromGlobPattern vous permet de limiter davantage l’applicabilité de l’extension en le rendant activé uniquement lorsque le chemin du fichier du document correspond à un modèle glob (wild carte) :

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

Le pattern paramètre représente un modèle glob mis en correspondance sur le chemin absolu du document.

Les modèles Glob peuvent avoir la syntaxe suivante :

  • * pour faire correspondre zéro ou plusieurs caractères dans un segment de chemin d’accès
  • ? pour correspondre à un caractère dans un segment de chemin d’accès
  • ** pour correspondre à n’importe quel nombre de segments de chemin d’accès, y compris aucun
  • {} pour regrouper des conditions (par exemple, **​/*.{ts,js} correspond à tous les fichiers TypeScript et JavaScript)
  • [] pour déclarer une plage de caractères à mettre en correspondance dans un segment de chemin d’accès (par exemple, example.[0-9] pour correspondre sur example.0, example.1...)
  • [!...] pour négation d’une plage de caractères à mettre en correspondance dans un segment de chemin d’accès (par exemple, example.[!0-9] pour correspondre sur example.a, example.bmais pas example.0)

Une barre oblique inverse (\) n’est pas valide dans un modèle glob. Veillez à convertir une barre oblique inverse en barre oblique lors de la création du modèle glob.

Fonctionnalités de l’éditeur d’accès

Vos classes d’extension d’éditeur héritent d’ExtensionPart. La ExtensionPart classe expose la propriété Extensibility . À l’aide de cette propriété, vous pouvez demander une instance de l’objet EditorExtensibility . Vous pouvez utiliser cet objet pour accéder aux fonctionnalités de l’éditeur en temps réel, telles que l’exécution de modifications.

EditorExtensibility editorService = this.Extensibility.Editor();

Accéder à l’état de l’éditeur dans une commande

ExecuteCommandAsync()dans chacun d’euxCommand, un IClientContext instantané de l’état de l’IDE au moment où la commande a été appelée. Vous pouvez accéder au document actif via l’interface ITextViewSnapshot , que vous obtenez à partir de l’objet EditorExtensibility en appelant la méthode GetActiveTextViewAsyncasynchrone :

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

Une fois que vous avez ITextViewSnapshot, vous pouvez accéder à l’état de l’éditeur. ITextViewSnapshot est une vue immuable de l’état de l’éditeur à un moment donné. Vous devez donc utiliser les autres interfaces du modèle objet Editor pour apporter des modifications.

Apporter des modifications dans un document texte à partir d’une extension

Les modifications apportées à un document texte ouvert dans l’éditeur Visual Studio peuvent provenir d’interactions utilisateur, de threads dans Visual Studio, tels que les services linguistiques et d’autres extensions. Votre extension doit être prête à traiter les modifications apportées au texte du document en temps réel.

Extensions s’exécutant en dehors du processus d’IDE Principal de Visual Studio qui utilisent des modèles de conception asynchrones pour communiquer avec le processus IDE Visual Studio. Cela signifie que l’utilisation d’appels de méthode asynchrones, comme indiqué par la async mot clé en C# et renforcée par le suffixe sur les Async noms de méthode. L’asynchronicité est un avantage significatif dans le contexte d’un éditeur qui devrait être réactif aux actions des utilisateurs. Un appel d’API synchrone traditionnel, s’il prend plus de temps que prévu, cesse de répondre à l’entrée utilisateur, créant un gel de l’interface utilisateur qui dure jusqu’à ce que l’appel d’API se termine. Les attentes des utilisateurs des applications interactives modernes sont que les éditeurs de texte restent toujours réactifs et ne les empêchent jamais de fonctionner. Il est donc essentiel de disposer d’extensions asynchrones pour répondre aux attentes des utilisateurs.

En savoir plus sur la programmation asynchrone dans la programmation asynchrone avec async et await.

Dans le nouveau modèle d’extensibilité Visual Studio, l’extension est de deuxième classe par rapport à l’utilisateur : elle ne peut pas modifier directement l’éditeur ou le document texte. Toutes les modifications d’état sont asynchrones et coopératives, avec l’IDE Visual Studio effectuant la modification demandée au nom de l’extension. L’extension peut demander une ou plusieurs modifications sur une version spécifique du document ou de la vue texte, mais les modifications d’une extension peuvent être rejetées, par exemple si cette zone du document a changé.

Les modifications sont demandées à l’aide de la EditAsync() méthode sur EditorExtensibility.

Si vous êtes familiarisé avec les extensions Visual Studio héritées, ITextDocumentEditor il est presque identique à l’état changeant les méthodes à partir d’ITextBuffer et ITextDocument et prend en charge la plupart des mêmes fonctionnalités.

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

Pour éviter les modifications mal placées, les modifications des extensions d’éditeur sont appliquées comme suit :

  1. L’extension demande une modification en fonction de sa version la plus récente du document.
  2. Cette demande peut contenir une ou plusieurs modifications de texte, des modifications de position de caresse, etc. Toute implémentation IEditable de type peut être modifiée dans une seule EditAsync() requête, y compris ITextViewSnapshot et ITextDocumentSnapshot. Les modifications sont effectuées par l’éditeur, qui peut être demandé sur une classe spécifique via AsEditable().
  3. Les demandes de modification sont envoyées à l’IDE Visual Studio, où elles réussissent uniquement si l’objet en cours de mutation n’a pas changé depuis la version que la demande a été effectuée. Si le document a changé, la modification peut être rejetée, ce qui oblige l’extension à réessayer sur une version plus récente. Le résultat de l’opération de mutation est stocké dans result.
  4. Les modifications sont appliquées atomiquement, ce qui signifie qu’elles ne sont pas interrompues par d’autres threads en cours d’exécution. La meilleure pratique consiste à effectuer toutes les modifications qui doivent se produire dans un délai limité en un seul EditAsync() appel, afin de réduire la probabilité d’un comportement inattendu résultant des modifications de l’utilisateur ou des actions de service de langage qui se produisent entre les modifications (par exemple, les modifications d’extension se trouvant entrelacées avec Roslyn C# déplaçant le caret).

Exécution asynchrone

ITextViewSnapshot.GetTextDocumentAsync ouvre une copie du document texte dans l’extension Visual Studio. Étant donné que les extensions s’exécutent dans un processus distinct, toutes les interactions d’extension sont asynchrones, coopératives et ont quelques mises en garde :

Attention

GetTextDocumentAsync peut échouer en cas d’appel sur un ancien ITextDocument, car il peut ne plus être mis en cache par le client Visual Studio, si l’utilisateur a apporté de nombreuses modifications depuis sa création. Pour cette raison, si vous envisagez de stocker un ITextView document pour accéder à son document ultérieurement et ne peut pas tolérer l’échec, il peut être judicieux d’appeler GetTextDocumentAsync immédiatement. Cela permet d’extraire le contenu texte de cette version du document dans votre extension, en veillant à ce qu’une copie de cette version soit envoyée à votre extension avant son expiration.

Attention

GetTextDocumentAsync ou MutateAsync peut échouer si l’utilisateur ferme le document.

Exécution simultanée

⚠️ Les extensions de l’éditeur peuvent parfois s’exécuter simultanément

La version initiale présente un problème connu qui peut entraîner l’exécution simultanée du code d’extension de l’éditeur. Chaque méthode asynchrone est garantie d’être appelée dans l’ordre correct, mais les continuations après la première await peuvent être entrelacées. Si votre extension s’appuie sur l’ordre d’exécution, envisagez de conserver une file d’attente de requêtes entrantes pour conserver l’ordre, jusqu’à ce que ce problème soit résolu.

Pour plus d’informations, consultez StreamJsonRpc Ordering default ordering and Concurrency.

Extension de l’éditeur Visual Studio avec une nouvelle marge

Les extensions peuvent contribuer à de nouvelles marges d’affichage de texte à l’éditeur Visual Studio. Une marge d’affichage de texte est un contrôle d’interface utilisateur rectangulaire attaché à une vue de texte sur l’un de ses quatre côtés.

Les marges de la vue de texte sont placées dans un conteneur de marge (voir ContainerMarginPlacement.KnownValues) et ordonnées avant ou après relativement à d’autres marges (voir MarginPlacement.KnownValues).

Les fournisseurs de marge d’affichage de texte implémentent l’interface ITextViewMarginProvider, configurent la marge qu’ils fournissent en implémentant TextViewMarginProviderConfiguration et lors de l’activation, fournissent un contrôle d’interface utilisateur à héberger dans la marge via CreateVisualElementAsync.

Étant donné que les extensions de VisualStudio.Extensibility peuvent être hors processus à partir de Visual Studio, nous ne pouvons pas utiliser directement WPF comme couche de présentation pour le contenu des marges d’affichage de texte. Au lieu de cela, la fourniture d’un contenu à une marge d’affichage de texte nécessite la création d’un RemoteUserControl et le modèle de données correspondant pour ce contrôle. Bien qu’il existe quelques exemples simples ci-dessous, nous vous recommandons de lire la documentation de l’interface utilisateur distante lors de la création du contenu de l’interface utilisateur de marge d’affichage de texte.

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

Outre la configuration de l’emplacement des marges, les fournisseurs de marge d’affichage de texte peuvent également configurer la taille de la cellule de grille dans laquelle la marge doit être placée à l’aide des propriétés GridCellLength et GridUnitType .

Les marges d’affichage de texte visualisent généralement certaines données liées à la vue de texte (par exemple, le numéro de ligne actuel ou le nombre d’erreurs) afin que la plupart des fournisseurs de marges d’affichage de texte souhaitent également écouter les événements d’affichage de texte pour réagir à l’ouverture, à la fermeture des affichages de texte et à la saisie de l’utilisateur.

Visual Studio crée une seule instance de votre fournisseur de marge d’affichage de texte, quel que soit le nombre d’affichages de texte applicables qu’un utilisateur ouvre. Par conséquent, si votre marge affiche des données avec état, votre fournisseur doit conserver l’état des affichages de texte actuellement ouverts.

Pour plus d’informations, consultez l’exemple de marge du nombre de mots.

Les marges d’affichage de texte verticale dont le contenu doit être aligné avec les lignes d’affichage de texte ne sont pas encore prises en charge.

Découvrez les interfaces et les types de l’éditeur dans les concepts de l’éditeur.

Passez en revue un exemple de code pour une extension simple basée sur l’éditeur :

Les utilisateurs avancés peuvent souhaiter en savoir plus sur la prise en charge rpc de l’éditeur.