blob: 73284c891d67dedbd347a456df728d1dcb1581bd [file] [log] [blame]
// Copyright (c) 2022, 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 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:analysis_server/src/lsp/constants.dart';
import 'package:analysis_server/src/lsp/handlers/code_actions/abstract_code_actions_producer.dart';
import 'package:analysis_server/src/lsp/mapping.dart';
import 'package:analysis_server/src/protocol_server.dart'
hide AnalysisOptions, Position;
import 'package:analysis_server/src/services/correction/fix_performance.dart';
import 'package:analysis_server/src/services/correction/refactoring_performance.dart';
import 'package:analysis_server/src/services/refactoring/framework/refactoring_context.dart';
import 'package:analysis_server/src/services/refactoring/framework/refactoring_processor.dart';
import 'package:analysis_server/src/services/refactoring/legacy/refactoring.dart';
import 'package:analysis_server/src/utilities/extensions/ast.dart';
import 'package:analysis_server_plugin/edit/assist/assist.dart';
import 'package:analysis_server_plugin/edit/assist/dart_assist_context.dart';
import 'package:analysis_server_plugin/edit/fix/dart_fix_context.dart';
import 'package:analysis_server_plugin/src/correction/assist_performance.dart';
import 'package:analysis_server_plugin/src/correction/assist_processor.dart';
import 'package:analysis_server_plugin/src/correction/dart_change_workspace.dart';
import 'package:analysis_server_plugin/src/correction/fix_processor.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/analysis/session.dart'
show InconsistentAnalysisException;
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/src/util/performance/operation_performance.dart';
import 'package:analyzer/utilities/extensions/ast.dart';
/// Produces [CodeActionLiteral]s from Dart source commands, fixes, assists and
/// refactors from the server.
class DartCodeActionsProducer extends AbstractCodeActionsProducer {
ResolvedLibraryResult libraryResult;
ResolvedUnitResult unitResult;
Range range;
final OptionalVersionedTextDocumentIdentifier docIdentifier;
final CodeActionTriggerKind? triggerKind;
final bool willBeDeduplicated;
DartCodeActionsProducer(
super.server,
super.file,
super.lineInfo,
this.docIdentifier,
this.libraryResult,
this.unitResult, {
required this.range,
required super.offset,
required super.length,
required super.shouldIncludeKind,
required super.capabilities,
required super.analysisOptions,
required this.triggerKind,
required this.willBeDeduplicated,
});
@override
String get name => 'ServerDartActionsComputer';
/// Helper to create a [CodeAction] for the given arguments in the current
/// file based on client capabilities.
CodeAction createAction(
CodeActionKind actionKind,
String title,
String command,
) {
assert(
(() => Commands.serverSupportedCommands.contains(command))(),
'serverSupportedCommands did not contain $command',
);
return _commandOrCodeActionLiteral(
actionKind,
Command(
title: title,
command: command,
arguments: [
{
'path': path,
if (triggerKind == CodeActionTriggerKind.Automatic)
'autoTriggered': true,
},
],
),
);
}
/// Helper to create refactors that execute commands provided with
/// the current file, location and document version.
CodeAction createRefactor(
CodeActionKind actionKind,
String name,
RefactoringKind refactorKind, [
Map<String, Object?>? options,
]) {
var command = Commands.performRefactor;
assert(
(() => Commands.serverSupportedCommands.contains(command))(),
'serverSupportedCommands did not contain $command',
);
return _commandOrCodeActionLiteral(
actionKind,
Command(
title: name,
command: command,
arguments: [
// TODO(dantup): Change this to a single entry that is a Map once
// enough time has passed that old versions of Dart-Code prior to
// to June 2022 need not be supported against newer SDKs.
refactorKind.toJson(clientUriConverter: server.uriConverter),
path,
docIdentifier.version,
offset,
length,
options,
],
),
);
}
@override
Future<List<CodeActionWithPriority>> getAssistActions({
OperationPerformanceImpl? performance,
}) async {
// These assists are only provided as literal CodeActions.
if (!supportsLiterals) {
return const [];
}
try {
var context = DartAssistContext(
server.instrumentationService,
DartChangeWorkspace(await server.currentSessions),
libraryResult,
unitResult,
offset,
length,
);
late List<Assist> assists;
if (performance != null) {
var performanceTracker = AssistPerformance();
assists = await computeAssists(
context,
performance: performanceTracker,
);
server.recentPerformance.getAssists.add(
GetAssistsPerformance(
performance: performance,
path: path,
content: unitResult.content,
offset: offset,
requestLatency: performanceTracker.computeTime!.inMilliseconds,
producerTimings: performanceTracker.producerTimings,
),
);
} else {
assists = await computeAssists(context);
}
return assists.map((assist) {
var action = createAssistAction(
assist.change,
assist.change.id,
unitResult.path,
unitResult.lineInfo,
);
return (action: action, priority: assist.kind.priority);
}).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 const [];
}
}
@override
Future<List<CodeActionWithPriority>> getFixActions(
OperationPerformance? performance,
) async {
// These fixes are only provided as literal CodeActions.
if (!supportsLiterals) {
return [];
}
var lineInfo = unitResult.lineInfo;
var codeActions = <CodeActionWithPriority>[];
try {
// If deduplicating the result only do the expensive "fix all in file"
// calculation when we haven't before.
Set<String>? skipAlreadyCalculatedIfNonNull =
willBeDeduplicated ? {} : null;
var workspace = DartChangeWorkspace(await server.currentSessions);
for (var error in unitResult.errors) {
// Return fixes for any part of the line where a diagnostic is.
// If a diagnostic spans multiple lines, the fix will be included for
// all of those lines.
// Server lineNumbers are one-based so subtract one.
var errorStartLine = lineInfo.getLocation(error.offset).lineNumber - 1;
var errorEndLine =
lineInfo.getLocation(error.offset + error.length).lineNumber - 1;
if (range.end.line < errorStartLine ||
range.start.line > errorEndLine) {
continue;
}
var context = DartFixContext(
instrumentationService: server.instrumentationService,
workspace: workspace,
libraryResult: libraryResult,
unitResult: unitResult,
error: error,
);
var performanceTracker = FixPerformance();
var fixes = await computeFixes(
context,
performance: performanceTracker,
skipAlreadyCalculatedIfNonNull: skipAlreadyCalculatedIfNonNull,
);
if (performance != null) {
server.recentPerformance.getFixes.add(
GetFixesPerformance(
performance: performance,
path: path,
content: unitResult.content,
offset: offset,
requestLatency: performanceTracker.computeTime!.inMilliseconds,
producerTimings: performanceTracker.producerTimings,
),
);
}
if (fixes.isNotEmpty) {
var diagnostic = toDiagnostic(
server.uriConverter,
unitResult,
error,
supportedTags: supportedDiagnosticTags,
clientSupportsCodeDescription: supportsCodeDescription,
);
codeActions.addAll(
fixes.map((fix) {
var action = createFixAction(
fix.change,
fix.change.id,
diagnostic,
path,
lineInfo,
);
return (action: action, priority: fix.kind.priority);
}),
);
}
}
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 [];
}
}
@override
Future<List<CodeAction>> getRefactorActions(
OperationPerformance? performance,
) async {
// If the client does not support workspace/applyEdit, we won't be able to
// run any of these.
if (!supportsApplyEdit) {
return const [];
}
var refactorActions = <CodeAction>[];
var performanceTracker = RefactoringPerformance();
try {
// New interactive refactors.
var context = RefactoringContext(
server: server,
startSessions: await server.currentSessions,
resolvedLibraryResult: libraryResult,
resolvedUnitResult: unitResult,
clientCapabilities: capabilities,
selectionOffset: offset,
selectionLength: length,
includeExperimental:
server.lspClientConfiguration.global.experimentalRefactors,
);
var processor = RefactoringProcessor(
context,
performance: performanceTracker,
);
var actions = await processor.compute();
refactorActions.addAll(actions.map(CodeAction.t1));
// Extracts
if (shouldIncludeKind(CodeActionKind.RefactorExtract)) {
var timer = Stopwatch()..start();
// Extract Method
if (ExtractMethodRefactoring(
server.searchEngine,
unitResult,
offset,
length,
).isAvailable()) {
refactorActions.add(
createRefactor(
CodeActionKind.RefactorExtract,
'Extract Method',
RefactoringKind.EXTRACT_METHOD,
),
);
}
performanceTracker.addTiming(
className: 'ExtractMethodRefactoring',
timer: timer,
);
// Extract Local Variable
if (ExtractLocalRefactoring(unitResult, offset, length).isAvailable()) {
refactorActions.add(
createRefactor(
CodeActionKind.RefactorExtract,
'Extract Local Variable',
RefactoringKind.EXTRACT_LOCAL_VARIABLE,
),
);
}
performanceTracker.addTiming(
className: 'ExtractLocalRefactoring',
timer: timer,
);
// Extract Widget
if (ExtractWidgetRefactoring(
server.searchEngine,
unitResult,
offset,
length,
).isAvailable()) {
refactorActions.add(
createRefactor(
CodeActionKind.RefactorExtract,
'Extract Widget',
RefactoringKind.EXTRACT_WIDGET,
),
);
}
performanceTracker.addTiming(
className: 'ExtractWidgetRefactoring',
timer: timer,
);
}
var timer = Stopwatch();
// Inlines
if (shouldIncludeKind(CodeActionKind.RefactorInline)) {
timer.start();
// Inline Local Variable
if (InlineLocalRefactoring(
server.searchEngine,
unitResult,
offset,
).isAvailable()) {
refactorActions.add(
createRefactor(
CodeActionKind.RefactorInline,
'Inline Local Variable',
RefactoringKind.INLINE_LOCAL_VARIABLE,
),
);
}
performanceTracker.addTiming(
className: 'InlineLocalRefactoring',
timer: timer,
);
// Inline Method
if (InlineMethodRefactoring(
server.searchEngine,
unitResult,
offset,
).isAvailable()) {
refactorActions.add(
createRefactor(
CodeActionKind.RefactorInline,
'Inline Method',
RefactoringKind.INLINE_METHOD,
),
);
}
performanceTracker.addTiming(
className: 'InlineMethodRefactoring',
timer: timer,
);
}
// Converts/Rewrites
if (shouldIncludeKind(CodeActionKind.RefactorRewrite)) {
timer.restart();
var node = unitResult.unit.nodeCovering(offset: offset);
var element = node?.getElement();
// Getter to Method
if (element is GetterElement &&
ConvertGetterToMethodRefactoring(
server.refactoringWorkspace,
unitResult.session,
element,
).isAvailable()) {
refactorActions.add(
createRefactor(
CodeActionKind.RefactorRewrite,
'Convert Getter to Method',
RefactoringKind.CONVERT_GETTER_TO_METHOD,
),
);
}
performanceTracker.addTiming(
className: 'ConvertGetterToMethodRefactoring',
timer: timer,
);
// Method to Getter
if (element is ExecutableElement &&
ConvertMethodToGetterRefactoring(
server.refactoringWorkspace,
unitResult.session,
element,
).isAvailable()) {
refactorActions.add(
createRefactor(
CodeActionKind.RefactorRewrite,
'Convert Method to Getter',
RefactoringKind.CONVERT_METHOD_TO_GETTER,
),
);
}
performanceTracker.addTiming(
className: 'ConvertMethodToGetterRefactoring',
timer: timer,
);
}
if (performance != null) {
server.recentPerformance.getRefactorings.add(
GetRefactoringsPerformance(
performance: performance,
path: path,
content: unitResult.content,
offset: offset,
requestLatency: performanceTracker.computeTime!.inMilliseconds,
producerTimings: performanceTracker.producerTimings,
),
);
}
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.
@override
Future<List<CodeAction>> getSourceActions() async {
// If the client does not support workspace/applyEdit, we won't be able to
// run any of these.
if (!supportsApplyEdit) {
return const [];
}
return [
if (shouldIncludeKind(DartCodeActionKind.SortMembers))
createAction(
DartCodeActionKind.SortMembers,
'Sort Members',
Commands.sortMembers,
),
if (shouldIncludeKind(CodeActionKind.SourceOrganizeImports))
createAction(
CodeActionKind.SourceOrganizeImports,
'Organize Imports',
Commands.organizeImports,
),
if (shouldIncludeKind(DartCodeActionKind.FixAll))
createAction(DartCodeActionKind.FixAll, 'Fix All', Commands.fixAll),
];
}
/// Wraps a command in a CodeAction if the client supports it so that a
/// CodeActionKind can be supplied.
CodeAction _commandOrCodeActionLiteral(CodeActionKind kind, Command command) {
return supportsLiterals
? CodeAction.t1(
CodeActionLiteral(title: command.title, kind: kind, command: command),
)
: CodeAction.t2(command);
}
}
extension on Stopwatch {
void restart() {
reset();
start();
}
}
extension on RefactoringPerformance {
void addTiming({required String className, required Stopwatch timer}) {
producerTimings.add((
className: 'InlineMethodRefactoring',
elapsedTime: timer.elapsedMilliseconds,
));
timer.restart();
}
}