Exemplarische Vorgehensweise: Gliedern

Richten Sie sprachbasierte Features ein, z. B. Gliedern, indem Sie die Arten von Textbereichen definieren, die Sie erweitern oder reduzieren möchten. Sie können Regionen im Kontext eines Sprachdiensts definieren oder eigene Dateinamenerweiterung und Inhaltstyp definieren und die Regionsdefinition nur auf diesen Typ anwenden oder die Regionsdefinition auf einen vorhandenen Inhaltstyp anwenden (z. B. "Text"). In dieser exemplarischen Vorgehensweise wird gezeigt, wie Sie Bereiche definieren und anzeigen.

Erstellen eines Projekts für verwaltetes Erweiterbarkeitsframework (MEF)

So erstellen Sie ein MEF-Projekt

  1. Erstellen eines VSIX-Projekts Nennen Sie die Projektmappe OutlineRegionTest.

  2. Fügen Sie dem Projekt eine Elementvorlage für Editorklassifizierer hinzu. Weitere Informationen finden Sie unter Erstellen einer Erweiterung mit einer Editorelementvorlage.

  3. Löschen Sie die vorhandenen Klassendateien.

Implementieren eines Gliedertags

Die Gliederung von Regionen wird durch eine Art von Tag (OutliningRegionTag) gekennzeichnet. Dieses Tag stellt das standardmäßige Gliederverhalten bereit. Der umrissierte Bereich kann erweitert oder reduziert werden. Der umrandete Bereich wird durch ein Pluszeichen (+) markiert, wenn er reduziert ist oder ein Minuszeichen (-) wenn er erweitert wird, und der erweiterte Bereich wird durch eine vertikale Linie abgegrenzt.

Die folgenden Schritte zeigen, wie Sie einen Tagger definieren, der Klammern für alle regionen erstellt, die durch die Klammern ([,]) getrennt sind.

So implementieren Sie einen Gliedertagger

  1. Fügen Sie eine Klassendatei hinzu, und nennen Sie sie OutliningTagger.

  2. Importieren Sie die folgenden Namespaces.

    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. Erstellen Sie eine Klasse mit dem Namen OutliningTagger, und implementieren Sie ITagger<T>folgendes:

    internal sealed class OutliningTagger : ITagger<IOutliningRegionTag>
    
  4. Fügen Sie einige Felder hinzu, um den Textpuffer nachzuverfolgen und Momentaufnahme und die Zeilensätze anzusammeln, die als Rahmenbereiche markiert werden sollen. Dieser Code enthält eine Liste von Region-Objekten (die später definiert werden sollen), die die Gliederungsbereiche darstellen.

    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. Fügen Sie einen Tagger-Konstruktor hinzu, der die Felder initialisiert, den Puffer analysiert, und fügt dem Changed Ereignis einen Ereignishandler hinzu.

    public OutliningTagger(ITextBuffer buffer)
    {
        this.buffer = buffer;
        this.snapshot = buffer.CurrentSnapshot;
        this.regions = new List<Region>();
        this.ReParse();
        this.buffer.Changed += BufferChanged;
    }
    
  6. Implementieren Sie die Methode, mit der GetTags die Tagspanne instanziiert wird. In diesem Beispiel wird davon ausgegangen, dass die spannen in der NormalizedSpanCollection übergebenen Methode zusammenhängend sind, obwohl dies möglicherweise nicht immer der Fall ist. Diese Methode instanziiert eine neue Tagspanne für jeden der Gliederbereiche.

    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. Deklarieren Sie einen TagsChanged Ereignishandler.

    public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
    
  8. Fügen Sie einen BufferChanged Ereignishandler hinzu, der auf Changed Ereignisse reagiert, indem Sie den Textpuffer analysieren.

    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. Fügen Sie eine Methode hinzu, die den Puffer analysiert. Das hier angegebene Beispiel dient nur zur Veranschaulichung. Er analysiert den Puffer synchron in geschachtelte Gliederbereiche.

    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. Die folgende Hilfsmethode ruft eine ganze Zahl ab, die die Ebene der Gliederung darstellt, sodass 1 das äußerst linke geschweifte Paar ist.

    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. Mit der folgenden Hilfsmethode wird eine Region (weiter unten in diesem Artikel definiert) in einen SnapshotSpan übersetzt.

    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. Der folgende Code dient nur zur Veranschaulichung. Sie definiert eine PartialRegion-Klasse, die die Zeilennummer und den Offset des Anfangs eines Gliederbereichs und einen Verweis auf den übergeordneten Bereich (sofern vorhanden) enthält. Mit diesem Code kann der Parser geschachtelte Gliederungsbereiche einrichten. Eine abgeleitete Region-Klasse enthält einen Verweis auf die Zeilennummer des Endes eines Gliederbereichs.

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

Implementieren eines Taggeranbieters

Exportieren Sie einen Tagger-Anbieter für Ihren Tagger. Der Taggeranbieter erstellt einen OutliningTagger Puffer des Inhaltstyps "Text" oder gibt einen OutliningTagger Wert zurück, wenn der Puffer bereits über einen verfügt.

So implementieren Sie einen Taggeranbieter

  1. Erstellen Sie eine Klasse mit dem Namen OutliningTaggerProvider , die sie implementiert ITaggerProvider, und exportieren Sie sie mit den Attributen ContentType und TagType.

    [Export(typeof(ITaggerProvider))]
    [TagType(typeof(IOutliningRegionTag))]
    [ContentType("text")]
    internal sealed class OutliningTaggerProvider : ITaggerProvider
    
  2. Implementieren Sie die CreateTagger Methode, indem Sie die Eigenschaften des Puffers hinzufügen OutliningTagger .

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

Erstellen und Testen des Codes

Um diesen Code zu testen, erstellen Sie die OutlineRegionTest-Lösung, und führen Sie sie in der experimentellen Instanz aus.

So erstellen und testen Sie die OutlineRegionTest-Lösung

  1. Erstellen Sie die Projektmappe.

  2. Wenn Sie dieses Projekt im Debugger ausführen, wird eine zweite Instanz von Visual Studio gestartet.

  3. Erstellen einer Textdatei Geben Sie Text ein, der sowohl die öffnenden Klammern als auch die schließenden Klammern enthält.

    [
       Hello
    ]
    
  4. Es sollte eine Klammernregion vorhanden sein, die beide Klammern enthält. Sie sollten links neben der geöffneten Klammer auf das Minuszeichen klicken können, um den Rahmenbereich zu reduzieren. Wenn der Bereich reduziert ist, sollte das Auslassungszeichen (...) links neben dem reduzierten Bereich angezeigt werden, und ein Popup mit dem Textzeigertext sollte angezeigt werden, wenn Sie den Mauszeiger über die Auslassungspunkte bewegen.