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.
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:
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:
await changeBuilder.addDartFileEdit(path,
(DartFileEditBuilder fileEditBuilder) {
// ...
});
where the path
is the path to the file to which the edits will be applied.
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:
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
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:
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.
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:
String className = 'NewClass';
fileEditBuilder.addReplacement(range, (DartEditBuilder editBuilder) {
editBuilder.writeClassDeclaration(className, memberWriter: () {
editBuilder.writeConstructorDeclaration(className);
editBuilder.writeOverride(
typeProvider.objectType.getMethod('toString').type);
});
});
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).
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:
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:
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.
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:
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.
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
.