Procedura dettagliata: Definizione della struttura

Configurare le funzionalità basate sul linguaggio, ad esempio la struttura, definendo i tipi di aree di testo da espandere o comprimere. È possibile definire aree nel contesto di un servizio linguistico oppure definire il proprio tipo di contenuto e estensione del nome file e applicare la definizione dell'area solo a tale tipo oppure applicare le definizioni di area a un tipo di contenuto esistente,ad esempio "text". Questa procedura dettagliata illustra come definire e visualizzare le aree di struttura.

Creare un progetto MEF (Managed Extensibility Framework)

Per creare un progetto MEF

  1. Creare un progetto VSIX. Assegnare alla soluzione il nome OutlineRegionTest.

  2. Aggiungere un modello di elemento classificatore dell'editor al progetto. Per altre informazioni, vedere Creare un'estensione con un modello di elemento dell'editor.

  3. Eliminare i file di classe esistenti.

Implementare un tagger di struttura

Le aree di struttura sono contrassegnate da un tipo di tag (OutliningRegionTag). Questo tag fornisce il comportamento di struttura standard. L'area evidenziata può essere espansa o compressa. L'area evidenziata è contrassegnata da un segno Più (+) se è compresso o un segno meno (-) se viene espanso e l'area espansa viene demarcata da una linea verticale.

I passaggi seguenti illustrano come definire un tagger che crea aree di struttura per tutte le aree delimitate dalle parentesi quadre ([,]).

Per implementare un tagger di struttura

  1. Aggiungere un file di classe e assegnargli il nome OutliningTagger.

  2. Importare gli spazi dei nomi seguenti.

    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. Creare una classe denominata OutliningTaggere implementarla ITagger<T>:

    internal sealed class OutliningTagger : ITagger<IOutliningRegionTag>
    
  4. Aggiungere alcuni campi per tenere traccia del buffer di testo e dello snapshot e per accumulare i set di righe che devono essere contrassegnati come aree strutturate. Questo codice include un elenco di oggetti Region (da definire in un secondo momento) che rappresentano le aree di struttura.

    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. Aggiungere un costruttore tagger che inizializza i campi, analizza il buffer e aggiunge un gestore eventi all'evento Changed .

    public OutliningTagger(ITextBuffer buffer)
    {
        this.buffer = buffer;
        this.snapshot = buffer.CurrentSnapshot;
        this.regions = new List<Region>();
        this.ReParse();
        this.buffer.Changed += BufferChanged;
    }
    
  6. Implementare il GetTags metodo , che crea un'istanza dell'intervallo di tag. In questo esempio si presuppone che gli intervalli nell'oggetto NormalizedSpanCollection passato al metodo siano contigui, anche se potrebbe non essere sempre il caso. Questo metodo crea un'istanza di un nuovo intervallo di tag per ogni area di struttura.

    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. Dichiarare un TagsChanged gestore eventi.

    public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
    
  8. Aggiungere un BufferChanged gestore eventi che risponda agli Changed eventi analizzando il buffer di testo.

    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. Aggiungere un metodo che analizza il buffer. L'esempio riportato qui è solo per l'illustrazione. Analizza in modo sincrono il buffer in aree di struttura nidificate.

    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. Il metodo helper seguente ottiene un numero intero che rappresenta il livello della struttura, in modo che 1 sia la coppia parentesi graffa più a sinistra.

    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. Il metodo helper seguente converte un'area (definita più avanti in questo articolo) in 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. Il codice seguente è solo per l'illustrazione. Definisce una classe PartialRegion che contiene il numero di riga e l'offset dell'inizio di un'area struttura e un riferimento all'area padre (se presente). Questo codice consente al parser di configurare le aree di struttura annidate. Una classe Region derivata contiene un riferimento al numero di riga della fine di un'area struttura.

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

Implementare un provider di tagger

Esportare un provider di tagger per il tagger. Il provider di tagger crea un oggetto OutliningTagger per un buffer del tipo di contenuto "text" oppure restituisce un valore OutliningTagger se il buffer ne ha già uno.

Per implementare un provider di tagger

  1. Creare una classe denominata OutliningTaggerProvider che implementa ITaggerProvidered esportarla con gli attributi ContentType e TagType.

    [Export(typeof(ITaggerProvider))]
    [TagType(typeof(IOutliningRegionTag))]
    [ContentType("text")]
    internal sealed class OutliningTaggerProvider : ITaggerProvider
    
  2. Implementare il CreateTagger metodo aggiungendo un OutliningTagger oggetto alle proprietà del 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);
    }
    

Compilare e testare il codice

Per testare questo codice, compilare la soluzione OutlineRegionTest ed eseguirla nell'istanza sperimentale.

Per compilare e testare la soluzione OutlineRegionTest

  1. Compilare la soluzione.

  2. Quando si esegue questo progetto nel debugger, viene avviata una seconda istanza di Visual Studio.

  3. Creare un file di testo. Digitare un testo che includa sia le parentesi quadre di apertura che le parentesi quadre di chiusura.

    [
       Hello
    ]
    
  4. Deve essere presente un'area di struttura che include entrambe le parentesi quadre. Dovrebbe essere possibile fare clic sul segno meno a sinistra della parentesi aperta per comprimere l'area di struttura. Quando l'area è compressa, il simbolo con i puntini di sospensione (...) dovrebbe apparire a sinistra dell'area compressa e quando si sposta il puntatore del puntatore sui puntini di sospensione verrà visualizzato un popup contenente il testo al passaggio del testo.