Home > English > Writing a BrightScript syntax highlight extension for Visual Studio 2010

Writing a BrightScript syntax highlight extension for Visual Studio 2010

I have recently been involved in a project where we have been required to develop using BrightScript.  BrightScript is the programming language for Roku devices.  Roku is the consumer product and there are also commercial offerings as well that allow you to build your own custom solution without ever needing to use BrightSign’s network.

Since I enjoy using Visual Studio 2010 (soon to be Visual Studio 2012)  for my development, I wanted to have a rich development experience with BrightScript.  Currently we either used NotePad++ or Visual Studio 2010 but without any syntax highlighting.  With NotePad++ it is possible to get syntax highlighting but it wasn’t the best experience.  I decided that I wanted to do some research and see just how difficult it is to write an extension to Visual Studio that would allow me to have syntax highlighting for BrightScript, specifically for “.brs” extensions.

Here is what I currently have for my development environment:

  • Visual Studio 2010 Ultimate SP1
  • Visual Studio 2010 SDK SP1

I don’t believe that you need anything else to do what I have here.  You will also need Visual Studio 2010 Professional or better to be able to do this.

Okay, so let’s get started.  First off, let’s create a project to represent our syntax.  When you launch Visual Studio, click on the Extensibility category and then select, “Editor Classifier”.  I am going to call this project “BrightScriptSyntax”.

When you click OK, Visual Studio will generate several files for you as shown below:

We will examine each of these files as well as introduce a couple others.  Let’s take a look at “BrightScriptSyntax.cs”:

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Windows.Media;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Utilities;
using Microsoft.VisualStudio.Language.StandardClassification;
using System.Text.RegularExpressions;

namespace BrightScriptSyntax
{

#region Provider definition
/// <summary>
/// This class causes a classifier to be added to the set of classifiers. Since
/// the content type is set to "text", this classifier applies to all text files
/// </summary>
[Export(typeof(IClassifierProvider))]
[ContentType("brs")]
internal class BrightScriptProvider : IClassifierProvider
{
/// <summary>
/// Import the classification registry to be used for getting a reference
/// to the custom classification type later.
/// </summary>
[Import]
internal IClassificationTypeRegistryService ClassificationRegistry = null;

public IClassifier GetClassifier(ITextBuffer buffer)
{
return buffer.Properties.GetOrCreateSingletonProperty<BrightScript>(delegate { return new BrightScript(ClassificationRegistry); });
}
}
#endregion //provider def

#region Classifier
/// <summary>
/// Classifier that classifies all text as an instance of the OrinaryClassifierType
/// </summary>
class BrightScript : IClassifier
{
BrightScriptLanguage _brightScriptLanguage;
IClassificationType _classificationType;
IClassificationType _whiteSpaceType;
IClassificationType _keywordType;
IClassificationType _commentType;
IClassificationType _stringType;
IClassificationType _identifierType;
IClassificationType _numericType;

internal BrightScript(IClassificationTypeRegistryService registry)
{
_brightScriptLanguage = new BrightScriptLanguage();
_classificationType = registry.GetClassificationType("BrightScript");
_whiteSpaceType = registry.GetClassificationType(
              PredefinedClassificationTypeNames.WhiteSpace);
_keywordType = registry.GetClassificationType(
              PredefinedClassificationTypeNames.Keyword);
_commentType = registry.GetClassificationType(
              PredefinedClassificationTypeNames.Comment);
_stringType = registry.GetClassificationType(
              PredefinedClassificationTypeNames.String);
_identifierType = registry.GetClassificationType(
              PredefinedClassificationTypeNames.Operator);
_numericType = registry.GetClassificationType("BrightScriptNumeric");
}

/// <summary>
/// This method scans the given SnapshotSpan for potential matches for this
        /// classification.
/// In this instance, it classifies everything and returns each span as a
        /// new ClassificationSpan.
/// </summary>
/// <param name="trackingSpan">The span currently being classified</param>
/// <returns>A list of ClassificationSpans that represent spans identified
        /// to be of this classification</returns>
public IList<ClassificationSpan> GetClassificationSpans(SnapshotSpan span)
{
//create a list to hold the results
List<ClassificationSpan> classifications = new List<ClassificationSpan>();
string current = span.GetText();
bool commentFound = false;
// Note:  Comments should go to the end of the line.
foreach (var item in _brightScriptLanguage.Comments)
{
Regex reg = new Regex(item, RegexOptions.IgnoreCase);
var matches = reg.Matches(current);
for (int i = 0; i < matches.Count; i++)
{
commentFound = true;
Match m = matches[i];
Span new_span = new Span(span.Start.Position + m.Index,
                      current.Length - m.Index);
SnapshotSpan new_snapshot = new SnapshotSpan(span.Snapshot,
                      new_span);
var newText = new_snapshot.GetText();
classifications.Add(new ClassificationSpan(new_snapshot,
                      _commentType));
}
}
if (commentFound)
return classifications;
Classify(classifications, current, span, _brightScriptLanguage.Custom,
              _classificationType);
Classify(classifications, current, span, _brightScriptLanguage.Quoted,
              _stringType);
Classify(classifications, current, span, _brightScriptLanguage.KeyWords,
              _keywordType);
Classify(classifications, current, span, _brightScriptLanguage.IdentifierTypes,
              _identifierType);
Classify(classifications, current, span, _brightScriptLanguage.Numeric,
              _numericType);
return classifications;
}
private void Classify(List<ClassificationSpan> classifications, string current,
          SnapshotSpan span, List<string> matchList, IClassificationType classificationType)
{
foreach (var item in matchList)
{
Regex reg = new Regex(item, RegexOptions.IgnoreCase);
var matches = reg.Matches(current);
for (int i = 0; i < matches.Count; i++)
{
Match m = matches[i];
Span new_span = new Span(span.Start.Position + m.Index, m.Length);
SnapshotSpan new_snapshot = new SnapshotSpan(span.Snapshot, new_span);
var newText = new_snapshot.GetText();
classifications.Add(new ClassificationSpan(new_snapshot,
                      classificationType));
}
}
}

#pragma warning disable 67
// This event gets raised if a non-text change would affect the classification
        // in some way, for example typing /* would cause the classification to
        // change in C# without directly affecting the span.
public event EventHandler<ClassificationChangedEventArgs> ClassificationChanged;
#pragma warning restore 67
}
#endregion //Classifier
}

