Archive for the ‘vseditor’ Category

NDjango editor for VS2010 – 2 down 1 to go

Monday, March 15th, 2010

From VS2010 Beta 1 to Beta 2 and now to RC. I will still need to port it (the editor) once more - from RC to the RTM, but for now enjoy the RC compatible version of the editor

This post is about my experience with the port and it will be really short because the port was pretty uneventful.

First of all the Intellisense Source interfaces (IQuickInfoSource, ICompletionSource) no longer have Get...Information methods. They have Augment...Session instead. Which for me makes more sense because now you can not only add completion sets from the source but also examine and even change existing elements in the completion set. These interfaces now also sport a Dispose method to help with the resource cleanup.

The changes were easyВ  - move the logic from GetCompletionInformation/GetToolTipContent method to the appropriate Augment method and add an empty Dispose method. Another small change is that there SquiggleTag is no longer which only made my life easier because now instead of deriving my own ErrorTag from SquiglleTag I can use the predefined ErrorTag.

And this is pretty much it.

Moving to VS2010 Beta 2

Wednesday, October 21st, 2009

First let me tell you that as much as I liked VS2010 Beta 1 I like the VS2010 Beta 2 much better. I will leave it to other guys to write about the new UI, new features, much improved speed etc., so that in my post I can focus on what is closer to me, VS Integration.

What I want to share with you here is a journal of what I did to port my Django editor from Beta 1 to Beta 2 - I just got it running on Beta2. Let me start with telling you that I did not run into any major issues. Here is a list of what changes in the API affected Django editor and what I did to address the issues:

  • Interface Microsoft.VisualStudio.ApplicationModel.Environments.IEnvironment is gone. As a matter of fact the entire Microsoft.VisualStudio.ApplicationModel namespace is no longer. This interface was used in a number of methods. In Beta 2 all these methods (at least the ones I used) are one parameter short. So the fix was easy - I just changed the signatures.
  • Microsoft.VisualStudio.Language.Intellisense.Completion constructor signature has changed. I need the insert value to be different from the display value, and the only constructor which gives a way to do that now also wants some information about icons. This is nothing too - gave it a null and an empty string to keep it happy
  • The Microsoft.VisualStudio.Text.ContentType.TypeName which used to be "text" now is "plaintext". This is a little more subtle - primarily because the compiler does not catch it, just need to be thorough to change it everywhere. Also I relied on context to skip certain buffers, in particular the buffers created to hold the tooltip text. I had to find a different way to do that.
  • The Microsoft.VisualStudio.Language.Intellisense.IQuickInfoSource.GetToolTipContent now returns ReadOnlyCollection<object> rather than an object - fine, I do not care about returning multiple, just wrapped my string into a collection and returned it
  • A few changes in the F# - namely due to rename rethrow() to reraise() and deprecation of the OverloadID attribute.
  • And finally the biggest change. Both Code Completion and Quick Info used to relay on broker map services, respectively ICompletionBrokerMapService andIQuickInfoBrokerMapService. These services maintained maps of brokers to textViews. No longer. Now Visual Studio maintains a single instance of ICompletionBroker as well as IQuickInfoBroker both to be accessed by directly importing them when necessary. An extra positive from this change was that simplifying the code by throwing away the code related to mapping services showed me the path to streamline the way I work with projection buffers in HTML editor and XML editor.

And this is pretty much it. Of course I had to mess around with project settings, missing F# dlls, changed format for vsix manifest etc., but this is to be expected. Anyway now I have a version of the Django editor for Beta 2.В  Right now I have it in the subversion. I will release it in binaries shortly.

Django Editor in VS2010 – more speed needed

Tuesday, September 22nd, 2009

Last week I asked Alex to give the editor a 'real try' and try to break it. Guess what - he did it. All he did was he opened a template of a reasonable size and started typing. He types fast and as he typed the editor slowed to a crawl.

And understandably so. Here is what's happening. Every keystroke is a change to the buffer with the template source code. It triggers the Change event on the buffer, which in turnВ initiates re-parsing of the template using updated template source. Yes, parsing is happening in a separate thread, and a single parsing request is something the parser can easily handle in the background without noticeable effect on the UI,В but for bigger templates it can take some time for parsing to complete and with the typing speed of 10-15 keystrokes per second the requestsВ start to pile up.

Thankfully there is no need to do it anew every time a key is hit - all we need is to parse the last one. Essentially when I type fast I do not really care for the parsing results, but when I pause in my typing I would like to see how good is what I've done so far.

