Django Editor in VS 2010 – Part 1 (Colors)
Friday, July 31st, 2009Look here for complete source
OK. Let's just get to it - shall we? What I want as the first step is to highlight djangoВ syntax constructsВ to make them clearly visible inside the text.
It is the job of the NDjango parser to identify them in the text. Based on the list of the syntax nodes generated by the parser I have to highlight chunks ofВ text representing django constructs. With new VSEditor it takes 3 simple steps:
- define your classification types
- write classifier - a class supplying the editor with a list of classification spans for a given text
- create a factory class, a provider, which will create an instance of the classifier for every appropriate editor window
Classification Types
A classification type definesВ the type of text to whichВ a particularВ coloring schema will be applied.В InВ my case I need a classification typeВ for django constructs.В To define a new classification type you need to define a member of type ClassificationTypeDefinition on any of your classes (you can make it static and private if you want).В You also have to give it a unique name with the Name attributeВ and mark it for export with the Export attribute.
The actual formatting forВ a content type isВ defined byВ creating a new class extending the ClassificationFormatDefinition class. Within the parameterless constructor of this class you can set various properties affecting background and foreground color, fonts, etc. This class has to be
- exported as EditorFormatDefinition,
- given a unique name with the Name attribute
- bound to your content type with the ClassificationType attribute
- decorated with 3 more attributes: DisplayName, UserVisible and Order. As of the time of this writing I am not sure what these attributes do but if they are not present, the classification format is ignored
Here is how the Django construct type definition looks like in my editor:
namespace NDjango.Designer static class Constants { /// /// Classifier definition for django tags /// internal const string DJANGO_CONSTRUCT = "ndjango.tag"; [Export] [Name(DJANGO_CONSTRUCT)] private static ClassificationTypeDefinition DjangoConstruct; [Export(typeof(EditorFormatDefinition))] [Name("ndjango.tag.format")] [DisplayName("NDjango Tag Format")] [UserVisible(true)] [ClassificationType(ClassificationTypeNames = DJANGO_CONSTRUCT)] [Order] internal sealed class NDjangoTagFormat : ClassificationFormatDefinition { public NDjangoTagFormat() { BackgroundColor = Colors.Yellow; } }
Classifier Provider
Classifier provider is a factory generating classifiers for editor windows. It is a class you have to write and it has to implement the IClassiferProvider interface. It also has to be exported as IClassifierProvider, given a unique name with the Name attribute and bound to content type with the ContentType attribute. Content type is how the Visual Studio editor figures out whether you classifier provider is to be called when a new editor window is created. The your classifier provider will only be consulted for the editor windows with matching content type. Here is how the Django Editor classifier provider looks like:
[Export(typeof(IClassifierProvider))] [ContentType(Constants.NDJANGO)] [Name("NDjango Classifier")] internal class ClassifierProvider : IClassifierProvider { [Import] internal IClassificationTypeRegistryService classificationTypeRegistry { get; set; } [Import] internal INodeProviderBroker nodeProviderBroker { get; set; } public IClassifier GetClassifier(ITextBuffer textBuffer, IEnvironment context) { if (nodeProviderBroker.IsNDjango(textBuffer)) return new Classifier(nodeProviderBroker, classificationTypeRegistry, textBuffer); else return null; } }
When a Visual StudioВ editor creates a new text buffer, it calls the GetClassifier method on the classifier provider. If this method returns a classifer object, the editor will use this object to format the text in the buffer.
Classifier
The last piece of the puzzle is the classifier object. It is a little bit more complex than what you've seen before, so let me just show you the code of my classifier and then I will walk you through the code:
internal class Classifier : IClassifier { private IClassificationTypeRegistryService classificationTypeRegistry; private NodeProvider nodeProvider; public Classifier(INodeProviderBroker nodeProviderBroker, IClassificationTypeRegistryService classificationTypeRegistry, ITextBuffer buffer) { this.classificationTypeRegistry = classificationTypeRegistry; nodeProvider = nodeProviderBroker.GetNodeProvider(buffer); nodeProvider.NodesChanged += new NodeProvider.SnapshotEvent(tokenizer_TagsChanged); } private void tokenizer_TagsChanged(SnapshotSpan snapshotSpan) { if (ClassificationChanged != null) ClassificationChanged(this, new ClassificationChangedEventArgs(snapshotSpan)); } public IList<ClassificationSpan> GetClassificationSpans(SnapshotSpan span) { List<ClassificationSpan> classifications = new List<ClassificationSpan>(); foreach (NodeSnapshot node in nodeProvider.GetTokens(span)) { if (node.SnapshotSpan.OverlapsWith(span)) classifications.Add( new ClassificationSpan( node.SnapshotSpan, classificationTypeRegistry.GetClassificationType(node.Type) )); } return classifications; } public event EventHandler<ClassificationChangedEventArgs> ClassificationChanged; }
As you can see a classifier is a class implementing the IClassifier interface. This interface defines a GetClassificationSpans method and ClassificationChanged event. When the editor thinks that some text span in the editor has been updated it calls this method to figure out what classification types have to be applied to the text. It is also listening to the ClassificationChanged event giving you a chance to inform it that you have reasons to believe that the classification of the text should be updated. This is pretty much the end of the story. The rest of what you see here is plumbing making all of this work together.
One thing worthy noting is the way this code is interacting with the django parser. The challenge here is that there are at least four different players relaying on the results of the text parsing. Classifier is one of them, I also need Tagger (for squiggles), QuickInfo (to show tooltips) and code completion.В Giving each one of them its own parser would not be a good solution. This is why this functionality is separated out into a NodeProvider class, which is also responsible for subscribing to OnChange event on the TextBuffer, parsing the text after it is modified and informing subscribers that the parsing results are ready to use by firing its NodesChanged event.
I will talk more about this in my next post in the series