OLD | NEW |
(Empty) | |
| 1 # Creating `SourceChange`s |
| 2 |
| 3 Several of the response objects take a `SourceChange` (specifically, assists, |
| 4 fixes, and refactorings). Because `SourceChange` is a structured object that |
| 5 can be difficult to create correctly, this package provides a set of utility |
| 6 classes to help you build those structures. |
| 7 |
| 8 Using these classes will not only simplify the work you need to do to implement |
| 9 your plugin, but will ensure a consistent user experience in terms of the code |
| 10 being generated by the analysis server. |
| 11 |
| 12 ## `DartChangeBuilder` |
| 13 |
| 14 The class used to create a `SourceChange` is `DartChangeBuilder`, defined in |
| 15 `package:analyzer_plugin/utilities/change_builder/change_builder_dart.dart`. |
| 16 You can create a `DartChangeBuilder` with the following: |
| 17 |
| 18 ```dart |
| 19 DartChangeBuilder changeBuilder = new DartChangeBuilder(session); |
| 20 ``` |
| 21 |
| 22 The constructor required an instance of the class `AnalysisSession`. How you get |
| 23 the correct instance depends on where the constructor is being invoked. |
| 24 |
| 25 A `SourceChange` can contain edits that are to be applied to multiple files. The |
| 26 edits for a single file are created by invoking the method `addFileEdit`, as |
| 27 illustrated by the following: |
| 28 |
| 29 ```dart |
| 30 changeBuilder.addFileEdit(path, (DartFileEditBuilder fileEditBuilder) { |
| 31 // ... |
| 32 } |
| 33 ``` |
| 34 |
| 35 where the `path` is the path to the file to which the edits will be applied. |
| 36 |
| 37 ## `DartFileEditBuilder` |
| 38 |
| 39 The class `DartFileEditBuilder` defines methods for creating three kinds of |
| 40 edits: deletions, insertions, and replacements. |
| 41 |
| 42 For deletions, you pass in the range of code to be deleted as a `SourceRange`. |
| 43 In addition to the constructor for `SourceRange`, there are a set of functions |
| 44 defined in `package:analyzer_plugin/utilities/range_factory.dart` that can be |
| 45 used to build a `SourceRange` from tokens, AST nodes, and elements. |
| 46 |
| 47 For example, if you need to remove the text in a given `range`, you could write: |
| 48 |
| 49 ```dart |
| 50 fileEditBuilder.addDeletion(range); |
| 51 ``` |
| 52 |
| 53 In the case of insertions and replacements, there are two styles of method. The |
| 54 first takes the string that is to be inserted; the second takes a closure in |
| 55 which the string can be composed. Insertions take the offset of the insertion, |
| 56 while replacements take a `SourceRange` indicating the location of the text to |
| 57 be replaced. |
| 58 |
| 59 For example, if you need to insert `text` at offset `offset`, you could write |
| 60 |
| 61 ```dart |
| 62 fileEditBuilder.addSimpleInsertion(offset, text); |
| 63 ``` |
| 64 |
| 65 The forms that take a closure are useful primarily because they give you access |
| 66 to a `DartEditBuilder`, which is described below. |
| 67 |
| 68 For example, to replace a given `range` of text with some yet to be constructed |
| 69 text, you could write: |
| 70 |
| 71 ```dart |
| 72 fileEditBuilder.addReplacement(range, (DartEditBuilder editBuilder) { |
| 73 // ... |
| 74 } |
| 75 ``` |
| 76 |
| 77 In addition, `DartFileEditBuilder` has some methods that allow you to build some |
| 78 common sets of edits more easily. For example, `importLibraries` allows you to |
| 79 pass in the `Source`s for one or more libraries and will create one or more |
| 80 edits to insert `import` directives in the correct locations. |
| 81 |
| 82 ## `DartEditBuilder` |
| 83 |
| 84 A `DartEditBuilder` allows you to compose source code by writing the individual |
| 85 pieces, much like a `StringSink`. It also provides additional methods to compose |
| 86 more complex code. For example, if you need to write a type annotation, the |
| 87 method `writeType` will handle writing all of the type arguments and will add |
| 88 import directives as needed. There are also methods to write class declarations |
| 89 and to write various members within a class. |
| 90 |
| 91 For example, if you're implementing a quick assist to insert a template for a |
| 92 class declaration, the code to create the insertion edit could look like the |
| 93 following: |
| 94 |
| 95 ```dart |
| 96 String className = 'NewClass'; |
| 97 fileEditBuilder.addReplacement(range, (DartEditBuilder editBuilder) { |
| 98 editBuilder.writeClassDeclaration(className, memberWriter: () { |
| 99 editBuilder.writeConstructorDeclaration(className); |
| 100 editBuilder.writeOverrideOfInheritedMember( |
| 101 typeProvider.objectType.getMethod('toString')); |
| 102 }); |
| 103 }); |
| 104 ``` |
| 105 |
| 106 ## Linked Edits |
| 107 |
| 108 Many clients support a style of editing in which multiple regions of text can be |
| 109 edited simultaneously. Server refers to these as "linked" edit groups. Many |
| 110 clients also support having multiple groups associated with the edits in a file |
| 111 and allow users to tab from one group to the next. Essentially, these edit |
| 112 groups mark placeholders for text that users might want to change after the |
| 113 edits are applied. |
| 114 |
| 115 The class `DartEditBuilder` provides support for creating linked edits through |
| 116 the method `addLinkedEdit`. As with the insertion and replacement methods |
| 117 provided by `DartFileEditBuilder` (see above), there are both a "simple" and a |
| 118 closure-based version of this method. |
| 119 |
| 120 For example, if you're implementing a quick assist to insert a for loop, you |
| 121 should add the places where the loop variable name appears to a linked edit |
| 122 group. You should also add the name of the list being iterated over to a |
| 123 different group. The code to create the insertion edit could look like the |
| 124 following: |
| 125 |
| 126 ```dart |
| 127 fileEditBuilder.addReplacement(range, (DartEditBuilder editBuilder) { |
| 128 String listName = 'list'; |
| 129 String listGroup = 'list_variable'; |
| 130 String variableName = 'i'; |
| 131 String variableGroup = 'loop_variable'; |
| 132 |
| 133 editBuilder.write('for (int '); |
| 134 editBuilder.addSimpleLinkedEdit(variableGroup, variableName); |
| 135 editBuilder.write(' = 0; '); |
| 136 editBuilder.addSimpleLinkedEdit(variableGroup, variableName); |
| 137 editBuilder.write(' < '); |
| 138 editBuilder.addSimpleLinkedEdit(listGroup, listName); |
| 139 editBuilder.write('.length; '); |
| 140 editBuilder.addSimpleLinkedEdit(variableGroup, variableName); |
| 141 editBuilder.write('++) {}'); |
| 142 } |
| 143 ``` |
| 144 |
| 145 One of the advantages of the closure-based form of `addLinkedEdit` is that you |
| 146 can specify suggested replacements for the values of each group. You do that by |
| 147 invoking either `addSuggestion` or `addSuggestions`. In the example above, you |
| 148 might choose to suggest `j` and `k` as other likely loop variable names. You |
| 149 could do that by replacing one of the places where the variable name is written |
| 150 with code like the following: |
| 151 |
| 152 ```dart |
| 153 editBuilder.addLinkedEdit(variableGroup, (LinkedEditBuilder linkedEditBuilder) { |
| 154 linkedEditBuilder.write(variableName); |
| 155 linkedEditBuilder.addSuggestions(['j', 'k']); |
| 156 }); |
| 157 ``` |
| 158 |
| 159 A more interesting use of this feature would be to find the names of all of the |
| 160 list-valued variables within scope and suggest those names as alternatives for |
| 161 the name of the list. |
| 162 |
| 163 That said, most of the methods on `DartEditBuilder` that help you generate Dart |
| 164 code take one or more optional arguments that allow you to create linked edit |
| 165 groups for appropriate pieces of text and even to specify the suggestions for |
| 166 those groups. |
| 167 |
| 168 ## Post-edit Selection |
| 169 |
| 170 A `SourceChange` also allows you to specify where the cursor should be placed |
| 171 after the edits are applied. There are two ways to specify this. |
| 172 |
| 173 The first is by invoking the method `setSelection` on a `DartChangeBuilder`. |
| 174 The method takes a `Position`, which encapsulates an offset in a particular |
| 175 file. This can be difficult to get right because the offset is required to be |
| 176 the offset *after* all of the edits for that file have been applied. |
| 177 |
| 178 The second, and easier, way is by invoking the method `selectHere` on a |
| 179 `DartEditBuilder`. This method does not require any arguments; it computes the |
| 180 offset for the position based on the edits that have previously been created. |
| 181 It does require that all of the edits that apply to text before the desired |
| 182 cursor location have been created before the method is invoked. |
| 183 |
| 184 For example, if you're implementing a quick assist to insert a to-do comment at |
| 185 the cursor location, the code to create the insertion edit could look like the |
| 186 following: |
| 187 |
| 188 ```dart |
| 189 fileEditBuilder.addReplacement(range, (DartEditBuilder editBuilder) { |
| 190 editBuilder.write('/* TODO '); |
| 191 editBuilder.selectHere(); |
| 192 editBuilder.write(' */'); |
| 193 } |
| 194 ``` |
| 195 |
| 196 This will cause the cursor to be placed between the two spaces inside the |
| 197 comment. |
| 198 |
| 199 ## Non-Dart Files |
| 200 |
| 201 All of the classes above are subclasses of more general classes (just drop the |
| 202 prefix "Dart" from the subclass names). If you are editing files that do not |
| 203 contain Dart code, the more general classes might be a better choice. These |
| 204 classes are defined in |
| 205 `package:analyzer_plugin/utilities/change_builder/change_builder_core.dart`. |
OLD | NEW |