Brace Matching and your Language Service
I’ve been meaning to write up some of my experiences developing for Visual Studio while I’ve been working on BooLangStudio, but I can never seem to find the time; either that, or when I can I’m not confident enough in what I’m doing to put it out here as a valid resource.
I’ll start small, here’s a quick guide to how I’ve implemented brace matching using the managed Visual Studio extensibility SDK.
What exactly is brace matching? Brace matching is where paired characters are highlighted when one or the other is selected in the editor.
There are a couple of things you need to consider when you implement brace matching.
Firstly, there are different types of braces that can be matched (depending on your language). Taking C# as an example, the brace matching works with parentheses (()
), box brackets ([]
), braces ({}
), and chevrons (<>
). These all need to be paired independently, as to avoid matching an open parenthesis with a closing brace.
Secondly, the matching has to work bi-directionally. The user can place their caret at either side of the bracket pair, and the highlighting should know where both sides are regardless.
LanguageService implementation
A lot of the legwork of implementing a language in Visual Studio is done in the LanguageService, and brace matching is no exception. You should already have a ParseSource
method in your LanguageService; this is where we’re going to work.
The ParseSource
method has a ParseRequest
parameter, which exposes a Reason
property. When this property is set to MatchBraces
, that’s when we need to do our processing.
public override AuthoringScope ParseSource(ParseRequest request)
{
if (request.Reason == ParseReason.MatchBraces)
{
// match braces here
}
}
What needs to be done is pretty simple: Parse the open document and find the partner to the brace that the caret is on.
In BooLangStudio I’ve implemented this in the following fashion:
- Run the document through a
BracketPairFinder
, which creates a list of bracket pairs in the document - Get the index of the caret (baring in mind you have to translate between the Request’s Line and Column, and an actual string index)
- Find the pair that the caret is positioned at
- Get the opposite bracket from the pair
- Get the Line and Column of the opposite bracket
It’s up to you how you implement the above steps, as long as you end up with the opposite bracket to the one you started with. I’ll illustrate using the BooLangStudio source.
public override AuthoringScope ParseSource(ParseRequest request)
{
if (request.Reason == ParseReason.MatchBraces)
{
// find all pairs
var bracketFinder = new BracketPairFinder();
var bracketPairs = bracketFinder.FindPairs(request.Text);
// get index of caret from source text
Source source = languageService.GetSource(request.View);
int indexOfCaret = source.GetPositionOfLineIndex(request.Line, request.Col);
// find the partner to the bracket at the caret
int? partner = bracketPairs.FindPartnerIndex(indexOfCaret);
if (partner != null)
{
// tell Visual Studio about the pair
}
}
}
The
Source
class has a helpfulGetPositionOfLineIndex
method, which translates between a Line and Column to a single string index. Very handy!
Once you’ve got your indices, we need to inform Visual Studio of our findings. You do that by setting the request.Sink.FoundMatchingBrace
to true
, then calling the MatchPair
method on the Sink. You need to pass two TextSpan
instances to the MatchPair
method; the first is the left brace, and the second the right.
public override AuthoringScope ParseSource(ParseRequest request)
{
if (request.Reason == ParseReason.MatchBraces)
{
...
if (partner != null)
{
// tell Visual Studio about the pair
request.Sink.FoundMatchingBrace = true;
int nextLine, nextCol;
source.GetLineIndexOfPosition(partner.Value, out nextLine, out nextCol);
request.Sink.MatchPair(
new TextSpan
{
iStartLine = request.Line,
iEndLine = request.Line,
iStartIndex = request.Col,
iEndIndex = request.Col
},
new TextSpan
{
iStartLine = nextLine,
iEndLine = nextLine,
iStartIndex = nextCol,
iEndIndex = nextCol + 1
}, 0);
}
return new AuthoringScope(); // replace with your implementation
}
}
The
Source
class has another helpful method:GetLineIndexOfPosition
method, which translates back to Line and Column from a single string index.
Finally, just return an empty AuthoringScope, as it isn’t used as part of this parse request.
That’s it! You should have now successfully implemented brace matching. You may need to tweak the TextSpan indexes depending on your parser implementation, but it shouldn’t be far wrong.
BooLangStudio implementation
If you’re interested in seeing how I handle the brace parsing, I may cover that in a future post. However, you can find all the source in my github BooLangStudio fork. Some interesting one’s in particular are:
- ParseRequestProcessor.HighlightBraces - Delegated to from the LanguageService.ParseSource method.
- BracketPairFinder - Class which parses a document to get all the bracket pairs. Uses ExcludingStringLiteralsStringWalker.
- StringWalker - A simple string parser used by the
BracketPairFinder
, which walks over a string maintaining a code-aware state. Basically, it lets you know whether it’s currently inside a bracket or string literal. Handy for not matching characters inside strings.