Passo a passo: estrutura de tópicos

Configure recursos baseados em idioma, como estrutura de tópicos, definindo os tipos de regiões de texto que você deseja expandir ou recolher. Você pode definir regiões no contexto de um serviço de idioma, ou definir sua própria extensão de nome de arquivo e tipo de conteúdo e aplicar a definição de área somente a esse tipo, ou aplicar as definições de área a um tipo de conteúdo existente (como "texto"). Este passo a passo mostra como definir e exibir regiões de estrutura de tópicos.

Criar um projeto MEF (Managed Extensibility Framework)

Para criar um projeto MEF

  1. Crie um projeto VSIX. Nomeie a solução OutlineRegionTest.

  2. Adicione um modelo de item Editor Classificador ao projeto. Para obter mais informações, consulte Criar uma extensão com um modelo de item do editor.

  3. Exclua os arquivos de classe existentes.

Implementar um tagger de estrutura de tópicos

As regiões de contorno são marcadas por uma espécie de tag (OutliningRegionTag). Essa tag fornece o comportamento de estrutura de tópicos padrão. A região delineada pode ser expandida ou colapsada. A região delineada é marcada por um sinal de mais () se estiver recolhida ou por um sinal de subtração (+-) se estiver expandida, e a região expandida é demarcada por uma linha vertical.

As etapas a seguir mostram como definir um tagger que cria regiões de estrutura de tópicos para todas as regiões delimitadas pelos colchetes ([,]).

Para implementar um tagger de estrutura de tópicos

  1. Adicione um arquivo de classe e nomeie-o OutliningTagger.

  2. Importe os namespaces a seguir.

    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. Crie uma classe chamada OutliningTagger, e peça-lhe que implemente ITagger<T>:

    internal sealed class OutliningTagger : ITagger<IOutliningRegionTag>
    
  4. Adicione alguns campos para controlar o buffer de texto e o instantâneo e para acumular os conjuntos de linhas que devem ser marcados como regiões de estrutura de tópicos. Esse código inclui uma lista de objetos Region (a serem definidos posteriormente) que representam as regiões de estrutura de tópicos.

    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. Adicione um construtor tagger que inicializa os campos, analisa o buffer e adiciona um manipulador de eventos ao 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 o GetTags método, que instancia as extensões de tag. Este exemplo pressupõe que as extensões no NormalizedSpanCollection passado para o método são contíguas, embora nem sempre seja o caso. Esse método instancia uma nova extensão de marca para cada uma das regiões de estrutura de tópicos.

    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 um manipulador de TagsChanged eventos.

    public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
    
  8. Adicione um BufferChanged manipulador de eventos que responda a Changed eventos analisando o buffer 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. Adicione um método que analisa o buffer. O exemplo dado aqui é apenas para ilustração. Ele analisa de forma síncrona o buffer em regiões de estrutura de tópicos aninhadas.

    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. O método auxiliar a seguir obtém um inteiro que representa o nível da estrutura de tópicos, de modo que 1 é o par de chaves mais à esquerda.

    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. O método auxiliar a seguir converte uma Região (definida posteriormente neste artigo) em um 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. O código a seguir é apenas para ilustração. Ele define uma classe PartialRegion que contém o número de linha e o deslocamento do início de uma região de estrutura de tópicos e uma referência à região pai (se houver). Esse código permite que o analisador configure regiões de estrutura de tópicos aninhadas. Uma classe Region derivada contém uma referência ao número da linha do final de uma região de estrutura de tópicos.

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

Implementar um provedor de tagger

Exporte um provedor de tagger para seu tagger. O provedor de tagger cria um para um buffer do tipo de conteúdo "texto", ou então retorna um se o buffer já tiver um OutliningTaggerOutliningTagger .

Para implementar um provedor de tagger

  1. Crie uma classe chamada OutliningTaggerProvider que implementa ITaggerProvidere exporte-a com os atributos ContentType e TagType.

    [Export(typeof(ITaggerProvider))]
    [TagType(typeof(IOutliningRegionTag))]
    [ContentType("text")]
    internal sealed class OutliningTaggerProvider : ITaggerProvider
    
  2. Implemente o CreateTagger método adicionando um OutliningTagger às propriedades do buffer.

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

Compilar e testar o código

Para testar esse código, crie a solução OutlineRegionTest e execute-a na instância experimental.

Para criar e testar a solução OutlineRegionTest

  1. Compile a solução.

  2. Quando você executa esse projeto no depurador, uma segunda instância do Visual Studio é iniciada.

  3. Crie um arquivo de texto. Digite algum texto que inclua os colchetes de abertura e os colchetes de fechamento.

    [
       Hello
    ]
    
  4. Deve haver uma região de contorno que inclua ambos os colchetes. Você deve ser capaz de clicar no sinal de subtração à esquerda do colchete aberto para recolher a região de contorno. Quando a região é recolhida, o símbolo de reticências (...) deve aparecer à esquerda da região recolhida, e um pop-up contendo o texto focalizado deve aparecer quando você move o ponteiro sobre as reticências.