This file differs slightly from what was originally generated by the Project Template. First, note that the ContentType is set to “brs”. This is the extension for which I want the syntax highlighting to work. I don’t want my syntax highlighter to affect all text files as is the default behavior.

The BrightScriptProvider class is has not been changed.

The next portion is where all of our work is going to be constrained from a development perspective. The BrightScript class is our classifier. It is this class that tell Visual Studio how to go about providing syntax highlighting for our BrightScript language. I want to point out that I did cheat a little and I included the following assembly to help make my development a little easier and more consistent with what Visual Studio already does with languages like C#. If you look at the screenshot below, you will see the project reference that I included:

As you can see, you will need to include the Microsoft.VisualStudio.Language.StandardClassification assembly.

It is this assembly that we get the bulk of our syntax formats. You will observe that I provide two custom formats and fall back on using the standard formats from Visual Studio.

Next, we will look at the GetClassificationSpans method. This method is the engine for performing our syntax highlighting. I keep a collection of ClassificationSpan objects that will tell Visual Studio how to classify the text it encounters. I start off by first trying to find comments in BrightScript. The rule is simple: if a comment is found, then take the rest of the current line and make it part of the comment. This is a basic single line comment rule. I have a custom class called, “BrightScriptLanguage” that represents the keywords and language of BrightScript. I loop through each keyword that is defined as a comment and create a new ClassificationSpan with the correct span information as well as indicated for it to use the comment classification type.

After I finish the loop, I then check to see if I previously found a comment. If this is true, then I return with the current list of ClassificationSpan objects. Otherwise, I then perform similar checks against Custom, Quoted (strings), Keywords, IdentifierTypes, and Numeric language types. We will look at the language definition very soon.

Finally, you will see the helper method that I wrote that is almost exactly like that of the comment check but does not assume the whole line when a match is found.

At the end of this class is a public EventHandler that is provided to us for free by the Project Template.

Let’s take a look at the “BrightScriptLanguage”:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.Text.Classification;

