Initial documentation for the plugin package

R=devoncarew@google.com

Review-Url: https://codereview.chromium.org/2973753003 .
This commit is contained in:
Brian Wilkerson 2017-07-06 11:37:50 -07:00
parent 6323bedc58
commit 0484f28335
7 changed files with 595 additions and 0 deletions

View file

@ -4,6 +4,18 @@ A framework for building plugins for the analysis server.
## Usage
**Note:** The plugin support is not currently available for general use.
Plugins are written in Dart and are run in the same VM as the analysis server.
The analysis server runs each plugin in a separate isolate and communicates with
the plugin using a [plugin API][pluginapi]. This API is similar to the API used
by the analysis server to communicate with clients.
Plugins are automatically discovered and run by the analysis server.
This package contains support code to make it easier to write a plugin. There is
a [tutorial][tutorial] describing how to use the support in this package.
## Support
Post issues and feature requests on the [issue tracker][issues].
@ -19,3 +31,4 @@ See the [LICENSE] file.
[LICENSE]: https://github.com/dart-lang/sdk/blob/master/pkg/analyzer/LICENSE
[list]: https://groups.google.com/a/dartlang.org/forum/#!forum/analyzer-discuss
[pluginapi]: https://htmlpreview.github.io/?https://github.com/dart-lang/sdk/blob/master/pkg/analyzer_plugin/doc/api.html
[tutorial]: doc/tutorial/tutorial.md

View file

@ -0,0 +1,106 @@
# Providing Quick Assists
A quick assist is used by clients to provide a set of possible changes to code
that are based on the structure of the code. Quick assists are intended to help
users safely make local changes to code when those changes do not require any
user interaction. (Modifications that require interaction with users or that
touch multiple files are usually implemented as refactorings.)
For example, if the user has a function whose body consists of a single return
statement in a block, server will provide an assist to convert the function body
from a block to an expression (`=>`).
Assists have a priority associated with them. The priority allows the client to
display the assists that are most likely to be of use closer to the top of the
list when there are multiple assists available.
## Implementation details
When appropriate, the analysis server will send your plugin an `edit.getAssists`
request. The request includes the `file`, `offset` and `length` associated with
the selected region of code.
When an `edit.getAssists` request is received, the method `handleEditGetAssists`
will be invoked. This method is responsible for returning a response that
contains the available assists.
The easiest way to implement this method is by adding the classes `AssistsMixin`
and `DartAssistsMixin` (from `package:analyzer_plugin/plugin/assist_mixin.dart`)
to the list of mixins for your subclass of `ServerPlugin`. This will leave you
with one abstract method that you need to implement: `getAssistContributors`.
That method is responsible for returning a list of `AssistContributor`s. It is
the assist contributors that produce the actual assists. (Most plugins will only
need a single assist contributor.)
To write an assist contributor, create a class that implements
`AssistContributor`. The interface defines a single method named
`computeAssists`. The method has two arguments: an `AssistRequest` that
describes the location at which assists were requested and an `AssistCollector`
through which assists are to be added.
The class `AssistContributorMixin` defines a support method that makes it easier
to implement `computeAssists`.
## Example
Start by creating a class that implements `AssistContributor` and that mixes in
the class `AssistContributorMixin`, then implement the method `computeAssists`.
This method is typically implemented as a sequence of invocations of methods
that check to see whether a given assist is appropriate in the context of the
request
To learn about the support available for creating the edits, see
[Creating Edits][creatingEdits].
For example, your contributor might look something like the following:
```dart
class MyAssistContributor extends Object
with AssistContributorMixin
implements AssistContributor {
static AssistKind wrapInIf =
new AssistKind('wrapInIf', 100, "Wrap in an 'if' statement");
DartAssistRequest request;
AssistCollector collector;
AnalysisSession get session => request.result.session;
@override
void computeAssists(DartAssistRequest request, AssistCollector collector) {
this.request = request;
this.collector = collector;
_wrapInIf();
_wrapInWhile();
// ...
}
void _wrapInIf() {
ChangeBuilder builder = new DartChangeBuilder(session);
// TODO Build the edit to wrap the selection in a 'if' statement.
addAssist(wrapInIf, builder);
}
void _wrapInWhile() {
// ...
}
}
```
Given a contributor like the one above, you can implement your plugin similar to
the following:
```dart
class MyPlugin extends ServerPlugin with AssistsMixin, DartAssistsMixin {
// ...
@override
List<AssistContributor> getAssistContributors(
covariant AnalysisDriver driver) {
return <AssistContributor>[new MyAssistContributor()];
}
}
```
[creatingEdits]: creating_edits.md

View file

