Async Completion API walkthrough
Please comment π±βπ€, and I will add emoji to your comment depending on my progress
π I will do it
π Done
Table of contents:
Basics
The API is defined in Microsoft.VisualStudio.Language nuget, in the Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion namespace.
All helper types are defined in Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data namespace.

Completion in VS uses MEF to discover extensions. Specifically, VS is 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.
In general, methods named ...Async are invoked on the background thread and are cancelable. Otherwise, methods are invoked on the UI thread.
When completion is available
Prior to starting, completion checks the following:
-
IFeatureService.IsEnabled(PredefinedEditorFeatureNames.Completion)
IFeatureService allows features to be disabled per view or per application
IFeatureService allows a group of features to be disabled, e.g. "disable all popups"
- Current and planned uses:
- Inline Rename, for the duration of rename session
- Multi Caret, until the two features are certified to work together
- ReSharper, because their hacks to disable a feature don't work with the asynchronous nature of Async Completion
-
Is flight "CompletionAPI" enabled in IVsExperimentationService
-
IAsyncCompletionBroker.IsCompletionSupported checks if there are any
IAsyncCompletionSourceProviders and
IAsyncCompletionItemManagers for a target content type
How to implement a language service that participates in async completion API
IAsyncCompletionSource
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. Despite being called on the UI thread, we a supply CancellationToken that you should check and respect.
If at least one IAsyncCompletionSource returned true from TryGetApplicableSpan, completion session will start 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 IAsyncCompletionSources, even if they returned false from TryGetApplicableSpan.
This is to accommodate for extensions who wish to add completion items without the need to care about the language's syntax, and potential interference with the main language service.
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.
IAsyncCompletionCommitManager
Implement IAsyncCompletionCommitManagerProvider that returns an instance of IAsyncCompletionCommitManager
We use this interface to determine under which circumstances to commit (insert the completion text into the text buffer and close the completion UI) a completion item.
There must be one IAsyncCompletionCommitManager available to begin completion session.
When we first create the completion session, we access the PotentialCommitCharacters property that returns characters that potentially commit completion when user types them. We access this property, therefore it should return a preallocated array.
Typically, the commit characters include 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 this collection. Characters from available IAsyncCompletionCommitManagers are combined into a single collection 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 IAsyncCompletionCommitManagers. 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 call TryCommit on another IAsyncCompletionCommitManager
- 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 IAsyncCompletionCommitManagers return CommitResult.Unhandled, Editor will simply insert the completion item into the text buffer in the appropriate location.
How to extend a language with new completion items
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.
How to implement custom sorting and filtering
Visual Studio provides standard sorting and filtering facilities, but you may want to provide custom behavior for ContentType and TextViewRoles of your choice.
Implement IAsyncCompletionItemManagerProvider that returns an instance of IAsyncCompletionItemManager. Decorate IAsyncCompletionItemManagerProvider with MEF metadata ContentType and optionally TextViewRoles to narrow the scope of your extension.
The completion feature in Visual Studio is represented by IAsyncCompletionSession (we'll now call it "Session"). This object is active from the moment completion is "triggered" (see TryGetApplicableSpan) until it is Dismissed (pressing Escape or clicking away from completion UI) or until a completion item is Commited. The Session object holds properties that don't drastically change throughout the lifetime of the session: it stores the ITrackingSpan where completion is happening, has a reference to the ITextView (which may be null in Cascade!) and has a PropertyBag.
Immediately after obtaining CompletionItems from IAsyncCompletionSources, the Session calls SortCompletionListAsync. Then, the Session calls UpdateCompletionListAsync and will do so as long as the user is typing. Both methods are called asynchronously and take similar paremeters which we will cover shortly.
The purpose of SortCompletionListAsync is to sort items we received from multiple IAsyncCompletionSources.
UpdateCompletionListAsync will receive this sorted list. This is merely a performance improvement so that UpdateCompletionListAsync doesn't need to sort.
The purpose of UpdateCompletionListAsync is to produce a final list of items to display in the UI, and to provide information on how to display items in the UI. All the information is bundled in FilteredCompletionModel
Important:
Let's go over the asynchronous computation model.
Suppose user is continuously typing. At every keystroke, we take a reference to the text snapshot in the editor. If we have time, we call UpdateCompletionListAsync. If user typed quickly, we won't call UpdateCompletionListAsync. Or perhaps user typed (and modified editor's contents) while you were processing UpdateCompletionListAsync.
This means that information from IAsyncCompletionSession or its reference to ITextView may be stale. This is why we maintain an immutable model that stores all important properties of the completion session. We select the relevant bits and put them into second parameter of SortCompletionListAsync and UpdateCompletionListAsync: SessionInitialData and SessionInstantenousData respectively.
We introduced these data transfer objects to keep method signatures short.
How to implement the UI
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 Spans 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.
The completion session depends on the ICompletionPresenter to accurately report the state of the UI using the following events:
FiltersChanged that computes new set of items to display after user changed completion filters
CompletionItemSelected that updates the selected item after user clicked it
CommitRequested when user double-clicked an item
CompletionClosed when the UI is closed.
How to interact with completion
IAsyncCompletionBroker is the entry point to the completion feature:
- method TriggerCompletion is used by VS to trigger a new session
- method IsCompletionActive lets you can see if there is an active session in a given
ITextView
- method GetSession gets the active session in a given
ITextView or returns null.
- method IsCompletionSupported returns whether there are any available
IAsyncCompletionSourceProviders for a given IContentType.
IAsyncCompletionSession exposes the following:
- property
TextView which is a reference to pertinent text view
- note its
ITextSnapshot may be different from what's used during computation. See SessionInstantenousData for the correct ITextSnapshot.
- property
ApplicableSpan which tracks span
- Whose content is used to filter completion
- That will be replaced by committed item
- event
CompletionTriggered when completion session is triggered
- event
ItemCommitted when completion commits and closes,
- event
Dismissed when completion closes without committing.
GetComputedItems method that blocks to finish computation and returns ComputedCompletionItems
- event
ItemsUpdated that retruns ComputedCompletionItems after computation finished.
The ComputedCompletionItems, available from IAsyncCompletionSession.ItemsUpdated and IAsyncCompletionSession.GetComputedItems() stores:
- Items
- Suggestion item
- Currently selected item
- Whether the selected item is a suggestion item
Best practices
Best practices for completion item source
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();
Async Completion API walkthrough
Please comment π±βπ€, and I will add emoji to your comment depending on my progress
π I will do it
π Done
Table of contents:
Basics
The API is defined in
Microsoft.VisualStudio.Languagenuget, in theMicrosoft.VisualStudio.Language.Intellisense.AsyncCompletionnamespace.All helper types are defined in
Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Datanamespace.Completion in VS uses MEF to discover extensions. Specifically, VS is looking for exports of type
IAsyncCompletionSourceProvider,IAsyncCompletionItemManagerProvider,IAsyncCompletionCommitManagerProviderandICompletionPresenterProvider. Each export must be decorated withNameandContentTypemetadata. It may be decorated withTextViewRolesandOrdermetadata.In general, methods named
...Asyncare invoked on the background thread and are cancelable. Otherwise, methods are invoked on the UI thread.When completion is available
Prior to starting, completion checks the following:
IFeatureService.IsEnabled(PredefinedEditorFeatureNames.Completion)IFeatureServiceallows features to be disabled per view or per applicationIFeatureServiceallows a group of features to be disabled, e.g. "disable all popups"Is flight
"CompletionAPI"enabled inIVsExperimentationServiceIAsyncCompletionBroker.IsCompletionSupportedchecks if there are anyIAsyncCompletionSourceProviders andIAsyncCompletionItemManagers for a target content typeHow to implement a language service that participates in async completion API
IAsyncCompletionSource
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. Despite being called on the UI thread, we a supply
CancellationTokenthat you should check and respect.If at least one
IAsyncCompletionSourcereturnedtruefromTryGetApplicableSpan, completion session will start 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
IAsyncCompletionSources, even if they returnedfalsefromTryGetApplicableSpan.This is to accommodate for extensions who wish to add completion items without the need to care about the language's syntax, and potential interference with the main language service.
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.
IAsyncCompletionCommitManager
Implement IAsyncCompletionCommitManagerProvider that returns an instance of IAsyncCompletionCommitManager
We use this interface to determine under which circumstances to commit (insert the completion text into the text buffer and close the completion UI) a completion item.
There must be one
IAsyncCompletionCommitManageravailable to begin completion session.When we first create the completion session, we access the PotentialCommitCharacters property that returns characters that potentially commit completion when user types them. We access this property, therefore it should return a preallocated array.
Typically, the commit characters include 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 this collection. Characters from availableIAsyncCompletionCommitManagers are combined into a single collection 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 inPotentialCommitCharacterswill trigger the commit behavior.When the completion item is about to be committed, Editor calls TryCommit on available
IAsyncCompletionCommitManagers. This method is also called on UI thread and offers complete access to theITextViewandITextBuffer, 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:boolthat indicates whether the item was committed - if not, Editor will callTryCommiton anotherIAsyncCompletionCommitManagerNone.Speaking of complicated language services -
TryCommitwas written for these language services. In most cases, feel free to return CommitResult.Unhandled. When allIAsyncCompletionCommitManagers returnCommitResult.Unhandled, Editor will simply insert the completion item into the text buffer in the appropriate location.How to extend a language with new completion items
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
applicableSpanasdefault. As long as a single language service returns true and sets theapplicableSpan, completion will start. Returningfalsedoes 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.
How to implement custom sorting and filtering
Visual Studio provides standard sorting and filtering facilities, but you may want to provide custom behavior for ContentType and TextViewRoles of your choice.
Implement IAsyncCompletionItemManagerProvider that returns an instance of IAsyncCompletionItemManager. Decorate
IAsyncCompletionItemManagerProviderwith MEF metadataContentTypeand optionallyTextViewRolesto narrow the scope of your extension.The completion feature in Visual Studio is represented by
IAsyncCompletionSession(we'll now call it "Session"). This object is active from the moment completion is "triggered" (see TryGetApplicableSpan) until it is Dismissed (pressing Escape or clicking away from completion UI) or until a completion item is Commited. The Session object holds properties that don't drastically change throughout the lifetime of the session: it stores theITrackingSpanwhere completion is happening, has a reference to theITextView(which may be null in Cascade!) and has aPropertyBag.Immediately after obtaining
CompletionItems fromIAsyncCompletionSources, the Session calls SortCompletionListAsync. Then, the Session calls UpdateCompletionListAsync and will do so as long as the user is typing. Both methods are called asynchronously and take similar paremeters which we will cover shortly.The purpose of
SortCompletionListAsyncis to sort items we received from multipleIAsyncCompletionSources.UpdateCompletionListAsyncwill receive this sorted list. This is merely a performance improvement so thatUpdateCompletionListAsyncdoesn't need to sort.The purpose of
UpdateCompletionListAsyncis to produce a final list of items to display in the UI, and to provide information on how to display items in the UI. All the information is bundled in FilteredCompletionModelImportant:
Let's go over the asynchronous computation model.
Suppose user is continuously typing. At every keystroke, we take a reference to the text snapshot in the editor. If we have time, we call
UpdateCompletionListAsync. If user typed quickly, we won't callUpdateCompletionListAsync. Or perhaps user typed (and modified editor's contents) while you were processingUpdateCompletionListAsync.This means that information from
IAsyncCompletionSessionor its reference toITextViewmay be stale. This is why we maintain an immutable model that stores all important properties of the completion session. We select the relevant bits and put them into second parameter ofSortCompletionListAsyncandUpdateCompletionListAsync: SessionInitialData and SessionInstantenousData respectively.We introduced these data transfer objects to keep method signatures short.
How to implement the UI
Visual Studio provides standard UI, but you may want to create a custom UI for specific ContentType or TextViewRoles. Decorate
ICompletionPresenterProviderwith 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
CompletionItemand array ofSpans that should be bolded in the UI.Completion filters are represented by CompletionFilterWithState that combines
CompletionFilterwith 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
CompletionFilteritself hasdisplayTextthat appears in the tooltip,accessKeywhich is bound to a keyboard shortcut andimageto represent it in the UI.When user clicks a filter button, create a new instance of
CompletionFilterWithStateby calling CompletionFilterWithState.WithSelected, then raiseCompletionFilterChanedevent (TODO: event's documentation is not available). Editor will recompute completion items and callUpdatewith 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.
The completion session depends on the
ICompletionPresenterto accurately report the state of the UI using the following events:FiltersChangedthat computes new set of items to display after user changed completion filtersCompletionItemSelectedthat updates the selected item after user clicked itCommitRequestedwhen user double-clicked an itemCompletionClosedwhen the UI is closed.How to interact with completion
IAsyncCompletionBroker is the entry point to the completion feature:
ITextViewITextViewor returns null.IAsyncCompletionSourceProviders for a givenIContentType.IAsyncCompletionSession exposes the following:
TextViewwhich is a reference to pertinent text viewITextSnapshotmay be different from what's used during computation. See SessionInstantenousData for the correctITextSnapshot.ApplicableSpanwhich tracks spanCompletionTriggeredwhen completion session is triggeredItemCommittedwhen completion commits and closes,Dismissedwhen completion closes without committing.GetComputedItemsmethod that blocks to finish computation and returnsComputedCompletionItemsItemsUpdatedthat retrunsComputedCompletionItemsafter computation finished.The
ComputedCompletionItems, available fromIAsyncCompletionSession.ItemsUpdatedandIAsyncCompletionSession.GetComputedItems()stores:Best practices
Best practices for completion item source
To minimize number of allocations, create icons and filters once, and use their references in
CompletionItem. For example: