Async Completion API discussion #9
Async Completion API walkthrough
Please comment 🐱👤, and I will add emoji to your comment depending on my progress
Table of contents:
The API is defined in
Completion in VS uses MEF to discover extensions. Specifically, VS is looking for exports of type
In general, methods named
When completion is available
Prior to starting, completion checks the following:
How to implement a language service that participates in async completion API
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
If at least one
We will attempt to get completion items by asynchronously calling GetCompletionContextAsync where you provide completion items.
This method will be called on all available
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.
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
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
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
When the completion item is about to be committed, Editor calls TryCommit on available
Speaking of complicated language services -
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
When you implement TryGetApplicableSpan, return false and leave
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
The completion feature in Visual Studio is represented by
Immediately after obtaining
The purpose of
The purpose of
Let's go over the asynchronous computation model.
This means that information from
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
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
Completion filters are represented by CompletionFilterWithState that combines
When user clicks a filter button, create a new instance of
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
How to interact with completion
IAsyncCompletionBroker is the entry point to the completion feature:
IAsyncCompletionSession exposes the following:
Best practices for completion item source
To minimize number of allocations, create icons and filters once, and use their references in
Given that IAsyncCompletionSource.TryGetApplicableSpan() is called on UI thread and can potentially affect typing perf it should at least provide a cancellation token, which you can hookup to the commanding cancellation or setup your own wait dialog if it's called outside of command execution.
Up to date list of things that are not in the API:
One thing I think is cause for concern is how we choose whether or not the new completion API is supported.
I don't think this logic is sufficient to prevent extenders from breaking languages that use pre-async completion.
For example, if I create a VS snippets extension that uses async completion and matches content type 'text' with the intention of providing language-agnostic snippets, the IsCompletionSupported() method will return true for all languages, because there is a provider that matches the content type.
This will work fine for language services that onboard to the new API, but for any language that hasn't moved (and likely won't move) over to the new API, like XML, they will get only my extensions completion items, and their language-specific completion will be broken.
The same is true in multi-language or embedded language scenarios. The embedder and the embeddee will have to use the same completion API or else we'll only show items for whichever uses the new API. For Web tools, we can get a commitment to standardize on the new APIs, but for other languages, like P#, which embed code from language services that will be on the new APIs (C#), this will be a breaking change.
As unsightly as shims are, this is the same manner of problem we had with quick info and being able to display content from different API generations in the same tip. Without a reasonable shim, I think we'll get a lot of negative feedback from extenders. At the very least, there might be value in having a way to annotate a provider as being important enough to trigger the new completion to be used so that the things like the hypothetical snippets provider don't trigger the new completion on languages it isn't supported by.
referenced this issue
May 17, 2018
I’ve made some changes to the completion API and would like to seek partner signoff.
Notably, most methods of the API now have a reference to IAsyncCompletionSession,
I believe that I’ve addressed some issues raised by @CyrusNajmabadi except for one:
Cyrus proposed that we expose “method of committing” – to distinguish whether user clicked, hit Ctrl+Space etc.
Open the attached dll may be opened using ILSpy to see the interface methods and their doc comments:
Thanks, Cyrus. That's a good point.
Do we have special behavior on click or Ctrl+Space?
As far as what triggered the completion goes, IAsyncCompletionSource may put the initial
we have different behavior on ctrl-space vs ctrl-j (i.e. commit unique, or bring up completino).
That feels enormously hacky. :)
But i get it shipped, and can work that way. so while i don't like it, i get taht it's likely the most expediant choice.
When we call
However, if the completion was already visible because of, for example, typing, and then user hit Ctrl+Space, the "initial trigger" would remain what it originally was. There is no way to tell that item is committed by Ctrl+Space.
What's the user scenario that's on your mind?
So Roslyn has the model that you can bring up completion (with something like ctrl-j or ctrl-space). Then, once completion is up you can optionally type more, and then hit ctrl-space again. Ctrl-space should now commit if the item is unique.
Is that behavior preserved with the new system?
Absolutely, this is preserved:
If there is no unique item, first Ctrl+Space will open completion and user has opportunity to refine the results.
What you can't do is know that user pressed Ctrl+Space in your code that handles commit. The purpose of the code is to modify a provided
My question is whether there is any user scenario, in which the code that handles commit needs to know about how user caused item to get committed (e.g. click or Ctrl+Space)
It looks like if you double-click the item we end up calling through teh same codepath that is used for "commit if unique" (when we know the item is unique).
So click vs control-space should always be the same.
Another thing to check: Roslyn completion items have the concept of 'absorbing' the typed character or not. i.e. for some characters (like tab) they will eat the character and not send into the buffer. however a character like ';' will be sent through.
It's feature dependent as some features want to control heavily exactly what actually makes it into the buffer.
That's still supported, right?
Yes, as far as Roslyn is concerned, there is the same code path for committing through double click, ctrl+space. Enter, Tab will additionally pass in typeChar for editing purposes
Yes, Roslyn may pass CommitBehavior:
In the previous version of the API, TryCommit had two triggers: initial trigger and update trigger. We consumed them a very strange way. With the current API, I just simplified the way we use the trigger. Based on the current test coverage, we may need just the update trigger.
However, if there is a scenario where we need both, let us arrange it as a unit test and then return back the initial trigger.