namespace BrightScriptSyntax
{
class BrightScriptLanguage
{
#region Member Variables

private List<string> _comments = new List<string>();
private List<string> _quoted = new List<string>();
private List<string> _numeric = new List<string>();
private List<string> _keyWords = new List<string>();
private List<string> _identiferTypes = new List<string>();
private List<string> _custom = new List<string>();

#endregion

#region Properties

public List<string> Comments
{
get { return _comments; }
}
public List<string> Quoted
{
get { return _quoted; }
}
public List<string> Numeric
{
get { return _numeric; }
}
public List<string> KeyWords
{
get { return _keyWords; }
}
public List<string> IdentifierTypes
{
get { return _identiferTypes; }
}
public List<string> Custom
{
get { return _custom; }
}

#endregion

#region ctor

public BrightScriptLanguage()
{
Initialize();
}

#endregion

#region Methods

private void Initialize()
{
_custom.Add(@"\breturn\b");

_comments.Add("\'");
_comments.Add(@"\brem\b");

_quoted.Add(@"([""'])(?:\\\1|.)*?\1");

_numeric.Add(@"\b\d+\b");
_numeric.Add(@"\>");
_numeric.Add(@"\<");
_numeric.Add(@"\<\>");
_numeric.Add(@"\=");
_numeric.Add(@"\=\=");

_keyWords.Add(@"\bpos\b");
_keyWords.Add(@"\band\b");
_keyWords.Add(@"\bprint\b");
_keyWords.Add(@"\bline_num\b");
_keyWords.Add(@"\bor\b");
_keyWords.Add(@"\bgoto\b");
_keyWords.Add(@"\bnext\b");
_keyWords.Add(@"\bnot\b");
_keyWords.Add(@"\bstep\b");
_keyWords.Add(@"\bdim\b");
_keyWords.Add(@"\bthen\b");
_keyWords.Add(@"\bstop\b");
_keyWords.Add(@"\belse\b");
_keyWords.Add(@"\bto\b");
_keyWords.Add(@"\btab\b");
_keyWords.Add(@"\bobjfun\b");
_keyWords.Add(@"\btype\b");
_keyWords.Add(@"\brnd\b");
_keyWords.Add(@"\btrue\b");
_keyWords.Add(@"\bfalse\b");
_keyWords.Add(@"\bcreateobject\b");
_keyWords.Add(@"\bend while\b");
_keyWords.Add(@"\bexit while\b");
_keyWords.Add(@"\bwhile\b");
_keyWords.Add(@"\bend sub\b");
_keyWords.Add(@"\bsub\b");
_keyWords.Add(@"\bend for\b");
_keyWords.Add(@"\bfor each\b");
_keyWords.Add(@"\bfor\b");
_keyWords.Add(@"\bexit\b");
_keyWords.Add(@"\bendif\b");
_keyWords.Add(@"\bif\b");
_keyWords.Add(@"\bend function\b");
_keyWords.Add(@"\bfunction\b");
_keyWords.Add(@"\bas\b");
// Types
_identiferTypes.Add(@"\bvoid\b");
_identiferTypes.Add(@"\bboolean\b");
_identiferTypes.Add(@"\binteger\b");
_identiferTypes.Add(@"\bfloat\b");
_identiferTypes.Add(@"\bdouble\b");
_identiferTypes.Add(@"\bstring\b");
_identiferTypes.Add(@"\bobject\b");
_identiferTypes.Add(@"\binterface\b");
_identiferTypes.Add(@"\binvalid\b");
}

#endregion
     };
}

This class basically provides the meta-data necessary for me to use regular expression to determine whether or not we have a match. It is pretty much explanatory but I wanted to show you my simple regular expression solution for parsing the text.

Let’s move on to the “BrightScriptSyntaxType.cs” file:

using System.ComponentModel.Composition;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Utilities;

namespace BrightScriptSyntax
{
internal static class BrightScriptClassificationDefinition
{
/// <summary>
/// Defines the "BrightScript" classification type.
/// </summary>
[Export(typeof(ClassificationTypeDefinition))]
[Name("BrightScript")]
internal static ClassificationTypeDefinition BrightScriptType = null;

[Export(typeof(ClassificationTypeDefinition))]
[Name("BrightScriptNumeric")]
internal static ClassificationTypeDefinition BrightScriptNumericType = null;

[Export]
[Name("brs")]
[BaseDefinition("text")]
internal static ContentTypeDefinition BrightScriptContentDefinition = null;

[Export]
[FileExtension(".brs")]
[ContentType("brs")]
internal static FileExtensionToContentTypeDefinition
          BrightScriptFileExtensionDefinition = null;
}
}

