通过定义要展开或折叠的文本区域类型来设置基于语言的功能,例如大纲显示。 可以在语言服务的上下文中定义区域,或定义自己的文件扩展名和内容类型,并将区域定义仅应用于该类型,或将区域定义应用于现有内容类型(如“text”)。 本演练演示如何定义和显示大纲显示区域。
创建托管扩展性框架 (MEF) 项目
创建 MEF 项目
创建 VSIX 项目。 将解决方案命名为 OutlineRegionTest
。
向项目添加编辑器分类器项模板。 有关详细信息,请参阅使用编辑器项模板创建扩展。
删除现有的类文件。
实现大纲标记器
大纲显示区域由一种标记(OutliningRegionTag) 标记。 此标记提供标准大纲显示行为。 大纲区域可以展开或折叠。 如果区域已折叠或减号(如果已展开),则标有加号(+-),并且扩展区域由垂直线分隔。
以下步骤演示如何定义一个标记器,该标记器为括号([,])分隔的所有区域创建大纲区域。
实现大纲标记器
添加一个类文件并将其命名为 OutliningTagger
。
导入以下命名空间。
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;
Imports System
Imports System.Collections.Generic
Imports System.Linq
Imports System.Text
Imports System.ComponentModel.Composition
Imports Microsoft.VisualStudio.Text.Outlining
Imports Microsoft.VisualStudio.Text.Tagging
Imports Microsoft.VisualStudio.Utilities
Imports Microsoft.VisualStudio.Text
创建名为 OutliningTagger
的类,并使其实现 ITagger<T>:
internal sealed class OutliningTagger : ITagger<IOutliningRegionTag>
Friend NotInheritable Class OutliningTagger
Implements ITagger(Of IOutliningRegionTag)
添加一些字段来跟踪文本缓冲区和快照,并累积应标记为大纲区域的行集。 此代码包括表示大纲区域的区域对象列表(稍后要定义)。
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;
'the characters that start the outlining region
Private startHide As String = "["
'the characters that end the outlining region
Private endHide As String = "]"
'the characters that are displayed when the region is collapsed
Private ellipsis As String = "..."
'the contents of the tooltip for the collapsed span
Private hoverText As String = "hover text"
Private buffer As ITextBuffer
Private snapshot As ITextSnapshot
Private regions As List(Of Region)
添加一个标记器构造函数,用于初始化字段、分析缓冲区并将事件处理程序添加到事件 Changed 。
public OutliningTagger(ITextBuffer buffer)
{
this.buffer = buffer;
this.snapshot = buffer.CurrentSnapshot;
this.regions = new List<Region>();
this.ReParse();
this.buffer.Changed += BufferChanged;
}
Public Sub New(ByVal buffer As ITextBuffer)
Me.buffer = buffer
Me.snapshot = buffer.CurrentSnapshot
Me.regions = New List(Of Region)()
Me.ReParse()
AddHandler Me.buffer.Changed, AddressOf BufferChanged
End Sub
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));
}
}
}
Public Function GetTags(ByVal spans As NormalizedSnapshotSpanCollection) As IEnumerable(Of ITagSpan(Of IOutliningRegionTag)) Implements ITagger(Of Microsoft.VisualStudio.Text.Tagging.IOutliningRegionTag).GetTags
If spans.Count = 0 Then
Return Nothing
Exit Function
End If
Dim currentRegions As List(Of Region) = Me.regions
Dim currentSnapshot As ITextSnapshot = Me.snapshot
Dim entire As SnapshotSpan = New SnapshotSpan(spans(0).Start, spans(spans.Count - 1).[End]).TranslateTo(currentSnapshot, SpanTrackingMode.EdgeExclusive)
Dim startLineNumber As Integer = entire.Start.GetContainingLine().LineNumber
Dim endLineNumber As Integer = entire.[End].GetContainingLine().LineNumber
Dim list As List(Of ITagSpan(Of IOutliningRegionTag))
list = New List(Of ITagSpan(Of IOutliningRegionTag))()
For Each region In currentRegions
If region.StartLine <= endLineNumber AndAlso region.EndLine >= startLineNumber Then
Dim startLine = currentSnapshot.GetLineFromLineNumber(region.StartLine)
Dim endLine = currentSnapshot.GetLineFromLineNumber(region.EndLine)
'the region starts at the beginning of the "[", and goes until the *end* of the line that contains the "]".
list.Add(New TagSpan(Of IOutliningRegionTag)(New SnapshotSpan(startLine.Start + region.StartOffset, endLine.End),
New OutliningRegionTag(False, False, ellipsis, hoverText)))
End If
Next
Return list
End Function
TagsChanged
声明事件处理程序。
public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
Public Event TagsChanged As EventHandler(Of SnapshotSpanEventArgs) Implements ITagger(Of IOutliningRegionTag).TagsChanged
添加一个 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();
}
Private Sub BufferChanged(ByVal sender As Object, ByVal e As TextContentChangedEventArgs)
' 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 IsNot buffer.CurrentSnapshot Then
Exit Sub
End If
Me.ReParse()
End Sub
添加分析缓冲区的方法。 此处给出的示例仅供参考。 它将缓冲区同步分析为嵌套的大纲区域。
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))));
}
}
Private Sub ReParse()
Dim newSnapshot As ITextSnapshot = buffer.CurrentSnapshot
Dim newRegions As New List(Of Region)()
'keep the current (deepest) partial region, which will have
' references to any parent partial regions.
Dim currentRegion As PartialRegion = Nothing
For Each line In newSnapshot.Lines
Dim regionStart As Integer = -1
Dim text As String = line.GetText()
'lines that contain a "[" denote the start of a new region.
If text.IndexOf(startHide, StringComparison.Ordinal) <> -1 Then
regionStart = text.IndexOf(startHide, StringComparison.Ordinal)
Dim currentLevel As Integer = If((currentRegion IsNot Nothing), currentRegion.Level, 1)
Dim newLevel As Integer
If Not TryGetLevel(text, regionStart, newLevel) Then
newLevel = currentLevel + 1
End If
'levels are the same and we have an existing region;
'end the current region and start the next
If currentLevel = newLevel AndAlso currentRegion IsNot Nothing Then
Dim newRegion = New Region()
newRegion.Level = currentRegion.Level
newRegion.StartLine = currentRegion.StartLine
newRegion.StartOffset = currentRegion.StartOffset
newRegion.EndLine = line.LineNumber
newRegions.Add(newRegion)
currentRegion = New PartialRegion()
currentRegion.Level = newLevel
currentRegion.StartLine = line.LineNumber
currentRegion.StartOffset = regionStart
currentRegion.PartialParent = currentRegion.PartialParent
Else
'this is a new (sub)region
currentRegion = New PartialRegion()
currentRegion.Level = newLevel
currentRegion.StartLine = line.LineNumber
currentRegion.StartOffset = regionStart
currentRegion.PartialParent = currentRegion
End If
'lines that contain "]" denote the end of a region
ElseIf (text.IndexOf(endHide, StringComparison.Ordinal)) <> -1 Then
regionStart = text.IndexOf(endHide, StringComparison.Ordinal)
Dim currentLevel As Integer = If((currentRegion IsNot Nothing), currentRegion.Level, 1)
Dim closingLevel As Integer
If Not TryGetLevel(text, regionStart, closingLevel) Then
closingLevel = currentLevel
End If
'the regions match
If currentRegion IsNot Nothing AndAlso currentLevel = closingLevel Then
Dim newRegion As Region
newRegion = New Region()
newRegion.Level = currentLevel
newRegion.StartLine = currentRegion.StartLine
newRegion.StartOffset = currentRegion.StartOffset
newRegion.EndLine = line.LineNumber
newRegions.Add(newRegion)
currentRegion = currentRegion.PartialParent
End If
End If
Next
'determine the changed span, and send a changed event with the new spans
Dim oldSpans As New List(Of Span)(Me.regions.[Select](Function(r) AsSnapshotSpan(r, Me.snapshot).TranslateTo(newSnapshot, SpanTrackingMode.EdgeExclusive).Span))
Dim newSpans As New List(Of Span)(newRegions.[Select](Function(r) AsSnapshotSpan(r, newSnapshot).Span))
Dim oldSpanCollection As New NormalizedSpanCollection(oldSpans)
Dim newSpanCollection As New NormalizedSpanCollection(newSpans)
'the changed regions are regions that appear in one set or the other, but not both.
Dim removed As NormalizedSpanCollection = NormalizedSpanCollection.Difference(oldSpanCollection, newSpanCollection)
Dim changeStart As Integer = Integer.MaxValue
Dim changeEnd As Integer = -1
If removed.Count > 0 Then
changeStart = removed(0).Start
changeEnd = removed(removed.Count - 1).[End]
End If
If newSpans.Count > 0 Then
changeStart = Math.Min(changeStart, newSpans(0).Start)
changeEnd = Math.Max(changeEnd, newSpans(newSpans.Count - 1).[End])
End If
Me.snapshot = newSnapshot
Me.regions = newRegions
If changeStart <= changeEnd Then
Dim snap As ITextSnapshot = Me.snapshot
RaiseEvent TagsChanged(Me, New SnapshotSpanEventArgs(New SnapshotSpan(Me.snapshot, Span.FromBounds(changeStart, changeEnd))))
End If
End Sub
以下帮助程序方法获取一个整数,该整数表示大纲的级别,以便 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;
}
Private Shared Function TryGetLevel(ByVal text As String, ByVal startIndex As Integer, ByRef level As Integer) As Boolean
level = -1
If text.Length > startIndex + 3 Then
If Integer.TryParse(text.Substring(startIndex + 1), level) Then
Return True
End If
End If
Return False
End Function
以下帮助程序方法将区域(本文稍后定义)转换为 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);
}
Private Shared Function AsSnapshotSpan(ByVal region As Region, ByVal snapshot As ITextSnapshot) As SnapshotSpan
Dim startLine = snapshot.GetLineFromLineNumber(region.StartLine)
Dim endLine = If((region.StartLine = region.EndLine), startLine, snapshot.GetLineFromLineNumber(region.EndLine))
Return New SnapshotSpan(startLine.Start + region.StartOffset, endLine.[End])
End Function
以下代码仅用于说明。 它定义一个 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; }
}
Private Class PartialRegion
Private _StartLine As Integer
Public Property StartLine() As Integer
Get
Return _StartLine
End Get
Set(ByVal value As Integer)
_StartLine = value
End Set
End Property
Private _StartOffset As Integer
Public Property StartOffset() As Integer
Get
Return _StartOffset
End Get
Set(ByVal value As Integer)
_StartOffset = value
End Set
End Property
Private _Level As Integer
Public Property Level() As Integer
Get
Return _Level
End Get
Set(ByVal value As Integer)
_Level = value
End Set
End Property
Private _PartialParent As PartialRegion
Public Property PartialParent() As PartialRegion
Get
Return _PartialParent
End Get
Set(ByVal value As PartialRegion)
_PartialParent = value
End Set
End Property
End Class
Private Class Region
Inherits PartialRegion
Private _EndLine As Integer
Public Property EndLine() As Integer
Get
Return _EndLine
End Get
Set(ByVal value As Integer)
_EndLine = value
End Set
End Property
End Class
实现标记器提供程序
导出标记器提供程序。 标记器提供程序为“text”内容类型的缓冲区创建一个 OutliningTagger
,否则返回 OutliningTagger
缓冲区(如果缓冲区已有该缓冲区)。
实现标记器提供程序
创建一个名为 OutliningTaggerProvider
实现 ITaggerProvider的类,并使用 ContentType 和 TagType 属性导出它。
[Export(typeof(ITaggerProvider))]
[TagType(typeof(IOutliningRegionTag))]
[ContentType("text")]
internal sealed class OutliningTaggerProvider : ITaggerProvider
<Export(GetType(ITaggerProvider))> _
<TagType(GetType(IOutliningRegionTag))> _
<ContentType("text")> _
Friend NotInheritable Class OutliningTaggerProvider
Implements ITaggerProvider
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);
}
Public Function CreateTagger(Of T As ITag)(ByVal buffer As ITextBuffer) As ITagger(Of T) Implements ITaggerProvider.CreateTagger
'create a single tagger for each buffer.
Dim sc As Func(Of ITagger(Of T)) = Function() TryCast(New OutliningTagger(buffer), ITagger(Of T))
Return buffer.Properties.GetOrCreateSingletonProperty(Of ITagger(Of T))(sc)
End Function
生成并测试代码
若要测试此代码,请生成 OutlineRegionTest 解决方案并在实验实例中运行它。
生成和测试 OutlineRegionTest 解决方案
生成解决方案。
在调试器中运行此项目时,将启动 Visual Studio 的第二个实例。
创建文本文件。 键入一些包含左括号和右括号的文本。
[
Hello
]
应该有一个包含两个括号的大纲区域。 应能够单击左括号左侧的减号以折叠大纲区域。 当区域折叠时,省略号(...)应显示在折叠区域左侧,在省略号上移动指针时,应显示包含文本 悬停文本 的弹出窗口。
相关内容