Archive for August, 2009

Django Editor in VS 2010 – Part 5 (Quick Info Source)

Thursday, August 13th, 2009

Look here for complete source

In the previous post I explained the two components of the Quick Info implementation - the controller and the source. I also covered the Quick Info controller. Now, before diving into the source, let me tell you that you may not necessarily need the controller (or the source).

The thing is that these two are to certain degree independent of each other. The task of the controller is to manage the session, but it does not explicitly invoke the source - the system knows what source(s) to invoke based on the attributes you provide for the source.В The controllerВ alsoВ may (or may not)В provide something to help source generate the tooltip, or the source can be smart enough to figure it out on its own. If the latter is true, and if some other Quick Info controller is already implemented, you do not need a new controller just to show tooltips from your new source - the existing one will do just fine. And vice versa - if you only need to change how the Quick Info session is triggered (or dismissed) you do not need to re-implement the source.

Alright, may be you are lucky, I am not, so let's get to it. Here it is, the QuickInfo source. Once more we have two objects a provider source and the actual provider. The provider source is decorated with the attributes the Visual Studio editor uses to determine when to invoke it. Here is the code:

    [Export(typeof(IQuickInfoSourceProvider))]
    [Name("NDjango QuickInfo Source")]
    [Order(Before = "default")]
    [ContentType(Constants.NDJANGO)]
    class SourceProvider : IQuickInfoSourceProvider
    {
        [Import]
        internal INodeProviderBroker nodeProviderBroker { get; set; }
 
        public IQuickInfoSource TryCreateQuickInfoSource(ITextBuffer textBuffer, IEnvironment environment)
        {
            if (nodeProviderBroker.IsNDjango(textBuffer))
                return new Source();
            return null;
        }
    }

There is nothing to add to what's written in the code. The actual source is not this much more complex than the provider:

    class Source : IQuickInfoSource
    {
 
        public object GetToolTipContent(IQuickInfoSession session, out Microsoft.VisualStudio.Text.ITrackingSpan applicableToSpan)
        {
            StringBuilder message = new StringBuilder();
            int position = session.SubjectBuffer.CurrentSnapshot.Length;
            int length = 0;
            List nodes;
            if (session.Properties.TryGetProperty>(typeof(SourceProvider), out nodes))
            {
                nodes.ForEach(
                    node =>
                    {
                        if (!String.IsNullOrEmpty(node.Description))
                            message.Insert(0, node.Description + "\n");
                        if (node.ErrorMessage.Severity >= 0)
                            message.Append("\n" + node.ErrorMessage.Message);
                        if (node.Length > length)
                            length = node.Length;
                        if (node.Position < position)
                            position = node.Position;
                    }
                        );
            }
 
            applicableToSpan = session.SubjectBuffer.CurrentSnapshot.CreateTrackingSpan(
                position,
                length,
                Microsoft.VisualStudio.Text.SpanTrackingMode.EdgeExclusive);
 
            if (message.Length > 0)
                return message.ToString();
            else
                return null;
        }
   }

As you can see the GetToolTipContent after some messing around with the node list returns the text message to be shown in the tooltip. Setting of the applicableToSpan forces dismissal of the QuickInfo session when the mouse cursor leaves the said span.

The next post will be about the final element - code completion

Django Editor in VS 2010 – Part 4 (Quick Info Controller)

Sunday, August 9th, 2009

Look here for complete source

Welcome to the wonders of Intellisense. You can do so many things with intellisense and with the new MEF based architecture all of the wonders are actually acheivable by mere mortals without stretching your R&D budget. OK almost all. Sometimes you still have to resort to the old ugly COM based stuff, but the promise is out it's gonna get better in the final release.

QuickInfo is only one and, probably one of the simplest, of the many features available under Intellisense umbrella. The code I will present your here has nothing of the scary COM. Still, as with any of the Intellisense it has more moving parts than a classifier and/or tagger. The reason is pretty obvious - with taggers/classifiers you have only one dimension to worry about - the text itself. If the text is the same, the set of tags/classification types will not change either.

With Intellisense this is no longer true. The behavior of Intellisense, and QuickInfo in particular depends on the content as well as on what the user is doing - where is the caret, how did it move, where is the mouse cursor, etc. Because of this in addition to the Source supplying the content for the QuickInfo you have to deal with the Controller, which controls the QuickInfo session based on what the user is doing with mouse and keyboard.

The plumbing necessary for both the Source and the Controller is very similar to what you've seen in previous posts - there is a provider to be exported and the provider generates the actual class performing the function.

Here is my controller provider:

