Tutorial: Esquematización

Configure características basadas en lenguaje, como la esquematización, definiendo los tipos de regiones de texto que desea expandir o contraer. Puede definir regiones en el contexto de un servicio de lenguaje, o definir su propia extensión de nombre de archivo y tipo de contenido y aplicar la definición de región solo a ese tipo, o aplicar las definiciones de región a un tipo de contenido existente (como "texto"). En este tutorial se muestra cómo definir y mostrar regiones de esquematización.

Creación de un proyecto de Managed Extensibility Framework (MEF)

Para crear un nuevo proyecto de MEF

  1. Crear un proyecto de VSIX. Asigne a la solución el nombre OutlineRegionTest.

  2. Agregue una plantilla de elemento clasificador del editor al proyecto. Para obtener más información, vea Creación de una extensión con una plantilla de elemento de editor.

  3. Elimine los archivos de clase existentes.

Implementación de un tagger de esquematización

Las regiones de esquematización se marcan mediante un tipo de etiqueta (OutliningRegionTag). Esta etiqueta proporciona el comportamiento de esquematización estándar. La región descrita se puede expandir o contraer. La región descrita está marcada por un signo más (+) si está contraído o un signo menos (-) si se expande y la región expandida se demarca mediante una línea vertical.

Los pasos siguientes muestran cómo definir un tagger que crea regiones de esquematización para todas las regiones delimitadas por los corchetes ([,]).

