Thursday, December 14, 2006

RichTextBox syntax highlighting

Today we'll learn how to create RichTextBox with syntax highlight ing. Syntax highlighting is a feature of some text editors that displays text—especially source code—in different colors and fonts according to the category of terms. This feature eases writing in a structured language such as a programming language or a markup language as both structures and syntax errors are visually distinct.

The first thing we need is RichTextBox. So, do it. I did some trick on it in order to prevent from <P> (Paragraph) inserting on each enter (with is default behavior of RichTextBox). I want only one line down when I pressed my Enter key.

<RichTextBox ScrollViewer.CanContentScroll="True" ScrollViewer.VerticalScrollBarVisibility="Visible" Name="TextInput" AcceptsReturn="True" TextChanged="TextChangedEventHandler">
      <RichTextBox.Resources>
        <Style TargetType="{x:Type Paragraph}">
          <Setter Property="Margin" Value="0"/>
        </Style>
      </RichTextBox.Resources>
    </RichTextBox>

Ok, not I want only plaintext in my TextBox TextChanged event 'cos I'm going to color it myself.

private void TextChangedEventHandler(object sender, TextChangedEventArgs e)
        {
            if (TextInput.Document == null)
                return;
 
            TextRange documentRange = new TextRange(TextInput.Document.ContentStart, TextInput.Document.ContentEnd);
            documentRange.ClearAllProperties();

 


Now let's create navigator to go though the text and hightlight it


TextPointer navigator = TextInput.Document.ContentStart;
            while (navigator.CompareTo(TextInput.Document.ContentEnd) < 0)
            {
                TextPointerContext context = navigator.GetPointerContext(LogicalDirection.Backward);
                if (context == TextPointerContext.ElementStart && navigator.Parent is Run)
                {
                    CheckWordsInRun((Run)navigator.Parent);
 
                }
                navigator = navigator.GetNextContextPosition(LogicalDirection.Forward);
            }

 


So the only thing we have to do is to check each word within my navigator, compare it to Highlighting dictionary and color it. Huh, where is my dictionary? Here it comes. I'll do it for ActionScript for my client.

class JSSyntaxProvider
   {
       static List<string> tags = new List<string>();
       static List<char> specials = new List<char>();
       #region ctor
       static JSSyntaxProvider()
       {
           string[] strs = {
               "Anchor",
               "Applet",
               "Area",
               "Array",
               "Boolean",.....
tags = new List<string>(strs);
 
We also want to know all possible delimiters so adding this stuff.
 
            char[] chrs = {
                '.',
                ')',
                '(',
                '[',
                ']',
                '>',
                '<',
                ':',
                ';',
                '\n',
                '\t'
            };
            specials = new List<char>(chrs);
 

Now I should check statically if the string I passed is legal and constants in my dictionary

public static bool IsKnownTag(string tag)
        {
            return tags.Exists(delegate(string s) { return s.ToLower().Equals(tag.ToLower()); });
        }

Also, I'll do kind of Intellisense later, so I want to check the beginnings of my tags as well

public static List<string> GetJSProvider(string tag)
        {
            return tags.FindAll(delegate(string s) { return s.ToLower().StartsWith(tag.ToLower()); });
        }

Wow. Great. Now I should separate words, that equals to my tags. For this propose we'll create new internal structure named Tag. This will help us to save words and its' positions.

new struct Tag
        {
            public TextPointer StartPosition;
            public TextPointer EndPosition;
            public string Word;
 
        }

How, let's go through our text and save all tags we have to save.

int sIndex = 0;
            int eIndex = 0;
            for (int i = 0; i < text.Length; i++)
            {
                if (Char.IsWhiteSpace(text[i]) | JSSyntaxProvider.GetSpecials.Contains(text[i]))
                {
                    if (i > 0 && !(Char.IsWhiteSpace(text[i - 1]) | JSSyntaxProvider.GetSpecials.Contains(text[i - 1])))
                    {
                        eIndex = i - 1;
                        string word = text.Substring(sIndex, eIndex - sIndex + 1);
 
                        if (JSSyntaxProvider.IsKnownTag(word))
                        {
                            Tag t = new Tag();
                            t.StartPosition = run.ContentStart.GetPositionAtOffset(sIndex, LogicalDirection.Forward);
                            t.EndPosition = run.ContentStart.GetPositionAtOffset(eIndex + 1, LogicalDirection.Backward);
                            t.Word = word;
                            m_tags.Add(t);
                        }
                    }
                    sIndex = i + 1;
                }
            }

How this works. But wait. If the word is last word in my text I'll never hightlight it, due I'm looking for separators. Let's add some fix for this case

string lastWord = text.Substring(sIndex, text.Length - sIndex);
            if (JSSyntaxProvider.IsKnownTag(lastWord))
            {
                Tag t = new Tag();
                t.StartPosition = run.ContentStart.GetPositionAtOffset(sIndex, LogicalDirection.Forward);
                t.EndPosition = run.ContentStart.GetPositionAtOffset(eIndex + 1, LogicalDirection.Backward);
                t.Word = lastWord;
                m_tags.Add(t);
            }

How I have all my words and its' positions in list. Let's color it! Dont forget to unsubscribe! text styling fires TextChanged event.


TextInput.TextChanged -= this.TextChangedEventHandler;
 
            for(int i=0;i<m_tags.Count;i++)
            {
                TextRange range = new TextRange(m_tags[i].StartPosition, m_tags[i].EndPosition);
                range.ApplyPropertyValue(TextElement.ForegroundProperty, new SolidColorBrush(Colors.Blue));
                range.ApplyPropertyValue(TextElement.FontWeightProperty, FontWeights.Bold);
            }
            m_tags.Clear();
 
            TextInput.TextChanged += this.TextChangedEventHandler;

That's all. We have nice RichTextBox, that understands and color any syntax we want. Maybe later we'll add Intellisense for it.


Source code of this article

1 comment:

Timothy Parez said...

Hi,

Great work, the only problem is performance which takes a real hit
once you have highlighted words.

One option would be to only
check the current line

using TextInput.CaretPosition.GetLineStartPosition(0) for the beginning of the current line and TextInput.CaretPosition.GetLineStartPosition(1) for the beginning of the next line and applying the highlighting only to that part of the text

You'd have to do some null checking on GetLineStartPosition(1) as well of course.

This way performance is reset on a new line, altough it still takes a hit on lines with highlighted text,
any suggestions?