Code completion

This document describes how code completion is implemented and how to fix bugs and extend it.

Basic operation

Code completion begins with the receipt of either a completion.getSuggestions2 request (from the legacy protocol) or a textDocument/completion request (from LSP). When the request is received the appropriate handler is invoked. The handler will compute a list of completion suggestions and will then translate the suggestions into the form required by the protocol.

Code completion is supported in .dart files as well as in the pubspec.yaml, analysis_options.yaml, and fix_data.yaml files.

Dart completion suggestions are computed using the DartCompletionManager by invoking the method computeFinalizedCandidateSuggestions.

Completion passes

The completion manager computes suggestions in two “passes”.

  • The InScopeCompletionPass computes suggestions for every appropriate element whose name is visible in the name scope surrounding the completion location.

  • The NotImportedCompletionPass computes suggestions for elements that are not yet visible in the name scope surrounding the completion location. This pass is skipped if there isn‘t time left in the budget or if there are already enough suggestions that the not-yet-imported elements wouldn’t be sent anyway.

The collector

Both passes add CandidateSuggestions to a SuggestionCollector. The collector keeps a list of candidates and uses an insertion sort to keep the list sorted. The sorting is done based on how well the suggestion matches the completion prefix (as computed by the FuzzyMatcher).

For performance reasons, there is a limit to the number of suggestions that are passed back to the client. When there are more candidates in the list than what will be sent back, then any candidates whose match score is less than the score of any candidates within the window will be dropped from the list.

Ranking

After the list of candidates has been computed, the candidates are ranked by the RelevanceComputer. They are then re-sorted based on the relevance and truncated to the maximum number of suggestions to be returned.

The relevance score is computed as follows. First, the FeatureComputer is used to measure certain features related to both the completion location and the suggestion. The features include such things as whether the type of the suggestion matches the context type, or how far from the completion location a local variable declaration is. Each feature is represented as a double between -1.0 and 1.0 inclusive. The feature values are combined using a weighted average and then adjusted to be between 0 and 1000 inclusive.

The set of features used are selected based on a statistical analysis of representative Dart code. It is easy to find specific cases where the existing ranking algorithm does a poor job, but the requirement is that the ranking algorithm must be optimized across all use cases, not just one use case. This sometimes results in relevance scores that seem counter-intuitive. When that happens, one possible path to explore is to add a new feature to the mix.

Changes to either the set of features or the computation of a specific feature should only be done if a statistical analysis indicates that the change will produce a better overall ranking (as measured by the MRR of the suggestion compared to the actual selection). The tool in analysis_server/tool/code_completion/completion_metrics.dart can be used to compare the quality of one or more experiments compared to the current implementation.

Maintaining and improving

The easiest way to fix bugs or add support for new features is to start by writing a test in the directory test/services/completion/dart/location. These tests are grouped based on the location in the grammar at which completion is being requested.

When you have a test that‘s failing, add a breakpoint to InScopeCompletionPass.computeSuggestions. When the debugger stops at the breakpoint, hover over _completionNode to see what kind of node will be visited. Add a breakpoint in the corresponding visit method and resume execution. (If the visit method doesn’t already exist, then add it and restart the debugger).

New language features

If a new language feature is being introduced that adds new syntax, then code completion support will need to be updated.

If the changes are limited to updating an existing node then you should be able to use the method above to update the corresponding visit method. If the changes required the addition of some new subclasses of AstNode, then you'll likely need to add a new visit method for the added nodes.

If the changes introduce a new kind of element then you might need to add a new subclass of CandidateSuggestion and update the DeclarationHelper to produce the suggestion under the appropriate conditions.