Usar extensibilidade do editor do Visual Studio

O editor do Visual Studio oferece suporte a extensões que adicionam aos seus recursos. Os exemplos incluem extensões que inserem e modificam código em um idioma existente.

Para a versão inicial do novo modelo de extensibilidade do Visual Studio, somente os seguintes recursos são suportados:

  • Escutar exibições de texto sendo abertas e fechadas.
  • Escutar as alterações de estado do modo de exibição de texto (editor).
  • Leitura do texto do documento e dos locais de seleções/acento circunflexos.
  • Execução de edições de texto e alterações de seleção/acento circunflexo.
  • Definição de novos tipos de documento.
  • Estendendo modos de exibição de texto com novas margens de exibição de texto.

O editor do Visual Studio geralmente se refere à funcionalidade de edição de arquivos de texto, conhecidos como documentos, de qualquer tipo. Arquivos individuais podem ser abertos para edição, e a janela aberta do editor é chamada de TextView.

O modelo de objeto do editor é descrito em Conceitos do editor.

Introdução

Seu código de extensão pode ser configurado para ser executado em resposta a vários pontos de entrada (situações que ocorrem quando um usuário interage com o Visual Studio). Atualmente, a extensibilidade do editor oferece suporte a três pontos de entrada: ouvintes, o objeto de serviço EditorExtensibility e comandos.

Os ouvintes de eventos são acionados quando determinadas ações ocorrem em uma janela do editor, representada no código por um TextViewarquivo . Por exemplo, quando um usuário digita algo no editor, ocorre um TextViewChanged evento. Quando uma janela do editor é aberta ou fechada TextViewOpened e TextViewClosed eventos ocorrem.

O objeto de serviço do editor é uma instância da classe, que expõe a funcionalidade do editor em tempo real, como a EditorExtensibility execução de edições de texto.

Os comandos são iniciados pelo usuário clicando em um item, que você pode colocar em um menu, menu de contexto ou barra de ferramentas.

Adicionar um ouvinte de exibição de texto

Há dois tipos de ouvintes, ITextViewChangedListener e ITextViewOpenClosedListener. Juntos, esses ouvintes podem ser usados para observar o abrir, fechar e modificar editores de texto.

Em seguida, crie uma nova classe, implementando a classe base ExtensionPart e , ou ambas, ITextViewOpenClosedListenere ITextViewChangedListeneradicione um atributo VisualStudioContribution.

Em seguida, implemente a propriedade TextViewExtensionConfiguration, conforme exigido por ITextViewChangedListener e ITextViewOpenClosedListener, fazendo com que o ouvinte se aplique ao editar arquivos C#:

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

Os tipos de documento disponíveis para outras linguagens de programação e tipos de arquivo são listados posteriormente neste artigo, e tipos de arquivo personalizados também podem ser definidos quando necessário.

Supondo que você decida implementar ambos os ouvintes, a declaração de classe concluída deve ter a seguinte aparência:

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

Como ITextViewOpenClosedListener e ITextViewChangedListener declaram a propriedade TextViewExtensionConfiguration, a configuração se aplica a ambos os ouvintes.

Ao executar sua extensão, você deverá ver:

A cada um desses métodos é passado um ITextViewSnapshot contendo o estado da exibição de texto e do documento de texto no momento em que o usuário invocou a ação e um CancellationToken que terá IsCancellationRequested == true quando o IDE desejar cancelar uma ação pendente.

Definir quando sua extensão é relevante

Sua extensão geralmente é relevante apenas para determinados tipos de documentos e cenários com suporte e, portanto, é importante definir claramente sua aplicabilidade. Você pode usar a configuração AppliesTo) de várias maneiras para definir claramente a aplicabilidade de uma extensão. Você pode especificar quais tipos de arquivo, como idiomas de código, a extensão oferece suporte e/ou refinar ainda mais a aplicabilidade de uma extensão combinando em um padrão com base no nome do arquivo ou caminho.

