Uso de la extensibilidad del editor de Visual Studio

El editor de Visual Studio admite extensiones que se agregan a sus funcionalidades. Algunos ejemplos incluyen extensiones que insertan y modifican código en un lenguaje existente.

Para la versión inicial del nuevo modelo de extensibilidad de Visual Studio, solo se admiten las siguientes funcionalidades:

  • Escuchando las vistas de texto que se abren y cierran.
  • Escuchando cambios de estado de la vista de texto (editor).
  • Leer el texto del documento y las selecciones o ubicaciones de intercalación.
  • Realizar modificaciones de texto y cambios de selección o intercalación.
  • Definición de nuevos tipos de documento.
  • Extensión de vistas de texto con nuevos márgenes de vista de texto.

Por lo general, el editor de Visual Studio hace referencia a la funcionalidad de editar archivos de texto, conocidos como documentos, de cualquier tipo. Los archivos individuales se pueden abrir para su edición y la ventana del editor abierto se conoce como .TextView

El modelo de objetos del editor se describe en Conceptos del editor.

Introducción

El código de extensión se puede configurar para ejecutarse en respuesta a varios puntos de entrada (situaciones que se producen cuando un usuario interactúa con Visual Studio). La extensibilidad del editor admite actualmente tres puntos de entrada: agentes de escucha, el objeto de servicio EditorExtensibility y comandos.

Los agentes de escucha de eventos se desencadenan cuando se producen determinadas acciones en una ventana del editor, representada en código por .TextView Por ejemplo, cuando un usuario escribe algo en el editor, se produce un TextViewChanged evento. Cuando se abre o cierra una ventana del editor, TextViewOpened y TextViewClosed se producen eventos.

El objeto de servicio editor es una instancia de la EditorExtensibility clase , que expone la funcionalidad del editor en tiempo real, como realizar modificaciones de texto.

Los comandos los inicia el usuario haciendo clic en un elemento, que puede colocar en un menú, menú contextual o barra de herramientas.

Agregar un agente de escucha de vista de texto

Hay dos tipos de agentes de escucha, ITextViewChangedListener e ITextViewOpenClosedListener. Juntos, estos agentes de escucha se pueden usar para observar la apertura, cierre y modificación de los editores de texto.

A continuación, cree una nueva clase, implemente la clase base ExtensionPart y ITextViewChangedListener, ITextViewOpenClosedListenero ambos, y agregue un atributo VisualStudioContribution .

A continuación, implemente la propiedad TextViewExtensionConfiguration , como requiere ITextViewChangedListener e ITextViewOpenClosedListener, lo que hace que el agente de escucha se aplique al editar archivos de C#:

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

Los tipos de documento disponibles para otros lenguajes de programación y tipos de archivo se enumeran más adelante en este artículo, y los tipos de archivo personalizados también se pueden definir cuando sea necesario.

Suponiendo que decida implementar ambos agentes de escucha, la declaración de clase finalizada debe ser similar a la siguiente:

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

Puesto que ITextViewOpenClosedListener e ITextViewChangedListener declaran la propiedad TextViewExtensionConfiguration , la configuración se aplica a ambos agentes de escucha.

Al ejecutar la extensión, debería ver lo siguiente:

Cada uno de estos métodos se pasa un ITextViewSnapshot que contiene el estado de la vista de texto y el documento de texto en el momento en que el usuario invocó la acción y un CancellationToken que tendrá IsCancellationRequested == true cuando el IDE desee cancelar una acción pendiente.

Definir cuándo es relevante la extensión

La extensión suele ser relevante solo para determinados escenarios y tipos de documentos admitidos, por lo que es importante definir claramente su aplicabilidad. Puede usar la configuración de AppliesTo) de varias maneras para definir claramente la aplicabilidad de una extensión. Puede especificar qué tipos de archivo, como los lenguajes de código que admite la extensión, o bien refinar aún más la aplicabilidad de una extensión mediante la coincidencia en un patrón basado en el nombre de archivo o la ruta de acceso.

