-
Notifications
You must be signed in to change notification settings - Fork 33
Modern completion walkthrough
Hello and welcome to the async completion API walkthrough.
The API is defined in Microsoft.VisualStudio.Language
nuget, in the Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion
namespace.
Completion uses MEF to discover extensions. Specifically, we are looking for exports of type IAsyncCompletionSourceProvider
, IAsyncCompletionItemManagerProvider
, IAsyncCompletionCommitManagerProvider
and ICompletionPresenterProvider
. Each export must be decorated with Name
and ContentType
metadata. It may be decorated with TextViewRoles
and Order
metadata.
I would like to move data transfer objects into
Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data
so that user's first interaction with Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion
is easy and not threatening. Once they begin to implement one of the fundamental APIs above, they will naturally use the .Data
child namespace
Implement IAsyncCompletionSourceProvider that returns an instance of IAsyncCompletionSource
When user interacts with Visual Studio, e.g. by typing, the editor will see if it is appropriate to begin a completion session. We do so by calling TryGetApplicableSpan. This method is invoked on UI thread while the user is typing, therefore it is important to return promptly. Usually, you just need to make a syntactic check whether completion is appropriate at the given location.
If at least one IAsyncCompletionSource
returned true
from TryGetApplicableSpan
, completion session will commence and begin processing on the background thread.
We will attempt to get completion items, by asynchronously calling GetCompletionContextAsync where you provide completion items.
This method will be called on all available IAsyncCompletionSource
, even if they returned false
from TryGetApplicableSpan
- this is to accommodate for extensions who wish to add completion items, but don't know about the language's syntax.
Items from all sources will be combined and eventually displayed in the UI. The UI will call GetDescriptionAsync to build tooltips for items you provided.
Implement IAsyncCompletionCommitManagerProvider that returns an instance of IAsyncCompletionCommitManager
We use this interface to determine under what circumstances to commit a completion item - that is, to insert the completion text into the text buffer and close the completion UI.
When we first create the completion session, we access the PotentialCommitCharacters property. This happens on UI thread, therefore this method should return a preallocated array of characters that potentially commit completion by being typed.
Typically, this includes space and other token delimeters such as .
, (
, )
. Don't worry about Tab and Enter, as they are handled separately. If a character is a commit character in some, but not all situations, you must add it to the list. Characters from available IAsyncCompletionCommitManager
s are combined into one list for the duration of the completion session.
We maintain this list so that editor's completion feature can quickly ignore characters that are not commit characters. If user types a character found in the provided array, Editor will call ShouldCommitCompletion on the UI thread. This is an opportunity to tell whether certain character is indeed a commit character in the given location. In most cases, simply return true
, which means that every character in PotentialCommitCharacters
will trigger the commit behavior.
When the completion item is about to be committed, Editor calls TryCommit on available IAsyncCompletionCommitManager
s. This method is also called on UI thread and offers complete access to the ITextView
and ITextBuffer
, so that the language service can customize the way text is entered into the buffer. This method returns CommitResult which provides two pieces of information:
-
bool
that indicates whether the item was committed - if not, Editor will callTryCommit
on anotherIAsyncCompletionCommitManager
-
CommitBehavior with instructions for how to proceed. This is used by complicated language services, and it's best to return
None
.
Speaking of complicated language services - TryCommit
was written for these language services. In most cases, feel free to return CommitResult.Unhandled. When all IAsyncCompletionCommitManager
s return CommitResult.Unhandled
, Editor will simply insert the completion item into the text buffer in the appropriate location.
The async completion API allows you to create extension that adds new completion items, without concerning you with the syntax tree or how to commit the item. These questions will be delegated to the language service. You just need to implement IAsyncCompletionSourceProvider that returns an instance of IAsyncCompletionSource
When you implement TryGetApplicableSpan, return false and leave applicableSpan
as default
. As long as a single language service returns true and sets the applicableSpan
, completion will start. Returning false
does not exclude you from participating in completion!
Implement GetCompletionContextAsync where you provide completion items. They will be added to items from other sources. Implement GetDescriptionAsync that will provide tooltips for items you provided.
Visual Studio provides standard sorting and filtering facilities, but you may want to provide custom behavior for specific ContentType or TextViewRoles. Decorate IAsyncCompletionItemManagerProvider
with MEF metadata to narrow the scope for your UI.
Implement IAsyncCompletionItemManagerProvider that returns an instance of IAsyncCompletionItemManager
To do: write this section, and write about IAsyncCompletionSession
Visual Studio provides standard UI, but you may want to create a custom UI for specific ContentType or TextViewRoles. Decorate ICompletionPresenterProvider
with MEF metadata to narrow the scope for your UI.
Implement ICompletionPresenterProvider that returns an instance of ICompletionPresenter
This interface represents a class that manages the user interface. When we first show the completion UI, we call the Open method, and subsequently we call the Update method. Both methods accept a single parameter of type CompletionPresentationViewModel which contains data required to render the UI. We call these methods on the UI thread.
Notice that completion items are represented by CompletionItemWithHighlight - a struct that combines CompletionItem
and array of Span
s that should be bolded in the UI.
Completion filters are represented by CompletionFilterWithState that combines CompletionFilter
with two bools that indicate whether filter is available and whether it is selected by the user. As the user types and narrows down the list of completion items, they also narrow down list of completion filters. Unavailable completion filters are not associated with any items that are visible at the moment, and we represent them with dim icons.
The CompletionFilter
itself has displayText
that appears in the tooltip, accessKey
which is bound to a keyboard shortcut and image
to represent it in the UI.
When user clicks a filter button, create a new instance of CompletionFilterWithState
by calling CompletionFilterWithState.WithSelected, then raise CompletionFilterChaned
event (TODO: event's documentation is not available). Editor will recompute completion items and call Update
with new data.
When user changes the selected item by clicking on it, call CompletionItemSelected event (TODO: event's documentation is not available)
Handling of Up, Down, Page Up and Page Down keys is done on the Editor's side, the UI should not handle these cases. When handling Page Up and Page Down keys, we use the ICompletionPresenterProvider.ResultsPerPage property to select appropriate item.
To minimize number of allocations, create icons and filters once, and use their references in CompletionItem
. For example:
static readonly ImageElement PropertyImage = new AccessibleImageElement(KnownMonikers.Property.ToImageId(), "Property image");
static readonly CompletionFilter PropertyFilter = new CompletionFilter("Properties", "P", PropertyImage);
static readonly ImmutableArray<CompletionFilter> PropertyFilters = new CompletionFilter[] { PropertyFilter }.ToImmutableArray();
This walkthrough does not cover IAsyncCompletionItemManager
and IAsyncCompletionSession
which are subject to change.
I would like to refactor ItemManager method calls that use many parameters to use dedicated data transfer object. In particular, IAsyncCompletionSession - for access to the property bag and text view AsyncCompletionSessionInstantenousState - for data pertinent to the particular call to ItemManager. It contains up to date information about the text snapshot and state of completion session