演练:大纲显示

通过定义要展开或折叠的文本区域类型来设置基于语言的功能,例如大纲显示。 可以在语言服务的上下文中定义区域,或定义自己的文件扩展名和内容类型,并将区域定义仅应用于该类型,或将区域定义应用于现有内容类型(如“text”)。 本演练演示如何定义和显示大纲显示区域。

创建托管扩展性框架 (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. CreateTagger通过向缓冲区的属性添加一个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);
    }
    

生成并测试代码

若要测试此代码,请生成 OutlineRegionTest 解决方案并在实验实例中运行它。

生成和测试 OutlineRegionTest 解决方案

  1. 生成解决方案。

  2. 在调试器中运行此项目时,将启动 Visual Studio 的第二个实例。

  3. 创建文本文件。 键入一些包含左括号和右括号的文本。

    [
       Hello
    ]
    
  4. 应该有一个包含两个括号的大纲区域。 应能够单击左括号左侧的减号以折叠大纲区域。 当区域折叠时,省略号(...)应显示在折叠区域左侧,在省略号上移动指针时,应显示包含文本 悬停文本 的弹出窗口。