| # 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`. |