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.