Extending VS2010 editor with IClassifier

I will show you in this post some internals of my HqlAddin project. The scenario is that I wanted to add highlighting for specific some specific things when editing hbm.xml files, and inside the query tag:

image

The first thing we need is a Format like this:

/// <summary>
/// Defines the string editor format for the hbm editor.
/// </summary>
[Export(typeof(EditorFormatDefinition))]
[ClassificationType(ClassificationTypeNames = Name)]
[Name(Name)] //The name of the Format
[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 StringFormat  : ClassificationFormatDefinition
{

    public const string Name = "StringFormat";

    /// <summary>
    /// Defines the visual format for the "StringClassifier" classification type
    /// </summary>
    public StringFormat()
    {
        DisplayName = Name;
        ForegroundColor = Colors.DarkRed;
    }
}

Pretty easy, this class define a format named StringFormat (which is internal to my extension), the format has a custom ForegroundColor. Nothing more, the other formats are exactly like this.

The next step is to write a IClassifierProvider :

[Export(typeof (IClassifierProvider))]
[ContentType("xml")]
internal sealed class StringClassifierProvider : IClassifierProvider
{
    [Import] private IClassificationTypeRegistryService classificationRegistry;

    #region IClassifierProvider Members

    public IClassifier GetClassifier(ITextBuffer textBuffer)
    {
        return textBuffer
            .Properties
            .GetOrCreateSingletonProperty(() => new StringClassifier(classificationRegistry));
    }

    #endregion
}

This classifier provider adds a Stringclassifier to the textbuffer for the content type “xml”. There are others content types that you can use like “code”, or the very basic “text”, on the other hand you can create your own Content Types for other file extensions.

Given the fact that all my classifier are based on regular expressions, I wrote a RegexBasedClassifier:

internal abstract class RegexBasedClassifier : IClassifier
{
    private readonly IClassificationTypeRegistryService classificationRegistry;

    protected RegexBasedClassifier(IClassificationTypeRegistryService classificationRegistry)
    {
        this.classificationRegistry = classificationRegistry;
    }

    public abstract string FormatterName { get; }

    public abstract IEnumerable<Regex> Regexs { get; }


    public IList<ClassificationSpan> GetClassificationSpans(SnapshotSpan span)
    {
        if (!span.IsInQueryTag()) return Enumerable.Empty<ClassificationSpan>().ToList();
        var startline = span.Start.GetContainingLine();
        var endline = (span.End - 1).GetContainingLine();
        var text = span.Snapshot.GetText(new SnapshotSpan(startline.Start, endline.End));

        return (from regex in Regexs
               from match in regex.Matches(text).OfType<Match>()
               select CreateSpan(span, match))
               .ToList();
    }

    private ClassificationSpan CreateSpan(SnapshotSpan span, Match match)
    {
        var snapshotSpan = new SnapshotSpan(span.Snapshot, 
                                            span.Start.Position + match.Index, 
                                            match.Length);
        return new ClassificationSpan(
            snapshotSpan, 
            classificationRegistry.GetClassificationType(FormatterName));
    }

    public event EventHandler<ClassificationChangedEventArgs> ClassificationChanged;
}

This base class check if the current line is inside a QueryTag (IsInQueryTag call), then it find the matches for all the regex defined in the derived class, and if it found a match then it create a ClassificationSpan with the FormatterName defined in the derived class.

So the implementations of the StringClassifier is:

internal class StringClassifier : RegexBasedClassifier
{
    public StringClassifier(IClassificationTypeRegistryService classificationRegistry)
        : base(classificationRegistry)
    {
    }

    public override string FormatterName
    {
        get { return StringFormat.Name; }
    }

    public override IEnumerable<Regex> Regexs
    {
        get { yield return new Regex(@"('[^']*(?:.[^']*)*'*)"); }
    }
}

And the KeywordClassifier is this:

internal class KeywordClassifier : RegexBasedClassifier
{
    private readonly string[] keywords = new[]
            {
                "join", "from", "select",
                "new", "where", "and",
                "or", "band", "between",
                "not", "join", "left",
                "inner", "fetch", "in",
                "group", "by", "sum", "count",
                "coalesce", "null", "is", "like"
            };

    public KeywordClassifier(IClassificationTypeRegistryService classificationRegistry)
        : base(classificationRegistry)
    {
    }

    public override string FormatterName
    {
        get { return KeywordFormat.Name; }
    }

    public override IEnumerable<Regex> Regexs
    {
        get
        {
            return from keyword in keywords
                   select new Regex(string.Format(@"b({0})b", keyword));
        }
    }
}

You can look at the tests for these classes in the HqlAddin.Test project. Thats all, I hope you find it useful!


blog comments powered by Disqus
  • Categories

  • Archives