How to: Highlight Document Syntax in the Rich Text Editor
- 8 minutes to read
The RichEditControl allows you to create a custom ISyntaxHighlightService implementation to display text in different colors and fonts according to the category of syntax sub-elements. These include keywords, comments, control-flow statements, variables, and other elements. This example describes how to highlight the T-SQL syntax.
Note
The syntax highlight implementation can affect the application’s performance.
Parse Document Into Tokens
A token represents a document range that should be highlighted. You can use third-party libraries or add custom syntax highlight logic to parse a document into tokens and highlight them. You can combine both approaches.
Tip
You can use the DevExpress CodeParser library to parse a document into tokens. Refer to the Rich Text Editor for WinForms - Implement ISyntaxHighlightService to Highlight C# and VB Code Syntax repository for a code sample. Note that the library supports limited amount of languages.
Take into account the following requirements when you parse document into tokens:
- Each range in a document should be marked by a token.
- Tokens cannot intersect.
- Tokens are continuous (start after the other token).
- Tokens cannot mark fields, bookmarks or hyperlinks.
Follow the steps below to parse the document into tokens:
Call the SubDocument.FindAll method to search for keywords or specific symbols.
You can use regular expressions to search for a syntax. Specify the DocumentSearchOptions.RegExResultMaxGuaranteedLength property to extend the maximum length of a string that can be obtained in a regular expression search. Access the property via the
richEditControl.Options.Search
notation.- Convert all occurrences to SyntaxHighlightToken objects. You can specify a token’s format options in the object constructor.
- Check whether the tokens intersect. If not, add them to the tokens collection.
- Parse the remaining text into tokens and add them to the same collection.
- Sort the objects in the collection according to their position in the original text.
Note
You can use the DocumentRange.Freeze() or DocumentRangeExtensions class methods to improve performance during syntax highlight.
After one of these methods is called, RichEditControl stops tracking the actual document position for this range, and the target ranges cannot be modified. The frozen document ranges become invalid after the document is modified. Don’t use these ranges for further document processing operations.
public class CustomSyntaxHighlightService : ISyntaxHighlightService
{
readonly Document document;
Regex _keywords;
// Declare a regular expression to search text in quotes (including embedded quotes)
Regex _quotedString = new Regex(@"'([^']|'')*'");
// Declare a regular expression to search commented text (including multiline)
Regex _commentedString = new Regex(@"(/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/)");
public CustomSyntaxHighlightService(Document document)
{
this.document = document;
// Declare keywords
string[] keywords = { "INSERT", "SELECT", "CREATE", "TABLE", "USE", "IDENTITY", "ON", "OFF", "NOT", "NULL", "WITH", "SET", "GO", "DECLARE", "EXECUTE", "NVARCHAR", "FROM", "INTO", "VALUES", "WHERE", "AND" };
this._keywords = new Regex(@"\b(" + string.Join("|", keywords.Select(w => Regex.Escape(w))) + @")\b");
}
private List<SyntaxHighlightToken> ParseTokens()
{
List<SyntaxHighlightToken> tokens = new List<SyntaxHighlightToken>();
DocumentRange[] ranges = null;
// Search for quoted strings
DocumentRange[] ranges = document.FindAll(_quotedString).GetAsFrozen() as DocumentRange[];
for (int i = 0; i < ranges.Length; i++) {
tokens.Add(CreateToken(ranges[i].Start.ToInt(),ranges[i].End.ToInt(), Color.Red));
}
// Extract all keywords
ranges = document.FindAll(_keywords).GetAsFrozen() as DocumentRange[];
for (int j = 0; j < ranges.Length; j++) {
if (!IsRangeInTokens(ranges[j], tokens))
tokens.Add(CreateToken(ranges[j].Start.ToInt(), ranges[j].End.ToInt(), Color.Blue));
}
// Find all comments
ranges = document.FindAll(_commentedString).GetAsFrozen() as DocumentRange[];
for (int j = 0; j < ranges.Length; j++) {
if (!IsRangeInTokens(ranges[j], tokens))
tokens.Add(CreateToken(ranges[j].Start.ToInt(), ranges[j].End.ToInt(), Color.Green));
}
// Sort tokens by their start position
tokens.Sort(new SyntaxHighlightTokenComparer());
// Fill in gaps in document coverage
tokens = CombineWithPlainTextTokens(tokens);
return tokens;
}
// Parse the remaining text into tokens:
List<SyntaxHighlightToken> CombineWithPlainTextTokens(List<SyntaxHighlightToken> tokens)
{
List<SyntaxHighlightToken> result = new List<SyntaxHighlightToken>(tokens.Count * 2 + 1);
int documentStart = this.document.Range.Start.ToInt();
int documentEnd = this.document.Range.End.ToInt();
if (tokens.Count == 0)
result.Add(CreateToken(documentStart, documentEnd, Color.Black));
else
{
SyntaxHighlightToken firstToken = tokens[0];
if (documentStart < firstToken.Start)
result.Add(CreateToken(documentStart, firstToken.Start, Color.Black));
result.Add(firstToken);
for (int i = 1; i < tokens.Count; i++)
{
SyntaxHighlightToken token = tokens[i];
SyntaxHighlightToken prevToken = tokens[i - 1];
if (prevToken.End != token.Start)
result.Add(CreateToken(prevToken.End, token.Start, Color.Black));
result.Add(token);
}
SyntaxHighlightToken lastToken = tokens[tokens.Count - 1];
if (documentEnd > lastToken.End)
result.Add(CreateToken(lastToken.End, documentEnd, Color.Black));
}
return result;
}
// Check whether tokens intersect
private bool IsRangeInTokens(DocumentRange range, List<SyntaxHighlightToken> tokens)
{
return tokens.Any(t => IsIntersect(range, t));
}
bool IsIntersect(DocumentRange range, SyntaxHighlightToken token)
{
int start = range.Start.ToInt();
if (start >= token.Start && start < token.End)
return true;
int end = range.End.ToInt() - 1;
if (end >= token.Start && end < token.End)
return true;
if (start < token.Start && end >= token.End)
return true;
return false;
}
}
// Compare token's initial positions to sort them
public class SyntaxHighlightTokenComparer : IComparer<SyntaxHighlightToken>
{
public int Compare(SyntaxHighlightToken x, SyntaxHighlightToken y)
{
return x.Start - y.Start;
}
}
Specify Token’s Format Options
The SyntaxHighlightProperties class represents a token’s format settings. You can pass this class’s object to the SyntaxHighlightToken object constructor or use it as the SyntaxHighlightToken.Properties property value.
The code sample below converts keywords occurrences to the highlight tokens and specifies their foreground color:
SyntaxHighlightToken CreateToken(int start, int end, Color foreColor)
{
SyntaxHighlightProperties properties = new SyntaxHighlightProperties();
properties.ForeColor = foreColor;
return new SyntaxHighlightToken(start, end - start, properties);
}
Apply Syntax Highlight
Note
The RichEditControl highlights unformatted text syntax only.
Call the SubDocument.ApplySyntaxHighlight within the ISyntaxHighlightService.Execute method to enable syntax highlighting.
The code sample below uses the ParseTokens
method shown above to create a list of SyntaxHighlightToken objects and pass it to the ApplySyntaxHighlight method.
public void Execute()
{
List<SyntaxHighlightToken> tSqltokens = ParseTokens();
document.ApplySyntaxHighlight(tSqltokens);
}
public void ForceExecute()
{
Execute();
}
In the main class, use the RichEditControl.ReplaceService<T> method to register the created implementation.
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
richEditControl1.Options.Search.RegExResultMaxGuaranteedLength = 500;
// Register the created service and load the document
richEditControl1.ReplaceService<ISyntaxHighlightService>(new CustomSyntaxHighlightService(richEditControl1.Document));
richEditControl1.LoadDocument("CarsXtraScheduling.sql");
// Specify the richEdit's layout settings
richEditControl1.ActiveViewType = DevExpress.XtraRichEdit.RichEditViewType.Draft;
richEditControl1.Document.Sections[0].Page.Width = Units.InchesToDocumentsF(80f);
richEditControl1.Document.DefaultCharacterProperties.FontName = "Courier New";
}