Archive for the ‘django’ Category

Django Editor in VS 2010 – Part 3 (Squiggles)

Monday, August 3rd, 2009

Look here for complete source

Squiggles are wavy lines marking problematic areas. I would like to use them to indicate that there is a problem with certain django construct - tag and/or keyword. It also would be nice to give more specific diagnostic about what's wrong. For this purpose I will use the SquiggleTag class.

Creating a squiggle using SquiggleTag is very similar to working with classifiers described in Part 1 of the series. As with the classifiers it takes 3 steps:

  • create tag definition
  • create tagger
  • create tagger provider

Because it is so similar, I think it is better just to show you the code and then give some brief explanation.

Error Tag definition:

        internal class ErrorTag : SquiggleTag
        {
            public ErrorTag()
                : base("error") { }
        }

The lonely parameter of the SquiggleTag constructor controls the squiggle color. The SquiggleTag also has a property ToolTip and initially I thought of using it to show the diagnostic message, but it appears that this property is broken in Beta 1, so I decided to go the same route the VsEditor team is going and use the quick info for this purpose. I plan to cover quick info in the next post. Among other things using quick info gives you more flexibility - i.e. you can show in ToolTip both the error message as well as regular tips.

Now the provider:

    [Export(typeof(ITaggerProvider))]
    [ContentType(Constants.NDJANGO)]
    [TagType(typeof(SquiggleTag))]
    class TaggerProvider : ITaggerProvider
    {
        [Import]
        internal INodeProviderBroker nodeProviderBroker { get; set; }
 
        public ITagger<T> CreateTagger<T>(ITextBuffer buffer, IEnvironment context) where T : ITag
        {
            if (nodeProviderBroker.IsNDjango(buffer))
                return (ITagger<T>)new Tagger(nodeProviderBroker, buffer);
            else
                return null;
        }
    }

No surprises here.

And finally the tagger:

    class Tagger : ITagger<Constants.ErrorTag>
    {
        private NodeProvider nodeProvider;
 
        public Tagger(INodeProviderBroker nodeProviderBroker, ITextBuffer buffer)
        {
            nodeProvider = nodeProviderBroker.GetNodeProvider(buffer);
            nodeProvider.NodesChanged += new NodeProvider.SnapshotEvent(provider_TagsChanged);
        }
 
        void provider_TagsChanged(SnapshotSpan snapshotSpan)
        {
            if (TagsChanged != null)
                TagsChanged(this, new SnapshotSpanEventArgs(snapshotSpan));
        }
 
        public IEnumerable<ITagSpan<Constants.ErrorTag>> GetTags(Microsoft.VisualStudio.Text.NormalizedSnapshotSpanCollection spans)
        {
            foreach (SnapshotSpan span in spans)
            {
                foreach (NodeSnapshot node in nodeProvider.GetNodes(span))
                {
                    if (node.SnapshotSpan.OverlapsWith(span) && node.Node.ErrorMessage.Severity > 0)
                        yield return new TagSpan<Constants.ErrorTag>(node.SnapshotSpan, new Constants.ErrorTag());
                }
            }
        }
 
        public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
    }

In the next post I will tackle something a little more complex - quick info

Django Editor in VS 2010 – Part 2 (Background Parsing)

Sunday, August 2nd, 2009

Look here for complete source

Every time the text in the editor window changes, all your classification, colorization, tagging etc. has to be re-evaluated. If you are lucky you can just take the text updates and run them through your parser. If it takes just a few statements to parse the updates, you do not need to read the rest of this post. If your parser is really that simple (or smart) then all you need in your classifier, tagger and/or quickinfo controller is an event handler attached to the ITextBuffer's Changed event:

            buffer.Changed += new EventHandler(buffer_Changed);

and then run the changes through your parser right in the event handler. You have to keep in mind though, that this code will be executed almost on each and every keystroke in your edit window.

So, if you are like the rest of us and have to worry about making the editor too sluggish to be useful, you might want to have a look at a few tricks which helped me to address this problem. Actually I have three:

  • Make sure that no matter how many classifiers, taggers, whatever need results of parsing, all changes are ran through the parser only once
  • Make parsing asynchronous - do not make editor wait for the parser to finish parsing.
  • Queue the parsing - delay parsing for a second or so. This way if the update notifications are coming really fast you will still run the parser no more often than once a second

At this time I do not have an implementation for the last one, but the overall structure of what I have lends itself well to parsing requests queuing.