В В В  [Export(typeof(IIntellisenseControllerProvider))]
В В В  [Name("NDjango Completion Controller")]
В В В  [Order]
В В В  [ContentType(Constants.NDJANGO)]
В В В  internal class CompletionControllerProvider : IIntellisenseControllerProvider
В В В  {
 
В В В В В В В  [Import(typeof(IQuickInfoBrokerMapService))]
В В В В В В В  private IQuickInfoBrokerMapService brokerMapService { get; set; }
 
В В В В В В В  [Import]
В В В В В В В  internal INodeProviderBroker nodeProviderBroker { get; set; }
 
В В В В В В В  public IIntellisenseController TryCreateIntellisenseController(ITextView textView, IList<ITextBuffer> subjectBuffers, IEnvironment context)
В В В В В В В  {
 
В В В В В В В В В В В  bool brokerCreated = false;
В В В В В В В В В В В  foreach (ITextBuffer subjectBuffer in subjectBuffers)
В В В В В В В В В В В  {
В В В В В В В В В В В В В В В  if (nodeProviderBroker.IsNDjango(subjectBuffer))
В В В В В В В В В В В В В В В В В В В  brokerCreated |= (brokerMapService.GetBrokerForTextView(textView, subjectBuffer) != null);
В В В В В В В В В В В  }
 
В В В В В В В В В В В  // There may not be a broker for any of the subject buffers for this text view.В  This can happen if there are no providers available.
В В В В В В В В В В В  if (brokerCreated)
В В В В В В В В В В В  {
В В В В В В В В В В В В В В В  return new Controller(nodeProviderBroker, subjectBuffers, textView, brokerMapService);
В В В В В В В В В В В  }
 
В В В В В В В В В В В  return null;
В В В В В В В  }
В В В  }

It looks very much like a classifier provider or a tagger provider. The only noticeable difference is that tagger provider and classifier provider operate on text buffers (ITextBuffer), because they do not really care about mouse cursor movements and keyboard - only about buffer content. In here we have to worry about them, and the only way to do that is to deal with the text view (ITextView) and a text view can have multiple buffers it is showing. The rest of the code is pretty much the same. Similarly to the other providers,В my QuickInfo controller providerВ tries to create a new QuickInfo controller for every django buffer. Here is the code for the QuickInfo controller:

    class Controller : IIntellisenseController
    {
        private IList subjectBuffers;
        private ITextView textView;
        private IQuickInfoBrokerMapService brokerMapService;
        private IQuickInfoSession activeSession;
        private INodeProviderBroker nodeProviderBroker;
 
        public Controller(INodeProviderBroker nodeProviderBroker, IList subjectBuffers, ITextView textView, IQuickInfoBrokerMapService brokerMapService)
        {
            this.nodeProviderBroker = nodeProviderBroker;
            this.subjectBuffers = subjectBuffers;
            this.textView = textView;
            this.brokerMapService = brokerMapService;
 
            textView.MouseHover += new EventHandler(textView_MouseHover);
 
        }
 
        void textView_MouseHover(object sender, MouseHoverEventArgs e)
        {
            if (activeSession != null)
                activeSession.Dismiss();
 
            SnapshotPoint? point = e.TextPosition.GetPoint(
                textBuffer =>
                    (
                        subjectBuffers.Contains(textBuffer)
                        && nodeProviderBroker.IsNDjango(textBuffer)
                        && brokerMapService.GetBrokerForTextView(textView, textBuffer) != null
                    )
                ,PositionAffinity.Predecessor);
 
            if (point.HasValue)
            {
                NodeProvider nodeProvider = nodeProviderBroker.GetNodeProvider(point.Value.Snapshot.TextBuffer);
                List quickInfoNodes = nodeProvider.GetNodes(point.Value);
                if (quickInfoNodes != null)
                {
                    // the invocation occurred in a subject buffer of interest to us
                    IQuickInfoBroker broker = brokerMapService.GetBrokerForTextView(textView, point.Value.Snapshot.TextBuffer);
                    ITrackingPoint triggerPoint = point.Value.Snapshot.CreateTrackingPoint(point.Value.Position, PointTrackingMode.Positive);
 
                    activeSession = broker.CreateQuickInfoSession(triggerPoint, true);
                    activeSession.Properties.AddProperty(typeof(SourceProvider), quickInfoNodes);
                    activeSession.Start();
                }
            }
        }
 
        public void ConnectSubjectBuffer(ITextBuffer subjectBuffer)
        { }
 
        public void Detach(ITextView textView)
        { }
 
        public void DisconnectSubjectBuffer(ITextBuffer subjectBuffer)
        { }
    }

It is the responsibility of the controller toВ initiate the quick info session when the moment is right. For the NDjango designer IВ want to trigger the quick info session with the mouse hover event.В So in the constructor I subscribe to the MouseHover event of the text view.

When this event is fired I am trying to acquire a point in the text buffer based on the current mouse position. I only want to consider points which satisfy the condition spelled out in the code. If a point (and a corresponding buffer) has been located, I request a node provider for the buffer and get a list of nodes from the provider based on the point. If the list is not empty, I initiate a new QuickInfo session. The list is placed on the session to be retrieved by the QuickInfo source when the content of the tooltip is requested.

So this is how the NDjango editor creates aВ QuickInfo session.В So far the session even when triggered will not show anything. In the next post I willВ cover thisВ with the code for NDjango editor Quick InfoВ Source

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.