Para implementar un tagger de esquematización

  1. Agregue un archivo de clase y asígnele el nombre OutliningTagger.

  2. Importe los siguientes espacios de nombres.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.ComponentModel.Composition;
    using Microsoft.VisualStudio.Text.Outlining;
    using Microsoft.VisualStudio.Text.Tagging;
    using Microsoft.VisualStudio.Utilities;
    using Microsoft.VisualStudio.Text;
    
  3. Cree una clase denominada OutliningTaggery haga que implemente ITagger<T>:

    internal sealed class OutliningTagger : ITagger<IOutliningRegionTag>
    
  4. Agregue algunos campos para realizar un seguimiento del búfer de texto y la instantánea y acumular los conjuntos de líneas que se deben etiquetar como regiones de esquematización. Este código incluye una lista de objetos Region (que se definirán más adelante) que representan las regiones de esquematización.

    string startHide = "[";     //the characters that start the outlining region
    string endHide = "]";       //the characters that end the outlining region
    string ellipsis = "...";    //the characters that are displayed when the region is collapsed
    string hoverText = "hover text"; //the contents of the tooltip for the collapsed span
    ITextBuffer buffer;
    ITextSnapshot snapshot;
    List<Region> regions;
    
  5. Agregue un constructor de etiquetas que inicialice los campos, analice el búfer y agregue un controlador de eventos al Changed evento.

    public OutliningTagger(ITextBuffer buffer)
    {
        this.buffer = buffer;
        this.snapshot = buffer.CurrentSnapshot;
        this.regions = new List<Region>();
        this.ReParse();
        this.buffer.Changed += BufferChanged;
    }
    
  6. Implemente el GetTags método , que crea una instancia de los intervalos de etiquetas. En este ejemplo se supone que los intervalos del NormalizedSpanCollection pasado al método son contiguos, aunque puede que no siempre sea el caso. Este método crea una instancia de un nuevo intervalo de etiquetas para cada una de las regiones de esquematización.

    public IEnumerable<ITagSpan<IOutliningRegionTag>> GetTags(NormalizedSnapshotSpanCollection spans)
    {
        if (spans.Count == 0)
            yield break;
        List<Region> currentRegions = this.regions;
        ITextSnapshot currentSnapshot = this.snapshot;
        SnapshotSpan entire = new SnapshotSpan(spans[0].Start, spans[spans.Count - 1].End).TranslateTo(currentSnapshot, SpanTrackingMode.EdgeExclusive);
        int startLineNumber = entire.Start.GetContainingLine().LineNumber;
        int endLineNumber = entire.End.GetContainingLine().LineNumber;
        foreach (var region in currentRegions)
        {
            if (region.StartLine <= endLineNumber &&
                region.EndLine >= startLineNumber)
            {
                var startLine = currentSnapshot.GetLineFromLineNumber(region.StartLine);
                var endLine = currentSnapshot.GetLineFromLineNumber(region.EndLine);
    
                //the region starts at the beginning of the "[", and goes until the *end* of the line that contains the "]".
                yield return new TagSpan<IOutliningRegionTag>(
                    new SnapshotSpan(startLine.Start + region.StartOffset,
                    endLine.End),
                    new OutliningRegionTag(false, false, ellipsis, hoverText));
            }
        }
    }
    
  7. Declare un TagsChanged controlador de eventos.

    public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
    
  8. Agregue un BufferChanged controlador de eventos que responda a Changed eventos mediante el análisis del búfer de texto.

    void BufferChanged(object sender, TextContentChangedEventArgs e)
    {
        // If this isn't the most up-to-date version of the buffer, then ignore it for now (we'll eventually get another change event).
        if (e.After != buffer.CurrentSnapshot)
            return;
        this.ReParse();
    }
    
  9. Agregue un método que analice el búfer. El ejemplo que se proporciona aquí es solo para ilustración. Analiza sincrónicamente el búfer en regiones de esquematización anidadas.

    void ReParse()
    {
        ITextSnapshot newSnapshot = buffer.CurrentSnapshot;
        List<Region> newRegions = new List<Region>();
    
        //keep the current (deepest) partial region, which will have
        // references to any parent partial regions.
        PartialRegion currentRegion = null;
    
        foreach (var line in newSnapshot.Lines)
        {
            int regionStart = -1;
            string text = line.GetText();
    
            //lines that contain a "[" denote the start of a new region.
            if ((regionStart = text.IndexOf(startHide, StringComparison.Ordinal)) != -1)
            {
                int currentLevel = (currentRegion != null) ? currentRegion.Level : 1;
                int newLevel;
                if (!TryGetLevel(text, regionStart, out newLevel))
                    newLevel = currentLevel + 1;
    
                //levels are the same and we have an existing region;
                //end the current region and start the next
                if (currentLevel == newLevel && currentRegion != null)
                {
                    newRegions.Add(new Region()
                    {
                        Level = currentRegion.Level,
                        StartLine = currentRegion.StartLine,
                        StartOffset = currentRegion.StartOffset,
                        EndLine = line.LineNumber
                    });
    
                    currentRegion = new PartialRegion()
                    {
                        Level = newLevel,
                        StartLine = line.LineNumber,
                        StartOffset = regionStart,
                        PartialParent = currentRegion.PartialParent
                    };
                }
                //this is a new (sub)region
                else
                {
                    currentRegion = new PartialRegion()
                    {
                        Level = newLevel,
                        StartLine = line.LineNumber,
                        StartOffset = regionStart,
                        PartialParent = currentRegion
                    };
                }
            }
            //lines that contain "]" denote the end of a region
            else if ((regionStart = text.IndexOf(endHide, StringComparison.Ordinal)) != -1)
            {
                int currentLevel = (currentRegion != null) ? currentRegion.Level : 1;
                int closingLevel;
                if (!TryGetLevel(text, regionStart, out closingLevel))
                    closingLevel = currentLevel;
    
                //the regions match
                if (currentRegion != null &&
                    currentLevel == closingLevel)
                {
                    newRegions.Add(new Region()
                    {
                        Level = currentLevel,
                        StartLine = currentRegion.StartLine,
                        StartOffset = currentRegion.StartOffset,
                        EndLine = line.LineNumber
                    });
    
                    currentRegion = currentRegion.PartialParent;
                }
            }
        }
    
        //determine the changed span, and send a changed event with the new spans
        List<Span> oldSpans =
            new List<Span>(this.regions.Select(r => AsSnapshotSpan(r, this.snapshot)
                .TranslateTo(newSnapshot, SpanTrackingMode.EdgeExclusive)
                .Span));
        List<Span> newSpans =
                new List<Span>(newRegions.Select(r => AsSnapshotSpan(r, newSnapshot).Span));
    
        NormalizedSpanCollection oldSpanCollection = new NormalizedSpanCollection(oldSpans);
        NormalizedSpanCollection newSpanCollection = new NormalizedSpanCollection(newSpans);
    
        //the changed regions are regions that appear in one set or the other, but not both.
        NormalizedSpanCollection removed =
        NormalizedSpanCollection.Difference(oldSpanCollection, newSpanCollection);
    
        int changeStart = int.MaxValue;
        int changeEnd = -1;
    
        if (removed.Count > 0)
        {
            changeStart = removed[0].Start;
            changeEnd = removed[removed.Count - 1].End;
        }
    
        if (newSpans.Count > 0)
        {
            changeStart = Math.Min(changeStart, newSpans[0].Start);
            changeEnd = Math.Max(changeEnd, newSpans[newSpans.Count - 1].End);
        }
    
        this.snapshot = newSnapshot;
        this.regions = newRegions;
    
        if (changeStart <= changeEnd)
        {
            ITextSnapshot snap = this.snapshot;
            if (this.TagsChanged != null)
                this.TagsChanged(this, new SnapshotSpanEventArgs(
                    new SnapshotSpan(this.snapshot, Span.FromBounds(changeStart, changeEnd))));
        }
    }
    
  10. El siguiente método auxiliar obtiene un entero que representa el nivel de esquematización, de modo que 1 es el par de llaves más a la izquierda.

    static bool TryGetLevel(string text, int startIndex, out int level)
    {
        level = -1;
        if (text.Length > startIndex + 3)
        {
            if (int.TryParse(text.Substring(startIndex + 1), out level))
                return true;
        }
    
        return false;
    }
    
  11. El siguiente método auxiliar traduce una región (definida más adelante en este artículo) en snapshotSpan.

    static SnapshotSpan AsSnapshotSpan(Region region, ITextSnapshot snapshot)
    {
        var startLine = snapshot.GetLineFromLineNumber(region.StartLine);
        var endLine = (region.StartLine == region.EndLine) ? startLine
             : snapshot.GetLineFromLineNumber(region.EndLine);
        return new SnapshotSpan(startLine.Start + region.StartOffset, endLine.End);
    }
    
  12. El código siguiente es solo para la ilustración. Define una clase PartialRegion que contiene el número de línea y el desplazamiento del inicio de una región de esquematización y una referencia a la región primaria (si existe). Este código permite al analizador configurar regiones de esquematización anidadas. Una clase Region derivada contiene una referencia al número de línea del final de una región de esquematización.

    class PartialRegion
    {
        public int StartLine { get; set; }
        public int StartOffset { get; set; }
        public int Level { get; set; }
        public PartialRegion PartialParent { get; set; }
    }
    
    class Region : PartialRegion
    {
        public int EndLine { get; set; }
    }
    

