Archive for the ‘django’ Category

NDjango: Getting cozy with ASP.MVC

Monday, August 2nd, 2010

Almost there. Just a few more touches, tests, spelling corrections... No, really, the new NDjango editor I was working on for some time now is almost ready. The major improvements over the existing one are in the overall usability as well as ASP.NET MVC compatibility. Here is a brief rundown on what's coming:

Code Completion for strongly typed models

Currently NDjango editor supports code completion for tag and filter names as well as for a few other syntax elements. With the new version two more elements are added to the list: block names and variables.

Code completion for variables works everywhere a variable reference can exist and shows a list of all variables available in the context. The code completion dropdown includes global (for the template) variables provided by the view engine as well as context based variables introduced by the django tags, i.e. forloop variable inside the for tag. A new tag <% model [Name:Type]... %> can be to used introduce additional global variables. The specified name will be shown on the code completion list.

Typing '.' after the name of a variable triggers another code completion dialog with a list of members/properties as defined by the variable type. This feature combined with the ability to provide the model type in the model tag provides great support for strongly typed models.

The list of predefined global variables depends on the project type. For Bistro projects the list will include all resources provided by the Bistro controllers. For ASP.NET MVC integration the context will only include the "Session" variable and the "Model" variable for strongly typed views (see below ASP.NET MVC integration)

Block names code completion dialog in the block tag shows a list of blocks available for overriding. The list includes names of the blocks defined in parent templates.

Add New Django View wizard

Add new view wizard simplifies creating new templates by providing dialogs to select base template, model type, and blocks to override

ASP.NET MVC integration

There are quiet a few changes in the ASP.NET MVC integration. The overriding goal is to make integration as seamless as possible. Specfically:

  • Django Views can be mixed and matched with WebForm views or any other types of views
  • Full support for Areas
  • HtmlHelpers can be used from inside of Django views
  • Views with strongly typed models provide full blown code completion support
  • New DjangoViewEngine constructor simplifies working with custom tag libraries
  • Session global variable provides direct access to the session object from the views
  • Extensive samples provide a good starting point for writing your own views/tags/filters

Sample Code

A new ASP.NET MVC sample project will be included in the shipment. The project is an extension of the stock ASP.NET MVC sample project created by the "Create a New ASP.NET MVC2 Web Application" wizard. In this project a new tab called "Django" is added to the menu. Also for the register new user view the aspx file is superceded by a django template.

This project can be used as guidance in respect to a few aspects of using NDjango view engine:

  • Using Django inheritance in lieu of master pages
  • Accessing context values with Django variables
  • Setting up NDjango to use custom tag libraries
  • Using HtmlHelpers from within Django templates
  • Using ascx controls from Django templates

Also included in the shipment is a Sample Library project. This project is a tag library with several tags providing access to various HtmlHelpers. These tags were written for the ASP.NET MVC sample project. They also can be used as starting point for developing your own custom tags.

I hope I did not miss anything. I hope I will be able to wrap it all up over the next week or so. I am also looking into the ASP.NET MVC3 and will include MVC3 integration as well if it will not get in the way too much.

See you soon.

NDjango: Closing the gap on the Visual Studio ASPX editor

Tuesday, March 23rd, 2010

One of glaring weaknesses of a proposition to use a view engine different from .aspx is that Visual Studio provides a very nice .aspx editor full of features both for editing the UI as well as .aspx source code, while this 'other' engine usually offers nothing of the sort - just a notepad to type in anything you want.

Not that I would ever dream of singlehandedly build something comparable to the Visual Studio .aspx designer, but the NDjango editor makes at least some of the .aspx editor features available for the NDjango users.

Let us go through the list:

Feature aspx editor NDjango editor
Syntax highlighting - clearly marks view engine constructs by highlighting the brackets separating the constructs from the rest of the template aspx syntax highlight ndjangosyntaxhhighlight
Code Completion - Shows a context specific list of available options as you type aspxcodecompletion ndjangoCodeCompletion
As you type diagnostics - highlights invalid items in the source code and provides diagnostic messages in the Visual Studio ErrorList DiagASP DiagNdjango
Item Hierarchy display - visualizes the nested constructs hierarchy HierarchyASP HierarchyNDjango

As anyone can clearly see the NDjango editor still has a long way to go, but IMHO the features it already supports can be of considerable help for the NDjango users.

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 for VS 2010 Beta 1 Released

Saturday, October 10th, 2009

Yesterday was a big day for us - the day of the first NDjango editor roll out. Check this out:В В NDjango Editor. This release is built for .NET 4.0 and Visual Studio 2010 - both in Beta 1 as of now. As they go to Beta 2, RTM etc. we will update the Editor. There are some problems in current release because of certain bugs in VS 2010 editor. I decided against building workarounds for them because according to Brittany Behrens of MicrosoftВ they are fixed in the upcoming VS 2010 Beta 2.

I have some great ideas about where to take the editor and ndjango in general from here, but for now I will justВ sit and wait for some feedback from the users.

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);
        }

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