Especificar linguagens de programação com a configuração AppliesTo

A configuração AppliesTo indica os cenários de linguagem de programação nos quais a extensão deve ser ativada. Ele é escrito como , onde AppliesTo = new[] { DocumentFilter.FromDocumentType("CSharp") }o tipo de documento é um nome bem conhecido de uma linguagem criada no Visual Studio, ou personalizada definida em uma extensão do Visual Studio.

Alguns tipos de documento conhecidos são mostrados na tabela a seguir:

DocumentType Descrição
"CSharp" C#
"C/C++" C, C++, cabeçalhos e IDL
"TypeScript" Linguagens de tipo TypeScript e JavaScript.
"HTML" HTML
"JSON" JSON
"texto" Arquivos de texto, incluindo descendentes hierárquicos de "código", que descende de "texto".
"código" C, C++, C# e assim por diante.

DocumentTypes são hierárquicos. Ou seja, C# e C++ descendem de "code", então declarar "code" faz com que sua extensão seja ativada para todas as linguagens de código, C#, C, C++ e assim por diante.

Definir um novo tipo de documento

Você pode definir um novo tipo de documento, por exemplo, para oferecer suporte a uma linguagem de código personalizada, adicionando uma propriedade estática DocumentTypeConfiguration a qualquer classe no projeto de extensão e marcando a propriedade com o VisualStudioContribution atributo.

DocumentTypeConfiguration Permite definir um novo tipo de documento, especificar que ele herda um ou mais outros tipos de documento e especificar uma ou mais extensões de arquivo usadas para identificar o tipo de arquivo:

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

As definições de tipo de documento são mescladas com as definições de tipo de conteúdo fornecidas pela extensibilidade herdada do Visual Studio, que permite mapear extensões de arquivo adicionais para tipos de documento existentes.

Seletores de documentos

Além de DocumentFilter.FromDocumentType, DocumentFilter.FromGlobPattern permite limitar ainda mais a aplicabilidade da extensão, tornando-a ativada somente quando o caminho do arquivo do documento corresponde a um padrão glob (curinga):

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

O pattern parâmetro representa um padrão glob que é correspondido no caminho absoluto do documento.