I already discussed the parsing speed issue in the Part 2 Background Parsing of the editor series. I listed there some things which can be done to address the speed issue. I implemented them all from the get go, except for the last one - the parsing requests queuing. And when Alex broke my beautiful editor, I realized that I am not getting away without implementing the queuing too.

Here is what I need: every time a parsing request comes in I schedule its execution for .5 sec in the future. If during the delay another request comes in, instead of creating a new request, I reschedule the active request so it is still set to run .5 sec after the last buffer modification. Once this delayВ is implementedВ parser will not run until there is a .5 sec pause in buffer modifications. In other words all requests with less then .5 sec time between them will be combined into one.

And here is the code to do this:

        public NodeProvider(IVsOutputWindowPane djangoDiagnostics, IParser parser, ITextBuffer buffer)
        {
            this.djangoDiagnostics = djangoDiagnostics;
            this.parser = parser;
            this.buffer = buffer;
            filePath = ((ITextDocument)buffer.Properties[typeof(ITextDocument)]).FilePath;
            buffer.Changed += new EventHandler(buffer_Changed);
            // we need to run rebuildNodes on a separate thread. Using timer
            // for this seems to be an overkill, but we need the timer anyway so - why not
            parserTimer =
                new Timer(rebuildNodes, buffer.CurrentSnapshot, 0, Timeout.Infinite);
        }
 
        /// Initiates the delayed parsing in response to the buffer changed event
        private void buffer_Changed(object sender, TextContentChangedEventArgs e)
        {
            // shut down the old one
            parserTimer.Dispose();
 
            // put the call to the rebuildNodes on timer
            parserTimer =  new Timer(rebuildNodes,  e.After,  PARSING_DELAY, Timeout.Infinite);
        }

Visual Studio services and VS Editor extensions

Tuesday, September 8th, 2009

If you are familiar withВ Visual Studio Integration SDK you already know that "...Creating a VSPackage is a powerful way to extend Visual Studio...". You probably also know that VSPackages use Services to interact with each other and the core of the Visual Studio. Many components of the Visual Studio core are packages and can be accessed as services. To access a service all you need is an appropriateВ service provider implementing the IServiceProvider interface. You also need to know the type of the service you want to access. Once you have both - call the GetService method on the provider and it will return the service you desire. All of this is good and handy when you are building your own VSPackage.

What about Visual Studio Editor extensions? What if all youВ are planningВ is an editor extension withВ just one small thing which goes beyond editor?

Guess what - all this wealth of the Visual Studio core services is still available to you without the formal frame of the VSPackage. It is available, that is, if you can get your hands onВ a service provider which is aware of the service you are after.

In my NDjango designerВ in addition to syntax colorization, code completion and other wonders of the Visual Studio Editor extensions I wanted to show the errors in both the Error List and a separate pane of the Output Window. There is aВ variety of ways to do it.В 

In NDjango designerВ it is done in NodeProviderBroker object byВ accessingВ the IVsOutputWindow service.В OnceВ I have the service I useВ its CreatePane method to create the Django Output pane (an IVsOutuptWindowPane object). And then, in the NodeSnapshot object I generate the messages using the OutpuTaskItemString method. You can find examples of how to use these services on the web and in the Visual Studio SDK, as well as in the source code of the NDjango designer

What is somewhat less obvious is how do you get to the starting point - the IVsOutputWindow service. It is promised that in the future it will be as easy as importing the ServiceProvider, and then calling the GetService method on the service provider. We are not there yet, so here is what I did to get by for now:

        private object GetService(ITextBuffer textBuffer, Type type)
        {
            var vsBuffer = adaptersFactory.GetBufferAdapter(textBuffer);
            if (vsBuffer == null)
                return null;
 
            Guid guidServiceProvider = VSConstants.IID_IUnknown;
            IObjectWithSite objectWithSite = vsBuffer as IObjectWithSite;
            IntPtr ptrServiceProvider = IntPtr.Zero;
            objectWithSite.GetSite(ref guidServiceProvider, out ptrServiceProvider);
            Microsoft.VisualStudio.OLE.Interop.IServiceProvider serviceProvider =
                (Microsoft.VisualStudio.OLE.Interop.IServiceProvider)Marshal.GetObjectForIUnknown(ptrServiceProvider);
 
            Guid guidService = typeof(SVsOutputWindow).GUID;
            Guid guidInterface = typeof(IVsOutputWindow).GUID;
            IntPtr ptrObject = IntPtr.Zero;
 
            int hr = serviceProvider.QueryService(ref guidService, ref guidInterface, out ptrObject);
            if (ErrorHandler.Failed(hr) || ptrObject == IntPtr.Zero)
                return null;
 
            IVsOutputWindow taskList = (IVsOutputWindow)Marshal.GetObjectForIUnknown(ptrObject);
            Marshal.Release(ptrObject);
 
            return taskList;
        }

