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>
1293 lines
40 KiB
Dart
1293 lines
40 KiB
Dart
// Copyright (c) 2023, 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/client_capabilities.dart';
|
|
import 'package:analysis_server/src/lsp/constants.dart';
|
|
import 'package:analysis_server/src/lsp/extensions/positions.dart';
|
|
import 'package:analysis_server/src/lsp/mapping.dart';
|
|
import 'package:analysis_server/src/services/completion/dart/feature_computer.dart';
|
|
import 'package:analyzer/source/line_info.dart';
|
|
import 'package:analyzer_plugin/src/utilities/client_uri_converter.dart';
|
|
import 'package:collection/collection.dart';
|
|
import 'package:language_server_protocol/json_parsing.dart';
|
|
import 'package:path/path.dart' as path;
|
|
import 'package:test/test.dart' as test show expect;
|
|
import 'package:test/test.dart';
|
|
|
|
import 'change_verifier.dart';
|
|
|
|
/// A mixin with helpers for applying LSP edits to strings.
|
|
mixin LspEditHelpersMixin {
|
|
String applyTextEdit(String content, TextEdit edit) {
|
|
var startPos = edit.range.start;
|
|
var endPos = edit.range.end;
|
|
var lineInfo = LineInfo.fromContent(content);
|
|
var start = lineInfo.getOffsetOfLine(startPos.line) + startPos.character;
|
|
var end = lineInfo.getOffsetOfLine(endPos.line) + endPos.character;
|
|
return content.replaceRange(start, end, edit.newText);
|
|
}
|
|
|
|
String applyTextEdits(String content, List<TextEdit> changes) {
|
|
// Complex text manipulations are described with an array of TextEdit's,
|
|
// representing a single change to the document.
|
|
//
|
|
// All text edits ranges refer to positions in the original document. Text
|
|
// edits ranges must never overlap, that means no part of the original
|
|
// document must be manipulated by more than one edit. It is possible
|
|
// that multiple edits have the same start position (eg. multiple inserts in
|
|
// reverse order), however since that involves complicated tracking and we
|
|
// only apply edits here sequentially, we don't supported them. We do sort
|
|
// edits to ensure we apply the later ones first, so we can assume the locations
|
|
// in the edit are still valid against the new string as each edit is applied.
|
|
|
|
/// Ensures changes are simple enough to apply easily without any complicated
|
|
/// logic.
|
|
void validateChangesCanBeApplied() {
|
|
for (var change1 in changes) {
|
|
for (var change2 in changes) {
|
|
if (change1 != change2 && change1.range.intersects(change2.range)) {
|
|
throw 'Test helper applyTextEdits does not support applying multiple edits '
|
|
'where the edits are not in reverse order.';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
validateChangesCanBeApplied();
|
|
|
|
var indexedEdits = changes.mapIndexed(TextEditWithIndex.new).toList();
|
|
indexedEdits.sort(TextEditWithIndex.compare);
|
|
return indexedEdits.map((e) => e.edit).fold(content, applyTextEdit);
|
|
}
|
|
|
|
/// Returns the text for [range] in [content].
|
|
String getTextForRange(String content, Range range) {
|
|
var lineInfo = LineInfo.fromContent(content);
|
|
var sourceRange = toSourceRange(lineInfo, range).result;
|
|
return content.substring(sourceRange.offset, sourceRange.end);
|
|
}
|
|
}
|
|
|
|
mixin LspProgressNotificationsMixin {
|
|
Stream<NotificationMessage> get notificationsFromServer;
|
|
|
|
/// A stream of strings (CREATE, BEGIN, END) corresponding to progress
|
|
/// requests and notifications for convenience in testing.
|
|
///
|
|
/// Analyzing statuses are not included.
|
|
Stream<String> get progressUpdates {
|
|
var controller = StreamController<String>();
|
|
|
|
requestsFromServer
|
|
.where((r) => r.method == Method.window_workDoneProgress_create)
|
|
.listen((request) {
|
|
var params = WorkDoneProgressCreateParams.fromJson(
|
|
request.params as Map<String, Object?>,
|
|
);
|
|
if (params.token != analyzingProgressToken) {
|
|
controller.add('CREATE');
|
|
}
|
|
}, onDone: controller.close);
|
|
notificationsFromServer.where((n) => n.method == Method.progress).listen((
|
|
notification,
|
|
) {
|
|
var params = ProgressParams.fromJson(
|
|
notification.params as Map<String, Object?>,
|
|
);
|
|
if (params.token != analyzingProgressToken) {
|
|
if (WorkDoneProgressBegin.canParse(params.value, nullLspJsonReporter)) {
|
|
controller.add('BEGIN');
|
|
} else if (WorkDoneProgressEnd.canParse(
|
|
params.value,
|
|
nullLspJsonReporter,
|
|
)) {
|
|
controller.add('END');
|
|
}
|
|
}
|
|
});
|
|
|
|
return controller.stream;
|
|
}
|
|
|
|
Stream<RequestMessage> get requestsFromServer;
|
|
}
|
|
|
|
/// Helpers to simplify building LSP requests for use in tests.
|
|
///
|
|
/// The actual sending of requests must be supplied by the implementing class
|
|
/// via [expectSuccessfulResponseTo].
|
|
///
|
|
/// These helpers can be used by in-process tests and out-of-process integration
|
|
/// tests and by both the native LSP server and using LSP over the legacy
|
|
/// protocol.
|
|
mixin LspRequestHelpersMixin {
|
|
int _id = 0;
|
|
|
|
final startOfDocPos = Position(line: 0, character: 0);
|
|
|
|
final startOfDocRange = Range(
|
|
start: Position(line: 0, character: 0),
|
|
end: Position(line: 0, character: 0),
|
|
);
|
|
|
|
/// Whether to include 'clientRequestTime' fields in outgoing messages.
|
|
bool includeClientRequestTime = false;
|
|
|
|
/// A stream of [DartTextDocumentContentDidChangeParams] for any
|
|
/// `dart/textDocumentContentDidChange` notifications.
|
|
Stream<DartTextDocumentContentDidChangeParams>
|
|
get dartTextDocumentContentDidChangeNotifications => notificationsFromServer
|
|
.where(
|
|
(notification) =>
|
|
notification.method ==
|
|
CustomMethods.dartTextDocumentContentDidChange,
|
|
)
|
|
.map(
|
|
(message) => DartTextDocumentContentDidChangeParams.fromJson(
|
|
message.params as Map<String, Object?>,
|
|
),
|
|
);
|
|
|
|
Stream<NotificationMessage> get notificationsFromServer;
|
|
|
|
Stream<RequestMessage> get requestsFromServer;
|
|
|
|
Future<List<CallHierarchyIncomingCall>?> callHierarchyIncoming(
|
|
CallHierarchyItem item,
|
|
) {
|
|
var request = makeRequest(
|
|
Method.callHierarchy_incomingCalls,
|
|
CallHierarchyIncomingCallsParams(item: item),
|
|
);
|
|
return expectSuccessfulResponseTo(
|
|
request,
|
|
_fromJsonList(CallHierarchyIncomingCall.fromJson),
|
|
);
|
|
}
|
|
|
|
Future<List<CallHierarchyOutgoingCall>?> callHierarchyOutgoing(
|
|
CallHierarchyItem item,
|
|
) {
|
|
var request = makeRequest(
|
|
Method.callHierarchy_outgoingCalls,
|
|
CallHierarchyOutgoingCallsParams(item: item),
|
|
);
|
|
return expectSuccessfulResponseTo(
|
|
request,
|
|
_fromJsonList(CallHierarchyOutgoingCall.fromJson),
|
|
);
|
|
}
|
|
|
|
Future<Null> connectToDtd(Uri uri, {bool? registerExperimentalHandlers}) {
|
|
var request = makeRequest(
|
|
CustomMethods.connectToDtd,
|
|
ConnectToDtdParams(
|
|
uri: uri,
|
|
registerExperimentalHandlers: registerExperimentalHandlers,
|
|
),
|
|
);
|
|
return expectSuccessfulResponseTo<Null, Null>(request, (Null n) => n);
|
|
}
|
|
|
|
Future<Null> editArgument(Uri uri, Position pos, ArgumentEdit edit) {
|
|
var request = makeRequest(
|
|
CustomMethods.dartTextDocumentEditArgument,
|
|
EditArgumentParams(
|
|
textDocument: TextDocumentIdentifier(uri: uri),
|
|
position: pos,
|
|
edit: edit,
|
|
),
|
|
);
|
|
return expectSuccessfulResponseTo(request, (Null n) => n);
|
|
}
|
|
|
|
/// Gets the entire range for [code].
|
|
Range entireRange(String code) =>
|
|
Range(start: startOfDocPos, end: positionFromOffset(code.length, code));
|
|
|
|
void expect(Object? actual, Matcher matcher, {String? reason}) =>
|
|
test.expect(actual, matcher, reason: reason);
|
|
|
|
/// Expects a [method] request from the server after executing [f].
|
|
Future<RequestMessage> expectRequest(
|
|
Method method,
|
|
FutureOr<void> Function() f, {
|
|
Duration timeout = const Duration(seconds: 5),
|
|
}) async {
|
|
var firstRequest = requestsFromServer.firstWhere((n) => n.method == method);
|
|
await f();
|
|
|
|
var requestFromServer = await firstRequest.timeout(
|
|
timeout,
|
|
onTimeout:
|
|
() =>
|
|
throw TimeoutException(
|
|
'Did not receive the expected $method request from the server in the timeout period',
|
|
timeout,
|
|
),
|
|
);
|
|
|
|
expect(requestFromServer, isNotNull);
|
|
return requestFromServer;
|
|
}
|
|
|
|
Future<T> expectSuccessfulResponseTo<T, R>(
|
|
RequestMessage request,
|
|
T Function(R) fromJson,
|
|
);
|
|
|
|
Future<List<TextEdit>?> formatDocument(Uri fileUri) {
|
|
var request = makeRequest(
|
|
Method.textDocument_formatting,
|
|
DocumentFormattingParams(
|
|
textDocument: TextDocumentIdentifier(uri: fileUri),
|
|
options: FormattingOptions(
|
|
tabSize: 2,
|
|
insertSpaces: true,
|
|
), // These currently don't do anything
|
|
),
|
|
);
|
|
return expectSuccessfulResponseTo(
|
|
request,
|
|
_fromJsonList(TextEdit.fromJson),
|
|
);
|
|
}
|
|
|
|
Future<List<TextEdit>?> formatOnType(
|
|
Uri fileUri,
|
|
Position pos,
|
|
String character,
|
|
) {
|
|
var request = makeRequest(
|
|
Method.textDocument_onTypeFormatting,
|
|
DocumentOnTypeFormattingParams(
|
|
ch: character,
|
|
options: FormattingOptions(
|
|
tabSize: 2,
|
|
insertSpaces: true,
|
|
), // These currently don't do anything
|
|
textDocument: TextDocumentIdentifier(uri: fileUri),
|
|
position: pos,
|
|
),
|
|
);
|
|
return expectSuccessfulResponseTo(
|
|
request,
|
|
_fromJsonList(TextEdit.fromJson),
|
|
);
|
|
}
|
|
|
|
Future<List<TextEdit>?> formatRange(Uri fileUri, Range range) {
|
|
var request = makeRequest(
|
|
Method.textDocument_rangeFormatting,
|
|
DocumentRangeFormattingParams(
|
|
options: FormattingOptions(
|
|
tabSize: 2,
|
|
insertSpaces: true,
|
|
), // These currently don't do anything
|
|
textDocument: TextDocumentIdentifier(uri: fileUri),
|
|
range: range,
|
|
),
|
|
);
|
|
return expectSuccessfulResponseTo(
|
|
request,
|
|
_fromJsonList(TextEdit.fromJson),
|
|
);
|
|
}
|
|
|
|
Future<Location?> getAugmentation(Uri uri, Position pos) {
|
|
var request = makeRequest(
|
|
CustomMethods.augmentation,
|
|
TextDocumentPositionParams(
|
|
textDocument: TextDocumentIdentifier(uri: uri),
|
|
position: pos,
|
|
),
|
|
);
|
|
return expectSuccessfulResponseTo(request, Location.fromJson);
|
|
}
|
|
|
|
Future<Location?> getAugmented(Uri uri, Position pos) {
|
|
var request = makeRequest(
|
|
CustomMethods.augmented,
|
|
TextDocumentPositionParams(
|
|
textDocument: TextDocumentIdentifier(uri: uri),
|
|
position: pos,
|
|
),
|
|
);
|
|
return expectSuccessfulResponseTo(request, Location.fromJson);
|
|
}
|
|
|
|
Future<List<Either2<CodeActionLiteral, Command>>> getCodeActions(
|
|
Uri fileUri, {
|
|
Range? range,
|
|
Position? position,
|
|
List<CodeActionKind>? kinds,
|
|
CodeActionTriggerKind? triggerKind,
|
|
ProgressToken? workDoneToken,
|
|
}) {
|
|
range ??=
|
|
position != null
|
|
? Range(start: position, end: position)
|
|
: throw 'Supply either a Range or Position for CodeActions requests';
|
|
var request = makeRequest(
|
|
Method.textDocument_codeAction,
|
|
CodeActionParams(
|
|
textDocument: TextDocumentIdentifier(uri: fileUri),
|
|
range: range,
|
|
context: CodeActionContext(
|
|
// TODO(dantup): We may need to revise the tests/implementation when
|
|
// it's clear how we're supposed to handle diagnostics:
|
|
// https://github.com/Microsoft/language-server-protocol/issues/583
|
|
diagnostics: [],
|
|
only: kinds,
|
|
triggerKind: triggerKind,
|
|
),
|
|
workDoneToken: workDoneToken,
|
|
),
|
|
);
|
|
return expectSuccessfulResponseTo(
|
|
request,
|
|
_fromJsonList(
|
|
_generateFromJsonFor(
|
|
CodeActionLiteral.canParse,
|
|
CodeActionLiteral.fromJson,
|
|
Command.canParse,
|
|
Command.fromJson,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<TextDocumentCodeLensResult> getCodeLens(Uri uri) {
|
|
var request = makeRequest(
|
|
Method.textDocument_codeLens,
|
|
CodeLensParams(textDocument: TextDocumentIdentifier(uri: uri)),
|
|
);
|
|
return expectSuccessfulResponseTo(
|
|
request,
|
|
_fromJsonList(CodeLens.fromJson),
|
|
);
|
|
}
|
|
|
|
Future<List<ColorPresentation>> getColorPresentation(
|
|
Uri fileUri,
|
|
Range range,
|
|
Color color,
|
|
) {
|
|
var request = makeRequest(
|
|
Method.textDocument_colorPresentation,
|
|
ColorPresentationParams(
|
|
textDocument: TextDocumentIdentifier(uri: fileUri),
|
|
range: range,
|
|
color: color,
|
|
),
|
|
);
|
|
return expectSuccessfulResponseTo(
|
|
request,
|
|
_fromJsonList(ColorPresentation.fromJson),
|
|
);
|
|
}
|
|
|
|
Future<List<CompletionItem>> getCompletion(
|
|
Uri uri,
|
|
Position pos, {
|
|
CompletionContext? context,
|
|
}) async {
|
|
var response = await getCompletionList(uri, pos, context: context);
|
|
return response.items;
|
|
}
|
|
|
|
Future<CompletionList> getCompletionList(
|
|
Uri uri,
|
|
Position pos, {
|
|
CompletionContext? context,
|
|
}) async {
|
|
var request = makeRequest(
|
|
Method.textDocument_completion,
|
|
CompletionParams(
|
|
context: context,
|
|
textDocument: TextDocumentIdentifier(uri: uri),
|
|
position: pos,
|
|
),
|
|
);
|
|
var completions = await expectSuccessfulResponseTo(
|
|
request,
|
|
CompletionList.fromJson,
|
|
);
|
|
_assertMinimalCompletionListPayload(completions);
|
|
return completions;
|
|
}
|
|
|
|
Future<DartTextDocumentContent?> getDartTextDocumentContent(Uri uri) {
|
|
var request = makeRequest(
|
|
CustomMethods.dartTextDocumentContent,
|
|
DartTextDocumentContentParams(uri: uri),
|
|
);
|
|
return expectSuccessfulResponseTo(
|
|
request,
|
|
DartTextDocumentContent.fromJson,
|
|
);
|
|
}
|
|
|
|
Future<Either2<List<Location>, List<LocationLink>>> getDefinition(
|
|
Uri uri,
|
|
Position pos,
|
|
) {
|
|
var request = makeRequest(
|
|
Method.textDocument_definition,
|
|
TextDocumentPositionParams(
|
|
textDocument: TextDocumentIdentifier(uri: uri),
|
|
position: pos,
|
|
),
|
|
);
|
|
return expectSuccessfulResponseTo(
|
|
request,
|
|
_generateFromJsonFor(
|
|
_canParseList(Location.canParse),
|
|
_fromJsonList(Location.fromJson),
|
|
_canParseList(LocationLink.canParse),
|
|
_fromJsonList(LocationLink.fromJson),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<List<Location>> getDefinitionAsLocation(Uri uri, Position pos) async {
|
|
var results = await getDefinition(uri, pos);
|
|
return results.map(
|
|
(locations) => locations,
|
|
(locationLinks) => throw 'Expected List<Location> got List<LocationLink>',
|
|
);
|
|
}
|
|
|
|
Future<List<LocationLink>> getDefinitionAsLocationLinks(
|
|
Uri uri,
|
|
Position pos,
|
|
) async {
|
|
var results = await getDefinition(uri, pos);
|
|
return results.map(
|
|
(locations) => throw 'Expected List<LocationLink> got List<Location>',
|
|
(locationLinks) => locationLinks,
|
|
);
|
|
}
|
|
|
|
Future<DartDiagnosticServer> getDiagnosticServer() {
|
|
var request = makeRequest(CustomMethods.diagnosticServer, null);
|
|
return expectSuccessfulResponseTo(request, DartDiagnosticServer.fromJson);
|
|
}
|
|
|
|
Future<List<ColorInformation>> getDocumentColors(Uri fileUri) {
|
|
var request = makeRequest(
|
|
Method.textDocument_documentColor,
|
|
DocumentColorParams(textDocument: TextDocumentIdentifier(uri: fileUri)),
|
|
);
|
|
return expectSuccessfulResponseTo(
|
|
request,
|
|
_fromJsonList(ColorInformation.fromJson),
|
|
);
|
|
}
|
|
|
|
Future<List<DocumentHighlight>?> getDocumentHighlights(
|
|
Uri uri,
|
|
Position pos,
|
|
) {
|
|
var request = makeRequest(
|
|
Method.textDocument_documentHighlight,
|
|
TextDocumentPositionParams(
|
|
textDocument: TextDocumentIdentifier(uri: uri),
|
|
position: pos,
|
|
),
|
|
);
|
|
return expectSuccessfulResponseTo(
|
|
request,
|
|
_fromJsonList(DocumentHighlight.fromJson),
|
|
);
|
|
}
|
|
|
|
Future<List<DocumentLink>?> getDocumentLinks(Uri uri) {
|
|
var request = makeRequest(
|
|
Method.textDocument_documentLink,
|
|
DocumentLinkParams(textDocument: TextDocumentIdentifier(uri: uri)),
|
|
);
|
|
return expectSuccessfulResponseTo(
|
|
request,
|
|
_fromJsonList(DocumentLink.fromJson),
|
|
);
|
|
}
|
|
|
|
Future<Either2<List<DocumentSymbol>, List<SymbolInformation>>>
|
|
getDocumentSymbols(Uri uri) {
|
|
var request = makeRequest(
|
|
Method.textDocument_documentSymbol,
|
|
DocumentSymbolParams(textDocument: TextDocumentIdentifier(uri: uri)),
|
|
);
|
|
return expectSuccessfulResponseTo(
|
|
request,
|
|
_generateFromJsonFor(
|
|
_canParseList(DocumentSymbol.canParse),
|
|
_fromJsonList(DocumentSymbol.fromJson),
|
|
_canParseList(SymbolInformation.canParse),
|
|
_fromJsonList(SymbolInformation.fromJson),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<EditableArguments?> getEditableArguments(Uri uri, Position pos) {
|
|
var request = makeRequest(
|
|
CustomMethods.dartTextDocumentEditableArguments,
|
|
TextDocumentPositionParams(
|
|
textDocument: TextDocumentIdentifier(uri: uri),
|
|
position: pos,
|
|
),
|
|
);
|
|
return expectSuccessfulResponseTo(request, EditableArguments.fromJson);
|
|
}
|
|
|
|
Future<List<FoldingRange>> getFoldingRanges(Uri uri) {
|
|
var request = makeRequest(
|
|
Method.textDocument_foldingRange,
|
|
FoldingRangeParams(textDocument: TextDocumentIdentifier(uri: uri)),
|
|
);
|
|
return expectSuccessfulResponseTo(
|
|
request,
|
|
_fromJsonList(FoldingRange.fromJson),
|
|
);
|
|
}
|
|
|
|
Future<Hover?> getHover(Uri uri, Position pos) {
|
|
var request = makeRequest(
|
|
Method.textDocument_hover,
|
|
TextDocumentPositionParams(
|
|
textDocument: TextDocumentIdentifier(uri: uri),
|
|
position: pos,
|
|
),
|
|
);
|
|
return expectSuccessfulResponseTo(request, Hover.fromJson);
|
|
}
|
|
|
|
Future<List<Location>> getImplementations(
|
|
Uri uri,
|
|
Position pos, {
|
|
includeDeclarations = false,
|
|
}) {
|
|
var request = makeRequest(
|
|
Method.textDocument_implementation,
|
|
TextDocumentPositionParams(
|
|
textDocument: TextDocumentIdentifier(uri: uri),
|
|
position: pos,
|
|
),
|
|
);
|
|
return expectSuccessfulResponseTo(
|
|
request,
|
|
_fromJsonList(Location.fromJson),
|
|
);
|
|
}
|
|
|
|
Future<List<Location>?> getImports(Uri uri, Position pos) {
|
|
var request = makeRequest(
|
|
CustomMethods.imports,
|
|
TextDocumentPositionParams(
|
|
textDocument: TextDocumentIdentifier(uri: uri),
|
|
position: pos,
|
|
),
|
|
);
|
|
return expectSuccessfulResponseTo(
|
|
request,
|
|
_fromJsonList(Location.fromJson),
|
|
);
|
|
}
|
|
|
|
Future<List<InlayHint>> getInlayHints(Uri uri, Range range) {
|
|
var request = makeRequest(
|
|
Method.textDocument_inlayHint,
|
|
InlayHintParams(
|
|
textDocument: TextDocumentIdentifier(uri: uri),
|
|
range: range,
|
|
),
|
|
);
|
|
return expectSuccessfulResponseTo(
|
|
request,
|
|
_fromJsonList(InlayHint.fromJson),
|
|
);
|
|
}
|
|
|
|
Future<List<InlineValue>?> getInlineValues(
|
|
Uri uri, {
|
|
required Range visibleRange,
|
|
required Position stoppedAt,
|
|
}) {
|
|
var request = makeRequest(
|
|
Method.textDocument_inlineValue,
|
|
InlineValueParams(
|
|
textDocument: TextDocumentIdentifier(uri: uri),
|
|
range: visibleRange,
|
|
context: InlineValueContext(
|
|
frameId: 0,
|
|
stoppedLocation: Range(start: stoppedAt, end: stoppedAt),
|
|
),
|
|
),
|
|
);
|
|
return expectSuccessfulResponseTo(
|
|
request,
|
|
// Use a custom function to deserialise the response into the correct
|
|
// kind of `InlineValue`.
|
|
_fromJsonList((Map<String, Object?> input) {
|
|
var reporter = nullLspJsonReporter;
|
|
// The overlap of these types is such that a Variable or Text would also
|
|
// match `InlineValueEvaluatableExpression.canParse` (because
|
|
// `expression` is optional), so the order of calling canParse here
|
|
// matters.
|
|
if (InlineValueVariableLookup.canParse(input, reporter)) {
|
|
return InlineValue.t3(InlineValueVariableLookup.fromJson(input));
|
|
}
|
|
if (InlineValueText.canParse(input, reporter)) {
|
|
return InlineValue.t2(InlineValueText.fromJson(input));
|
|
}
|
|
if (InlineValueEvaluatableExpression.canParse(input, reporter)) {
|
|
return InlineValue.t1(
|
|
InlineValueEvaluatableExpression.fromJson(input),
|
|
);
|
|
}
|
|
throw 'InlineValue did not match any valid types';
|
|
}),
|
|
);
|
|
}
|
|
|
|
Future<List<Location>> getReferences(
|
|
Uri uri,
|
|
Position pos, {
|
|
bool includeDeclarations = false,
|
|
}) {
|
|
var request = makeRequest(
|
|
Method.textDocument_references,
|
|
ReferenceParams(
|
|
context: ReferenceContext(includeDeclaration: includeDeclarations),
|
|
textDocument: TextDocumentIdentifier(uri: uri),
|
|
position: pos,
|
|
),
|
|
);
|
|
return expectSuccessfulResponseTo(
|
|
request,
|
|
_fromJsonList(Location.fromJson),
|
|
);
|
|
}
|
|
|
|
Future<CompletionItem> getResolvedCompletion(
|
|
Uri uri,
|
|
Position pos,
|
|
String label, {
|
|
CompletionContext? context,
|
|
}) async {
|
|
var completions = await getCompletion(uri, pos, context: context);
|
|
|
|
var completion = completions.singleWhere((c) => c.label == label);
|
|
expect(completion, isNotNull);
|
|
|
|
var result = await resolveCompletion(completion);
|
|
_assertMinimalCompletionItemPayload(result);
|
|
return result;
|
|
}
|
|
|
|
Future<List<SelectionRange>?> getSelectionRanges(
|
|
Uri uri,
|
|
List<Position> positions,
|
|
) {
|
|
var request = makeRequest(
|
|
Method.textDocument_selectionRange,
|
|
SelectionRangeParams(
|
|
textDocument: TextDocumentIdentifier(uri: uri),
|
|
positions: positions,
|
|
),
|
|
);
|
|
return expectSuccessfulResponseTo(
|
|
request,
|
|
_fromJsonList(SelectionRange.fromJson),
|
|
);
|
|
}
|
|
|
|
Future<SemanticTokens> getSemanticTokens(Uri uri) {
|
|
var request = makeRequest(
|
|
Method.textDocument_semanticTokens_full,
|
|
SemanticTokensParams(textDocument: TextDocumentIdentifier(uri: uri)),
|
|
);
|
|
return expectSuccessfulResponseTo(request, SemanticTokens.fromJson);
|
|
}
|
|
|
|
Future<SemanticTokens> getSemanticTokensRange(Uri uri, Range range) {
|
|
var request = makeRequest(
|
|
Method.textDocument_semanticTokens_range,
|
|
SemanticTokensRangeParams(
|
|
textDocument: TextDocumentIdentifier(uri: uri),
|
|
range: range,
|
|
),
|
|
);
|
|
return expectSuccessfulResponseTo(request, SemanticTokens.fromJson);
|
|
}
|
|
|
|
Future<SignatureHelp?> getSignatureHelp(
|
|
Uri uri,
|
|
Position pos, [
|
|
SignatureHelpContext? context,
|
|
]) {
|
|
var request = makeRequest(
|
|
Method.textDocument_signatureHelp,
|
|
SignatureHelpParams(
|
|
textDocument: TextDocumentIdentifier(uri: uri),
|
|
position: pos,
|
|
context: context,
|
|
),
|
|
);
|
|
return expectSuccessfulResponseTo(request, SignatureHelp.fromJson);
|
|
}
|
|
|
|
Future<Location> getSuper(Uri uri, Position pos) {
|
|
var request = makeRequest(
|
|
CustomMethods.super_,
|
|
TextDocumentPositionParams(
|
|
textDocument: TextDocumentIdentifier(uri: uri),
|
|
position: pos,
|
|
),
|
|
);
|
|
return expectSuccessfulResponseTo(request, Location.fromJson);
|
|
}
|
|
|
|
Future<TextDocumentTypeDefinitionResult> getTypeDefinition(
|
|
Uri uri,
|
|
Position pos,
|
|
) {
|
|
var request = makeRequest(
|
|
Method.textDocument_typeDefinition,
|
|
TypeDefinitionParams(
|
|
textDocument: TextDocumentIdentifier(uri: uri),
|
|
position: pos,
|
|
),
|
|
);
|
|
|
|
// TextDocumentTypeDefinitionResult is a nested Either, so we need to handle
|
|
// nested fromJson/canParse here.
|
|
// TextDocumentTypeDefinitionResult: Either2<Definition, List<DefinitionLink>>?
|
|
// Definition: Either2<List<Location>, Location>
|
|
|
|
// Definition = Either2<List<Location>, Location>
|
|
var definitionCanParse = _generateCanParseFor(
|
|
_canParseList(Location.canParse),
|
|
Location.canParse,
|
|
);
|
|
var definitionFromJson = _generateFromJsonFor(
|
|
_canParseList(Location.canParse),
|
|
_fromJsonList(Location.fromJson),
|
|
Location.canParse,
|
|
Location.fromJson,
|
|
);
|
|
|
|
return expectSuccessfulResponseTo(
|
|
request,
|
|
_generateFromJsonFor(
|
|
definitionCanParse,
|
|
definitionFromJson,
|
|
_canParseList(DefinitionLink.canParse),
|
|
_fromJsonList(DefinitionLink.fromJson),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<List<Location>> getTypeDefinitionAsLocation(
|
|
Uri uri,
|
|
Position pos,
|
|
) async {
|
|
var results = (await getTypeDefinition(uri, pos))!;
|
|
return results.map(
|
|
(locationOrList) => locationOrList.map(
|
|
(locations) => locations,
|
|
(location) => [location],
|
|
),
|
|
(locationLinks) => throw 'Expected Locations, got LocationLinks',
|
|
);
|
|
}
|
|
|
|
Future<List<LocationLink>> getTypeDefinitionAsLocationLinks(
|
|
Uri uri,
|
|
Position pos,
|
|
) async {
|
|
var results = (await getTypeDefinition(uri, pos))!;
|
|
return results.map(
|
|
(locationOrList) => throw 'Expected LocationLinks, got Locations',
|
|
(locationLinks) => locationLinks,
|
|
);
|
|
}
|
|
|
|
Future<List<SymbolInformation>> getWorkspaceSymbols(String query) {
|
|
var request = makeRequest(
|
|
Method.workspace_symbol,
|
|
WorkspaceSymbolParams(query: query),
|
|
);
|
|
return expectSuccessfulResponseTo(
|
|
request,
|
|
_fromJsonList(SymbolInformation.fromJson),
|
|
);
|
|
}
|
|
|
|
/// Executes [f] then waits for a request of type [method] from the server which
|
|
/// is passed to [handler] to process, then waits for (and returns) the
|
|
/// response to the original request.
|
|
///
|
|
/// This is used for testing things like code actions, where the client initiates
|
|
/// a request but the server does not respond to it until it's sent its own
|
|
/// request to the client and it received a response.
|
|
///
|
|
/// Client Server
|
|
/// 1. |- Req: textDocument/codeAction ->
|
|
/// 1. <- Resp: textDocument/codeAction -|
|
|
///
|
|
/// 2. |- Req: workspace/executeCommand ->
|
|
/// 3. <- Req: textDocument/applyEdits -|
|
|
/// 3. |- Resp: textDocument/applyEdits ->
|
|
/// 2. <- Resp: workspace/executeCommand -|
|
|
///
|
|
/// Request 2 from the client is not responded to until the server has its own
|
|
/// response to the request it sends (3).
|
|
Future<T> handleExpectedRequest<T, R, RR>(
|
|
Method method,
|
|
R Function(Map<String, dynamic>) fromJson,
|
|
Future<T> Function() f, {
|
|
required FutureOr<RR> Function(R) handler,
|
|
Duration timeout = const Duration(seconds: 5),
|
|
}) async {
|
|
late Future<T> outboundRequest;
|
|
Object? outboundRequestError;
|
|
|
|
// Run [f] and wait for the incoming request from the server.
|
|
var incomingRequest = await expectRequest(method, () {
|
|
// Don't return/await the response yet, as this may not complete until
|
|
// after we have handled the request that comes from the server.
|
|
outboundRequest = f();
|
|
|
|
// Because we don't await this future until "later", if it throws the
|
|
// error is treated as unhandled and will fail the test even if expected.
|
|
// Instead, capture the error and suppress it. But if we time out (in
|
|
// which case we will never return outboundRequest), then we'll raise this
|
|
// error.
|
|
outboundRequest.then(
|
|
(_) {},
|
|
onError: (e) {
|
|
outboundRequestError = e;
|
|
return null;
|
|
},
|
|
);
|
|
}, timeout: timeout).catchError((Object timeoutException) {
|
|
// We timed out waiting for the request from the server. Probably this is
|
|
// because our outbound request for some reason, so if we have an error
|
|
// for that, then throw it. Otherwise, propogate the timeout.
|
|
throw outboundRequestError ?? timeoutException;
|
|
}, test: (e) => e is TimeoutException);
|
|
|
|
// Handle the request from the server and send the response back.
|
|
var clientsResponse = await handler(
|
|
fromJson(incomingRequest.params as Map<String, Object?>),
|
|
);
|
|
respondTo(incomingRequest, clientsResponse);
|
|
|
|
// Return a future that completes when the response to the original request
|
|
// (from [f]) returns.
|
|
return outboundRequest;
|
|
}
|
|
|
|
RequestMessage makeRequest(Method method, Object? params) {
|
|
var id = Either2<int, String>.t1(_id++);
|
|
return RequestMessage(
|
|
id: id,
|
|
method: method,
|
|
params: params is ToJsonable ? params.toJson() : params,
|
|
jsonrpc: jsonRpcVersion,
|
|
clientRequestTime:
|
|
includeClientRequestTime
|
|
? DateTime.now().millisecondsSinceEpoch
|
|
: null,
|
|
);
|
|
}
|
|
|
|
Future<WorkspaceEdit> onWillRename(List<FileRename> renames) {
|
|
var request = makeRequest(
|
|
Method.workspace_willRenameFiles,
|
|
RenameFilesParams(files: renames),
|
|
);
|
|
return expectSuccessfulResponseTo(request, WorkspaceEdit.fromJson);
|
|
}
|
|
|
|
Position positionFromOffset(int offset, String contents) {
|
|
var lineInfo = LineInfo.fromContent(contents);
|
|
return toPosition(lineInfo.getLocation(offset));
|
|
}
|
|
|
|
Future<List<CallHierarchyItem>?> prepareCallHierarchy(Uri uri, Position pos) {
|
|
var request = makeRequest(
|
|
Method.textDocument_prepareCallHierarchy,
|
|
CallHierarchyPrepareParams(
|
|
textDocument: TextDocumentIdentifier(uri: uri),
|
|
position: pos,
|
|
),
|
|
);
|
|
return expectSuccessfulResponseTo(
|
|
request,
|
|
_fromJsonList(CallHierarchyItem.fromJson),
|
|
);
|
|
}
|
|
|
|
Future<PrepareRenamePlaceholder?> prepareRename(Uri uri, Position pos) {
|
|
var request = makeRequest(
|
|
Method.textDocument_prepareRename,
|
|
TextDocumentPositionParams(
|
|
textDocument: TextDocumentIdentifier(uri: uri),
|
|
position: pos,
|
|
),
|
|
);
|
|
return expectSuccessfulResponseTo(
|
|
request,
|
|
PrepareRenamePlaceholder.fromJson,
|
|
);
|
|
}
|
|
|
|
Future<List<TypeHierarchyItem>?> prepareTypeHierarchy(Uri uri, Position pos) {
|
|
var request = makeRequest(
|
|
Method.textDocument_prepareTypeHierarchy,
|
|
TypeHierarchyPrepareParams(
|
|
textDocument: TextDocumentIdentifier(uri: uri),
|
|
position: pos,
|
|
),
|
|
);
|
|
return expectSuccessfulResponseTo(
|
|
request,
|
|
_fromJsonList(TypeHierarchyItem.fromJson),
|
|
);
|
|
}
|
|
|
|
Future<CompletionItem> resolveCompletion(CompletionItem item) {
|
|
var request = makeRequest(Method.completionItem_resolve, item);
|
|
return expectSuccessfulResponseTo(request, CompletionItem.fromJson);
|
|
}
|
|
|
|
/// Sends [responseParams] to the server as a successful response to
|
|
/// a server-initiated [request].
|
|
void respondTo<T>(RequestMessage request, T responseParams) {
|
|
sendResponseToServer(
|
|
ResponseMessage(
|
|
id: request.id,
|
|
result: responseParams,
|
|
jsonrpc: jsonRpcVersion,
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Sends a ResponseMessage to the server, completing a reverse
|
|
/// (server-to-client) request.
|
|
void sendResponseToServer(ResponseMessage response);
|
|
|
|
Future<List<TypeHierarchyItem>?> typeHierarchySubtypes(
|
|
TypeHierarchyItem item,
|
|
) {
|
|
var request = makeRequest(
|
|
Method.typeHierarchy_subtypes,
|
|
TypeHierarchySubtypesParams(item: item),
|
|
);
|
|
return expectSuccessfulResponseTo(
|
|
request,
|
|
_fromJsonList(TypeHierarchyItem.fromJson),
|
|
);
|
|
}
|
|
|
|
Future<List<TypeHierarchyItem>?> typeHierarchySupertypes(
|
|
TypeHierarchyItem item,
|
|
) {
|
|
var request = makeRequest(
|
|
Method.typeHierarchy_supertypes,
|
|
TypeHierarchySupertypesParams(item: item),
|
|
);
|
|
return expectSuccessfulResponseTo(
|
|
request,
|
|
_fromJsonList(TypeHierarchyItem.fromJson),
|
|
);
|
|
}
|
|
|
|
Future<void> updateDiagnosticInformation(Map<String, Object?>? params) {
|
|
var request = makeRequest(
|
|
CustomMethods.updateDiagnosticInformation,
|
|
params,
|
|
);
|
|
return expectSuccessfulResponseTo(request, (Null n) => n);
|
|
}
|
|
|
|
/// A helper that performs some checks on a completion sent back during
|
|
/// tests to check for any unnecessary data in the payload that could be
|
|
/// reduced.
|
|
void _assertMinimalCompletionItemPayload(CompletionItem completion) {
|
|
var labelDetails = completion.labelDetails;
|
|
var textEditInsertRange = completion.textEdit?.map(
|
|
(ranges) => ranges.insert,
|
|
(range) => null,
|
|
);
|
|
var textEditReplaceRange = completion.textEdit?.map(
|
|
(ranges) => ranges.replace,
|
|
(range) => null,
|
|
);
|
|
var sortText = completion.sortText;
|
|
|
|
// Check fields that default to label if not supplied.
|
|
void expectNotLabel(String? value, String name) {
|
|
expect(
|
|
value,
|
|
isNot(completion.label),
|
|
reason: '$name should not be set if same as label',
|
|
);
|
|
}
|
|
|
|
expectNotLabel(completion.insertText, 'insertText');
|
|
expectNotLabel(completion.filterText, 'filterText');
|
|
expectNotLabel(completion.sortText, 'sortText');
|
|
expectNotLabel(completion.textEditText, 'textEditText');
|
|
|
|
// If we have separate insert/replace ranges, they should not be the same.
|
|
if (textEditInsertRange != null) {
|
|
expect(textEditReplaceRange, isNot(textEditInsertRange));
|
|
}
|
|
|
|
// Check for empty labelDetails.
|
|
if (labelDetails != null) {
|
|
expect(
|
|
labelDetails.description != null || labelDetails.detail != null,
|
|
isTrue,
|
|
reason: 'labelDetails should be null if all fields are null',
|
|
);
|
|
}
|
|
|
|
// Check for empty lists.
|
|
void expectNotEmpty(Object? value, String name) {
|
|
expect(
|
|
value,
|
|
anyOf(isNull, isNotEmpty),
|
|
reason: '$name should be null if no items',
|
|
);
|
|
}
|
|
|
|
expectNotEmpty(completion.additionalTextEdits, 'additionalTextEdits');
|
|
expectNotEmpty(completion.commitCharacters, 'commitCharacters');
|
|
expectNotEmpty(completion.tags, 'tags');
|
|
|
|
// We convert numeric relevance scores into text because LSP uses text
|
|
// for sorting. We never need to use more digits in the sortText than there
|
|
// are in the maximum relevance for this.
|
|
//
|
|
// Only do this for sort texts that are numeric because we also use zzz
|
|
// prefixes with text for snippets.
|
|
if (sortText != null && int.tryParse(sortText) != null) {
|
|
expect(
|
|
sortText.length,
|
|
lessThanOrEqualTo(maximumRelevance.toString().length),
|
|
reason:
|
|
'sortText never needs to have more characters '
|
|
'than "$maximumRelevance" (${maximumRelevance.bitLength})',
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A helper that performs some checks on all completions sent back during
|
|
/// tests to check for any unnecessary data in the payload that could be
|
|
/// reduced.
|
|
void _assertMinimalCompletionListPayload(CompletionList completions) {
|
|
for (var completion in completions.items) {
|
|
var data = completion.data;
|
|
var commitCharacters = completion.commitCharacters;
|
|
var insertRange = completion.textEdit?.map(
|
|
(insertReplace) => insertReplace.insert,
|
|
(textEdit) => null,
|
|
);
|
|
var replaceRange = completion.textEdit?.map(
|
|
(insertReplace) => insertReplace.replace,
|
|
(textEdit) => null,
|
|
);
|
|
var combinedRange = completion.textEdit?.map(
|
|
(insertReplace) => null,
|
|
(textEdit) => textEdit.range,
|
|
);
|
|
var insertTextFormat = completion.insertTextFormat;
|
|
var insertTextMode = completion.insertTextMode;
|
|
|
|
var defaults = completions.itemDefaults;
|
|
var defaultInsertRange = defaults?.editRange?.map(
|
|
(editRange) => editRange.insert,
|
|
(range) => null,
|
|
);
|
|
var defaultReplaceRange = defaults?.editRange?.map(
|
|
(editRange) => editRange.replace,
|
|
(range) => null,
|
|
);
|
|
var defaultCombinedRange = defaults?.editRange?.map(
|
|
(editRange) => null,
|
|
(range) => range,
|
|
);
|
|
|
|
_assertMinimalCompletionItemPayload(completion);
|
|
|
|
// Check for fields matching defaults.
|
|
if (defaults != null) {
|
|
void expectNotDefault(Object? value, Object? default_, String name) {
|
|
if (value == null) return;
|
|
expect(
|
|
value,
|
|
isNot(default_),
|
|
reason: '$name should be omitted if same as default',
|
|
);
|
|
}
|
|
|
|
expectNotDefault(data?.toJson(), defaults.data, 'data');
|
|
expectNotDefault(
|
|
commitCharacters,
|
|
defaults.commitCharacters,
|
|
'commitCharacters',
|
|
);
|
|
|
|
expectNotDefault(insertRange, defaultInsertRange, 'insertRange');
|
|
expectNotDefault(replaceRange, defaultReplaceRange, 'replaceRange');
|
|
expectNotDefault(combinedRange, defaultCombinedRange, 'combined range');
|
|
expectNotDefault(
|
|
insertTextFormat,
|
|
defaults.insertTextFormat,
|
|
'insertTextFormat',
|
|
);
|
|
expectNotDefault(
|
|
insertTextMode,
|
|
defaults.insertTextMode,
|
|
'insertTextMode',
|
|
);
|
|
}
|
|
|
|
// If we have separate insert/replace ranges, they should not be the same.
|
|
if (defaultInsertRange != null) {
|
|
expect(defaultReplaceRange, isNot(defaultInsertRange));
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Function(Object?, LspJsonReporter) _canParseList<T>(
|
|
bool Function(Map<String, Object?>, LspJsonReporter) canParse,
|
|
) =>
|
|
(input, reporter) =>
|
|
input is List &&
|
|
input.cast<Map<String, Object?>>().every(
|
|
(item) => canParse(item, reporter),
|
|
);
|
|
|
|
List<T> Function(List<Object?>) _fromJsonList<T>(
|
|
T Function(Map<String, Object?>) fromJson,
|
|
) => (input) => input.cast<Map<String, Object?>>().map(fromJson).toList();
|
|
|
|
/// Creates a `canParse()` function for an `Either2<T1, T2>` using
|
|
/// the `canParse` function for each type.
|
|
static bool Function(Object?, LspJsonReporter) _generateCanParseFor<T1, T2>(
|
|
bool Function(Object?, LspJsonReporter) canParse1,
|
|
bool Function(Object?, LspJsonReporter) canParse2,
|
|
) {
|
|
return (input, reporter) =>
|
|
canParse1(input, reporter) || canParse2(input, reporter);
|
|
}
|
|
|
|
/// Creates a `fromJson()` function for an `Either2<T1, T2>` using
|
|
/// the `canParse` and `fromJson` functions for each type.
|
|
static Either2<T1, T2> Function(Object?) _generateFromJsonFor<T1, T2, R1, R2>(
|
|
bool Function(Object?, LspJsonReporter) canParse1,
|
|
T1 Function(R1) fromJson1,
|
|
bool Function(Object?, LspJsonReporter) canParse2,
|
|
T2 Function(R2) fromJson2, [
|
|
LspJsonReporter? reporter,
|
|
]) {
|
|
reporter ??= nullLspJsonReporter;
|
|
return (input) {
|
|
reporter!;
|
|
if (canParse1(input, reporter)) {
|
|
return Either2<T1, T2>.t1(fromJson1(input as R1));
|
|
}
|
|
if (canParse2(input, reporter)) {
|
|
return Either2<T1, T2>.t2(fromJson2(input as R2));
|
|
}
|
|
throw '$input was not one of ($T1, $T2)';
|
|
};
|
|
}
|
|
}
|
|
|
|
/// A mixin with helpers for verifying LSP edits in a given project.
|
|
///
|
|
/// Extends [LspEditHelpersMixin] with methods for accessing file state and
|
|
/// information about the project to build paths.
|
|
mixin LspVerifyEditHelpersMixin on LspEditHelpersMixin {
|
|
LspClientCapabilities get editorClientCapabilities;
|
|
|
|
path.Context get pathContext;
|
|
|
|
String get projectFolderPath;
|
|
|
|
ClientUriConverter get uriConverter;
|
|
|
|
/// Executes a function which is expected to call back to the client to apply
|
|
/// a [WorkspaceEdit].
|
|
///
|
|
/// Returns a [LspChangeVerifier] that can be used to verify changes.
|
|
Future<LspChangeVerifier> executeForEdits(
|
|
Future<Object?> Function() function, {
|
|
ApplyWorkspaceEditResult? applyEditResult,
|
|
}) async {
|
|
ApplyWorkspaceEditParams? editParams;
|
|
|
|
var commandResponse = await handleExpectedRequest<
|
|
Object?,
|
|
ApplyWorkspaceEditParams,
|
|
ApplyWorkspaceEditResult
|
|
>(
|
|
Method.workspace_applyEdit,
|
|
ApplyWorkspaceEditParams.fromJson,
|
|
function,
|
|
handler: (edit) {
|
|
// When the server sends the edit back, just keep a copy and say we
|
|
// applied successfully (it'll be verified by the caller).
|
|
editParams = edit;
|
|
return applyEditResult ?? ApplyWorkspaceEditResult(applied: true);
|
|
},
|
|
);
|
|
// Successful edits return an empty success() response.
|
|
expect(commandResponse, isNull);
|
|
|
|
// Ensure the edit came back, and using the expected change type.
|
|
expect(editParams, isNotNull);
|
|
var edit = editParams!.edit;
|
|
|
|
var expectDocumentChanges = editorClientCapabilities.documentChanges;
|
|
expect(edit.documentChanges, expectDocumentChanges ? isNotNull : isNull);
|
|
expect(edit.changes, expectDocumentChanges ? isNull : isNotNull);
|
|
|
|
return LspChangeVerifier(this, edit);
|
|
}
|
|
|
|
/// A function to get the current contents of a file to apply edits.
|
|
String? getCurrentFileContent(Uri uri);
|
|
|
|
Future<T> handleExpectedRequest<T, R, RR>(
|
|
Method method,
|
|
R Function(Map<String, dynamic>) fromJson,
|
|
Future<T> Function() f, {
|
|
required FutureOr<RR> Function(R) handler,
|
|
Duration timeout = const Duration(seconds: 5),
|
|
});
|
|
|
|
/// Formats a path relative to the project root always using forward slashes.
|
|
///
|
|
/// This is used in the text format for comparing edits.
|
|
String relativePath(String filePath) => pathContext
|
|
.relative(filePath, from: projectFolderPath)
|
|
.replaceAll(r'\', '/');
|
|
|
|
/// Formats a path relative to the project root always using forward slashes.
|
|
///
|
|
/// This is used in the text format for comparing edits.
|
|
String relativeUri(Uri uri) => relativePath(uriConverter.fromClientUri(uri));
|
|
}
|