blob: 29a70ea2e0f54c2feb171970a4e742f045632f3a [file] [log] [blame]
// 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 'dart:async';
import 'dart:collection';
import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
import 'package:analysis_server/lsp_protocol/protocol_special.dart';
import 'package:analysis_server/plugin/edit/assist/assist_core.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/protocol_server.dart';
import 'package:analysis_server/src/services/correction/assist.dart';
import 'package:analysis_server/src/services/correction/assist_internal.dart';
import 'package:analysis_server/src/services/correction/change_workspace.dart';
import 'package:analysis_server/src/services/correction/fix.dart';
import 'package:analysis_server/src/services/correction/fix/dart/top_level_declarations.dart';
import 'package:analysis_server/src/services/correction/fix_internal.dart';
import 'package:analysis_server/src/services/refactoring/refactoring.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;
class CodeActionHandler extends MessageHandler<CodeActionParams,
List<Either2<Command, CodeAction>>> {
CodeActionHandler(LspAnalysisServer server) : super(server);
Method get handlesMessage => Method.textDocument_codeAction;
@override
LspJsonHandler<CodeActionParams> get jsonHandler =>
CodeActionParams.jsonHandler;
Future<ErrorOr<List<Either2<Command, CodeAction>>>> handle(
CodeActionParams params, CancellationToken token) async {
if (!isDartDocument(params.textDocument)) {
return success(const []);
}
final capabilities = server?.clientCapabilities?.textDocument?.codeAction;
final clientSupportsWorkspaceApplyEdit =
server?.clientCapabilities?.workspace?.applyEdit == true;
final clientSupportsLiteralCodeActions =
capabilities?.codeActionLiteralSupport != null;
final clientSupportedCodeActionKinds = new HashSet<CodeActionKind>.of(
capabilities?.codeActionLiteralSupport?.codeActionKind?.valueSet ?? []);
final path = pathOfDoc(params.textDocument);
final unit = await path.mapResult(requireResolvedUnit);
return unit.mapResult((unit) {
final startOffset = toOffset(unit.lineInfo, params.range.start);
final endOffset = toOffset(unit.lineInfo, params.range.end);
return startOffset.mapResult((startOffset) {
return endOffset.mapResult((endOffset) {
final offset = startOffset;
final length = endOffset - startOffset;
return _getCodeActions(
clientSupportedCodeActionKinds,
clientSupportsLiteralCodeActions,
clientSupportsWorkspaceApplyEdit,
path.result,
params.range,
offset,
length,
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);
}
/// Creates a CodeAction to apply this assist. Note: This code will fetch the
/// version of each document being modified so it's important to call this
/// immediately after computing edits to ensure the document is not modified
/// before the version number is read.
Either2<Command, CodeAction> _createAssistAction(Assist assist) {
return new Either2<Command, CodeAction>.t2(new CodeAction(
assist.change.message,
CodeActionKind.Refactor,
const [],
createWorkspaceEdit(server, assist.change.edits),
null,
));
}
/// Creates a CodeAction to apply this fix. Note: This code will fetch the
/// version of each document being modified so it's important to call this
/// immediately after computing edits to ensure the document is not modified
/// before the version number is read.
Either2<Command, CodeAction> _createFixAction(
Fix fix, Diagnostic diagnostic) {
return new Either2<Command, CodeAction>.t2(new CodeAction(
fix.change.message,
CodeActionKind.QuickFix,
[diagnostic],
createWorkspaceEdit(server, fix.change.edits),
null,
));
}
Future<List<Either2<Command, CodeAction>>> _getAssistActions(
HashSet<CodeActionKind> clientSupportedCodeActionKinds,
bool clientSupportsLiteralCodeActions,
int offset,
int length,
ResolvedUnitResult unit,
) async {
// We only support these for clients that advertise codeActionLiteralSupport.
if (!clientSupportsLiteralCodeActions ||
!clientSupportedCodeActionKinds.contains(CodeActionKind.Refactor)) {
return const [];
}
try {
var context = new DartAssistContextImpl(
DartChangeWorkspace(server.currentSessions),
unit,
offset,
length,
);
final processor = new AssistProcessor(context);
final assists = await processor.compute();
assists.sort(Assist.SORT_BY_RELEVANCE);
return assists.map(_createAssistAction).toList();
} on InconsistentAnalysisException {
// If an InconsistentAnalysisException occurs, it's likely the user modified
// the source and therefore is no longer interested in the results, so
// just return an empty set.
return [];
}
}
Future<ErrorOr<List<Either2<Command, CodeAction>>>> _getCodeActions(
HashSet<CodeActionKind> kinds,
bool supportsLiterals,
bool supportsWorkspaceApplyEdit,
String path,
Range range,
int offset,
int length,
ResolvedUnitResult unit,
) async {
final results = await Future.wait([
_getSourceActions(
kinds, supportsLiterals, supportsWorkspaceApplyEdit, path),
_getAssistActions(kinds, supportsLiterals, offset, length, unit),
_getRefactorActions(kinds, supportsLiterals, path, offset, length, unit),
_getFixActions(kinds, supportsLiterals, range, unit),
]);
final flatResults = results.expand((x) => x).toList();
return success(flatResults);
}
Future<List<Either2<Command, CodeAction>>> _getFixActions(
HashSet<CodeActionKind> clientSupportedCodeActionKinds,
bool clientSupportsLiteralCodeActions,
Range range,
ResolvedUnitResult unit,
) async {
// We only support these for clients that advertise codeActionLiteralSupport.
if (!clientSupportsLiteralCodeActions ||
!clientSupportedCodeActionKinds.contains(CodeActionKind.QuickFix)) {
return const [];
}
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 workspace = DartChangeWorkspace(server.currentSessions);
var context = new DartFixContextImpl(workspace, unit, error, (name) {
var tracker = server.declarationsTracker;
return TopLevelDeclarationsProvider(tracker).get(
unit.session.analysisContext,
unit.path,
name,
);
});
final fixes = await fixContributor.computeFixes(context);
if (fixes.isNotEmpty) {
fixes.sort(Fix.SORT_BY_RELEVANCE);
final diagnostic = toDiagnostic(unit, error);
codeActions.addAll(
fixes.map((fix) => _createFixAction(fix, diagnostic)),
);
}
}
}
return codeActions;
} on InconsistentAnalysisException {
// If an InconsistentAnalysisException occurs, it's likely the user modified
// the source and therefore is no longer interested in the results, so
// just return an empty set.
return [];
}
}
Future<List<Either2<Command, CodeAction>>> _getRefactorActions(
HashSet<CodeActionKind> clientSupportedCodeActionKinds,
bool clientSupportsLiteralCodeActions,
String path,
int offset,
int length,
ResolvedUnitResult unit,
) async {
// The refactor actions supported are only valid for Dart files.
if (!AnalysisEngine.isDartFileName(path)) {
return const [];
}
// If the client told us what kinds they support but it does not include
// Refactor then don't return any.
if (clientSupportsLiteralCodeActions &&
!clientSupportedCodeActionKinds.contains(CodeActionKind.Refactor)) {
return const [];
}
/// Helper to create refactors that execute commands provided with
/// the current file, location and document version.
createRefactor(
CodeActionKind actionKind,
String name,
RefactoringKind refactorKind, [
Map<String, dynamic> options,
]) {
return _commandOrCodeAction(
clientSupportsLiteralCodeActions,
actionKind,
new Command(name, Commands.performRefactor, [
refactorKind.toJson(),
path,
server.getVersionedDocumentIdentifier(path).version,
offset,
length,
options,
]));
}
try {
final refactorActions = <Either2<Command, CodeAction>>[];
// Extract Method
if (ExtractMethodRefactoring(server.searchEngine, unit, offset, length)
.isAvailable()) {
refactorActions.add(createRefactor(CodeActionKind.RefactorExtract,
'Extract Method', RefactoringKind.EXTRACT_METHOD));
}
// Extract Widget
if (ExtractWidgetRefactoring(server.searchEngine, unit, offset, length)
.isAvailable()) {
refactorActions.add(createRefactor(CodeActionKind.RefactorExtract,
'Extract Widget', RefactoringKind.EXTRACT_WIDGET));
}
return refactorActions;
} on InconsistentAnalysisException {
// If an InconsistentAnalysisException occurs, it's likely the user modified
// the source and therefore is no longer interested in the results, so
// just return an empty set.
return [];
}
}
/// Gets "Source" CodeActions, which are actions that apply to whole files of
/// source such as Sort Members and Organise Imports.
Future<List<Either2<Command, CodeAction>>> _getSourceActions(
HashSet<CodeActionKind> clientSupportedCodeActionKinds,
bool clientSupportsLiteralCodeActions,
bool clientSupportsWorkspaceApplyEdit,
String path,
) async {
// The source actions supported are only valid for Dart files.
if (!AnalysisEngine.isDartFileName(path)) {
return const [];
}
// If the client told us what kinds they support but it does not include
// Source then don't return any.
if (clientSupportsLiteralCodeActions &&
!clientSupportedCodeActionKinds.contains(CodeActionKind.Source)) {
return const [];
}
// If the client does not support workspace/applyEdit, we won't be able to
// run any of these.
if (!clientSupportsWorkspaceApplyEdit) {
return const [];
}
return [
_commandOrCodeAction(
clientSupportsLiteralCodeActions,
DartCodeActionKind.SortMembers,
new Command('Sort Members', Commands.sortMembers, [path]),
),
_commandOrCodeAction(
clientSupportsLiteralCodeActions,
CodeActionKind.SourceOrganizeImports,
new Command('Organize Imports', Commands.organizeImports, [path]),
),
];
}
}