9831fc4e98
The term "CodeAction" is a bit overloaded. It could mean both an individual result from the `textDocument/codeAction` request (which is a `Command` or a `CodeAction`), or the `CodeAction` type defined in the spec (which the spec refers to as a "Code Action literal"). To reduce confusion where we have similar APIs that operate on "Code Actions" (CodeAction|Command), this renames the `CodeAction` class to `CodeActionLiteral` and we will use the term `CodeAction` to mean either of those types. To make things simpler to review, this change _only_ renames the class, and also swaps the order of the types in some places that used `Either2<Command, CodeAction>` (which is opposite to the spec and some other code). Some further clean up will be done in a separate change. Change-Id: Idcd8265f9229c3450004e68334e98a7b530330a4 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/425300 Reviewed-by: Samuel Rawlins <srawlins@google.com> Commit-Queue: Brian Wilkerson <brianwilkerson@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
251 lines
7.6 KiB
Dart
251 lines
7.6 KiB
Dart
// 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.dart';
|
|
import 'package:analysis_server/src/services/correction/assist_internal.dart';
|
|
import 'package:analysis_server/src/services/correction/fix_internal.dart';
|
|
import 'package:analyzer/src/test_utilities/test_code_format.dart';
|
|
import 'package:collection/collection.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
import '../utils/test_code_extensions.dart';
|
|
import 'change_verifier.dart';
|
|
import 'server_abstract.dart';
|
|
|
|
abstract class AbstractCodeActionsTest extends AbstractLspAnalysisServerTest {
|
|
/// Initializes the server with some basic configuration and expects to find
|
|
/// a [CodeActionLiteral] with [kind]/[command]/[title].
|
|
Future<CodeActionLiteral> expectAction(
|
|
String content, {
|
|
CodeActionKind? kind,
|
|
String? command,
|
|
List<Object>? commandArgs,
|
|
String? title,
|
|
CodeActionTriggerKind? triggerKind,
|
|
String? filePath,
|
|
bool openTargetFile = false,
|
|
}) async {
|
|
filePath ??= mainFilePath;
|
|
var code = TestCode.parse(content);
|
|
newFile(filePath, code.code);
|
|
|
|
await initialize();
|
|
|
|
var fileUri = uriConverter.toClientUri(filePath);
|
|
if (openTargetFile) {
|
|
await openFile(fileUri, code.code);
|
|
}
|
|
|
|
var codeActions = await getCodeActions(
|
|
fileUri,
|
|
position: code.positions.isNotEmpty ? code.position.position : null,
|
|
range: code.ranges.isNotEmpty ? code.range.range : null,
|
|
triggerKind: triggerKind,
|
|
);
|
|
|
|
var action = findAction(
|
|
codeActions,
|
|
kind: kind,
|
|
command: command,
|
|
commandArgs: commandArgs,
|
|
title: title,
|
|
);
|
|
if (action == null) {
|
|
fail('Failed to find a code action titled "$title".');
|
|
}
|
|
return action;
|
|
}
|
|
|
|
/// Expects that command [commandName] was logged to the analytics manager.
|
|
void expectCommandLogged(String commandName) {
|
|
expect(
|
|
server.analyticsManager
|
|
.getRequestData(Method.workspace_executeCommand.toString())
|
|
.additionalEnumCounts['command']!
|
|
.keys,
|
|
contains(commandName),
|
|
);
|
|
}
|
|
|
|
/// Initializes the server with some basic configuration and expects not to
|
|
/// find a [CodeActionLiteral] with [kind]/[command]/[title].
|
|
Future<void> expectNoAction(
|
|
String content, {
|
|
String? filePath,
|
|
CodeActionKind? kind,
|
|
String? command,
|
|
String? title,
|
|
ProgressToken? workDoneToken,
|
|
}) async {
|
|
filePath ??= mainFilePath;
|
|
var code = TestCode.parse(content);
|
|
newFile(filePath, code.code);
|
|
|
|
if (workDoneToken != null) {
|
|
setWorkDoneProgressSupport();
|
|
}
|
|
await initialize();
|
|
|
|
var codeActions = await getCodeActions(
|
|
uriConverter.toClientUri(filePath),
|
|
position: code.positions.isNotEmpty ? code.position.position : null,
|
|
range: code.ranges.isNotEmpty ? code.range.range : null,
|
|
workDoneToken: workDoneToken,
|
|
);
|
|
|
|
expect(
|
|
findAction(codeActions, kind: kind, command: command, title: title),
|
|
isNull,
|
|
);
|
|
}
|
|
|
|
/// Finds the single action matching [title], [kind] and [command].
|
|
///
|
|
/// If [command] and/or [commandArgs] are supplied, ensures the action has
|
|
/// a matching command/args.
|
|
///
|
|
/// Throws if zero or more than one actions match.
|
|
CodeActionLiteral? findAction(
|
|
List<Either2<CodeActionLiteral, Command>> actions, {
|
|
String? title,
|
|
CodeActionKind? kind,
|
|
String? command,
|
|
List<Object>? commandArgs,
|
|
}) {
|
|
return findActions(
|
|
actions,
|
|
title: title,
|
|
kind: kind,
|
|
command: command,
|
|
commandArgs: commandArgs,
|
|
).singleOrNull;
|
|
}
|
|
|
|
List<CodeActionLiteral> findActions(
|
|
List<Either2<CodeActionLiteral, Command>> actions, {
|
|
String? title,
|
|
CodeActionKind? kind,
|
|
String? command,
|
|
List<Object>? commandArgs,
|
|
}) {
|
|
return actions
|
|
.map((action) => action.map((action) => action, (cmd) => null))
|
|
.where((action) => title == null || action?.title == title)
|
|
.where((action) => kind == null || action?.kind == kind)
|
|
// Some tests filter by only supplying a command, so if there is no
|
|
// title given, filter by the command. If a title was given, don't
|
|
// filter by the command and assert it below. This results in a better
|
|
// failure message if the action existed by title but without the correct
|
|
// command.
|
|
.where(
|
|
(action) =>
|
|
title != null ||
|
|
command == null ||
|
|
action?.command?.command == command,
|
|
)
|
|
.map((action) {
|
|
// Always expect a command (either to execute, or for logging)
|
|
expect(action!.command, isNotNull);
|
|
|
|
if (command != null) {
|
|
expect(action.command!.command, command);
|
|
} else {
|
|
// Expect an edit if we weren't looking for a command-action.
|
|
expect(action.edit, isNotNull);
|
|
}
|
|
|
|
if (commandArgs != null) {
|
|
expect(action.command!.arguments, equals(commandArgs));
|
|
}
|
|
|
|
return action;
|
|
})
|
|
.nonNulls
|
|
.toList();
|
|
}
|
|
|
|
Either2<CodeActionLiteral, Command>? findCommand(
|
|
List<Either2<CodeActionLiteral, Command>> actions,
|
|
String commandID, [
|
|
String? wantedTitle,
|
|
]) {
|
|
for (var codeAction in actions) {
|
|
var id = codeAction.map(
|
|
(action) => action.command?.command,
|
|
(cmd) => cmd.command,
|
|
);
|
|
var title = codeAction.map((cmd) => cmd.title, (action) => action.title);
|
|
if (id == commandID && (wantedTitle == null || wantedTitle == title)) {
|
|
return codeAction;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@override
|
|
void setUp() {
|
|
super.setUp();
|
|
|
|
// Fix tests are likely to have diagnostics that need fixing.
|
|
failTestOnErrorDiagnostic = false;
|
|
|
|
// Some defaults that most tests use. Tests can opt-out by overwriting these
|
|
// before initializing.
|
|
setApplyEditSupport();
|
|
setDocumentChangesSupport();
|
|
registerBuiltInAssistGenerators();
|
|
registerBuiltInFixGenerators();
|
|
}
|
|
|
|
/// Initializes the server with some basic configuration and expects to find
|
|
/// a [CodeActionLiteral] with [kind]/[title] that applies edits resulting in
|
|
/// [expected].
|
|
Future<LspChangeVerifier> verifyActionEdits(
|
|
String content,
|
|
String expected, {
|
|
String? filePath,
|
|
CodeActionKind? kind,
|
|
String? command,
|
|
List<Object>? commandArgs,
|
|
String? title,
|
|
ProgressToken? commandWorkDoneToken,
|
|
bool openTargetFile = false,
|
|
}) async {
|
|
filePath ??= mainFilePath;
|
|
|
|
// For convenience, if a test doesn't provide an full set of edits
|
|
// we assume only a single edit of the file that was being modified.
|
|
if (!expected.startsWith(LspChangeVerifier.editMarkerStart)) {
|
|
expected = '''
|
|
${LspChangeVerifier.editMarkerStart} ${relativePath(filePath)}
|
|
$expected''';
|
|
}
|
|
|
|
var action = await expectAction(
|
|
filePath: filePath,
|
|
content,
|
|
kind: kind,
|
|
command: command,
|
|
commandArgs: commandArgs,
|
|
title: title,
|
|
openTargetFile: openTargetFile,
|
|
);
|
|
|
|
// Verify the edits either by executing the command we expected, or
|
|
// the edits attached directly to the code action.
|
|
// Don't try to execute 'dart.logAction' because it will never produce
|
|
// edits.
|
|
if (command != null && command != 'dart.logAction') {
|
|
return await verifyCommandEdits(
|
|
action.command!,
|
|
expected,
|
|
workDoneToken: commandWorkDoneToken,
|
|
);
|
|
} else {
|
|
var edit = action.edit!;
|
|
return verifyEdit(edit, expected);
|
|
}
|
|
}
|
|
}
|