This class uses MEF by exporting the required information necessary for this extension. First off, we see our two custom classification types: “BrightScript” and “BrightScriptNumeric”. Next we see our entries for satisfying the file extension requirement. This is two-part in that we need a ContentTypeDefinition as well as a FileExtensionToContentTypeDefinition entry. We use the ContentTypeDefinition to define the name and base definition of our content type. Next we use the “.brs” file extension as well as reference the “brs” content type.

The final file that wee need to take a look at is the, “BrightScriptSyntaxFormat.cs” file:

using System.ComponentModel.Composition;
using System.Windows.Media;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Utilities;

namespace BrightScriptSyntax
{
#region Format definition
/// <summary>
/// Defines an editor format for the BrightScript type that has a purple background
/// and is underlined.
/// </summary>
[Export(typeof(EditorFormatDefinition))]
[ClassificationType(ClassificationTypeNames = "BrightScript")]
[Name("BrightScript")]
[UserVisible(true)] //this should be visible to the end user
[Order(Before = Priority.Default)] //set the priority to be after the default classifiers
internal sealed class BrightScriptFormat : ClassificationFormatDefinition
{
/// <summary>
/// Defines the visual format for the "BrightScript" classification type
/// </summary>
public BrightScriptFormat()
{
this.DisplayName = "BrightScript"; //human readable version of the name
this.ForegroundColor = Colors.OrangeRed;
}
}
#endregion //Format definition

#region Format definition
/// <summary>
/// Defines an editor format for the BrightScript type that has a purple background
/// and is underlined.
/// </summary>
[Export(typeof(EditorFormatDefinition))]
[ClassificationType(ClassificationTypeNames = "BrightScriptNumeric")]
[Name("BrightScriptNumeric")]
[UserVisible(true)] //this should be visible to the end user
[Order(Before = Priority.Default)] //set the priority to be after the default classifiers
internal sealed class BrightScriptNumericFormat : ClassificationFormatDefinition
{
/// <summary>
/// Defines the visual format for the "BrightScript" classification type
/// </summary>
public BrightScriptNumericFormat()
{
this.DisplayName = "BrightScriptNumeric"; //human readable version of the name
this.ForegroundColor = Colors.Goldenrod;
}
}
#endregion //Format definition
}

It is this file that defines the custom formats we want for “BrightScript” and “BrightScriptNumeric”. Most of these attributes are already provided by the Project Template. The only custom thing that I am performing here is setting the ForegroundColor to the corresponding value for each format.

Before we can upload this extension, we need to be sure that we have all of our information correct in the “vsixmanifest” file as shown below:

Just fill in the information that best describes your extension.  I also provided an icon and an image preview so that when developers search for my extension, they can see a sample of what it does.

Finally, I uploaded the extension with the Visual Studio Extension Gallery as shown below:

That’s all there is to it! I hope this helps anyone else who has wondered or wanted to write a custom Visual Studio Extension

Here is a link to the project.

Advertisements
  1. May 2, 2013 at 8:09 pm

    I enjoy, result in I discovered just what I used to be taking a look for.

    You’ve ended my four day lengthy hunt! God Bless you man. Have a great day. Bye

  2. May 16, 2013 at 6:37 pm

    Hi Matt,

    I have to say this is one of the best plugins i have seen.. thanks a million for sharing it..
    I wonder if this also does “intellisense” for methods, when you are working with an instance of a given script object..

    if not now, what would be the effort required to do that? i am willing to help.

    Regards,
    Hitesh.

    • September 25, 2014 at 11:57 am

      Hi Hitesh,

      It wouldn’t be too hard to have some basic intellisense. I am not sure how large an effort it would be to support very large documents though.

      Regards,

      Matt

  3. September 3, 2014 at 11:14 pm

    Thanks for every other wonderful post. Where else could
    anyone get that type of info in such an ideal approach of writing?
    I have a presentation next week, and I’m on the look for such info.

  4. Richie Hindle
    September 25, 2014 at 11:06 am

    Matt – I just wanted to send a big Thank You for this article – I needed something similar but couldn’t justify spending a lot of time on it, and your article meant I could write it very quickly. Thanks!

    • September 25, 2014 at 11:54 am

      Hi Richie,

      Thank you so very much for you feedback. I am glad that it helped!

  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: