Walkthrough: Displaying Statement Completion
You can implement language-based features such as statement completion by defining the identifiers for which you want to provide completion, and then triggering a completion session. You can define statement completion in the context of a language service, or you can define your own file name extension and content type, and display completion for just that type, or you can trigger completion for an existing content type (such as "plaintext"). This walkthrough shows how to trigger statement completion for the "plaintext" content type, which is the content type of text files. The "text" content type is the ancestor of all other content types, including code and XML files.
Statement completion is typically triggered by typing certain characters, for example, by typing the beginning of an identifier such as "using". It is typically dismissed by pressing the SPACEBAR, TAB, or ENTER to commit a selection. IntelliSense features that are triggered by typing a character can be implemented by using a command handler for the keystrokes (the IOleCommandTarget interface) and a handler provider that implements the IVsTextViewCreationListener interface. To create the completion source, which is the list of identifiers that participate in completion, implement the ICompletionSource interface and a completion source provider (the ICompletionSourceProvider interface). The providers are Managed Extensibility Framework (MEF) component parts, and are responsible for exporting the source and controller classes and importing services and brokers, for example, the ITextStructureNavigatorSelectorService, which lets you navigate in the text buffer, and the ICompletionBroker, which triggers the completion session.
This walkthrough shows how to implement statement completion for a hard-coded set of identifiers. In full implementations, the language service and the language documentation are responsible for providing that content.
Prerequisites
To complete this walkthrough, you must install the Visual Studio 2012 SDK.
Note
For more information about the Visual Studio SDK, see Extending Visual Studio Overview. To find out how to download the Visual Studio SDK, see Visual Studio Extensibility Developer Center on the MSDN Web site.
Creating a MEF Project
To create a MEF project
Create an Editor Classifier project. Name the solution CompletionTest.
Open the source.extension.vsixmanifest file in the VSIX Manifest Editor.
Make sure that the Content heading contains a MEF Component content type and that the Path is set to CompletionTest.dll.
Save and close source.extension.vsixmanifest.
Add the following references to the project and make sure that CopyLocal is set to false:
Microsoft.VisualStudio.Editor
Microsoft.VisualStudio.Language.Intellisense
Microsoft.VisualStudio.OLE.Interop
Microsoft.VisualStudio.Shell.11.0
Microsoft.VisualStudio.Shell.Immutable.11.0
Microsoft.VisualStudio.TextManager.Interop
Delete the existing class files.
Implementing the Completion Source
The completion source is responsible for collecting the set of identifiers and adding the content to the completion window when a user types a completion trigger, such as the first letters of an identifier. In this example, the identifiers and their descriptions are hard-coded in theAugmentCompletionSession method. In most real-world uses, you would use your languageās parser to get the tokens to populate the completion list.
To implement the completion source
Add a class file and name it TestCompletionSource.
Add the following imports.
Imports System Imports System.Collections.Generic Imports System.Linq Imports System.Text Imports System.ComponentModel.Composition Imports Microsoft.VisualStudio.Language.Intellisense Imports Microsoft.VisualStudio.Text Imports Microsoft.VisualStudio.Text.Operations Imports Microsoft.VisualStudio.Utilities
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.ComponentModel.Composition; using Microsoft.VisualStudio.Language.Intellisense; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Operations; using Microsoft.VisualStudio.Utilities;
Modify the class declaration for TestCompletionSource so that it implements ICompletionSource.
Friend Class TestCompletionSource Implements ICompletionSource
internal class TestCompletionSource : ICompletionSource
Add private fields for the source provider, the text buffer, and a list of Completion objects (which correspond to the identifiers that will participate in the completion session).
Private m_sourceProvider As TestCompletionSourceProvider Private m_textBuffer As ITextBuffer Private m_compList As List(Of Completion)
private TestCompletionSourceProvider m_sourceProvider; private ITextBuffer m_textBuffer; private List<Completion> m_compList;
Add a constructor that sets the source provider and buffer. The TestCompletionSourceProvider class is defined in later steps.
Public Sub New(ByVal sourceProvider As TestCompletionSourceProvider, ByVal textBuffer As ITextBuffer) m_sourceProvider = sourceProvider m_textBuffer = textBuffer End Sub
public TestCompletionSource(TestCompletionSourceProvider sourceProvider, ITextBuffer textBuffer) { m_sourceProvider = sourceProvider; m_textBuffer = textBuffer; }
Implement the AugmentCompletionSession method by adding a completion set that contains the completions you want to provide in the context. Each completion set contains a set of Completion completions, and corresponds to a tab of the completion window. (In Visual Basic projects, the completion window tabs are named Common and All.) The FindTokenSpanAtPosition method is defined in the next step.
Private Sub AugmentCompletionSession(ByVal session As ICompletionSession, ByVal completionSets As IList(Of CompletionSet)) Implements ICompletionSource.AugmentCompletionSession Dim strList As New List(Of String)() strList.Add("addition") strList.Add("adaptation") strList.Add("subtraction") strList.Add("summation") m_compList = New List(Of Completion)() For Each str As String In strList m_compList.Add(New Completion(str, str, str, Nothing, Nothing)) Next str completionSets.Add(New CompletionSet( "Tokens", "Tokens", FindTokenSpanAtPosition(session.GetTriggerPoint(m_textBuffer), session), m_compList, Nothing)) End Sub
void ICompletionSource.AugmentCompletionSession(ICompletionSession session, IList<CompletionSet> completionSets) { List<string> strList = new List<string>(); strList.Add("addition"); strList.Add("adaptation"); strList.Add("subtraction"); strList.Add("summation"); m_compList = new List<Completion>(); foreach (string str in strList) m_compList.Add(new Completion(str, str, str, null, null)); completionSets.Add(new CompletionSet( "Tokens", //the non-localized title of the tab "Tokens", //the display title of the tab FindTokenSpanAtPosition(session.GetTriggerPoint(m_textBuffer), session), m_compList, null)); }
The following method is used to find the current word from the position of the caret.
Private Function FindTokenSpanAtPosition(ByVal point As ITrackingPoint, ByVal session As ICompletionSession) As ITrackingSpan Dim currentPoint As SnapshotPoint = (session.TextView.Caret.Position.BufferPosition) - 1 Dim navigator As ITextStructureNavigator = m_sourceProvider.NavigatorService.GetTextStructureNavigator(m_textBuffer) Dim extent As TextExtent = navigator.GetExtentOfWord(currentPoint) Return currentPoint.Snapshot.CreateTrackingSpan(extent.Span, SpanTrackingMode.EdgeInclusive) End Function
private ITrackingSpan FindTokenSpanAtPosition(ITrackingPoint point, ICompletionSession session) { SnapshotPoint currentPoint = (session.TextView.Caret.Position.BufferPosition) - 1; ITextStructureNavigator navigator = m_sourceProvider.NavigatorService.GetTextStructureNavigator(m_textBuffer); TextExtent extent = navigator.GetExtentOfWord(currentPoint); return currentPoint.Snapshot.CreateTrackingSpan(extent.Span, SpanTrackingMode.EdgeInclusive); }
Implement the Dispose() method:
Private m_isDisposed As Boolean Public Sub Dispose() Implements IDisposable.Dispose If Not m_isDisposed Then GC.SuppressFinalize(Me) m_isDisposed = True End If End Sub
private bool m_isDisposed; public void Dispose() { if (!m_isDisposed) { GC.SuppressFinalize(this); m_isDisposed = true; } }
Implementing the Completion Source Provider
The completion source provider is the MEF component part that instantiates the completion source.
To implement the completion source provider
Add a class named TestCompletionSourceProvider that implements ICompletionSourceProvider. Export this class with a ContentTypeAttribute of "plaintext" and a NameAttribute of "test completion".
<Export(GetType(ICompletionSourceProvider)), ContentType("plaintext"), Name("token completion")> Friend Class TestCompletionSourceProvider Implements ICompletionSourceProvider
[Export(typeof(ICompletionSourceProvider))] [ContentType("plaintext")] [Name("token completion")] internal class TestCompletionSourceProvider : ICompletionSourceProvider
Import a ITextStructureNavigatorSelectorService, which is to be used to find the current word in the completion source.
<Import()> Friend Property NavigatorService() As ITextStructureNavigatorSelectorService
[Import] internal ITextStructureNavigatorSelectorService NavigatorService { get; set; }
Implement the TryCreateCompletionSource method to instantiate the completion source.
Public Function TryCreateCompletionSource(ByVal textBuffer As ITextBuffer) As ICompletionSource Implements ICompletionSourceProvider.TryCreateCompletionSource Return New TestCompletionSource(Me, textBuffer) End Function
public ICompletionSource TryCreateCompletionSource(ITextBuffer textBuffer) { return new TestCompletionSource(this, textBuffer); }
Implementing the Completion Command Handler Provider
The completion command handler provider is derived from a IVsTextViewCreationListener, which listens for a text view creation event and converts the view from an IVsTextView (which allows the command to be added to the command chain of the Visual Studio shell) to an ITextView. Since this class is a MEF export, you can also use it to import the services that will be needed by the command handler itself.
To implement the completion command handler provider
Add a file named TestCompletionCommandHandler.
Add the following using statements.
Imports System Imports System.Collections.Generic Imports System.Linq Imports System.Text Imports System.ComponentModel.Composition Imports System.Runtime.InteropServices Imports Microsoft.VisualStudio Imports Microsoft.VisualStudio.Editor Imports Microsoft.VisualStudio.Language.Intellisense Imports Microsoft.VisualStudio.OLE.Interop Imports Microsoft.VisualStudio.Shell Imports Microsoft.VisualStudio.Text Imports Microsoft.VisualStudio.Text.Editor Imports Microsoft.VisualStudio.TextManager.Interop Imports Microsoft.VisualStudio.Utilities
using System; using System.ComponentModel.Composition; using System.Runtime.InteropServices; using Microsoft.VisualStudio; using Microsoft.VisualStudio.Editor; using Microsoft.VisualStudio.Language.Intellisense; using Microsoft.VisualStudio.OLE.Interop; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.TextManager.Interop; using Microsoft.VisualStudio.Utilities;
Add a class named TestCompletionHandlerProvider that implements IVsTextViewCreationListener. Export this class with a NameAttribute of "token completion handler", a ContentTypeAttribute of "plaintext", and a TextViewRoleAttribute of Editable.
<Export(GetType(IVsTextViewCreationListener))> <Name("token completion handler")> <ContentType("plaintext")> <TextViewRole(PredefinedTextViewRoles.Editable)> Friend Class TestCompletionHandlerProvider Implements IVsTextViewCreationListener
[Export(typeof(IVsTextViewCreationListener))] [Name("token completion handler")] [ContentType("plaintext")] [TextViewRole(PredefinedTextViewRoles.Editable)] internal class TestCompletionHandlerProvider : IVsTextViewCreationListener
Import the IVsEditorAdaptersFactoryService, which lets you convert from a IVsTextView to a ITextView, a ICompletionBroker, and a SVsServiceProvider, which allows you to access standard Visual Studio services..
<Import()> Friend AdapterService As IVsEditorAdaptersFactoryService = Nothing <Import()> Friend Property CompletionBroker() As ICompletionBroker <Import()> Friend Property ServiceProvider() As SVsServiceProvider
[Import] internal IVsEditorAdaptersFactoryService AdapterService = null; [Import] internal ICompletionBroker CompletionBroker { get; set; } [Import] internal SVsServiceProvider ServiceProvider { get; set; }
Implement the VsTextViewCreated method to instantiate the command handler.
Public Sub VsTextViewCreated(ByVal textViewAdapter As IVsTextView) Implements IVsTextViewCreationListener.VsTextViewCreated Dim textView As ITextView = AdapterService.GetWpfTextView(textViewAdapter) If textView Is Nothing Then Return End If Dim createCommandHandler As Func(Of TestCompletionCommandHandler) = Function() New TestCompletionCommandHandler(textViewAdapter, textView, Me) textView.Properties.GetOrCreateSingletonProperty(createCommandHandler) End Sub
public void VsTextViewCreated(IVsTextView textViewAdapter) { ITextView textView = AdapterService.GetWpfTextView(textViewAdapter); if (textView == null) return; Func<TestCompletionCommandHandler> createCommandHandler = delegate() { return new TestCompletionCommandHandler(textViewAdapter, textView, this); }; textView.Properties.GetOrCreateSingletonProperty(createCommandHandler); }
Implementing the Completion Command Handler
Because statement completion is triggered by keystrokes, you must implement the IOleCommandTarget interface in order to receive and process the keystrokes that trigger, commit, and dismiss the completion session.
To implement the completion command handler
Add a class named TestCompletionCommandHandler that implements IOleCommandTarget.
Friend Class TestCompletionCommandHandler Implements IOleCommandTarget
internal class TestCompletionCommandHandler : IOleCommandTarget
Add private fields for the next command handler (to which you pass the command), the text view, the command handler provider (which allows you to access various services), and a completion session.
Private m_nextCommandHandler As IOleCommandTarget Private m_textView As ITextView Private m_provider As TestCompletionHandlerProvider Private m_session As ICompletionSession
private IOleCommandTarget m_nextCommandHandler; private ITextView m_textView; private TestCompletionHandlerProvider m_provider; private ICompletionSession m_session;
Add a constructor that sets the text view and the provider fields, and adds the command to the command chain.
Friend Sub New(ByVal textViewAdapter As IVsTextView, ByVal textView As ITextView, ByVal provider As TestCompletionHandlerProvider) Me.m_textView = textView Me.m_provider = provider 'add the command to the command chain textViewAdapter.AddCommandFilter(Me, m_nextCommandHandler) End Sub
internal TestCompletionCommandHandler(IVsTextView textViewAdapter, ITextView textView, TestCompletionHandlerProvider provider) { this.m_textView = textView; this.m_provider = provider; //add the command to the command chain textViewAdapter.AddCommandFilter(this, out m_nextCommandHandler); }
Implement the QueryStatus method by passing the command along.
Public Function QueryStatus(ByRef pguidCmdGroup As Guid, ByVal cCmds As UInteger, ByVal prgCmds() As OLECMD, ByVal pCmdText As IntPtr) As Integer Implements IOleCommandTarget.QueryStatus Return m_nextCommandHandler.QueryStatus(pguidCmdGroup, cCmds, prgCmds, pCmdText) End Function
public int QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText) { return m_nextCommandHandler.QueryStatus(ref pguidCmdGroup, cCmds, prgCmds, pCmdText); }
Implement the Exec method. When this method receives a keystroke, it must do one of these things:
Allow the character to be written to the buffer, and then trigger or filter completion (printing characters do this).
Commit the completion, but do not allow the character to be written to the buffer (whitespace, TAB, and ENTER do this when a completion session is displayed).
Allow the command to be passed on to the next handler (all other commands).
Since this method may display UI, you should make sure that it is not called in an automation context by calling IsInAutomationFunction.
Public Function Exec(ByRef pguidCmdGroup As Guid, ByVal nCmdID As UInteger, ByVal nCmdexecopt As UInteger, ByVal pvaIn As IntPtr, ByVal pvaOut As IntPtr) As Integer Implements IOleCommandTarget.Exec If VsShellUtilities.IsInAutomationFunction(m_provider.ServiceProvider) Then Return m_nextCommandHandler.Exec(pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut) End If 'make a copy of this so we can look at it after forwarding some commands Dim commandID As UInteger = nCmdID Dim typedChar As Char = Char.MinValue 'make sure the input is a char before getting it If pguidCmdGroup = VSConstants.VSStd2K AndAlso nCmdID = CUInt(VSConstants.VSStd2KCmdID.TYPECHAR) Then typedChar = CChar(ChrW(CUShort(Marshal.GetObjectForNativeVariant(pvaIn)))) End If 'check for a commit character If nCmdID = CUInt(VSConstants.VSStd2KCmdID.RETURN) OrElse nCmdID = CUInt(VSConstants.VSStd2KCmdID.TAB) OrElse (Char.IsWhiteSpace(typedChar) OrElse Char.IsPunctuation(typedChar)) Then 'check for a a selection If m_session IsNot Nothing AndAlso (Not m_session.IsDismissed) Then 'if the selection is fully selected, commit the current session If m_session.SelectedCompletionSet.SelectionStatus.IsSelected Then m_session.Commit() 'also, don't add the character to the buffer Return VSConstants.S_OK Else 'if there is no selection, dismiss the session m_session.Dismiss() End If End If End If 'pass along the command so the char is added to the buffer Dim retVal As Integer = m_nextCommandHandler.Exec(pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut) Dim handled As Boolean = False If (Not typedChar.Equals(Char.MinValue)) AndAlso Char.IsLetterOrDigit(typedChar) Then If m_session Is Nothing OrElse m_session.IsDismissed Then ' If there is no active session, bring up completion Me.TriggerCompletion() m_session.Filter() Else 'the completion session is already active, so just filter m_session.Filter() End If handled = True ElseIf commandID = CUInt(VSConstants.VSStd2KCmdID.BACKSPACE) OrElse commandID = CUInt(VSConstants.VSStd2KCmdID.DELETE) Then 'redo the filter if there is a deletion If m_session IsNot Nothing AndAlso (Not m_session.IsDismissed) Then m_session.Filter() End If handled = True End If If handled Then Return VSConstants.S_OK End If Return retVal End Function
public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut) { if (VsShellUtilities.IsInAutomationFunction(m_provider.ServiceProvider)) { return m_nextCommandHandler.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut); } //make a copy of this so we can look at it after forwarding some commands uint commandID = nCmdID; char typedChar = char.MinValue; //make sure the input is a char before getting it if (pguidCmdGroup == VSConstants.VSStd2K && nCmdID == (uint)VSConstants.VSStd2KCmdID.TYPECHAR) { typedChar = (char)(ushort)Marshal.GetObjectForNativeVariant(pvaIn); } //check for a commit character if (nCmdID == (uint)VSConstants.VSStd2KCmdID.RETURN || nCmdID == (uint)VSConstants.VSStd2KCmdID.TAB || (char.IsWhiteSpace(typedChar) || char.IsPunctuation(typedChar))) { //check for a a selection if (m_session != null && !m_session.IsDismissed) { //if the selection is fully selected, commit the current session if (m_session.SelectedCompletionSet.SelectionStatus.IsSelected) { m_session.Commit(); //also, don't add the character to the buffer return VSConstants.S_OK; } else { //if there is no selection, dismiss the session m_session.Dismiss(); } } } //pass along the command so the char is added to the buffer int retVal = m_nextCommandHandler.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut); bool handled = false; if (!typedChar.Equals(char.MinValue) && char.IsLetterOrDigit(typedChar)) { if (m_session == null || m_session.IsDismissed) // If there is no active session, bring up completion { this.TriggerCompletion(); m_session.Filter(); } else //the completion session is already active, so just filter { m_session.Filter(); } handled = true; } else if (commandID == (uint)VSConstants.VSStd2KCmdID.BACKSPACE //redo the filter if there is a deletion || commandID == (uint)VSConstants.VSStd2KCmdID.DELETE) { if (m_session != null && !m_session.IsDismissed) m_session.Filter(); handled = true; } if (handled) return VSConstants.S_OK; return retVal; }
The following code is a private method that triggers the completion session.
Private Function TriggerCompletion() As Boolean 'the caret must be in a non-projection location Dim caretPoint As SnapshotPoint? = m_textView.Caret.Position.Point.GetPoint(Function(textBuffer) ((Not textBuffer.ContentType.IsOfType("projection"))), PositionAffinity.Predecessor) If Not caretPoint.HasValue Then Return False End If m_session = m_provider.CompletionBroker.CreateCompletionSession(m_textView, caretPoint.Value.Snapshot.CreateTrackingPoint(caretPoint.Value.Position, PointTrackingMode.Positive), True) 'subscribe to the Dismissed event on the session AddHandler m_session.Dismissed, AddressOf OnSessionDismissed m_session.Start() Return True End Function
private bool TriggerCompletion() { //the caret must be in a non-projection location SnapshotPoint? caretPoint = m_textView.Caret.Position.Point.GetPoint( textBuffer => (!textBuffer.ContentType.IsOfType("projection")), PositionAffinity.Predecessor); if (!caretPoint.HasValue) { return false; } m_session = m_provider.CompletionBroker.CreateCompletionSession (m_textView, caretPoint.Value.Snapshot.CreateTrackingPoint(caretPoint.Value.Position, PointTrackingMode.Positive), true); //subscribe to the Dismissed event on the session m_session.Dismissed += this.OnSessionDismissed; m_session.Start(); return true; }
The following code is a private method that unsubscribes from the Dismissed event.
Private Sub OnSessionDismissed(ByVal sender As Object, ByVal e As EventArgs) RemoveHandler m_session.Dismissed, AddressOf OnSessionDismissed m_session = Nothing End Sub
private void OnSessionDismissed(object sender, EventArgs e) { m_session.Dismissed -= this.OnSessionDismissed; m_session = null; }
Building and Testing the Code
To test this code, build the CompletionTest solution and run it in the experimental instance.
To build and test the CompletionTest solution
Build the solution.
When you run this project in the debugger, a second instance of Visual Studio is instantiated.
Create a text file and type some text that includes the word "add".
As you type first "a" and then "d", a list that contains "addition" and "adaptation" should be displayed. Notice that addition is selected. When you type another "d", the list should contain only "addition", which is now selected. You can commit "addition" by pressing the SPACEBAR, TAB, or ENTER, or dismiss the list by typing ESC or any other character.
See Also
Tasks
Walkthrough: Linking a Content Type to a File Name Extension