逐步解說︰大綱

藉由定義想要展開或摺疊的文字區域類型,設定語言型功能,例如:大綱。 您可以在語言服務的內容中定義區域,或定義您自己的副檔名和內容類型,並將區域定義只套用至該類型,或將區域定義套用至現有的內容類型 (例如「text」)。 本逐步解說顯示如何定義及顯示大綱區域。

建立 Managed Extensibility Framework (MEF) 專案

建立 MEF 專案

  1. 建立 VSIX 專案。 將方案命名為 OutlineRegionTest

  2. 將編輯器分類器項目範本新增至專案。 如需詳細資訊,請參閱 使用編輯器項目範本建立擴充功能

  3. 刪除現有類別檔案。

實作大綱標籤器

大綱區域會以一種標籤 (OutliningRegionTag) 標示。 此標籤提供標準大綱行為。 大綱區域可以展開或摺疊。 如果已摺疊,則大綱區域會以加號 (+) 標示,如果已展開,則為減號 (-),而展開的區域則以垂直線來標示。

以下步驟顯示如何定義一個標籤器,該標籤器為所有由括號界定的區域建立摺疊區域 ([])。

實作大綱標籤器

  1. 加入類別檔案,並將它命名為 OutliningTagger

  2. 匯入下列命名空間。

    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. 建立名為 OutliningTagger 的類別,並讓其實作 ITagger<T>

    internal sealed class OutliningTagger : ITagger<IOutliningRegionTag>
    
  4. 新增一些欄位來追蹤文字緩衝區和快照集,並累積應該標記為大綱區域的行集。 此程式碼包含代表大綱區域的區域物件清單 (稍後定義)。

    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. 新增標籤器建構函式,初始化欄位、剖析緩衝區,並將事件處理常式新增至 Changed 事件。

    public OutliningTagger(ITextBuffer buffer)
    {
        this.buffer = buffer;
        this.snapshot = buffer.CurrentSnapshot;
        this.regions = new List<Region>();
        this.ReParse();
        this.buffer.Changed += BufferChanged;
    }
    
  6. 實作 GetTags 方法,以具現化標籤範圍。 此範例假設傳遞給該方法的 NormalizedSpanCollection 中的範圍是連續的,但情況可能並不總是如此。 這個方法會具現化每個大綱區域的新標籤範圍。

    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. 宣告 TagsChanged 事件處理常式。

    public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
    
  8. 新增 BufferChanged 事件處理常式,該常式以剖析文字緩衝區來回應 Changed 事件。

    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. 新增剖析緩衝區的方法。 此處提供的範例僅供說明。 它會同步將緩衝區剖析成巢狀大綱區域。

    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. 下列輔助方法會取得代表大綱層級的整數,因此 1 是最左邊的大括號配對。

    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. 下列輔助方法會將區域 (本文稍後定義) 轉譯為 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. 下列程式碼僅供說明。 它定義了 PartialRegion 類別,該類包含了一個大綱區域開始的行號和位移,以及對上層區域 (如果有) 的參考。 此程式碼可讓剖析器設定巢狀大綱區域。 衍生的區域類別包含大綱區域結尾行號的參考。

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

實作標籤器提供者

匯出標籤器的標籤器提供者。 標籤器提供者會為「text」內容類型的緩衝區建立 OutliningTagger,或者如果緩衝區已有,則傳回 OutliningTagger

實作標籤器提供者

  1. 建立名為 OutliningTaggerProvider 的類別,以實作 ITaggerProvider,並使用 ContentType 和 TagType 屬性將其匯出。

    [Export(typeof(ITaggerProvider))]
    [TagType(typeof(IOutliningRegionTag))]
    [ContentType("text")]
    internal sealed class OutliningTaggerProvider : ITaggerProvider
    
  2. OutliningTagger 新增至緩衝器的屬性來實作 CreateTagger 方法。

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

建置並測試程式碼

若要測試此程式碼,請建置 OutlineRegionTest 方案,並在實驗執行個體中執行它。

建置並測試 OutlineRegionTest 方案

  1. 建置方案。

  2. 當您在偵錯工具中執行這個專案時,會啟動第二個 Visual Studio 執行個體。

  3. 建立文字檔 輸入一些包含左括號和右括號的文字。

    [
       Hello
    ]
    
  4. 應該有一個包含這兩個括號的大綱區域。 您應該能夠按一下左方括號左邊的減號,摺疊大綱區域。 當區域摺疊時,省略號 (...) 應該會出現在摺疊區域的左邊,而且當您將指標移至省略號上方時,應該會出現包含懸停文字的彈出視窗。