Especificar lenguajes de programación con la configuración de AppliesTo

La configuración de AppliesTo indica los escenarios del lenguaje de programación en los que se debe activar la extensión. Se escribe como AppliesTo = new[] { DocumentFilter.FromDocumentType("CSharp") }, donde el tipo de documento es un nombre conocido de un lenguaje integrado en Visual Studio o personalizado definido en una extensión de Visual Studio.

Algunos tipos de documento conocidos se muestran en la tabla siguiente:

DocumentType Descripción
"CSharp" C#
"C/C++" C, C++, encabezados e IDL
"TypeScript" Lenguajes de tipos TypeScript y JavaScript.
"HTML" HTML
"JSON" JSON
"text" Archivos de texto, incluidos descendientes jerárquicos de "código", que descienden de "text".
"código" C, C++, C#, etc.

DocumentTypes son jerárquicos. Es decir, C# y C++ descienden de "código", por lo que declarar "código" hace que la extensión se active para todos los lenguajes de código, C#, C, C++, etc.

Definición de un nuevo tipo de documento

Puede definir un nuevo tipo de documento, por ejemplo, para admitir un lenguaje de código personalizado, agregando una propiedad DocumentTypeConfiguration estática a cualquier clase del proyecto de extensión y marcando la propiedad con el VisualStudioContribution atributo .

DocumentTypeConfiguration permite definir un nuevo tipo de documento, especificar que hereda uno o varios otros tipos de documento y especificar una o varias extensiones de archivo que se usan para identificar el tipo de archivo:

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

Las definiciones de tipo de documento se combinan con definiciones de tipo de contenido proporcionadas por la extensibilidad heredada de Visual Studio, lo que permite asignar extensiones de archivo adicionales a los tipos de documento existentes.

Selectores de documentos

Además de DocumentFilter.FromDocumentType, DocumentFilter.FromGlobPattern permite limitar aún más la aplicabilidad de la extensión haciendo que se active solo cuando la ruta de acceso del archivo del documento coincida con un patrón global (comodín):

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

El pattern parámetro representa un patrón global que coincide con la ruta de acceso absoluta del documento.

