Creating SourceChanges

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.

DartChangeBuilder

The class used to create a SourceChange is DartChangeBuilder, defined in package:analyzer_plugin/utilities/change_builder/change_builder_dart.dart. You can create a DartChangeBuilder with the following:

DartChangeBuilder changeBuilder = new DartChangeBuilder(session);

The constructor required 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 addFileEdit, as illustrated by the following:

changeBuilder.addFileEdit(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:

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 Sources 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 of 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);
  });
});

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:

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.

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 DartChangeBuilder. 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 of the edits for that 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 of 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.

Non-Dart Files

All of 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.