| # Creating `SourceChange`s |
| |
| Several of the response objects take a `SourceChange` (specifically, assists, |
| fixes, and refactorings). Because `SourceChange` is a structured object that |
| can be difficult to create correctly, this package provides a set of utility |
| classes to help you build those structures. |
| |
| Using these classes will not only simplify the work you need to do to implement |
| your plugin, but will ensure a consistent user experience in terms of the code |
| being generated by the analysis server. |
| |
| ## `ChangeBuilder` |
| |
| The class used to create a `SourceChange` is `ChangeBuilder`, defined in |
| `package:analyzer_plugin/utilities/change_builder/change_builder_core.dart`. |
| You can create a `ChangeBuilder` with the following: |
| |
| ```dart |
| ChangeBuilder changeBuilder = new ChangeBuilder(session: session); |
| ``` |
| |
| The constructor requires an instance of the class `AnalysisSession`. How you get |
| the correct instance depends on where the constructor is being invoked. |
| |
| A `SourceChange` can contain edits that are to be applied to multiple files. The |
| edits for a single file are created by invoking the method `addDartFileEdit`, as |
| illustrated by the following: |
| |
| ```dart |
| await changeBuilder.addDartFileEdit(path, |
| (DartFileEditBuilder fileEditBuilder) { |
| // ... |
| }); |
| ``` |
| |
| where the `path` is the path to the file to which the edits will be applied. |
| |
| ## `DartFileEditBuilder` |
| |
| The class `DartFileEditBuilder` defines methods for creating three kinds of |
| edits: deletions, insertions, and replacements. |
| |
| For deletions, you pass in the range of code to be deleted as a `SourceRange`. |
| In addition to the constructor for `SourceRange`, there are a set of functions |
| defined in `package:analyzer_plugin/utilities/range_factory.dart` that can be |
| used to build a `SourceRange` from tokens, AST nodes, and elements. |
| |
| For example, if you need to remove the text in a given `range`, you could write: |
| |
| ```dart |
| fileEditBuilder.addDeletion(range); |
| ``` |
| |
| In the case of insertions and replacements, there are two styles of method. The |
| first takes the string that is to be inserted; the second takes a closure in |
| which the string can be composed. Insertions take the offset of the insertion, |
| while replacements take a `SourceRange` indicating the location of the text to |
| be replaced. |
| |
| For example, if you need to insert `text` at offset `offset`, you could write |
| |
| ```dart |
| fileEditBuilder.addSimpleInsertion(offset, text); |
| ``` |
| |
| The forms that take a closure are useful primarily because they give you access |
| to a `DartEditBuilder`, which is described below. |
| |
| For example, to replace a given `range` of text with some yet to be constructed |
| text, you could write: |
| |
| ```dart |
| fileEditBuilder.addReplacement(range, (DartEditBuilder editBuilder) { |
| // ... |
| } |
| ``` |
| |
| In addition, `DartFileEditBuilder` has some methods that allow you to build some |
| common sets of edits more easily. For example, `importLibraries` allows you to |
| pass in the `Source`s for one or more libraries and will create one or more |
| edits to insert `import` directives in the correct locations. |
| |
| ## `DartEditBuilder` |
| |
| A `DartEditBuilder` allows you to compose source code by writing the individual |
| pieces, much like a `StringSink`. It also provides additional methods to compose |
| more complex code. For example, if you need to write a type annotation, the |
| method `writeType` will handle writing all the type arguments and will add |
| import directives as needed. There are also methods to write class declarations |
| and to write various members within a class. |
| |
| For example, if you're implementing a quick assist to insert a template for a |
| class declaration, the code to create the insertion edit could look like the |
| following: |
| |
| ```dart |
| String className = 'NewClass'; |
| fileEditBuilder.addReplacement(range, (DartEditBuilder editBuilder) { |
| editBuilder.writeClassDeclaration(className, memberWriter: () { |
| editBuilder.writeConstructorDeclaration(className); |
| editBuilder.writeOverride( |
| typeProvider.objectType.getMethod('toString').type); |
| }); |
| }); |
| ``` |
| |
| ## Utility methods |
| |
| As was mentioned briefly above, many of these classes provide utility methods. |
| For the best UX, it's important to use these utility method when possible. They |
| often do more than just write some simple text; they can take care of other |
| details, such as adding required imports, and adherence to style preferences |
| that the user has expressed (typically by enabling lints). |
| |
| ## Linked edits |
| |
| Many clients support a style of editing in which multiple regions of text can be |
| edited simultaneously. Server refers to these as "linked" edit groups. Many |
| clients also support having multiple groups associated with the edits in a file |
| and allow users to tab from one group to the next. Essentially, these edit |
| groups mark placeholders for text that users might want to change after the |
| edits are applied. |
| |
| The class `DartEditBuilder` provides support for creating linked edits through |
| the method `addLinkedEdit`. As with the insertion and replacement methods |
| provided by `DartFileEditBuilder` (see above), there are both a "simple" and a |
| closure-based version of this method. |
| |
| For example, if you're implementing a quick assist to insert a for loop, you |
| should add the places where the loop variable name appears to a linked edit |
| group. You should also add the name of the list being iterated over to a |
| different group. The code to create the insertion edit could look like the |
| following: |
| |
| ```dart |
| fileEditBuilder.addReplacement(range, (DartEditBuilder editBuilder) { |
| String listName = 'list'; |
| String listGroup = 'list_variable'; |
| String variableName = 'i'; |
| String variableGroup = 'loop_variable'; |
| |
| editBuilder.write('for (int '); |
| editBuilder.addSimpleLinkedEdit(variableGroup, variableName); |
| editBuilder.write(' = 0; '); |
| editBuilder.addSimpleLinkedEdit(variableGroup, variableName); |
| editBuilder.write(' < '); |
| editBuilder.addSimpleLinkedEdit(listGroup, listName); |
| editBuilder.write('.length; '); |
| editBuilder.addSimpleLinkedEdit(variableGroup, variableName); |
| editBuilder.write('++) {}'); |
| } |
| ``` |
| |
| One of the advantages of the closure-based form of `addLinkedEdit` is that you |
| can specify suggested replacements for the values of each group. You do that by |
| invoking either `addSuggestion` or `addSuggestions`. In the example above, you |
| might choose to suggest `j` and `k` as other likely loop variable names. You |
| could do that by replacing one of the places where the variable name is written |
| with code like the following: |
| |
| ```dart |
| editBuilder.addLinkedEdit(variableGroup, (LinkedEditBuilder linkedEditBuilder) { |
| linkedEditBuilder.write(variableName); |
| linkedEditBuilder.addSuggestions(['j', 'k']); |
| }); |
| ``` |
| |
| A more interesting use of this feature would be to find the names of all of the |
| list-valued variables within scope and suggest those names as alternatives for |
| the name of the list. |
| |
| That said, most of the methods on `DartEditBuilder` that help you generate Dart |
| code take one or more optional arguments that allow you to create linked edit |
| groups for appropriate pieces of text and even to specify the suggestions for |
| those groups. |
| |
| ## Post-edit selection |
| |
| A `SourceChange` also allows you to specify where the cursor should be placed |
| after the edits are applied. There are two ways to specify this. |
| |
| The first is by invoking the method `setSelection` on a `ChangeBuilder`. |
| The method takes a `Position`, which encapsulates an offset in a particular |
| file. This can be difficult to get right because the offset is required to be |
| the offset *after* all the edits for the file have been applied. |
| |
| The second, and easier, way is by invoking the method `selectHere` on a |
| `DartEditBuilder`. This method does not require any arguments; it computes the |
| offset for the position based on the edits that have previously been created. |
| It does require that all the edits that apply to text before the desired |
| cursor location have been created before the method is invoked. |
| |
| For example, if you're implementing a quick assist to insert a to-do comment at |
| the cursor location, the code to create the insertion edit could look like the |
| following: |
| |
| ```dart |
| fileEditBuilder.addReplacement(range, (DartEditBuilder editBuilder) { |
| editBuilder.write('/* TODO '); |
| editBuilder.selectHere(); |
| editBuilder.write(' */'); |
| } |
| ``` |
| |
| This will cause the cursor to be placed between the two spaces inside the |
| comment. |
| |
| ## Non-dart files |
| |
| The classes above are subclasses of more general classes (just drop the |
| prefix "Dart" from the subclass names). If you are editing files that do not |
| contain Dart code, the more general classes might be a better choice. These |
| classes are defined in |
| `package:analyzer_plugin/utilities/change_builder/change_builder_core.dart`. |