展開したり折りたたんだりするテキスト領域の種類を定義することで、アウトラインなどの言語ベースの機能を設定します。 言語サービスのコンテキストで領域を定義することも、独自のファイル名拡張子やコンテンツ タイプを定義して、そのタイプにのみ領域の定義を適用することもできます。または、既存のコンテンツ タイプ ("text" など) に領域の定義を適用できます。 このチュートリアルでは、アウトライン領域を定義して表示する方法について説明します。
Managed Extensibility Framework (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)
テキスト バッファーとスナップショットを追跡し、アウトライン領域としてタグ付けする必要がある行のセットを収集するため、いくつかのフィールドを追加します。 このコードには、アウトライン領域を表す Region オブジェクト (後で定義されます) の一覧が含まれています。
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
テキスト バッファーを解析することによって Changed イベントに応答する BufferChanged
イベント ハンドラーを追加します。
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
次のヘルパー メソッドでは、(この記事の後方で定義されている) 1 つの Region を 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 クラスを定義しています。 このコードにより、パーサーは、入れ子になったアウトライン領域を設定できるようになります。 派生した Region クラスには、アウトライン領域の終了の行番号への参照が含まれています。
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
を返します。
タガー プロバイダーを実装するには
ITaggerProvider を実装する OutliningTaggerProvider
という名前のクラスを作成し、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
バッファーのプロパティに 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);
}
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 の 2 つ目のインスタンスが起動されます。
テキスト ファイルを作成します。 左角かっこと右角かっこの両方を含む何らかのテキストを入力します。
[
Hello
]
両方の角かっこを含むアウトライン領域が存在する必要があります。 左角かっこの左側にあるマイナス記号をクリックすると、アウトライン領域を折りたたむことができるはずです。 領域が折りたたまれているときには、折りたたまれた領域の左側に省略記号 (...) が表示されるはずです。また、ポインターを省略記号の上に移動すると、そのテキストのホバー テキストが含まれるポップアップが表示されるはずです。
関連するコンテンツ