To cover the first part I created my own interface INodeProviderBroker. I also wrote a class implementing the interface and exported it as a MEF component:

    internal interface INodeProviderBroker
    {
        NodeProvider GetNodeProvider(ITextBuffer buffer);
        bool IsNDjango(ITextBuffer buffer);
    }
 
    [Export(typeof(INodeProviderBroker))]
    internal class NodeProviderBroker : INodeProviderBroker
    {
 
        //the real parser
        IParser parser = new Parser();
 
        public bool IsNDjango(ITextBuffer buffer)
        {
            switch (buffer.ContentType.TypeName)
            {
                case "text":
                case "HTML":
                    return true;
                default: return false;
            }
        }
 
        public NodeProvider GetNodeProvider(ITextBuffer buffer)
        {
            NodeProvider provider;
            if (!buffer.Properties.TryGetProperty(typeof(NodeProvider), out provider))
                buffer.Properties.AddProperty(typeof(NodeProvider), provider = new NodeProvider(parser, buffer));
            return provider;
        }
 
    }

Each tagger, classifier what have you can import the provider broker and use the GetNodeProvider method to get a node provider for a given buffer. As you can see, the way GetNodeProvider works is it creates a new NodeProvider only once - upon first request for a given buffer. From this moment on, the GetNodeProvider will return the existing node provider taken from the buffer's property bag. Using MEF in this particular case might have been an overkill, but I decided to follow the lead of how this sort of things is done in VsEditor.

As to the second part, let me step you through the code for the node provider:

    class NodeProvider
    {
        private List<NodeSnapshot> nodes = new List<NodeSnapshot>();
        private object node_lock = new object();
        private IParser parser;
        private ITextBuffer buffer;
 
        public NodeProvider(IParser parser, ITextBuffer buffer)
        {
            this.parser = parser;
            this.buffer = buffer;
            rebuildNodes(buffer.CurrentSnapshot);
            buffer.Changed += new EventHandler(buffer_Changed);
        }
 
        public delegate void SnapshotEvent (SnapshotSpan snapshotSpan);
 
        void buffer_Changed(object sender, TextContentChangedEventArgs e)
        {
            rebuildNodes(e.After);
        }
 
        private void rebuildNodes(ITextSnapshot snapshot)
        {
            ThreadPool.QueueUserWorkItem(rebuildNodesAsynch, snapshot);
        }
 
        public event SnapshotEvent NodesChanged;
 
        private void rebuildNodesAsynch(object snapshotObject)
        {
            ITextSnapshot snapshot = (ITextSnapshot)snapshotObject;
            List<NodeSnapshot> nodes = parser.Parse(snapshot.Lines.ToList().ConvertAll(line => line.GetTextIncludingLineBreak()))
                .ToList()
                    .ConvertAll<NodeSnapshot>
                        (node => new NodeSnapshot(snapshot, node));
            lock (node_lock)
            {
                this.nodes = nodes;
            }
            if (NodesChanged != null)
                NodesChanged(new SnapshotSpan(snapshot, 0, snapshot.Length));
        }
 
        internal List<NodeSnapshot> GetNodes(SnapshotSpan snapshotSpan)
        {
            List<NodeSnapshot> nodes;
            lock (node_lock)
            {
                nodes = this.nodes;
            }
            if (nodes.Count == 0)
                return nodes;
 
            // just in case if while the tokens list was being rebuilt
            // another modification was made
            if (this.nodes[0].SnapshotSpan.Snapshot != snapshotSpan.Snapshot)
                this.nodes.ForEach(node => node.TranslateTo(snapshotSpan.Snapshot));
 
            return nodes;
        }
 
        internal List<INode> GetNodes(SnapshotPoint point)
        {
            List<NodeSnapshot>result = GetNodes(new SnapshotSpan(point.Snapshot, point.Position, 0))
                            .FindAll(node => node.SnapshotSpan.IntersectsWith(new SnapshotSpan(point.Snapshot, point.Position, 0)));
            if (result == null)
                return null;
            return result.ConvertAll(node => node.Node);
        }
    }

In the constructor (lines 8-14) the node provider submits a request to rebuild nodes with a call to the rebuildNodes method and subscribes to the buffer onChanged event. Within the event handler (line 20) it calls the same method. The rebuildNodes method uses thread pool to queue an asynchronous call to rebuildNodesAsynch. The rebuildNodesAsynch (lines 30 - 43) does the heavy lifting of the text parsing, but runs it on a separate thread. When parsing is completed it fires the NodesChanged event to inform the world that there is a fresh parsing result reflecting the latest updates.

Methods GetNode and GetNodes can be called at any moment. They will return the syntax node (list of syntax nodes) as of the last parsing completed by the moment the call was made regardless of whether there is a parsing  which is not completed yet.

BTW, this interaction between rebuildNodes and rebuildNodesAsynch is a good point to insert an implementation for trick #3 - parsing requests queuing. The rest of the code does not have to be changed.

If you go back now to Part 1 you will see how the node provider is used to retrieve parsing results. In the next post I will show how it is used with some other consumers.

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.