Walkthrough: Displaying Matching Braces
The new home for Visual Studio documentation is Visual Studio 2017 Documentation on docs.microsoft.com.
The latest version of this topic can be found at Walkthrough: Displaying Matching Braces.
You can implement language-based features such as brace matching by defining the braces you want to match, and then adding a text marker tag to the matching braces when the caret is on one of the braces. You can define braces in the context of a language, or you can define your own file name extension and content type and apply the tags to just that type, or you can apply the tags to an existing content type (such as "text"). The following walkthrough shows how to apply brace matching tags to the "text" content type.
Starting in Visual Studio 2015, you do not install the Visual Studio SDK from the download center. It is included as an optional feature in Visual Studio setup. You can also install the VS SDK later on. For more information, see Installing the Visual Studio SDK.
To create a MEF project
Create an Editor Classifier project. Name the solution
BraceMatchingTest.Add an Editor Classifier item template to the project. For more information, see Creating an Extension with an Editor Item Template.
Delete the existing class files.
To get a brace highlighting effect that resembles the one that is used in Visual Studio, you can implement a tagger of type TextMarkerTag. The following code shows how to define the tagger for brace pairs at any level of nesting. In this example, the brace pairs []. [], and {} are defined in the tagger constructor, but in a full language implementation the relevant brace pairs would be defined in the language specification.
To implement a brace matching tagger
Add a class file and name it BraceMatching.
Import the following namespaces.
Define a class
BraceMatchingTaggerthat inherits from ITagger<T> of type TextMarkerTag.Add properties for the text view, the source buffer, and the current snapshot point, and also a set of brace pairs.
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)In the tagger constructor, set the properties and subscribe to the view change events PositionChanged and LayoutChanged. In this example, for illustrative purposes, the matching pairs are also defined in the constructor.
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 SubAs part of the ITagger<T> implementation, declare a TagsChanged event.
The event handlers update the current caret position of the
CurrentCharproperty and raise the TagsChanged event.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 SubImplement the GetTags method to match braces either when the current character is an open brace or when the previous character is a close brace, as in Visual Studio. When the match is found, this method instantiates two tags, one for the open brace and one for the close brace.
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 FunctionThe following private methods find the matching brace at any level of nesting. The first method finds the close character that matches the open character:
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 FunctionThe following helper method finds the open character that matches a close character:
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
In addition to implementing a tagger, you must also implement and export a tagger provider. In this case, the content type of the provider is "text". This means that brace matching will appear in all types of text files, but a fuller implementation would apply brace matching only to a specific content type.
To implement a brace matching tagger provider
Declare a tagger provider that inherits from IViewTaggerProvider, name it BraceMatchingTaggerProvider, and export it with a ContentTypeAttribute of "text" and a TagTypeAttribute of TextMarkerTag.
Implement the CreateTagger<T> method to instantiate a BraceMatchingTagger.
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
To test this code, build the BraceMatchingTest solution and run it in the experimental instance.
To build and test BraceMatchingTest solution
Build the solution.
When you run this project in the debugger, a second instance of Visual Studio is instantiated.
Create a text file and type some text that includes matching braces.
hello { goodbye} {} {hello}When you position the caret before an open brace, both that brace and the matching close brace should be highlighted. When you position the cursor just after the close brace, both that brace and the matching open brace should be highlighted.
Walkthrough: Linking a Content Type to a File Name Extension