Los patrones Glob pueden tener la sintaxis siguiente:

  • * para que coincida con cero o más caracteres en un segmento de ruta de acceso
  • ? para que coincida con un carácter en un segmento de ruta de acceso
  • ** para que coincida con cualquier número de segmentos de ruta de acceso, incluidos ninguno
  • {} para agrupar condiciones (por ejemplo, **​/*.{ts,js} coincide con todos los archivos TypeScript y JavaScript)
  • [] para declarar un intervalo de caracteres que coincidan en un segmento de ruta de acceso (por ejemplo, example.[0-9] para que coincida con en example.0, example.1, ...)
  • [!...] para negar un intervalo de caracteres que coincidan en un segmento de ruta de acceso (por ejemplo, example.[!0-9] para que coincida con en example.a, example.b, pero no example.0)

Una barra diagonal inversa (\) no es válida dentro de un patrón global. Asegúrese de convertir las barras diagonales inversas a la barra diagonal al crear el patrón global.

Funcionalidad del editor de acceso

Las clases de extensión del editor heredan de ExtensionPart. La ExtensionPart clase expone la propiedad Extensibilidad . Con esta propiedad, puede solicitar una instancia del objeto EditorExtensibility . Puede usar este objeto para acceder a la funcionalidad del editor en tiempo real, como realizar modificaciones.

EditorExtensibility editorService = this.Extensibility.Editor();

Acceso al estado del editor dentro de un comando

ExecuteCommandAsync() en cada Command se pasa un IClientContext que contiene una instantánea del estado del IDE en el momento en que se invocó el comando. Puede acceder al documento activo a través de la ITextViewSnapshot interfaz , que obtiene desde el EditorExtensibility objeto llamando al método GetActiveTextViewAsyncasincrónico :

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

Una vez que tenga ITextViewSnapshot, puede acceder al estado del editor. ITextViewSnapshot es una vista inmutable del estado del editor en un momento dado, por lo que debe usar las otras interfaces del modelo de objetos editor para realizar modificaciones.

Realizar cambios en un documento de texto de una extensión

Las modificaciones, es decir, los cambios en un documento de texto abierto en el editor de Visual Studio pueden surgir de interacciones de usuario, subprocesos en Visual Studio, como servicios de lenguaje y otras extensiones. La extensión debe estar preparada para tratar los cambios en el texto del documento que se produce en tiempo real.

Extensiones que se ejecutan fuera del proceso principal del IDE de Visual Studio que usan patrones de diseño asincrónicos para comunicarse con el proceso del IDE de Visual Studio. Esto significa el uso de llamadas de método asincrónico, como se indica en la async palabra clave en C# y reforzado por el sufijo en los Async nombres de método. La asincronía es una ventaja significativa en el contexto de un editor que se espera que responda a las acciones del usuario. Una llamada api sincrónica tradicional, si tarda más de lo esperado, dejará de responder a la entrada del usuario, creando una inmovilización de la interfaz de usuario que dura hasta que se complete la llamada API. Las expectativas del usuario de las aplicaciones interactivas modernas son que los editores de texto siempre siguen respondiendo y nunca los impide que funcionen. Por lo tanto, tener extensiones asincrónicas es esencial para satisfacer las expectativas del usuario.

Obtenga más información sobre la programación asincrónica en Programación asincrónica con async y await.

En el nuevo modelo de extensibilidad de Visual Studio, la extensión es la segunda clase relativa al usuario: no puede modificar directamente el editor ni el documento de texto. Todos los cambios de estado son asincrónicos y cooperativos, con el IDE de Visual Studio que realiza el cambio solicitado en nombre de la extensión. La extensión puede solicitar uno o varios cambios en una versión específica del documento o la vista de texto, pero se pueden rechazar los cambios de una extensión, como si ese área del documento haya cambiado.

Las modificaciones se solicitan mediante el EditAsync() método en EditorExtensibility.

Si está familiarizado con las extensiones heredadas de Visual Studio, ITextDocumentEditor es casi igual que los métodos de cambio de estado de ITextBuffer e ITextDocument y admite la mayoría de las mismas funcionalidades.

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

Para evitar modificaciones mal colocadas, las modificaciones de las extensiones del editor se aplican de la siguiente manera:

  1. La extensión solicita que se realice una edición, en función de su versión más reciente del documento.
  2. Esa solicitud puede contener una o más modificaciones de texto, cambios de posición de intercalación, etc. Cualquier tipo de implementación IEditable se puede cambiar en una sola EditAsync() solicitud, incluidos ITextViewSnapshot y ITextDocumentSnapshot. El editor realiza modificaciones, que se pueden solicitar en una clase específica a través de AsEditable().
  3. Las solicitudes de edición se envían al IDE de Visual Studio, donde solo se realiza correctamente si el objeto que se va a mutar no ha cambiado desde la versión en la que se realizó la solicitud. Si el documento ha cambiado, se puede rechazar el cambio, lo que requiere que la extensión vuelva a intentarlo en una versión más reciente. El resultado de la operación de mutación se almacena en result.
  4. Las modificaciones se aplican de forma atómica, lo que significa que sin interrupción de otros subprocesos en ejecución. El procedimiento recomendado consiste en realizar todos los cambios que deben producirse dentro de un período de tiempo limitado en una sola EditAsync() llamada, para reducir la probabilidad de un comportamiento inesperado derivado de las ediciones del usuario o las acciones del servicio de lenguaje que se producen entre ediciones (por ejemplo, las modificaciones de extensión se intercalan con Roslyn C# moviendo el símbolo de intercalación).

Ejecución asincrónica

ITextViewSnapshot.GetTextDocumentAsync abre una copia del documento de texto en la extensión de Visual Studio. Dado que las extensiones se ejecutan en un proceso independiente, todas las interacciones de extensión son asincrónicas, cooperativas y tienen algunas advertencias:

Precaución

GetTextDocumentAsync es posible que se produzca un error si se llama a en un antiguo ITextDocument, ya que el cliente de Visual Studio ya no la almacena en caché, si el usuario ha realizado muchos cambios desde que se creó. Por este motivo, si planea almacenar un ITextView para acceder a su documento más adelante y no puede tolerar errores, puede ser una buena idea llamar GetTextDocumentAsync inmediatamente. Al hacerlo, captura el contenido de texto de esa versión del documento en la extensión, asegurándose de que se envía una copia de esa versión a la extensión antes de que expire.

Precaución

GetTextDocumentAsync o MutateAsync puede producirse un error si el usuario cierra el documento.

Ejecución simultánea

⚠️ A veces, las extensiones del editor se pueden ejecutar simultáneamente

La versión inicial tiene un problema conocido que puede dar lugar a la ejecución simultánea del código de extensión del editor. Se garantiza que se llame a cada método asincrónico en el orden correcto, pero las continuaciones después de la primera await se pueden intercalar. Si la extensión se basa en el orden de ejecución, considere la posibilidad de mantener una cola de solicitudes entrantes para conservar el orden, hasta que se corrigiera este problema.

Para obtener más información, consulte StreamJsonRpc Default Ordering and Concurrency( Ordenación y simultaneidad predeterminada de StreamJsonRpc).

Extensión del editor de Visual Studio con un nuevo margen

Las extensiones pueden contribuir a los nuevos márgenes de vista de texto al editor de Visual Studio. Un margen de vista de texto es un control de interfaz de usuario rectangular adjunto a una vista de texto en uno de sus cuatro lados.

Los márgenes de la vista de texto se colocan en un contenedor de margen (vea ContainerMarginPlacement.KnownValues) y se ordenan antes o después de otros márgenes (vea MarginPlacement.KnownValues).

Los proveedores de margen de vista de texto implementan la interfaz ITextViewMarginProvider, configuran el margen que proporcionan mediante la implementación de TextViewMarginProviderConfiguration y, cuando se activan, proporcionan control de interfaz de usuario que se hospedará en el margen a través de CreateVisualElementAsync.

Dado que las extensiones de VisualStudio.Extensibility podrían estar fuera de proceso desde Visual Studio, no podemos usar directamente WPF como capa de presentación para el contenido de los márgenes de la vista de texto. En su lugar, proporcionar un contenido a un margen de vista de texto requiere la creación de remoteUserControl y la plantilla de datos correspondiente para ese control. Aunque hay algunos ejemplos sencillos a continuación, se recomienda leer la documentación de la interfaz de usuario remota al crear contenido de la interfaz de usuario de margen de vista 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);
}

Además de configurar la colocación de margen, los proveedores de margen de vista de texto también pueden configurar el tamaño de la celda de cuadrícula en la que se debe colocar el margen mediante las propiedades GridCellLength y GridUnitType .

Los márgenes de vista de texto suelen visualizar algunos datos relacionados con la vista de texto (por ejemplo, el número de línea actual o el recuento de errores), por lo que la mayoría de los proveedores de margen de vista de texto también querrían escuchar los eventos de vista de texto para reaccionar a la apertura, el cierre de las vistas de texto y la escritura del usuario.

Visual Studio solo crea una instancia del proveedor de margen de vista de texto independientemente de cuántas vistas de texto aplicables se abra un usuario, por lo que si el margen muestra algunos datos con estado, el proveedor debe mantener el estado de las vistas de texto abiertas actualmente.

Para obtener más información, vea Ejemplo de margen de recuento de palabras.

Los márgenes de la vista de texto vertical cuyo contenido debe alinearse con las líneas de vista de texto aún no se admiten.

Obtenga información sobre las interfaces y los tipos del editor en Conceptos del editor.

Revise el código de ejemplo para obtener una extensión sencilla basada en editor:

Es posible que los usuarios avanzados deseen obtener información sobre la compatibilidad con RPC del editor.