Implementación de un proveedor de etiquetas

Exporte un proveedor de etiquetas para el tagger. El proveedor de etiquetas crea un OutliningTagger para un búfer del tipo de contenido "texto", o bien devuelve un OutliningTagger si el búfer ya tiene uno.

Para implementar un proveedor de etiquetas

  1. Cree una clase denominada OutliningTaggerProvider que implemente ITaggerProvidery expórtela con los atributos ContentType y TagType.

    [Export(typeof(ITaggerProvider))]
    [TagType(typeof(IOutliningRegionTag))]
    [ContentType("text")]
    internal sealed class OutliningTaggerProvider : ITaggerProvider
    
  2. Implemente el CreateTagger método agregando un OutliningTagger objeto a las propiedades del búfer.

    public ITagger<T> CreateTagger<T>(ITextBuffer buffer) where T : ITag
    {
        //create a single tagger for each buffer.
        Func<ITagger<T>> sc = delegate() { return new OutliningTagger(buffer) as ITagger<T>; };
        return buffer.Properties.GetOrCreateSingletonProperty<ITagger<T>>(sc);
    }
    

Compilación y prueba del código

Para probar este código, compile la solución OutlineRegionTest y ejecútela en la instancia experimental.

Para compilar y probar la solución OutlineRegionTest

  1. Compile la solución.

  2. Al ejecutar este proyecto en el depurador, se inicia una segunda instancia de Visual Studio.

  3. Crear un archivo de texto Escriba algún texto que incluya los corchetes de apertura y los corchetes de cierre.

    [
       Hello
    ]
    
  4. Debe haber una región de esquematización que incluya ambos corchetes. Debería poder hacer clic en el signo menos a la izquierda del corchete abierto para contraer la región de esquematización. Cuando se contrae la región, el símbolo de puntos suspensivos (...) debería aparecer a la izquierda de la región contraída y aparecerá un elemento emergente que contenga el texto que mantiene el puntero sobre los puntos suspensivos.