Archive for July, 2009

Django Editor in VS 2010 – Part 1 (Colors)

Friday, July 31st, 2009

Look 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

Django Editor in VS 2010 – Part 0 (Background)

Wednesday, July 29th, 2009

Look here for complete source

The purpose of this post series is to give you a concrete proof (with code samples) of how easy it is to build custom text editors with Visual Studio 2010. But before we dive into the details please bear with me as I give you some background.

Ok we've got NDjango - nice language, everybody's happy with it, just loves it, fine. Now, how about an editor for ndjango templates? NDjango is django ported to .NET so, naturally, the editor is supposed to be integrated with Visual Studio.

What would be necessary here is a text editor with syntax highlighting, error reporting and code completion to support django syntax in the templates built for .NET applications. Django templates can be used to generate text files of various types - including html, json, xml to name a few. So all features of the django template editor should work in addition to the features already provided, i.e. for an html file syntax highlighting and code completion should work both for django as well as for the existing html.

Visual Studio 2008 provides variety of ways for custom editor  integration. It also allows building custom editors on top of the generic text editor. While building the editor this way is certainly doable, it is really heavy lifting. You need to implement your own language service with your own implementation of IVsLanguageInfo, IVsCompletionSet, IVsLanguageDebugInfo, - the list goes on and on.

And this is not the worst of it. After you are done with all the plumbing registering and have your nice editor for your nice language, you still have a problem. The Visual Studio Core Editor used as a starting point is a bare bones editor with not much beyond simple cut copy paste. If you need more - you are the one to implement it. If you need html support (at least I do) - build it.

Wouldn't it be nice if instead of building all this stuff from scratch I could piggy back on the existing editors? - Oh well...

As I was gathering my courage before I plunge head first into the task of building the NDjango Language service somebody told me to have a look at Visual Studio 2010 beta 1. So I did and now I am writing this still in awe at how much simpler it is.  

The simplicity comes in two flavors. First of all now rather than writing your own you indeed amend the behavior of the existing editors providing simple plug-ins for the functions you want to affect - i.e. coloring your tags. Secondly, thanks to MEF, the plumbing necessary for your plugins to play nice with Visual Studio is virtually non-existing.

OK. Now it is time to stop blabbering and start dig into specifics. In the next post I will start with the colorization code for the NDjango template editor.

Injecting functionality into Visual Studio projects, or if neither of the two options are good, go for the third one

Tuesday, July 28th, 2009

For some time now I was dancing around a question: How do I inject additional functionality into a Visual Studio project.

One option would be to build a VSPackage which will be always loaded and will be watching every move you make and interfere when it thinks the time is right. Another option is to implement a custom project type so that these extra commands are only available for the projects of this type.

I was going back and forth between these 2 options. The first one seems to be too intrusive, the second one is too much work - even if you use project flavoring, you still need to create a flavor for every project you intend to support. It also makes it difficult to convert existing projects to bistro projects.

It looks like the right chice is neither and all this time it was right under my nose. The option I am going to go with is based on the VsProjectStartupServices interface. This interface allows to add to the project file a GUID of a global service to be started when the project is opened.

Here is how this approach works:

First, in your package definition you have to declare the service as a global service:

// This attribute registers a service used to
// bootstrap the package for converted projects
[ProvideService(typeof(SProjectManager))]

The next step is to actually instantiate and register the object implementing the service:

protected override void Initialize()
{
   base.Initialize();
   solutionEvents = new SolutionEventSinks();
   projectManager = new ProjectManager(this, solutionEvents);
   IServiceContainer container = this as IServiceContainer;
   container.AddService(typeof(SProjectManager), projectManager, true);
 
    ...
 
}

Do not miss the last parameter to the AddService method. Setting it to true promotes the service registration. In other words makes it global.

Based on this registartion a call to the Package.GetGlobalService(typeof(SProjectManager)) will always return an instance of the ProjectManager. It means that if the package is not active yet, Visual Studio will load the package first, giving it a chance to instantiate and Register the ProjectManager service. This is what will happen if any project with a reference to the ProjectManager service will be opened in Visual Studio.

Now inserting such a reference into a project is pretty simple too. All what is necessary is a command executing the following:

internal void Convert()
{
   object startupService;
 
   ErrorHandler.ThrowOnFailure(
         hierarchy.GetProperty(
                VSConstants.VSITEMID_ROOT,
                (int)__VSHPROPID.VSHPROPID_StartupServices,
                out startupService));
 
   Guid guid = new Guid(Guids.guidProjectManager);
 
   ErrorHandler.ThrowOnFailure(
        ((IVsProjectStartupServices)startupService)
                .AddStartupService(ref guid));
}

Such command can be placed somewhere in the menu and configured in such a way that the command is visible even if the package is not loaded.