@ -0,0 +1,81 @@
# Providing Code Completions
A code completion is used by clients to provide a set of possible completions to
partially entered code. Completions are intended to address two use cases: to
help users enter code with less effort and to help users discover the behavior
of an object.
For example, if the user has typed `o.toSt` and then requested completions, one
suggestion might be `toString`.
That said, the completion suggestions that your plugin returns should include
all of the options that would be valid if the partial identifier did not exist.
The reason is that most clients are implemented such that they send a single
request for completions when the dialog with the user begins and cannot send any
subsequent requests. If the user presses the backspace key during the dialog the
client needs to have already received the expanded list of options that now
match the prefix (or all options if the prefix has completely been deleted).
Clients will filter the list of suggestions displayed as appropriate.
Hence, in the example above, plugins should return suggestions as if the user
had requested completions after typing `o.`;
## Implementation details
When appropriate, the analysis server will send your plugin a
`completion.getSuggestions` request. The request includes the `file` and
`offset` at which completions are being requested.
When a `completion.getSuggestions` request is received, the method
`handleCompletionGetSuggestions` will be invoked. This method is responsible for
returning a response that contains the available suggestions.
The easiest way to implement this method is by adding the classes
`CompletionMixin` and `DartCompletionMixin` (from
`package:analyzer_plugin/plugin/completion_mixin.dart`) to the list of mixins
for your subclass of `ServerPlugin`. This will leave you with one abstract
method that you need to implement: `getCompletionContributors`. That method is
responsible for returning a list of `CompletionContributor`s. It is the
completion contributors that produce the actual completion suggestions. (Most
plugins will only need a single completion contributor.)
To write a completion contributor, create a class that implements
`CompletionContributor`. The interface defines a single method named
`computeSuggestions`. The method has two arguments: a `CompletionRequest` that
describes the where completions are being requested and a `CompletionCollector`
through which suggestions are to be added.
## Example
Start by creating a class that implements `CompletionContributor`, then
implement the method `computeSuggestions`. Your contributor should invoke the
method `checkAborted`, defined on the `CompletionRequest` object, before
starting any slow work. This allows the computation of completion suggestions
to be preempted if the client no longer needs the results.
For example, your contributor might look something like the following:
```dart
class MyCompletionContributor implements CompletionContributor {
@override
Future<Null> computeSuggestions(covariant DartCompletionRequest request,
CompletionCollector collector) async {
// ...
}
}
```
Given a contributor like the one above, you can implement your plugin similar to
the following:
```dart
class MyPlugin extends ServerPlugin with CompletionMixin, DartCompletionMixin {
// ...
@override
List<CompletionContributor> getCompletionContributors(
covariant AnalysisDriverGeneric driver) {
return <CompletionContributor>[new MyCompletionContributor()];
}
}
```

View file

@ -0,0 +1,205 @@
# 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.
## `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:
```dart
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:
```dart
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:
```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 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:
```dart
String className = 'NewClass';
fileEditBuilder.addReplacement(range, (DartEditBuilder editBuilder) {
editBuilder.writeClassDeclaration(className, memberWriter: () {
editBuilder.writeConstructorDeclaration(className);
editBuilder.writeOverrideOfInheritedMember(
typeProvider.objectType.getMethod('toString'));
});
});
```
## 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 `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:
```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
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`.

View file

