I have changed SourceKit-LSP master branch to perform code-completion filtering on the server side and limit the number of completion items to 200 results by default. This should radically speed up completion for global results when large modules are imported like in the following example.
import Cocoa
func test() {
<complete here> // used to take 3 seconds; now <100 ms on my machine
}
Note: the very first global completion after (re-)starting sourcekit-lsp populates our module completion cache, so make sure you do a second completion to see the performance improvement.
I verified that server-side filtering is working in VSCode, Sublime Text, and vim (with coc.nvim). If any editors do not support LSP's isIncomplete
flag yet, it is possible to disable server-side filtering.
How to try it out
If you have seen slow completion in the past, please give it a try and let me know if you run into any issues with the new behaviour. You can report issues at bugs.swift.org with the SourceKit-LSP component, or mention them in this thread.
Option 1) Building SourceKit-LSP from Source
Server-side completion filtering is enabled by default on the master branch, so you can try it by following the instructions to build sourcekit-lsp from source.
Option 2) Toolchain development snapshot
I will update this thread when the change is available in a downloadable snapshot.
Update: now available in Trunk Development (master) snapshots starting with the August 4 snapshot (swift-DEVELOPMENT-SNAPSHOT-2020-08-04-a).
You can try it out using the instructions at getting started for a Swift.org toolchain.
Configuration
The new server-side filtering behaviour is on by default, but if you need to change or disable it there are a couple of ways.
- There are new command-line options for sourcekit-lsp
sourcekit-lsp \
-completion-server-side-filtering=true \
-completion-max-results=200
- Alternatively, you can pass
initializationOptions
to SourceKit-LSP during theinitialize
request:
"initializationOptions": {
"completion": {
"serverSideFiltering": true,
"maxResults": 200
}
}
Note: In the initial implementation, setting maxResults
to unlimited (0 or nil) will have worse performance than if you disabled server-side filtering. We should be able to close that gap, but for now I don't recommend using large values for maxResults.
How does it work?
The speedup comes from limiting the number of results we return, saving unnecessary serialization. This affects both the internal serialization between sourcekitd and sourcekit-lsp (built-in binary format) and sourcekit-lsp's public api (json-rpc). For completions with tens of thousands of results, avoiding serializing several MB of JSON is a major savings, but even for smaller cases it can be a win.
In order to limit the number of results, we need the editor to re-query sourcekit-lsp for completions whenever the user types additional characters to filter the completions (we call this the "filter text"). In LSP, this is accomplished by setting the isIncomplete: true
flag on results, and the editor can then use triggerKind == triggerFromIncompleteCompletions
to tell sourcekit-lsp that it is in the re-filtering case.
When using server-side filtering, we keep track of a code-completion "session" for a given source file and completion location.
foo.barB|
^ ^
| ` completion location
` start of identifier
In the above example, the completion session will be for the location after "." at the start of the identifier, and the completion location (usually the cursor location) determines what the "filter text" for the completion will be, in this case "barB".
Once we open a session, any subsequent completion requests that have the same location and use triggerKind == triggerFromIncompleteCompletions
will efficiently re-filter the existing completions. If the new completion location is not compatible with the session, we return an error rather than recompute completions so that editors can rely on triggerFromIncompleteCompletions
always being fast. Currently, any other trigger kind (manual or trigger character like ".") will open a new completion session, although we could improve that in the future to reuse the session if no other changes have been made to the document.
The underlying filtering uses sourcekitd's codecomplete.open
, codecomplete.update
and codecomplete.close
requests that have been around for a while.
Future improvements
There is still some low-hanging fruit to speed up completion in sourcekitd, since the codecomplete.open
code path hasn't been as heavily optimized as the original codecomplete
request.
- Remove the experimental "grouped" completion support from sourcekitd's
codecomplete.open
code path. This code is not being used, and it slows down the implementation of sorting and filtering. - Migrate
codecomplete.open
to use the more efficient binary serialization format internally in sourcekitd to match what we do for the regularcodecomplete
request (this doesn't affect the json format in sourcekit-lsp). - Speed up repeated filtering: currently each "update" request re-filters from scratch, but in the common case where an update has fewer results than the previous one (going from "S" to "Str") we could do less work. Of course, since the filtering is fuzzy, we still need to account for low priority results getting higher priority in a subsequent update.