かっこのマッチングなど、言語ベースの機能を実装します。それには、対応させたいかっこを定義し、かっこの一方にあるのがキャレットであるときには対応するかっこにテキスト マーカー タグを追加します。 言語のコンテキストでかっこを定義し、独自のファイル名拡張子とコンテンツ タイプを定義して、そのタイプだけにタグを適用するか、既存のコンテンツ タイプ ("text" など) にタグを適用することができます。 以下のチュートリアルでは、かっこ照合タグを "text" コンテンツ タイプに適用する方法を示します。
Managed Extensibility Framework (MEF) プロジェクトを作成する
MEF プロジェクトを作成するには
エディター分類子プロジェクトを作成します。 ソリューション BraceMatchingTest
の名前を指定します。
プロジェクトに、[エディター分類子] 項目テンプレートを追加します。 詳細については、「エディター項目テンプレートを使用して拡張機能を作成する」を参照してください。
既存のクラス ファイルを削除します。
かっこ照合タガーを実装する
Visual Studio で使用されているものに似たかっこの強調表示効果を得る場合は、TextMarkerTag 型のタガーを実装できます。 以下のコードでは、どの入れ子レベルにあってもかっこのペアに対してタガーを定義する方法を示しています。 この例のタガー コンストラクターでは、[] と {} のかっこのペアが定義されていますが、完全な言語実装では、適切なかっこのペアが、言語仕様において定義されます。
かっこ照合タガーを実装するには
クラス ファイルを追加し、その名前を BraceMatching にします。
次の名前空間をインポートします。
using System;
using System.Linq;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Tagging;
using Microsoft.VisualStudio.Utilities;
Imports System.ComponentModel.Composition
Imports Microsoft.VisualStudio.Text
Imports Microsoft.VisualStudio.Text.Editor
Imports Microsoft.VisualStudio.Text.Tagging
Imports Microsoft.VisualStudio.Utilities
TextMarkerTag 型の ITagger<T> を継承する BraceMatchingTagger
クラスを定義します。
internal class BraceMatchingTagger : ITagger<TextMarkerTag>
Friend Class BraceMatchingTagger
Implements ITagger(Of TextMarkerTag)
テキスト ビュー、ソース バッファー、現在のスナップショット ポイントのプロパティに加えて、かっこのペアの組のためのプロパティも追加します。
ITextView View { get; set; }
ITextBuffer SourceBuffer { get; set; }
SnapshotPoint? CurrentChar { get; set; }
private Dictionary<char, char> m_braceList;
Private _View As ITextView
Private Property View() As ITextView
Get
Return _View
End Get
Set(ByVal value As ITextView)
_View = value
End Set
End Property
Private _SourceBuffer As ITextBuffer
Private Property SourceBuffer() As ITextBuffer
Get
Return _SourceBuffer
End Get
Set(ByVal value As ITextBuffer)
_SourceBuffer = value
End Set
End Property
Private _CurrentChar As System.Nullable(Of SnapshotPoint)
Private Property CurrentChar() As System.Nullable(Of SnapshotPoint)
Get
Return _CurrentChar
End Get
Set(ByVal value As System.Nullable(Of SnapshotPoint))
_CurrentChar = value
End Set
End Property
Private m_braceList As Dictionary(Of Char, Char)
タガー コンストラクター内で、プロパティを設定し、ビュー変更イベントの PositionChanged と LayoutChanged をサブスクライブします。 この例では、説明目的で、対応するペアがコンストラクターでも定義されています。
internal BraceMatchingTagger(ITextView view, ITextBuffer sourceBuffer)
{
//here the keys are the open braces, and the values are the close braces
m_braceList = new Dictionary<char, char>();
m_braceList.Add('{', '}');
m_braceList.Add('[', ']');
m_braceList.Add('(', ')');
this.View = view;
this.SourceBuffer = sourceBuffer;
this.CurrentChar = null;
this.View.Caret.PositionChanged += CaretPositionChanged;
this.View.LayoutChanged += ViewLayoutChanged;
}
Friend Sub New(ByVal view As ITextView, ByVal sourceBuffer As ITextBuffer)
'here the keys are the open braces, and the values are the close braces
m_braceList = New Dictionary(Of Char, Char)()
m_braceList.Add("{"c, "}"c)
m_braceList.Add("["c, "]"c)
m_braceList.Add("("c, ")"c)
Me.View = view
Me.SourceBuffer = sourceBuffer
Me.CurrentChar = Nothing
AddHandler Me.View.Caret.PositionChanged, AddressOf Me.CaretPositionChanged
AddHandler Me.View.LayoutChanged, AddressOf Me.ViewLayoutChanged
End Sub
ITagger<T> 実装の一部として、TagsChanged イベントを宣言します。
public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
Public Event TagsChanged As EventHandler(Of SnapshotSpanEventArgs) _
Implements ITagger(Of TextMarkerTag).TagsChanged
イベント ハンドラーでは、CurrentChar
プロパティの現在のキャレット位置を更新し、TagsChanged イベントを発生させます。
void ViewLayoutChanged(object sender, TextViewLayoutChangedEventArgs e)
{
if (e.NewSnapshot != e.OldSnapshot) //make sure that there has really been a change
{
UpdateAtCaretPosition(View.Caret.Position);
}
}
void CaretPositionChanged(object sender, CaretPositionChangedEventArgs e)
{
UpdateAtCaretPosition(e.NewPosition);
}
void UpdateAtCaretPosition(CaretPosition caretPosition)
{
CurrentChar = caretPosition.Point.GetPoint(SourceBuffer, caretPosition.Affinity);
if (!CurrentChar.HasValue)
return;
var tempEvent = TagsChanged;
if (tempEvent != null)
tempEvent(this, new SnapshotSpanEventArgs(new SnapshotSpan(SourceBuffer.CurrentSnapshot, 0,
SourceBuffer.CurrentSnapshot.Length)));
}
Private Sub ViewLayoutChanged(ByVal sender As Object, ByVal e As TextViewLayoutChangedEventArgs)
If e.NewSnapshot IsNot e.OldSnapshot Then
'make sure that there has really been a change
UpdateAtCaretPosition(View.Caret.Position)
End If
End Sub
Private Sub CaretPositionChanged(ByVal sender As Object, ByVal e As CaretPositionChangedEventArgs)
UpdateAtCaretPosition(e.NewPosition)
End Sub
Private Sub UpdateAtCaretPosition(ByVal caretPosition As CaretPosition)
CurrentChar = caretPosition.Point.GetPoint(SourceBuffer, caretPosition.Affinity)
If Not CurrentChar.HasValue Then
Exit Sub
End If
RaiseEvent TagsChanged(Me, New SnapshotSpanEventArgs(New SnapshotSpan(SourceBuffer.CurrentSnapshot, 0, SourceBuffer.CurrentSnapshot.Length)))
End Sub
Visual Studio でのように、現在の文字が左かっこであるとき、または前の文字が右かっこであるときにかっこを照合する GetTags メソッドを実装します。 一致が検出されると、このメソッドは 2 つのタグをインスタンス化します。1 つは左かっこ用、もう 1 つは右かっこ用です。
public IEnumerable<ITagSpan<TextMarkerTag>> GetTags(NormalizedSnapshotSpanCollection spans)
{
if (spans.Count == 0) //there is no content in the buffer
yield break;
//don't do anything if the current SnapshotPoint is not initialized or at the end of the buffer
if (!CurrentChar.HasValue || CurrentChar.Value.Position >= CurrentChar.Value.Snapshot.Length)
yield break;
//hold on to a snapshot of the current character
SnapshotPoint currentChar = CurrentChar.Value;
//if the requested snapshot isn't the same as the one the brace is on, translate our spans to the expected snapshot
if (spans[0].Snapshot != currentChar.Snapshot)
{
currentChar = currentChar.TranslateTo(spans[0].Snapshot, PointTrackingMode.Positive);
}
//get the current char and the previous char
char currentText = currentChar.GetChar();
SnapshotPoint lastChar = currentChar == 0 ? currentChar : currentChar - 1; //if currentChar is 0 (beginning of buffer), don't move it back
char lastText = lastChar.GetChar();
SnapshotSpan pairSpan = new SnapshotSpan();
if (m_braceList.ContainsKey(currentText)) //the key is the open brace
{
char closeChar;
m_braceList.TryGetValue(currentText, out closeChar);
if (BraceMatchingTagger.FindMatchingCloseChar(currentChar, currentText, closeChar, View.TextViewLines.Count, out pairSpan) == true)
{
yield return new TagSpan<TextMarkerTag>(new SnapshotSpan(currentChar, 1), new TextMarkerTag("blue"));
yield return new TagSpan<TextMarkerTag>(pairSpan, new TextMarkerTag("blue"));
}
}
else if (m_braceList.ContainsValue(lastText)) //the value is the close brace, which is the *previous* character
{
var open = from n in m_braceList
where n.Value.Equals(lastText)
select n.Key;
if (BraceMatchingTagger.FindMatchingOpenChar(lastChar, (char)open.ElementAt<char>(0), lastText, View.TextViewLines.Count, out pairSpan) == true)
{
yield return new TagSpan<TextMarkerTag>(new SnapshotSpan(lastChar, 1), new TextMarkerTag("blue"));
yield return new TagSpan<TextMarkerTag>(pairSpan, new TextMarkerTag("blue"));
}
}
}
Public Function GetTags(ByVal spans As NormalizedSnapshotSpanCollection) As IEnumerable(Of ITagSpan(Of TextMarkerTag)) Implements ITagger(Of Microsoft.VisualStudio.Text.Tagging.TextMarkerTag).GetTags
If spans.Count = 0 Then
'there is no content in the buffer
Exit Function
End If
'don't do anything if the current SnapshotPoint is not initialized or at the end of the buffer
If Not CurrentChar.HasValue OrElse CurrentChar.Value.Position >= CurrentChar.Value.Snapshot.Length Then
Exit Function
End If
'hold on to a snapshot of the current character
Dim currentChar__1 As SnapshotPoint = CurrentChar.Value
'if the requested snapshot isn't the same as the one the brace is on, translate our spans to the expected snapshot
If spans(0).Snapshot IsNot currentChar__1.Snapshot Then
currentChar__1 = currentChar__1.TranslateTo(spans(0).Snapshot, PointTrackingMode.Positive)
End If
'get the current char and the previous char
Dim currentText As Char = currentChar__1.GetChar()
Dim lastChar As SnapshotPoint = If(CInt(currentChar__1) = 0, currentChar__1, currentChar__1 - 1)
'if currentChar is 0 (beginning of buffer), don't move it back
Dim lastText As Char = lastChar.GetChar()
Dim pairSpan As New SnapshotSpan()
If m_braceList.ContainsKey(currentText) Then
'the key is the open brace
Dim closeChar As Char
m_braceList.TryGetValue(currentText, closeChar)
If BraceMatchingTagger.FindMatchingCloseChar(currentChar__1, currentText, closeChar, View.TextViewLines.Count, pairSpan) = True Then
Exit Function
End If
ElseIf m_braceList.ContainsValue(lastText) Then
'the value is the close brace, which is the *previous* character
Dim open = From n In m_braceList _
Where n.Value.Equals(lastText) _
Select n.Key
If BraceMatchingTagger.FindMatchingOpenChar(lastChar, CChar(open.ElementAt(0)), lastText, View.TextViewLines.Count, pairSpan) = True Then
Exit Function
End If
End If
End Function
次のプライベート メソッドでは、どのレベルの入れ子でも対応するかっこを検索します。 最初のメソッドでは、左文字に対応する右文字を検索します。
private static bool FindMatchingCloseChar(SnapshotPoint startPoint, char open, char close, int maxLines, out SnapshotSpan pairSpan)
{
pairSpan = new SnapshotSpan(startPoint.Snapshot, 1, 1);
ITextSnapshotLine line = startPoint.GetContainingLine();
string lineText = line.GetText();
int lineNumber = line.LineNumber;
int offset = startPoint.Position - line.Start.Position + 1;
int stopLineNumber = startPoint.Snapshot.LineCount - 1;
if (maxLines > 0)
stopLineNumber = Math.Min(stopLineNumber, lineNumber + maxLines);
int openCount = 0;
while (true)
{
//walk the entire line
while (offset < line.Length)
{
char currentChar = lineText[offset];
if (currentChar == close) //found the close character
{
if (openCount > 0)
{
openCount--;
}
else //found the matching close
{
pairSpan = new SnapshotSpan(startPoint.Snapshot, line.Start + offset, 1);
return true;
}
}
else if (currentChar == open) // this is another open
{
openCount++;
}
offset++;
}
//move on to the next line
if (++lineNumber > stopLineNumber)
break;
line = line.Snapshot.GetLineFromLineNumber(lineNumber);
lineText = line.GetText();
offset = 0;
}
return false;
}
Private Shared Function FindMatchingCloseChar(ByVal startPoint As SnapshotPoint, ByVal open As Char, ByVal close As Char, ByVal maxLines As Integer, ByRef pairSpan As SnapshotSpan) As Boolean
pairSpan = New SnapshotSpan(startPoint.Snapshot, 1, 1)
Dim line As ITextSnapshotLine = startPoint.GetContainingLine()
Dim lineText As String = line.GetText()
Dim lineNumber As Integer = line.LineNumber
Dim offset As Integer = startPoint.Position - line.Start.Position + 1
Dim stopLineNumber As Integer = startPoint.Snapshot.LineCount - 1
If maxLines > 0 Then
stopLineNumber = Math.Min(stopLineNumber, lineNumber + maxLines)
End If
Dim openCount As Integer = 0
While True
'walk the entire line
While offset < line.Length
Dim currentChar As Char = lineText(offset)
If currentChar = close Then
'found the close character
If openCount > 0 Then
openCount -= 1
Else
'found the matching close
pairSpan = New SnapshotSpan(startPoint.Snapshot, line.Start + offset, 1)
Return True
End If
ElseIf currentChar = open Then
' this is another open
openCount += 1
End If
offset += 1
End While
'move on to the next line
If System.Threading.Interlocked.Increment(lineNumber) > stopLineNumber Then
Exit While
End If
line = line.Snapshot.GetLineFromLineNumber(lineNumber)
lineText = line.GetText()
offset = 0
End While
Return False
End Function
次のヘルパー メソッドでは、右文字に対応する左文字を検索します。
private static bool FindMatchingOpenChar(SnapshotPoint startPoint, char open, char close, int maxLines, out SnapshotSpan pairSpan)
{
pairSpan = new SnapshotSpan(startPoint, startPoint);
ITextSnapshotLine line = startPoint.GetContainingLine();
int lineNumber = line.LineNumber;
int offset = startPoint - line.Start - 1; //move the offset to the character before this one
//if the offset is negative, move to the previous line
if (offset < 0)
{
line = line.Snapshot.GetLineFromLineNumber(--lineNumber);
offset = line.Length - 1;
}
string lineText = line.GetText();
int stopLineNumber = 0;
if (maxLines > 0)
stopLineNumber = Math.Max(stopLineNumber, lineNumber - maxLines);
int closeCount = 0;
while (true)
{
// Walk the entire line
while (offset >= 0)
{
char currentChar = lineText[offset];
if (currentChar == open)
{
if (closeCount > 0)
{
closeCount--;
}
else // We've found the open character
{
pairSpan = new SnapshotSpan(line.Start + offset, 1); //we just want the character itself
return true;
}
}
else if (currentChar == close)
{
closeCount++;
}
offset--;
}
// Move to the previous line
if (--lineNumber < stopLineNumber)
break;
line = line.Snapshot.GetLineFromLineNumber(lineNumber);
lineText = line.GetText();
offset = line.Length - 1;
}
return false;
}
Private Shared Function FindMatchingOpenChar(ByVal startPoint As SnapshotPoint, ByVal open As Char, ByVal close As Char, ByVal maxLines As Integer, ByRef pairSpan As SnapshotSpan) As Boolean
pairSpan = New SnapshotSpan(startPoint, startPoint)
Dim line As ITextSnapshotLine = startPoint.GetContainingLine()
Dim lineNumber As Integer = line.LineNumber
Dim offset As Integer = startPoint - line.Start - 1
'move the offset to the character before this one
'if the offset is negative, move to the previous line
If offset < 0 Then
line = line.Snapshot.GetLineFromLineNumber(System.Threading.Interlocked.Decrement(lineNumber))
offset = line.Length - 1
End If
Dim lineText As String = line.GetText()
Dim stopLineNumber As Integer = 0
If maxLines > 0 Then
stopLineNumber = Math.Max(stopLineNumber, lineNumber - maxLines)
End If
Dim closeCount As Integer = 0
While True
' Walk the entire line
While offset >= 0
Dim currentChar As Char = lineText(offset)
If currentChar = open Then
If closeCount > 0 Then
closeCount -= 1
Else
' We've found the open character
pairSpan = New SnapshotSpan(line.Start + offset, 1)
'we just want the character itself
Return True
End If
ElseIf currentChar = close Then
closeCount += 1
End If
offset -= 1
End While
' Move to the previous line
If System.Threading.Interlocked.Decrement(lineNumber) < stopLineNumber Then
Exit While
End If
line = line.Snapshot.GetLineFromLineNumber(lineNumber)
lineText = line.GetText()
offset = line.Length - 1
End While
Return False
End Function
かっこ照合タガー プロバイダーを実装する
タガーを実装するのに加えて、タガー プロバイダーを実装してエクスポートする必要もあります。 この場合、プロバイダーのコンテンツ タイプは "text" です。 そのため、かっこ照合はすべての種類のテキスト ファイルに表示されますが、より完全な実装では、特定のコンテンツ タイプに対してのみ、かっこの照合が適用されます。
かっこ照合タガー プロバイダーを実装するには
IViewTaggerProvider から継承するタガー プロバイダーを宣言して、その名前を BraceMatchingTaggerProvider とし、ContentTypeAttribute として "text"、TagTypeAttribute として TextMarkerTag を指定して、それをエクスポートします。
[Export(typeof(IViewTaggerProvider))]
[ContentType("text")]
[TagType(typeof(TextMarkerTag))]
internal class BraceMatchingTaggerProvider : IViewTaggerProvider
<Export(GetType(IViewTaggerProvider))> _
<ContentType("text")> _
<TagType(GetType(TextMarkerTag))> _
Friend Class BraceMatchingTaggerProvider
Implements IViewTaggerProvider
BraceMatchingTagger をインスタンス化する CreateTagger メソッドを実装します。
public ITagger<T> CreateTagger<T>(ITextView textView, ITextBuffer buffer) where T : ITag
{
if (textView == null)
return null;
//provide highlighting only on the top-level buffer
if (textView.TextBuffer != buffer)
return null;
return new BraceMatchingTagger(textView, buffer) as ITagger<T>;
}
Public Function CreateTagger(Of T As ITag)(ByVal textView As ITextView, ByVal buffer As ITextBuffer) As ITagger(Of T) Implements IViewTaggerProvider.CreateTagger
If textView Is Nothing Then
Return Nothing
End If
'provide highlighting only on the top-level buffer
If textView.TextBuffer IsNot buffer Then
Return Nothing
End If
Return TryCast(New BraceMatchingTagger(textView, buffer), ITagger(Of T))
End Function
コードのビルドとテスト
このコードをテストするには、BraceMatchingTest ソリューションをビルドし、それを実験用インスタンスで実行します。
BraceMatchingTest ソリューションをビルドしてテストするには
ソリューションをビルドします。
デバッガーでこのプロジェクトを実行すると、Visual Studio の 2 つ目のインスタンスが起動されます。
テキスト ファイルを作成し、対応するかっこを含む何らかのテキストを入力します。
hello {
goodbye}
{}
{hello}
左かっこの前にキャレットを置くときには、そのかっこと、対応する右かっこの両方を強調表示する必要があります。 右かっこのすぐ後にカーソルを置くときには、そのかっこと、対応する左かっこの両方を強調表示する必要があります。
関連するコンテンツ