@ -0,0 +1,121 @@
# Providing Quick Fixes
A quick fix is used by clients to provide a set of possible changes to code that
are based on diagnostics reported against the code. Quick fixes are intended to
help users resolve the issue being reported.
If your plugin generates any diagnostics then you should consider providing
support for automatically fixing those diagnostics. There is often more than one
potential way of fixing a given problem, so it is possible for your plugin to
provide multiple fixes for a single problem.
For example, if an undefined identifier is used in the code, you might return
a fix to create an appropriate definition for the identifier. If there is a
similar identifier that is already defined, you might also return a second fix
to replace the undefined identifier with the defined identifier.
The latter example illustrates that fixes can be conditionally returned. You
will produce a better UX if only those fixes that actually make sense in the
given context are returned. If a lot of work is required to determine which
fixes make sense, it is possible to improve performance by generating different
diagnostics for the same issue, depending on the context in which the issue
occurs.
In addition, fixes have a priority associated with them. The priority allows the
client to display the fixes that are most likely to be of use closer to the top
of the list when there are multiple fixes available.
## Implementation details
When appropriate, the analysis server will send your plugin an `edit.getFixes`
request. The request includes the `file` and `offset` associated with the
diagnostics for which fixes should be generated. Fixes are typically produced
for all of the diagnostics on a given line of code. Your plugin should only
return fixes associated with the errors that it produced earlier.
When an `edit.getFixes` request is received, the method `handleEditGetFixes`
will be invoked. This method is responsible for returning a response that
contains the available fixes.
The easiest way to implement this method is by adding the classes `FixesMixin`
and `DartFixesMixin` (from `package:analyzer_plugin/plugin/fix_mixin.dart`) to
the list of mixins for your subclass of `ServerPlugin`. This will leave you with
one abstract method that you need to implement: `getFixContributors`. That
method is responsible for returning a list of `FixContributor`s. It is the fix
contributors that produce the actual fixes. (Most plugins will only need a
single fix contributor.)
To write a fix contributor, create a class that implements `FixContributor`. The
interface defines a single method named `computeFixes`. The method has two
arguments: a `FixesRequest` that describes the errors that should be fixed and a
`FixCollector` through which fixes are to be added. (If you use the mixins above
then the list of errors available through the request object will only include
the errors for which fixes should be returned.)
The class `FixContributorMixin` defines a simple implementation of this method
that captures the two arguments in fields, iterates through the errors, and
invokes a method named `computeFixesForError` for each of the errors for which
fixes are to be computed.
## Example
Start by creating a class that implements `FixContributor` and that mixes in the
class `FixContributorMixin`, then implement the method `computeFixesForError`.
This method is typically implemented by a series of `if` statements that test
the error code and invoke individual methods that compute the actual fixes to be
proposed. (In addition to keeping the method `computeFixesForError` shorter,
this also allows some fixes to be used for multiple error codes.)
To learn about the support available for creating the edits, see
[Creating Edits][creatingEdits].
For example, your contributor might look something like the following:
```dart
class MyFixContributor extends Object
with FixContributorMixin
implements FixContributor {
static FixKind defineComponent =
new FixKind('defineComponent', 100, "Define a component named {0}");
AnalysisSession get session => request.result.session;
@override
void computeFixesForError(AnalysisError error) {
ErrorCode code = error.errorCode;
if (code == MyErrorCode.undefinedComponent) {
_defineComponent(error);
_useExistingComponent(error);
}
}
void _defineComponent(AnalysisError error) {
// TODO Get the name from the source code.
String componentName = null;
ChangeBuilder builder = new DartChangeBuilder(session);
// TODO Build the edit to insert the definition of the component.
addFix(error, defineComponent, builder, args: [componentName]);
}
void _useExistingComponent(AnalysisError error) {
// ...
}
}
```
Given a contributor like the one above, you can implement your plugin similar to
the following:
```dart
class MyPlugin extends ServerPlugin with FixesMixin, DartFixesMixin {
// ...
@override
List<FixContributor> getFixContributors(
covariant AnalysisDriverGeneric driver) {
return <FixContributor>[new MyFixContributor()];
}
}
```
[creatingEdits]: creating_edits.md

View file

@ -0,0 +1,39 @@
# Getting Started
## Creating a Minimal Plugin
To implement a plugin, start by creating a simple package and create a class
that is a subclass of `ServerPlugin`. This class will need to implement a
constructor, three getters, and two methods. The getters provide some basic
information about your plugin: the name and version, both of which are included
in error messages if there is a problem encountered, and a list of glob patterns
for the files that the plugin cares about. The methods ...
Here's an example of what a minimal plugin might look like.
```dart
class MyPlugin extends ServerPlugin {
MyPlugin(ResourceProvider provider) : super(provider);
@override
List<String> get fileGlobsToAnalyze => <String>['**/*.dart'];
@override
String get name => 'My fantastic plugin';
@override
String get version => '1.0.0';
@override
AnalysisDriverGeneric createAnalysisDriver(ContextRoot contextRoot) {
// TODO: implement createAnalysisDriver
return null;
}
@override
void sendNotificationsForSubscriptions(
Map<String, List<AnalysisService>> subscriptions) {
// TODO: implement sendNotificationsForSubscriptions
}
}
```

View file

@ -0,0 +1,30 @@
# Building a Plugin
This is the introduction page to a set of pages that describe how to implement a
plugin. You should probably read the [Getting Started][gettingStarted] page
first, but there is no specific order for the remaining pages.
## Pages
The following is a list of the pages available in this tutorial.
[Getting Started][gettingStarted] -
How to write a minimal plugin.
[Creating Edits][creatingEdits] -
How to compose the edits used in assists, fixes, and refactorings.
[Providing Quick Assists][assists] -
How to provide quick assists.
[Providing Quick Fixes][fixes] -
How to provide quick fixes associated with errors.
[Providing Code Completions][completion] -
How to provide code completion suggestions.
[assists]: assists.md
[completion]: completion.md
[creatingEdits]: creating_edits.md
[fixes]: fixes.md
[gettingStarted]: getting_started.md