This code is based on the code posted by Noah RichardsВ and it uses the text buffer (an object implementingВ IVsTextBuffer interface) as a gateway to access Visual Studio global services. Works for me.

Django Editor in VS 2010 – Part 7 (Code Completion – Source)

Saturday, September 5th, 2009

Look here for complete source

Ok. This post is the last and, I hope, the shortest and the simplest one in the Django Editor series. All what's left to cover is the source for the code completion part of the NDjango editor.

To continue line of reasoning from the previous post, the code completion source works in very much the same way as the quick info (see posts 4 and 5 of the series). The source provider is registered as an MEF component and creates source objects for text buffers as necessary. Here is the code:

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

And the source itself is responsible for providing the code completion data:

    internal class Source : ICompletionSource
    {
        public ReadOnlyCollection GetCompletionInformation(ICompletionSession session)
        {
            List completionNodes = session.Properties[typeof(Source)] as List;
            if (completionNodes != null)
            {
                // Calculate the location of the textspan to be replaced with
                // the selection. We always want to replace the entire word
                ITextSnapshot snapshot = session.SubjectBuffer.CurrentSnapshot;
                int triggerPoint = session.TriggerPoint.GetPosition(snapshot);
                ITextSnapshotLine line = snapshot.GetLineFromPosition(triggerPoint);
                string lineString = line.GetText();
                // position of the first non-space character before the tag name
                int start = lineString.Substring(0, triggerPoint - line.Start.Position).
                    LastIndexOfAny(new char[] {' ', '\t', '%'})
                    + line.Start.Position + 1;
                // length of the word currently in the tag name position in the tag
                int length = lineString.Substring(triggerPoint - line.Start.Position).
                    IndexOfAny(new char[] {' ', '\t', '%'} )
                    + triggerPoint - start;
 
                CompletionSet completionSet = new CompletionSet(
                    "ndjango.completions",
                    session.SubjectBuffer.CurrentSnapshot.CreateTrackingSpan(
                    start, length, SpanTrackingMode.EdgeInclusive),
                    CompletionsForNodes(completionNodes),
                    null);
                return new ReadOnlyCollection(new List { completionSet });
            }
 
            return null;
        }
 
        private IEnumerable CompletionsForNodes(IEnumerable nodes)
        {
            foreach (INode node in nodes)
                foreach (string value in node.Values)
                    yield return new Completion(value, value, value);
        }
    }

Even though it is pretty easy to create a custom code completion presenter, I decided against creating my own presenter. And the only thing you need to do to make the standard one work is to feed it with a list of code completion sets in return value of the GetCompletionInformation method. One of the parameters you have to provide when creating your completion set is the tracking span. This tracking span determines what text will be replaced by the value selected by the user from the list of available values.

The rest of the voodoo in the code shown above has to do with calculation of this tracking span as well as building the list of values to show.

And this concludes our discussion of building django editor for Visual Studio 2010. As of the time of this writing I am getting ready to roll out a beta version of the django designer.

Looking back at as the project comes to completion, I can safely say that asВ planned the project consisted of two parts -В extending the django parser for more detailed diagnostics and building the Visual Studio Editor capable of consuming the information. Of course, during the course of the projectВ there wereВ some designВ challenges. To my pleasant surprise almost all of them were related to the django parser rather than the actual editor code.

When I just started the project after the first look at the VSEditor I was saying that building custom editors in Visual Studio 2010 is a breeze. Now, when the project is at its completion, I still stand byВ my statement.

P.S. The project is open source, so feel free toВ browse the source code in the subversion repository. Currently the source code is in the development branch called VS2010Designer. The project is still in development, so there will be changes from the code published in the log. If you are want to look at the code matching exactly the code published in the posts look at the tag I created for this purpose. The tag name is Designer_blog_version. I hope you will find it helpful

Django Editor in VS 2010 – Part 6 (Code Completion – Controller)

Thursday, September 3rd, 2009

Look here for complete source

Code completion is the final piece of the NDjango designer. With code completion out of the way, the NDjango template editor all of us care so much about will be complete. OK. Let us get it over with.

