Brace Matching (Managed Package Framework)
Brace matching is a powerful feature that helps the developer keep paired language elements such as braces ("{" and "}") together. When entering a closing brace, the opening brace is highlighted and the developer can see at a glance that it matches the correct location. This is brace matching.
Implementation
Brace matching is actually a subset of a more general concept of paired language elements. All programming languages have some form of paired elements such as braces or parentheses and manually keeping these elements properly paired together can become very tedious, which is where brace matching comes in.
For a managed package framework (MPF) language service to support brace matching, it has to be able to identify all paired elements in the language and then locate all matching pairs. This is typically accomplished by using the IScanner scanner to detect when a paired language element is found and then using the ParseSource method parser to locate the matching element.
The scanner indicates a language element pair has been found by setting a token trigger value of MatchBraces. When a command is processed in the OnCommand method on the Source class, the OnCommand method looks at the current token under the caret to see if a trigger has been set. To get the current token, the OnCommand method sets up a call to the scanner to tokenize the line and return the token just before the caret. If the current token contains a trigger, the OnCommand method activates the appropriate task. For example, when the MatchBraces trigger is seen, the OnCommand method calls the MatchBraces method that in turn calls the ParseSource method with the parse reason value of MatchBraces to locate the matching language element. When the matching language element is found, both elements are highlighted.
For a complete description of how typing a brace triggers the brace highlighting, see the "Example Parse Operation" in The Language Service Parser and Scanner (Managed Package Framework).
Enabling Support for Brace Matching
There are three registry entries that control various aspects of language pair matching when they are set to 1. Each of these entries is used to set corresponding properties on the LanguagePreferences class. In addition, the registry entries can be set with named parameters supplied to the ProvideLanguageServiceAttribute user attribute.
Registry Entry |
Property |
Description |
---|---|---|
MatchBraces |
Enables language pair matching |
|
MatchBracesAtCaret |
Enables language pair matching as the caret moves. |
|
ShowMatchingBrace |
Enables showing the matching brace in the status bar. |
Pairs and Triples
In addition to paired language elements, there are also triple language elements. These are elements that naturally go together in three's. For example, in C#, the foreach statement forms a triple: "foreach()", "{", and "}". All three elements are highlighted when the closing brace is typed. Since you provide the scanner and parser, you can decide if highlighting triples is useful in your language. The AuthoringSink class supports both pairs and triples with the MatchPair and MatchTriple methods, respectively.
Cascading Highlights
In a more general sense, conditional statements such as if, else if, and else, or #if, #elif, #else, #endif, are all connected as part of a single logic statement. All of the elements of the logic statement can be highlighted to clearly show each part of the logic statement. The MPF supports only pairs and triples by default. To support highlighting the entire logic statement, you can derive a class from the AuthoringSink class and provide a method that can take an arbitrary number of spans and add those spans to the internal array. The base MPF classes can highlight any number of spans automatically.
Setting the Trigger
Here is a simplified example of detecting a matching language pair, one of { }, ( ), or [ ], and setting the trigger for it in the scanner. The OnCommand method on the Source class detects the trigger and sets up a call to the parser to find the matching pair (see the "Finding the Match" section in this topic). This example is for illustrative purposes only and does not reflect how a proper scanner should be written.
using Microsoft.VisualStudio.Package;
using Microsoft.VisualStudio.TextManager.Interop;
namespace MyLanguagePackage
{
public class MyScanner : IScanner
{
private const string braces = "()[]{}";
private string m_line; // line of text to parse.
private int m_offset; // where next token starts in line.
public void SetSource(string source, int offset)
{
m_line = source;
m_offset = offset;
}
public bool ScanTokenAndProvideInfoAboutIt(TokenInfo tokenInfo,
ref int state)
{
bool fFoundToken = GetNextToken(m_offset, tokenInfo, ref state);
if (fFoundToken)
{
m_offset = tokenInfo.EndIndex + 1;
}
return fFoundToken;
}
private bool GetNextToken(int startIndex, TokenInfo tokenInfo, ref int state)
{
bool bFoundToken = false; // Assume that we are done with this line.
int index = startIndex;
if (index < m_line.Length)
{
bFound = true; // We are not done with this line.
tokenInfo.StartIndex = index;
char c = m_line[index];
if (Char.IsPunctuation(c))
{
tokenInfo.Type = TokenType.Operator;
tokenInfo.Color = TokenColor.Keyword;
tokenInfo.EndIndex = index;
if (braces.IndexOf(c) != -1)
{
tokenInfo.Trigger = TokenTriggers.MatchBraces;
}
}
else if (Char.IsWhitespace(c))
{
do
{
++index;
}
while(index < m_line.Length &&
Char.IsWhiteSpace(m_line[index]));
tokenInfo.Type = TokenType.Whitespace;
tokenInfo.Color = TokenColor.Text;
tokenInfo.EndIndex = index - 1;
}
else
{
do
{
++index;
}
while(index < m_line.Length &&
!Char.IsPunctuation(m_line[index]) &&
!Char.IsWhiteSpace(m_line[index]));
tokenInfo.Type = TokenType.Identifier;
tokenInfo.Color = TokenColor.Identifier;
tokenInfo.EndIndex = index - 1;
}
}
return bFoundToken;
}
}
}
Finding the Match
Here is a simplified example for finding the matching language pairs { }, ( ), and [ ] and adding their spans to the AuthoringSink object. This approach is not a recommended approach to parsing source code; it is for illustrative purposes only.
Nota
The MyAuthoringScope class is not shown. For this example, all methods on the MyAuthoringScope class return null values. Also, the GetPositionOfLineIndex and GetLineIndexOfPosition methods are not shown: these depend on the source being parsed to identify lines. These two methods are the equivalent to the same methods on the IVsTextLines interface.
using Microsoft.VisualStudio.Package;
using Microsoft.VisualStudio.TextManager.Interop;
namespace MyLanguagePackage
{
public class MyLanguageService : LanguageService
{
private const string braceChars = "{([])}";
private const string braceToMatchChars = "})][({";
private const string braceSearchDir = "+++---";
private AuthoringScope ParseSource(ParseRequest req)
{
if (req.Sink.BraceMatching)
{
char matchingChar = '\0';
char charToMatch = '\0';
int charIndex;
int dir = -1;
// Convert line/col to offset into text.
GetPositionOfLineIndex(req.Line, req.Col, out charIndex);
if (charIndex > 0)
{
--charIndex;
charToMatch = req.Text[charIndex];
int braceIndex = braceChars.IndexOf(charToMatch);
if (braceIndex != -1)
{
matchingChar = braceToMatchChars[braceIndex];
if (braceSearchDir[braceIndex] == '+')
{
dir = 1;
}
}
}
if (matchingChar != '\0')
{
TextSpan span1 = new TextSpan();
TextSpan span2 = new TextSpan();
int spansFound = 1;
span1.iStartLine = req.Line;
span1.iStartIndex = req.Col - 1;
span1.iEndLine = req.Line;
span1.iEndIndex = req.Col;
int nestLevel = 0;
while ((dir == -1 && charIndex > 0) ||
(dir == 1 && (charIndex + 1) < req.Text.Length))
{
charIndex += dir;
char c = req.Text[charIndex];
// Note that comments and strings should be
// checked at this point to avoid matching
// a character that should not be matched.
// Such code has been left out for brevity.
if (c == matchingChar)
{
if (nestLevel == 0)
{
int line;
int col;
// Convert offset to line/col.
GetLineIndexOfPosition(charIndex, out line, out col);
span2 = new TextSpan();
span2.iStartLine = line;
span2.iStartIndex = col;
span2.iEndLine = line;
span2.iEndIndex = col + 1;
++spansFound;
break;
}
else
{
--nestLevel;
}
}
if (c == charToMatch)
{
++nestLevel;
}
}
if (spansFound == 2)
{
req.Sink.MatchPair(span1, span2, nestLevel);
}
}
}
return new MyAuthoringScope();
}
}
}
See Also
Concepts
Language Service Features (Managed Package Framework)
The Language Service Parser and Scanner (Managed Package Framework)