일치시킬 중괄호를 정의하고 중괄호 중 하나에 캐럿이 있으면 일치하는 중괄호에 텍스트 마커 태그를 추가하여 중괄호 일치와 같은 언어 기반 기능을 구현합니다. 언어 컨텍스트에서 중괄호를 정의하고 고유한 파일 이름 확장명과 콘텐츠 형식을 정의하고 해당 형식에만 태그를 적용하거나 기존 콘텐츠 형식(예: "text")에 태그를 적용할 수 있습니다. 다음 연습에서는 "text" 콘텐츠 형식에 중괄호 일치 태그를 적용하는 방법을 설명합니다.
MEF(Managed Extensibility Framework) 프로젝트 만들기
MEF 프로젝트를 만들려면
편집기 분류자 프로젝트를 만듭니다. 솔루션의 이름을 BraceMatchingTest로 지정합니다.
Visual Studio에서 사용되는 것과 유사한 중괄호 강조 표시 효과를 얻으려면 TextMarkerTag 형식 태거를 구현하면 됩니다. 다음 코드에서는 중첩 수준에서 중괄호 쌍에 대한 태거를 정의하는 방법을 보여 줍니다. 이 예제에서는 [] 및 {}의 중괄호 쌍이 태거 생성자에서 정의되지만 전체 언어 구현에서는 관련 중괄호 쌍이 언어 사양에서 정의됩니다.
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;
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)
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
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
현재 문자가 GetTags 여는 중괄호이거나 이전 문자가 닫는 중괄호인 경우 Visual Studio에서와 같이 중괄호가 일치하도록 메서드를 구현합니다. 일치 항목이 발견되면 이 메서드는 여는 중괄호와 닫는 중괄호에 각각 하나씩 태그 두 개를 인스턴스화합니다.
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"입니다. 따라서 중괄호 일치는 모든 형식의 텍스트 파일에 나타나지만 보다 완전한 구현은 특정 콘텐츠 형식에만 중괄호 일치를 적용합니다.
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의 두 번째 인스턴스가 시작됩니다.
텍스트 파일을 만들고 일치하는 중괄호가 포함된 텍스트 일부를 입력합니다.
hello {
goodbye}
{}
{hello}
여는 중괄호 앞에 캐럿을 배치하면 해당 중괄호와 일치하는 닫는 중괄호 모두 강조 표시되어야 합니다. 닫는 중괄호 뒤에 커서를 배치하면 해당 중괄호와 일치하는 여는 중괄호 모두 강조 표시되어야 합니다.