The overall structure of the code completion code is very similar to the QuickInfo code I covered in two previous postings. As was the case with the QuickInfo, there are two classes, Controller and ControllerProvider controlling the session - in this case the Completion session and two more classes Source and SourceProvider providing the data to be displayed. As with the QuickInfo the Controller class instances are created by the Controller provider for all relevant TextViews and once attached to the buffers, Controllers create and dismiss the sessions.

Here is the code of my code completion controller provider:

    [Export(typeof(IIntellisenseControllerProvider))]
    [Name("NDjango Completion Controller")]
    [Order(Before = "Default Completion Controller")]
    [ContentType(Constants.NDJANGO)]
    internal class ControllerProvider : IIntellisenseControllerProvider
    {
        [Import]
        internal ICompletionBrokerMapService CompletionBrokerMapService { get; set; }
 
        [Import]
        internal INodeProviderBroker nodeProviderBroker { get; set; }
 
        [Import]
        internal IVsEditorAdaptersFactoryService adaptersFactory { get; set; }
 
        public IIntellisenseController TryCreateIntellisenseController(ITextView textView, IList subjectBuffers, IEnvironment context)
        {
            bool brokerCreated = false;
            foreach (ITextBuffer subjectBuffer in subjectBuffers)
                if (nodeProviderBroker.IsNDjango(subjectBuffer, context))
                    brokerCreated |= (CompletionBrokerMapService.GetBrokerForTextView(textView, subjectBuffer) != null);
 
            if (brokerCreated)
                return new Controller(this, subjectBuffers, textView, context);
 
            return null;
        }
    }

