Add code fixes to LSP code actions
Change-Id: I5ec0932945cc6215cb671f89e8bed21609894ba2
Reviewed-on: https://dart-review.googlesource.com/c/87066
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Danny Tuppeny <dantup@google.com>
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_code_actions.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_code_actions.dart
index cb8ecd3..8fe44b1 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_code_actions.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_code_actions.dart
@@ -7,11 +7,18 @@
import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
import 'package:analysis_server/lsp_protocol/protocol_special.dart';
+import 'package:analysis_server/plugin/edit/fix/fix_core.dart';
import 'package:analysis_server/src/lsp/constants.dart';
import 'package:analysis_server/src/lsp/handlers/handlers.dart';
import 'package:analysis_server/src/lsp/lsp_analysis_server.dart';
import 'package:analysis_server/src/lsp/mapping.dart';
+import 'package:analysis_server/src/lsp/source_edits.dart';
+import 'package:analysis_server/src/protocol_server.dart' show SourceChange;
+import 'package:analysis_server/src/services/correction/fix.dart';
+import 'package:analysis_server/src/services/correction/fix_internal.dart';
import 'package:analyzer/dart/analysis/results.dart';
+import 'package:analyzer/dart/analysis/session.dart'
+ show InconsistentAnalysisException;
import 'package:analyzer/src/generated/engine.dart' show AnalysisEngine;
typedef ActionHandler = Future<List<Either2<Command, CodeAction>>> Function(
@@ -22,20 +29,6 @@
CodeActionHandler(LspAnalysisServer server) : super(server);
Method get handlesMessage => Method.textDocument_codeAction;
- /// Wraps a command in a CodeAction if the client supports it so that a
- /// CodeActionKind can be supplied.
- Either2<Command, CodeAction> commandOrCodeAction(
- bool clientSupportsLiteralCodeActions,
- CodeActionKind kind,
- Command command,
- ) {
- return clientSupportsLiteralCodeActions
- ? Either2<Command, CodeAction>.t2(
- new CodeAction(command.title, kind, null, null, command),
- )
- : Either2<Command, CodeAction>.t1(command);
- }
-
@override
CodeActionParams convertParams(Map<String, dynamic> json) =>
CodeActionParams.fromJson(json);
@@ -60,6 +53,42 @@
unit));
}
+ /// Wraps a command in a CodeAction if the client supports it so that a
+ /// CodeActionKind can be supplied.
+ Either2<Command, CodeAction> _commandOrCodeAction(
+ bool clientSupportsLiteralCodeActions,
+ CodeActionKind kind,
+ Command command,
+ ) {
+ return clientSupportsLiteralCodeActions
+ ? Either2<Command, CodeAction>.t2(
+ new CodeAction(command.title, kind, null, null, command),
+ )
+ : Either2<Command, CodeAction>.t1(command);
+ }
+
+ Either2<Command, CodeAction> _createFixAction(
+ Fix fix, Diagnostic diagnostic) {
+ return new Either2<Command, CodeAction>.t2(new CodeAction(
+ fix.change.message,
+ CodeActionKind.QuickFix,
+ [diagnostic],
+ _createWorkspaceEdit(fix.change),
+ null,
+ ));
+ }
+
+ WorkspaceEdit _createWorkspaceEdit(SourceChange change) {
+ return toWorkspaceEdit(
+ server.clientCapabilities?.workspace,
+ change.edits
+ .map((e) => new FileEditInformation(
+ server.getVersionedDocumentIdentifier(e.file),
+ server.getLineInfo(e.file),
+ e.edits))
+ .toList());
+ }
+
Future<List<Either2<Command, CodeAction>>> _getAssistActions(
HashSet<CodeActionKind> clientSupportedCodeActionKinds,
bool clientSupportsLiteralCodeActions,
@@ -104,8 +133,40 @@
Range range,
ResolvedUnitResult unit,
) async {
- // TODO(dantup): Implement fixes.
- return [];
+ // TODO(dantup): Is it acceptable not to support these for clients that can't
+ // handle Code Action literals? (Doing so requires we encode this into a
+ // command/arguments set and allow the client to call us back later).
+ if (!clientSupportsLiteralCodeActions) {
+ return const [];
+ }
+ // Keep trying until we run without getting an `InconsistentAnalysisException`.
+ while (true) {
+ final lineInfo = unit.lineInfo;
+ final codeActions = <Either2<Command, CodeAction>>[];
+ final fixContributor = new DartFixContributor();
+ try {
+ for (final error in unit.errors) {
+ // Server lineNumber is one-based so subtract one.
+ int errorLine = lineInfo.getLocation(error.offset).lineNumber - 1;
+ if (errorLine >= range.start.line && errorLine <= range.end.line) {
+ var context = new DartFixContextImpl(unit, error);
+ final fixes = await fixContributor.computeFixes(context);
+ if (fixes.isNotEmpty) {
+ fixes.sort(Fix.SORT_BY_RELEVANCE);
+
+ final diagnostic = toDiagnostic(lineInfo, error);
+ codeActions.addAll(
+ fixes.map((fix) => _createFixAction(fix, diagnostic)),
+ );
+ }
+ }
+ }
+
+ return codeActions;
+ } on InconsistentAnalysisException {
+ // Loop around to try again to compute the fixes.
+ }
+ }
}
Future<List<Either2<Command, CodeAction>>> _getRefactorActions(
@@ -141,12 +202,12 @@
}
return [
- commandOrCodeAction(
+ _commandOrCodeAction(
clientSupportsLiteralCodeActions,
DartCodeActionKind.SortMembers,
new Command('Sort Members', Commands.sortMembers, [path]),
),
- commandOrCodeAction(
+ _commandOrCodeAction(
clientSupportsLiteralCodeActions,
CodeActionKind.SourceOrganizeImports,
new Command('Organize Imports', Commands.organizeImports, [path]),
diff --git a/pkg/analysis_server/test/lsp/code_actions_abstract.dart b/pkg/analysis_server/test/lsp/code_actions_abstract.dart
new file mode 100644
index 0000000..64aea35
--- /dev/null
+++ b/pkg/analysis_server/test/lsp/code_actions_abstract.dart
@@ -0,0 +1,70 @@
+// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
+import 'package:analysis_server/lsp_protocol/protocol_special.dart';
+import 'package:test/test.dart';
+
+import 'server_abstract.dart';
+
+abstract class AbstractCodeActionsTest extends AbstractLspAnalysisServerTest {
+ Future<void> checkCodeActionAvailable(
+ Uri uri,
+ String command,
+ String title, {
+ bool asCodeActionLiteral = false,
+ bool asCommand = false,
+ }) async {
+ final codeActions = await getCodeActions(uri.toString());
+ final codeAction = findCommand(codeActions, command);
+ expect(codeAction, isNotNull);
+
+ codeAction.map(
+ (command) {
+ if (!asCommand) {
+ throw 'Got Command but expected CodeAction literal';
+ }
+ expect(command.title, equals(title));
+ expect(command.arguments, equals([uri.toFilePath()]));
+ },
+ (codeAction) {
+ if (!asCodeActionLiteral) {
+ throw 'Got CodeAction literal but expected Command';
+ }
+ expect(codeAction, isNotNull);
+ expect(codeAction.title, equals(title));
+ expect(codeAction.command.title, equals(title));
+ expect(codeAction.command.arguments, equals([uri.toFilePath()]));
+ },
+ );
+ }
+
+ Either2<Command, CodeAction> findCommand(
+ List<Either2<Command, CodeAction>> actions, String commandID) {
+ for (var codeAction in actions) {
+ final id = codeAction.map(
+ (cmd) => cmd.command, (action) => action.command.command);
+ if (id == commandID) {
+ return codeAction;
+ }
+ }
+ return null;
+ }
+
+ CodeAction findEditAction(List<Either2<Command, CodeAction>> actions,
+ CodeActionKind actionKind, String title) {
+ for (var codeAction in actions) {
+ final codeActionLiteral =
+ codeAction.map((cmd) => null, (action) => action);
+ if (codeActionLiteral?.kind == actionKind &&
+ codeActionLiteral?.title == title) {
+ // We're specifically looking for an action that contains an edit.
+ assert(codeActionLiteral.command == null);
+ assert(codeActionLiteral.edit != null);
+ return codeActionLiteral;
+ }
+ }
+ return null;
+ }
+}
diff --git a/pkg/analysis_server/test/lsp/code_actions_fixes_test.dart b/pkg/analysis_server/test/lsp/code_actions_fixes_test.dart
new file mode 100644
index 0000000..2f824c3
--- /dev/null
+++ b/pkg/analysis_server/test/lsp/code_actions_fixes_test.dart
@@ -0,0 +1,96 @@
+// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'code_actions_abstract.dart';
+
+main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(FixesCodeActionsTest);
+ });
+}
+
+@reflectiveTest
+class FixesCodeActionsTest extends AbstractCodeActionsTest {
+ test_appliesCorrectEdits_withDocumentChangesSupport() async {
+ // This code should get a fix to remove the unused import.
+ const content = '''
+ import 'dart:async';
+ [[import]] 'dart:convert';
+
+ Future foo;
+ ''';
+
+ const expectedContent = '''
+ import 'dart:async';
+
+ Future foo;
+ ''';
+ await newFile(mainFilePath, content: withoutMarkers(content));
+ await initialize(
+ textDocumentCapabilities: withCodeActionKinds(
+ emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
+ workspaceCapabilities:
+ withDocumentChangesSupport(emptyWorkspaceClientCapabilities),
+ );
+
+ final codeActions = await getCodeActions(mainFileUri.toString(),
+ range: rangeFromMarkers(content));
+ final fixAction = findEditAction(
+ codeActions, CodeActionKind.QuickFix, 'Remove unused import');
+
+ // Ensure the edit came back, and using documentChanges.
+ expect(fixAction, isNotNull);
+ expect(fixAction.edit.documentChanges, isNotNull);
+ expect(fixAction.edit.changes, isNull);
+
+ // Ensure applying the changes will give us the expected content.
+ final contents = {
+ mainFilePath: content,
+ };
+ applyDocumentChanges(contents, fixAction.edit.documentChanges);
+ expect(contents[mainFilePath], equals(expectedContent));
+ }
+
+ test_appliesCorrectEdits_withoutDocumentChangesSupport() async {
+ // This code should get a fix to remove the unused import.
+ const content = '''
+ import 'dart:async';
+ [[import]] 'dart:convert';
+
+ Future foo;
+ ''';
+
+ const expectedContent = '''
+ import 'dart:async';
+
+ Future foo;
+ ''';
+ await newFile(mainFilePath, content: withoutMarkers(content));
+ await initialize(
+ textDocumentCapabilities: withCodeActionKinds(
+ emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
+ );
+
+ final codeActions = await getCodeActions(mainFileUri.toString(),
+ range: rangeFromMarkers(content));
+ final fixAction = findEditAction(
+ codeActions, CodeActionKind.QuickFix, 'Remove unused import');
+
+ // Ensure the edit came back, and using changes.
+ expect(fixAction, isNotNull);
+ expect(fixAction.edit.changes, isNotNull);
+ expect(fixAction.edit.documentChanges, isNull);
+
+ // Ensure applying the changes will give us the expected content.
+ final contents = {
+ mainFilePath: content,
+ };
+ applyChanges(contents, fixAction.edit.changes);
+ expect(contents[mainFilePath], equals(expectedContent));
+ }
+}
diff --git a/pkg/analysis_server/test/lsp/code_actions_source_test.dart b/pkg/analysis_server/test/lsp/code_actions_source_test.dart
index 92e6671..e064771 100644
--- a/pkg/analysis_server/test/lsp/code_actions_source_test.dart
+++ b/pkg/analysis_server/test/lsp/code_actions_source_test.dart
@@ -3,13 +3,12 @@
// BSD-style license that can be found in the LICENSE file.
import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
-import 'package:analysis_server/lsp_protocol/protocol_special.dart';
import 'package:analysis_server/src/lsp/constants.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../tool/lsp_spec/matchers.dart';
-import 'server_abstract.dart';
+import 'code_actions_abstract.dart';
main() {
defineReflectiveSuite(() {
@@ -18,67 +17,6 @@
});
}
-abstract class AbstractCodeActionsTest extends AbstractLspAnalysisServerTest {
- Future<void> checkCodeActionAvailable(
- Uri uri,
- String command,
- String title, {
- bool asCodeActionLiteral = false,
- bool asCommand = false,
- }) async {
- final codeActions = await getCodeActions(uri.toString());
- final codeAction = findCommand(codeActions, command);
- expect(codeAction, isNotNull);
-
- codeAction.map(
- (command) {
- if (!asCommand) {
- throw 'Got Command but expected CodeAction literal';
- }
- expect(command.title, equals(title));
- expect(command.arguments, equals([uri.toFilePath()]));
- },
- (codeAction) {
- if (!asCodeActionLiteral) {
- throw 'Got CodeAction literal but expected Command';
- }
- expect(codeAction, isNotNull);
- expect(codeAction.title, equals(title));
- expect(codeAction.command.title, equals(title));
- expect(codeAction.command.arguments, equals([uri.toFilePath()]));
- },
- );
- }
-
- Either2<Command, CodeAction> findCommand(
- List<Either2<Command, CodeAction>> actions, String commandID) {
- for (var codeAction in actions) {
- final id = codeAction.map(
- (cmd) => cmd.command, (action) => action.command.command);
- if (id == commandID) {
- return codeAction;
- }
- }
- return null;
- }
-
- CodeAction findEditAction(List<Either2<Command, CodeAction>> actions,
- CodeActionKind actionKind, String title) {
- for (var codeAction in actions) {
- final codeActionLiteral =
- codeAction.map((cmd) => null, (action) => action);
- if (codeActionLiteral?.kind == actionKind &&
- codeActionLiteral?.title == title) {
- // We're specifically looking for an action that contains an edit.
- assert(codeActionLiteral.command == null);
- assert(codeActionLiteral.edit != null);
- return codeActionLiteral;
- }
- }
- return null;
- }
-}
-
@reflectiveTest
class OrganizeImportsSourceCodeActionsTest extends AbstractCodeActionsTest {
test_appliesCorrectEdits_withDocumentChangesSupport() async {
diff --git a/pkg/analysis_server/test/lsp/test_all.dart b/pkg/analysis_server/test/lsp/test_all.dart
index 998542f..daa1b01 100644
--- a/pkg/analysis_server/test/lsp/test_all.dart
+++ b/pkg/analysis_server/test/lsp/test_all.dart
@@ -16,6 +16,7 @@
import 'server_test.dart' as server_test;
import 'signature_help_test.dart' as signature_help_test;
import 'code_actions_source_test.dart' as source_code_action_tests;
+import 'code_actions_fixes_test.dart' as fixes_code_action_tests;
main() {
defineReflectiveSuite(() {
@@ -31,5 +32,6 @@
server_test.main();
signature_help_test.main();
source_code_action_tests.main();
+ fixes_code_action_tests.main();
}, name: 'lsp');
}