Os padrões de Glob podem ter a seguinte sintaxe:

  • * Para corresponder a zero ou mais caracteres em um segmento de caminho
  • ? para corresponder em um caractere em um segmento de caminho
  • ** para corresponder a qualquer número de segmentos de caminho, incluindo nenhum
  • {} para agrupar condições (por exemplo, **​/*.{ts,js} corresponde a todos os arquivos TypeScript e JavaScript)
  • []Para declarar um intervalo de caracteres a ser correspondido em um segmento de caminho (por exemplo, para corresponder em example.0, example.[0-9]example.1, ...)
  • [!...]para negar um intervalo de caracteres a serem correspondidos em um segmento de caminho (por exemplo, para corresponder em , , example.[!0-9]example.bmas não example.0)example.a

Uma barra invertida (\) não é válida dentro de um padrão glob. Certifique-se de converter qualquer barra invertida em barra ao criar o padrão glob.

Funcionalidade do editor de acesso

Suas classes de extensão do editor herdam de ExtensionPart. A ExtensionPart classe expõe a propriedade Extensibility . Usando essa propriedade, você pode solicitar uma instância do objeto EditorExtensibility . Você pode usar esse objeto para acessar a funcionalidade do editor em tempo real, como executar edições.

EditorExtensibility editorService = this.Extensibility.Editor();

Estado do editor de acesso em um comando

ExecuteCommandAsync() em cada Command um é passado um que contém um IClientContext instantâneo do estado do IDE no momento em que o comando foi invocado. Você pode acessar o documento ativo através da ITextViewSnapshot interface, que você obtém do EditorExtensibility objeto chamando o método GetActiveTextViewAsyncassíncrono :

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

Depois de ter ITextViewSnapshoto , você pode acessar o estado do editor. ITextViewSnapshoté uma exibição imutável do estado do editor em um ponto no tempo, portanto, você precisa usar as outras interfaces no modelo de objeto Editor para fazer edições.

Fazer alterações em um documento de texto a partir de uma extensão

As edições, ou seja, alterações em um documento de texto aberto no editor do Visual Studio, podem surgir de interações do usuário, threads no Visual Studio, como serviços de linguagem e outras extensões. Sua extensão deve estar preparada para lidar com alterações no texto do documento que ocorrem em tempo real.

Extensões em execução fora do processo principal do IDE do Visual Studio que usam padrões de design assíncronos para se comunicar com o processo do IDE do Visual Studio. Isso significa o uso de chamadas de método assíncronas, conforme indicado pela async palavra-chave em C# e reforçada pelo sufixo Async em nomes de método. A assincronicidade é uma vantagem significativa no contexto de um editor que se espera que responda às ações do usuário. Uma chamada de API síncrona tradicional, se demorar mais do que o esperado, deixará de responder à entrada do usuário, criando um congelamento da interface do usuário que dura até que a chamada de API seja concluída. As expectativas dos usuários dos aplicativos interativos modernos são que os editores de texto permaneçam sempre responsivos e nunca os bloqueiem de trabalhar. Ter extensões assíncronas é, portanto, essencial para atender às expectativas do usuário.

Saiba mais sobre programação assíncrona em Programação assíncrona com assíncrono e aguarde.

No novo modelo de extensibilidade do Visual Studio, a extensão é de segunda classe em relação ao usuário: ela não pode modificar diretamente o editor ou o documento de texto. Todas as alterações de estado são assíncronas e cooperativas, com o IDE do Visual Studio executando a alteração solicitada em nome da extensão. A extensão pode solicitar uma ou mais alterações em uma versão específica do documento ou do modo de exibição de texto, mas as alterações de uma extensão podem ser rejeitadas, como se essa área do documento tiver sido alterada.

As edições são solicitadas usando o EditAsync() método em EditorExtensibility.

Se você estiver familiarizado com extensões herdadas do Visual Studio, ITextDocumentEditor é quase o mesmo que os métodos de alteração de estado de ITextBuffer e ITextDocument e oferece suporte à maioria dos mesmos recursos.

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

Para evitar edições extraviadas, as edições das extensões do editor são aplicadas da seguinte maneira:

  1. A extensão solicita que uma edição seja feita, com base em sua versão mais recente do documento.
  2. Essa solicitação pode conter uma ou mais edições de texto, alterações de posição do cursor e assim por diante. Qualquer tipo de implementação IEditable pode ser alterado em uma única EditAsync() solicitação, incluindo ITextViewSnapshot e ITextDocumentSnapshot. As edições são feitas pelo editor, que pode ser solicitado em uma classe específica via AsEditable().
  3. As solicitações de edição são enviadas para o IDE do Visual Studio, onde ele é bem-sucedido somente se o objeto que está sendo mutado não tiver sido alterado desde a versão em que a solicitação foi feita. Se o documento tiver sido alterado, a alteração poderá ser rejeitada, exigindo que a extensão tente novamente na versão mais recente. O resultado da operação de mutação é armazenado em result.
  4. As edições são aplicadas atomicamente, ou seja, sem interrupção de outros threads em execução. A prática recomendada é fazer todas as alterações que devem ocorrer dentro de um período de tempo estreito em uma única EditAsync() chamada, para reduzir a probabilidade de comportamento inesperado decorrente de edições do usuário ou ações de serviço de idioma que ocorrem entre as edições (por exemplo, edições de extensão sendo intercaladas com o Roslyn C# movendo o acento circunflexo).

Execução assíncrona

ITextViewSnapshot.GetTextDocumentAsync abre uma cópia do documento de texto na extensão do Visual Studio. Como as extensões são executadas em um processo separado, todas as interações de extensão são assíncronas, cooperativas e têm algumas ressalvas:

Cuidado

GetTextDocumentAsync pode falhar se chamado em um ITextDocumentantigo , porque ele pode não ser mais armazenado em cache pelo cliente do Visual Studio, se o usuário tiver feito muitas alterações desde que foi criado. Por esse motivo, se você planeja armazenar um ITextView para acessar seu documento mais tarde e não pode tolerar falhas, pode ser uma boa ideia ligar GetTextDocumentAsync imediatamente. Isso busca o conteúdo de texto dessa versão do documento em sua extensão, garantindo que uma cópia dessa versão seja enviada para sua extensão antes que ela expire.

Cuidado

GetTextDocumentAsync ou MutateAsync pode falhar se o usuário fechar o documento.

Execução simultânea

⚠️ Às vezes, as extensões do editor podem ser executadas simultaneamente

A versão inicial tem um problema conhecido que pode resultar na execução simultânea do código de extensão do editor. É garantido que cada método assíncrono seja chamado na ordem correta, mas as continuações após o primeiro await podem ser intercaladas. Se sua extensão depender da ordem de execução, considere manter uma fila de solicitações de entrada para preservar a ordem, até que esse problema seja corrigido.

Para obter mais informações, consulte Ordenação e simultaneidade padrão do StreamJsonRpc.

Estendendo o editor do Visual Studio com uma nova margem

As extensões podem contribuir com novas margens de exibição de texto para o editor do Visual Studio. Uma margem de exibição de texto é um controle de interface do usuário retangular anexado a um modo de exibição de texto em um de seus quatro lados.

As margens de exibição de texto são colocadas em um contêiner de margem (consulte ContainerMarginPlacement.KnownValues) e ordenadas antes ou depois relativamente a outras margens (consulte MarginPlacement.KnownValues).

Os provedores de margem de exibição de texto implementam a interface ITextViewMarginProvider, configuram a margem que fornecem implementando TextViewMarginProviderConfiguration e, quando ativados, fornecem controle de interface do usuário a ser hospedado na margem por meio de CreateVisualElementAsync.

Como as extensões no VisualStudio.Extensibility podem estar fora do processo do Visual Studio, não podemos usar diretamente o WPF como uma camada de apresentação para o conteúdo das margens de exibição de texto. Em vez disso, fornecer um conteúdo para uma margem de exibição de texto requer a criação de um RemoteUserControl e o modelo de dados correspondente para esse controle. Embora existam alguns exemplos simples abaixo, recomendamos a leitura da documentação da interface do usuário remota ao criar conteúdo da interface do usuário de margem de exibição de texto.

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

Além de configurar o posicionamento da margem, os provedores de margem de exibição de texto também podem configurar o tamanho da célula de grade na qual a margem deve ser colocada usando as propriedades GridCellLength e GridUnitType .

As margens de exibição de texto normalmente visualizam alguns dados relacionados à exibição de texto (por exemplo, o número de linha atual ou a contagem de erros), portanto, a maioria dos provedores de margem de exibição de texto também gostaria de ouvir eventos de exibição de texto para reagir à abertura, fechamento de exibições de texto e digitação do usuário.

O Visual Studio cria apenas uma instância do seu provedor de margem de exibição de texto, independentemente de quantas exibições de texto aplicáveis um usuário abre, portanto, se sua margem exibir alguns dados com monitoração de estado, seu provedor precisará manter o estado das exibições de texto abertas no momento.

Para obter mais informações, consulte Exemplo de margem de contagem de palavras.

Ainda não há suporte para margens de exibição de texto vertical cujo conteúdo precisa ser alinhado com linhas de exibição de texto.

Saiba mais sobre as interfaces e os tipos do editor em Conceitos do editor.

Revise o código de exemplo para obter uma extensão simples baseada em editor:

Usuários avançados podem querer saber mais sobre o suporte a RPC do Editor.