Nothing really new or exciting here. The controller itself is somewhat longer. I still decided to include the complete source code for the class, but you are welcome to scroll all the way through the code to the notes down below:

    class Controller : IIntellisenseController, IOleCommandTarget
    {
        private IList subjectBuffers;
        private ITextView subjectTextView;
        private IWpfTextView WpfTextView;
        private ICompletionSession activeSession;
        private IEnvironment context;
        private ControllerProvider provider;
 
        public Controller(ControllerProvider provider, IList subjectBuffers, ITextView subjectTextView, IEnvironment context)
        {
            this.provider = provider;
            this.subjectBuffers = subjectBuffers;
            this.subjectTextView = subjectTextView;
            this.context = context;
 
            WpfTextView = subjectTextView as IWpfTextView;
            if (WpfTextView != null)
            {
                WpfTextView.VisualElement.KeyDown += new System.Windows.Input.KeyEventHandler(VisualElement_KeyDown);
                WpfTextView.VisualElement.KeyUp += new System.Windows.Input.KeyEventHandler(VisualElement_KeyUp);
            }
        }
 
        void VisualElement_KeyUp(object sender, System.Windows.Input.KeyEventArgs e)
        {
            if (activeSession != null)
            {
                if (e.Key == Key.Escape)
                {
                    activeSession.Dismiss();
                    e.Handled = true;
                }
 
                if (e.Key == Key.Enter)
                {
                    if (this.activeSession.SelectedCompletionSet.SelectionStatus != null )
                        activeSession.Commit();
                    else
                        activeSession.Dismiss();
                    e.Handled = true;
                }
            }
        }
 
        void VisualElement_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
        {
            ITextView textView = sender as ITextView;
            if (this.subjectTextView != textView)
                return;  // Make sure that this event happened on the same text view to which we're attached.
 
            if (!(e.Key >= Key.A && e.Key <= Key.Z))
               return;   // we only start the session when an alphanumeric key is pressed          
 
            if (activeSession != null)
                 return;  // if there is a session already leave it be
 
             // determine which subject buffer is affected by looking at the caret position
             SnapshotPoint? caretPoint = textView.Caret.Position.Point.GetPoint
                 (textBuffer =>
                    (
                        subjectBuffers.Contains(textBuffer)
                        && provider.nodeProviderBroker.IsNDjango(textBuffer, context)
                        && provider.CompletionBrokerMapService.GetBrokerForTextView(textView, textBuffer) != null
                    ),  PositionAffinity.Predecessor);
 
            if (!caretPoint.HasValue)
                return;   // return if no suitable buffer found
 
            List completionNodes =
                provider.nodeProviderBroker.GetNodeProvider(caretPoint.Value.Snapshot.TextBuffer).GetNodes(caretPoint.Value)
                    .FindAll(node => node.Values.Count() > 0);
 
            if (completionNodes.Count == 0)
                return;  // return if there is no information to show
 
            attachKeyboardFilter();   // attach filter to intercept the Enter key
 
            ICompletionBroker broker = provider.CompletionBrokerMapService.GetBrokerForTextView
                  (textView,caretPoint.Value.Snapshot.TextBuffer);
            ITrackingPoint triggerPoint = caretPoint.Value.Snapshot.CreateTrackingPoint(caretPoint.Value.Position, PointTrackingMode.Positive);
            activeSession = broker.CreateCompletionSession(triggerPoint, true);
            activeSession.Properties.AddProperty(typeof(Source), completionNodes);
            activeSession.Dismissed += new System.EventHandler(OnActiveSessionDismissed);
            activeSession.Committed += new System.EventHandler(OnActiveSessionCommitted);
            activeSession.Start();
        }
 
        void OnActiveSessionDismissed(object sender, System.EventArgs e)
        {
            detachKeyboardFilter();
            activeSession = null;
        }
 
        void OnActiveSessionCommitted(object sender, System.EventArgs e)
        {
            detachKeyboardFilter();
            activeSession = null;
        }
 
        public void ConnectSubjectBuffer(ITextBuffer subjectBuffer) { }
 
        public void Detach(Microsoft.VisualStudio.Text.Editor.ITextView textView)
        {
            detachKeyboardFilter();
        }
 
        public void DisconnectSubjectBuffer(ITextBuffer subjectBuffer)
        {
            WpfTextView = subjectTextView as IWpfTextView;
            if (WpfTextView != null)
            {
                WpfTextView.VisualElement.KeyDown -= new System.Windows.Input.KeyEventHandler(VisualElement_KeyDown);
                WpfTextView.VisualElement.KeyUp -= new System.Windows.Input.KeyEventHandler(VisualElement_KeyUp);
                detachKeyboardFilter();
            }
        }
 
        private void attachKeyboardFilter()
        {
            ErrorHandler.ThrowOnFailure(provider.adaptersFactory.GetViewAdapter(subjectTextView).AddCommandFilter(this, out oldFilter));
        }
 
        private void detachKeyboardFilter()
        {
            ErrorHandler.ThrowOnFailure(provider.adaptersFactory.GetViewAdapter(subjectTextView).RemoveCommandFilter(this));
        }
 
        // The code below intercepts the ECMD_RETURN command before it is sent to the editor window.
        private IOleCommandTarget oldFilter;
 
        private static readonly Guid CMDSETID_StandardCommandSet2k = new Guid("1496a755-94de-11d0-8c3f-00c04fc2aae2");
        private static readonly uint ECMD_RETURN = 3;
 
        public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut)
        {
            if (pguidCmdGroup == CMDSETID_StandardCommandSet2k && nCmdID == ECMD_RETURN)
                return VSConstants.S_OK;
            return oldFilter.Exec(pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut);
        }
 
        public int QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText)
        {
            return oldFilter.QueryStatus(pguidCmdGroup, cCmds, prgCmds, pCmdText);
        }
    }

With this one even though the overall approach is the same as with the Quick Info, there are a few things deserving some explanation.

First the similarities: As with the Quick Info when the controller is created it hooks itself up to the view. It does so by subscribing to appropriate events on the view and in the event handlers it supplies the Intellisense sessions are created, started and dismissed as necessary. Also the information to be used by the source is attached to the session using session.Properties.

Now the differences: First of all the events here are different. In QuickInfo we relied on the OnMouseHover event to fire up the session and the session dismissal was handled automatically based on the mouse moves. With the Code completion we have to relay on the keyboard events for this purpose. See the implementation of the KeyUp and KeyDown events for more details.

The second and a little more unsettling difference is the games we have to play with the standard editor window. The problem is that when the user presses Enter, this keypress in addition to being sent to our KeyUp event is also sent to the editor window itself. As a result, the window gets updated which causes the update events to fire which in turn causes the current selection in the CompletionSet to be reset according to the matching rules. In other words, if the user selects something from the completion dropdown using arrows and presses enter, before the selection is applied to the code it is reset to whatever is considered to be a match to the letters keyed in before that. From the user standpoint, it causes the code completionВ code to ignore the selection with the arrow keys.

The attachKeyBoardFilter method above shows a way to suppress passing KeyUp event to the standard window. Also take a note of the detachKeyBoardFilter method, implementing the cleanup to be performed when the code completion session is completed one way or another.

And this is all I have to say about code completion controller. Bear with me - the only thing left is the code completion source, which is coming up next.

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.