| # Writing a quick assist |
| |
| This document describes what a quick assist is and outlines the basic steps for |
| writing a quick assist. |
| |
| ## Overview |
| |
| A quick assist is an automated [code edit](code_edits.md) that is both |
| [local in scope](code_edits.md#scope) and doesn't require any user input. If a |
| code edit requires user input, might depend on knowledge from outside the local |
| library, or could make changes outside the local library, then it should be |
| implemented using a refactoring. |
| |
| Unlike quick fixes, quick assists are displayed even when there are no |
| diagnostics being reported against the code. They are not intended to fix |
| problems that are being reported, but are used to perform common and |
| straightforward code transformations. |
| |
| In this document we'll use a simple example of writing an assist to convert a |
| decimal representation of an integer into a hexadecimal representation. While |
| this wouldn't be a good assist to offer, it's simple enough that the details of |
| the implementation won't mask the general process used to implement an assist. |
| |
| ## Design considerations |
| |
| Because quick assists are computed on demand, they need to be computed quickly. |
| (Some clients request quick assists every time the user repositions the cursor.) |
| That places a performance requirement on quick assists, one that requires that |
| the code to compute a quick assist can't perform any potentially lengthy |
| computations such as searching all of the user's code or accessing the network. |
| That, in turn, generally means that assists can only support localized changes. |
| They can add or remove text in the local library, but generally can't do more |
| than that. |
| |
| Unlike quick fixes, there is no signal to indicate which assists might apply at |
| a given location in the code. That means that we have to test every assist to |
| see whether it's appropriate, which puts a practical limit on the number of |
| assists that we can implement. (Even if each assist only takes 100 milliseconds |
| to determine whether it applies, if we have 100 assists it will take 10 seconds |
| to return the list of assists to the user, which is too slow.) That means that |
| we need to be discerning about which assists are implemented and work to make |
| assists return quickly if they are not appropriate. |
| |
| ## Describing the assist |
| |
| Each assist has an instance of the class `AssistKind` associated with it. The |
| existing assists for Dart are defined in the class `DartAssistKind`. An |
| assist kind has an identifier, a priority, and a message. |
| |
| The identifier is used by some LSP-based clients to provide user-defined |
| shortcuts. It's a hierarchical dot-separated identifier and should follow the |
| pattern seen in the existing assist kinds. |
| |
| The priority is used to order the list of assists when presented to the user. |
| The larger the value the closer to the top of the list it will appear. If you're |
| implementing an assist for Dart files, you should use one of the constants |
| defined in `DartAssistKindPriority` (typically |
| `DartAssistKindPriority.DEFAULT`), or add a new constant if there's a need for |
| it. |
| |
| The message is what will be displayed to the user by the client. This should be |
| a sentence fragment (no terminating period) that would be appropriate as a label |
| in a menu or on a button. It should describe the change that will be made to the |
| user's code. |
| |
| Create a static field whose value is an `AssistKind` describing the assist |
| you're implementing. For our example you might define a new constant in |
| `DartAssistKind` like this: |
| |
| ```dart |
| static const CONVERT_TO_HEX = AssistKind( |
| 'dart.assist.convert.toHex', |
| DartAssistKindPriority.DEFAULT, |
| "Convert to hexadecimal", |
| ); |
| ``` |
| |
| ## Implementing the assist, part 1 |
| |
| To implement the assist you'll create a subclass of `CorrectionProducer`. Most |
| of the existing correction producers are in the directory |
| `analysis_server/lib/src/services/correction/dart`, so we'll start by creating |
| a file named `convert_to_hex.dart` in that directory that contains the following |
| (with the year updated appropriately): |
| |
| ```dart |
| // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'package:analysis_server/src/services/correction/assist.dart'; |
| import 'package:analysis_server/src/services/correction/dart/abstract_producer.dart'; |
| import 'package:analyzer/dart/ast/ast.dart'; |
| import 'package:analyzer_plugin/utilities/assist/assist.dart'; |
| import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; |
| import 'package:analyzer_plugin/utilities/range_factory.dart'; |
| |
| class ConvertToHex extends CorrectionProducer { |
| @override |
| AssistKind get assistKind => DartAssistKind.CONVERT_TO_HEX; |
| |
| @override |
| Future<void> compute(ChangeBuilder builder) async { |
| } |
| } |
| ``` |
| |
| The `compute` method is where the assist will be built. We'll come back to it in |
| "Implementing the assist, part 2". |
| |
| The `assistKind` getter is how you associate the assist kind we created earlier |
| with the assist produced by the `compute` method. |
| |
| There's another getter you might need to override. The message associated with |
| the assist kind is actually a template that can be filled in at runtime. The |
| placeholders in the message are denoted by integers inside curly braces (such as |
| `{0}`). The integers are indexes into a list of replacement values (hence, zero |
| based), and the getter `assistArguments` returns the list of replacement values. |
| The message we used above doesn't have any placeholders, but if we'd written the |
| message as `"Convert to '{0}'"`, then we could return the replacement values by |
| implementing: |
| |
| ```dart |
| String _hexRepresentation = ''; |
| |
| @override |
| List<Object> get assistArguments => [_hexRepresentation]; |
| ``` |
| |
| and assigning the replacement string to the field inside the `compute` method. |
| |
| If you don't implement this getter, then the inherited getter will return an |
| empty list. The number of elements in the list must match the largest index used |
| in the message. If it doesn't, an exception will be thrown at runtime. |
| |
| ## Testing the assist |
| |
| Before we look at implementing the `compute` method, we should probably write |
| some tests. Even if you don't normally use a test-driven approach to coding, we |
| recommend it in this case because writing the tests can help you think of corner |
| cases that the implementation will need to handle. The corresponding tests are |
| in the directory `analysis_server/test/src/services/correction/assist`, so we'll |
| create a file named `convert_to_hex_test.dart` that contains the following: |
| |
| ```dart |
| // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'package:analysis_server/src/services/correction/assist.dart'; |
| import 'package:analyzer_plugin/utilities/assist/assist.dart'; |
| import 'package:test_reflective_loader/test_reflective_loader.dart'; |
| |
| import 'assist_processor.dart'; |
| |
| void main() { |
| defineReflectiveSuite(() { |
| defineReflectiveTests(ConvertToHexTest); |
| }); |
| } |
| |
| @reflectiveTest |
| class ConvertToHexTest extends AssistProcessorTest { |
| @override |
| AssistKind get kind => DartAssistKind.CONVERT_TO_HEX; |
| } |
| ``` |
| |
| This getter tells the test framework to expect an assist of the returned kind. |
| |
| The test can then be written in a method that looks something like this: |
| |
| ```dart |
| Future<void> test_positive() async { |
| await resolveTestCode(''' |
| var c = ^42; |
| '''); |
| await assertHasAssist(''' |
| var c = 0x2a; |
| '''); |
| } |
| ``` |
| |
| The test framework will look for the marker `^`, remember it's offset, |
| create a file containing the first piece of code with the marker removed, run |
| the assist over the code, use our correction producer to build an edit, apply |
| the edit to the file, and textually compare the results with the second piece of |
| code. |
| |
| ## Registering the assist |
| |
| Before we can run the test, we need to register the correction producer so that |
| it can be run. |
| |
| The list of assists is computed by the `AssistProcessor`, which has two static |
| lists named `generators` and `multiGenerators` that contain the correction |
| producers used to compute the assists. |
| |
| Actually, the lists contain functions used to create the producers. We do that |
| so that producers can't accidentally carry state over from one use to the next. |
| These functions are usually a tear-off of the correction producer's constructor. |
| |
| The last step is to add your correction producer to the list named `generators`. |
| We'll talk about the other list in "Multi-assist producers". |
| |
| At this point you should be able to run the test and see it failing. |
| |
| ## Implementing the assist, part 2 |
| |
| We're now at a point where we can finish the implementation of the assist by |
| implementing the `compute` method. |
| |
| The correction producer has access to most of the information you should need in |
| order to write the assist. The change builder passed to `compute` is how you |
| construct the edit that will be sent back to the client. |
| |
| The first step in the implementation of any assist is to find the location in |
| the AST where the cursor is positioned and verify that all the conditions on |
| which the assist is predicated are valid. For example, for our assist we'll need |
| to ensure that the cursor is on an integer literal, that the literal consists of |
| decimal digits, and that the value is a valid integer. |
| |
| Finding the AST node is easy because it's done for you by the assist processor |
| before `compute` is invoked. All you have to do is use the getter `node` to find |
| the node at the cursor. |
| |
| ```dart |
| @override |
| Future<void> compute(ChangeBuilder builder) async { |
| final node = this.node; |
| } |
| ``` |
| |
| Then we need to verify that this node is an integer literal: |
| |
| ```dart |
| @override |
| Future<void> compute(ChangeBuilder builder) async { |
| final node = this.node; |
| if (node is! IntegerLiteral) { |
| return; |
| } |
| } |
| ``` |
| |
| If it isn't, then we'll return. Because we haven't used the builder to create a |
| assist, returning now means that no assist from this producer will be sent to |
| the client. |
| |
| We'll also check that the integer has the right form and is valid: |
| |
| ```dart |
| @override |
| Future<void> compute(ChangeBuilder builder) async { |
| final node = this.node; |
| if (node is! IntegerLiteral) { |
| return; |
| } |
| var value = node.value; |
| if (value == null) { |
| return; |
| } |
| if (node.literal.lexeme.contains(RegExp('[^0-9]'))) { |
| return; |
| } |
| } |
| ``` |
| |
| After all those checks we now know that we have a decimal integer that we can |
| convert. Note that we check for a `null` value before checking for non-decimal |
| digits because it's a faster check and we want the assist to fail as quickly as |
| possible. |
| |
| We're now ready to create the edit. To do that we're going to use the |
| `ChangeBuilder` passed to the `compute` method. In the example below we'll |
| introduce a couple of the methods on `ChangeBuilder`, but for more information |
| you can read [Creating `SourceChange`s](https://github.com/dart-lang/sdk/blob/main/pkg/analyzer_plugin/doc/tutorial/creating_edits.md). |
| |
| ```dart |
| @override |
| Future<void> compute(ChangeBuilder builder) async { |
| final node = this.node; |
| if (node is! IntegerLiteral) { |
| return; |
| } |
| var value = node.value; |
| if (value == null) { |
| return; |
| } |
| if (node.literal.lexeme.contains(RegExp('[^0-9]'))) { |
| return; |
| } |
| await builder.addDartFileEdit(file, (builder) { |
| var hexDigits = value.toRadixString(16); |
| builder.addSimpleReplacement(range.node(node), '0x$hexDigits'); |
| }); |
| } |
| ``` |
| |
| We're using `addDartFileEdit` to create an edit in a `.dart` file. In this case |
| the edit is simple: we're just replacing one representation of the integer |
| literal with a representation of the same value in a different base. The getter |
| `range` returns a `RangeFactory`, a utility class with lots of methods to make |
| it easier to create `SourceRange`s. |
| |
| We're missing several test cases. Minimally we should test that the assist works |
| correctly with negative values (it doesn't), and that it will not produce an |
| edit if the integer literal isn't valid. We'll leave adding such tests as an |
| exercise for the reader. |
| |
| In this example we're just making a simple replacement, so we're avoiding any |
| need to worry about formatting. As a general principle we don't attempt to |
| format the code after it's been modified, but we do make an effort to leave the |
| code in a reasonably readable state. There's a getter (`eol`) that you can use |
| to get the end-of-line marker that should be used in the file, and there's |
| another getter (`utils`) that will return an object with several utility methods |
| that help with things like getting the right indentation for nested code. |
| |
| ## Multi-assist producers |
| |
| We skipped over the list named `multiGenerators` earlier, promising that we'd |
| return to it later. You'll probably never have a need for it, but in case you do |
| this section will hopefully tell you what you need to know. |
| |
| There's a subclass of `CorrectionProducer` named `MultiCorrectionProducer` and |
| this list is how you register one of them. That class exists for rare cases |
| where you need to use a single correction producer to produce multiple assists. |
| This is generally only needed when you can't know in advance the maximum number |
| of assists that might need to be produced. For example, there's a set of assists |
| to wrap a Flutter `Widget` in another widget, but the set of widgets that can |
| wrap a given widget depends on the widget being wrapped. |
| |
| If you are able to enumerate the possible assists ahead of time, then you're |
| probably better off to create one subclass of `CorrectionProducer` for each of |
| the assists. |