diff --git a/pkg/analysis_server/doc/api.html b/pkg/analysis_server/doc/api.html index 0e3703b01c9..b688b6a9660 100644 --- a/pkg/analysis_server/doc/api.html +++ b/pkg/analysis_server/doc/api.html @@ -110,7 +110,7 @@ a:focus, a:hover {
This document contains a specification of the API provided by the @@ -245,6 +245,18 @@ a:focus, a:hover { ignoring the item or treating it with some default/fallback handling.
response: {
"id": String
@@ -593,6 +606,17 @@ a:focus, a:hover {
showMessageRequest
+ True if the client supports the server sending URIs in place of file paths. +
++ In this mode, the server will use URIs in all protocol fields with the type FilePath. Returned URIs may be `file://` URIs or custom schemes. The client can fetch the file contents for URIs with custom schemes (and receive modification events) through the LSP protocol (see the "lsp" domain). +
++ LSP notifications are automatically enabled when the client sets this capability. +
request: {
"id": String
"method": "server.openUrlRequest"
diff --git a/pkg/analysis_server/lib/protocol/protocol_constants.dart b/pkg/analysis_server/lib/protocol/protocol_constants.dart
index e15a9142ce3..b27fb1ef191 100644
--- a/pkg/analysis_server/lib/protocol/protocol_constants.dart
+++ b/pkg/analysis_server/lib/protocol/protocol_constants.dart
@@ -6,7 +6,7 @@
// To regenerate the file, use the script
// "pkg/analysis_server/tool/spec/generate_files".
-const String PROTOCOL_VERSION = '1.35.0';
+const String PROTOCOL_VERSION = '1.36.0';
const String ANALYSIS_NOTIFICATION_ANALYZED_FILES = 'analysis.analyzedFiles';
const String ANALYSIS_NOTIFICATION_ANALYZED_FILES_DIRECTORIES = 'directories';
@@ -273,6 +273,8 @@ const String FLUTTER_REQUEST_SET_WIDGET_PROPERTY_VALUE_ID = 'id';
const String FLUTTER_REQUEST_SET_WIDGET_PROPERTY_VALUE_VALUE = 'value';
const String FLUTTER_RESPONSE_GET_WIDGET_DESCRIPTION_PROPERTIES = 'properties';
const String FLUTTER_RESPONSE_SET_WIDGET_PROPERTY_VALUE_CHANGE = 'change';
+const String LSP_NOTIFICATION_NOTIFICATION = 'lsp.notification';
+const String LSP_NOTIFICATION_NOTIFICATION_LSP_NOTIFICATION = 'lspNotification';
const String LSP_REQUEST_HANDLE = 'lsp.handle';
const String LSP_REQUEST_HANDLE_LSP_MESSAGE = 'lspMessage';
const String LSP_RESPONSE_HANDLE_LSP_RESPONSE = 'lspResponse';
@@ -334,6 +336,8 @@ const String SERVER_REQUEST_OPEN_URL_REQUEST_URL = 'url';
const String SERVER_REQUEST_SET_CLIENT_CAPABILITIES =
'server.setClientCapabilities';
const String SERVER_REQUEST_SET_CLIENT_CAPABILITIES_REQUESTS = 'requests';
+const String SERVER_REQUEST_SET_CLIENT_CAPABILITIES_SUPPORTS_URIS =
+ 'supportsUris';
const String SERVER_REQUEST_SET_SUBSCRIPTIONS = 'server.setSubscriptions';
const String SERVER_REQUEST_SET_SUBSCRIPTIONS_SUBSCRIPTIONS = 'subscriptions';
const String SERVER_REQUEST_SHOW_MESSAGE_REQUEST = 'server.showMessageRequest';
diff --git a/pkg/analysis_server/lib/protocol/protocol_generated.dart b/pkg/analysis_server/lib/protocol/protocol_generated.dart
index 82197fa8562..bf372a97dfe 100644
--- a/pkg/analysis_server/lib/protocol/protocol_generated.dart
+++ b/pkg/analysis_server/lib/protocol/protocol_generated.dart
@@ -12028,6 +12028,66 @@ class LspHandleResult implements ResponseResult {
int get hashCode => lspResponse.hashCode;
}
+/// lsp.notification params
+///
+/// {
+/// "lspNotification": object
+/// }
+///
+/// Clients may not extend, implement or mix-in this class.
+class LspNotificationParams implements HasToJson {
+ /// The LSP NotificationMessage sent by the server.
+ Object lspNotification;
+
+ LspNotificationParams(this.lspNotification);
+
+ factory LspNotificationParams.fromJson(
+ JsonDecoder jsonDecoder, String jsonPath, Object? json) {
+ json ??= {};
+ if (json is Map) {
+ Object lspNotification;
+ if (json.containsKey('lspNotification')) {
+ lspNotification = json['lspNotification'] as Object;
+ } else {
+ throw jsonDecoder.mismatch(jsonPath, 'lspNotification');
+ }
+ return LspNotificationParams(lspNotification);
+ } else {
+ throw jsonDecoder.mismatch(jsonPath, 'lsp.notification params', json);
+ }
+ }
+
+ factory LspNotificationParams.fromNotification(Notification notification) {
+ return LspNotificationParams.fromJson(
+ ResponseDecoder(null), 'params', notification.params);
+ }
+
+ @override
+ Map toJson() {
+ var result = {};
+ result['lspNotification'] = lspNotification;
+ return result;
+ }
+
+ Notification toNotification() {
+ return Notification('lsp.notification', toJson());
+ }
+
+ @override
+ String toString() => json.encode(toJson());
+
+ @override
+ bool operator ==(other) {
+ if (other is LspNotificationParams) {
+ return lspNotification == other.lspNotification;
+ }
+ return false;
+ }
+
+ @override
+ int get hashCode => lspNotification.hashCode;
+}
+
/// MessageAction
///
/// {
@@ -15482,6 +15542,7 @@ class ServerService implements Enum {
///
/// {
/// "requests": List
+/// "supportsUris": optional bool
/// }
///
/// Clients may not extend, implement or mix-in this class.
@@ -15501,7 +15562,20 @@ class ServerSetClientCapabilitiesParams implements RequestParams {
/// - showMessageRequest
List requests;
- ServerSetClientCapabilitiesParams(this.requests);
+ /// True if the client supports the server sending URIs in place of file
+ /// paths.
+ ///
+ /// In this mode, the server will use URIs in all protocol fields with the
+ /// type FilePath. Returned URIs may be `file://` URIs or custom schemes. The
+ /// client can fetch the file contents for URIs with custom schemes (and
+ /// receive modification events) through the LSP protocol (see the "lsp"
+ /// domain).
+ ///
+ /// LSP notifications are automatically enabled when the client sets this
+ /// capability.
+ bool? supportsUris;
+
+ ServerSetClientCapabilitiesParams(this.requests, {this.supportsUris});
factory ServerSetClientCapabilitiesParams.fromJson(
JsonDecoder jsonDecoder, String jsonPath, Object? json) {
@@ -15514,7 +15588,13 @@ class ServerSetClientCapabilitiesParams implements RequestParams {
} else {
throw jsonDecoder.mismatch(jsonPath, 'requests');
}
- return ServerSetClientCapabilitiesParams(requests);
+ bool? supportsUris;
+ if (json.containsKey('supportsUris')) {
+ supportsUris = jsonDecoder.decodeBool(
+ '$jsonPath.supportsUris', json['supportsUris']);
+ }
+ return ServerSetClientCapabilitiesParams(requests,
+ supportsUris: supportsUris);
} else {
throw jsonDecoder.mismatch(
jsonPath, 'server.setClientCapabilities params', json);
@@ -15530,6 +15610,10 @@ class ServerSetClientCapabilitiesParams implements RequestParams {
Map toJson() {
var result = {};
result['requests'] = requests;
+ var supportsUris = this.supportsUris;
+ if (supportsUris != null) {
+ result['supportsUris'] = supportsUris;
+ }
return result;
}
@@ -15545,13 +15629,17 @@ class ServerSetClientCapabilitiesParams implements RequestParams {
bool operator ==(other) {
if (other is ServerSetClientCapabilitiesParams) {
return listEqual(
- requests, other.requests, (String a, String b) => a == b);
+ requests, other.requests, (String a, String b) => a == b) &&
+ supportsUris == other.supportsUris;
}
return false;
}
@override
- int get hashCode => Object.hashAll(requests);
+ int get hashCode => Object.hash(
+ Object.hashAll(requests),
+ supportsUris,
+ );
}
/// server.setClientCapabilities result
diff --git a/pkg/analysis_server/lib/src/analysis_server.dart b/pkg/analysis_server/lib/src/analysis_server.dart
index df80ceca931..c0532fde9f7 100644
--- a/pkg/analysis_server/lib/src/analysis_server.dart
+++ b/pkg/analysis_server/lib/src/analysis_server.dart
@@ -6,8 +6,7 @@ import 'dart:async';
import 'dart:io' as io;
import 'dart:io';
-import 'package:analysis_server/lsp_protocol/protocol.dart' as lsp
- show MessageType, OptionalVersionedTextDocumentIdentifier;
+import 'package:analysis_server/lsp_protocol/protocol.dart' as lsp;
import 'package:analysis_server/src/analytics/analytics_manager.dart';
import 'package:analysis_server/src/collections.dart';
import 'package:analysis_server/src/context_manager.dart';
@@ -15,6 +14,7 @@ import 'package:analysis_server/src/domains/completion/available_suggestions.dar
import 'package:analysis_server/src/legacy_analysis_server.dart';
import 'package:analysis_server/src/lsp/client_capabilities.dart';
import 'package:analysis_server/src/lsp/client_configuration.dart';
+import 'package:analysis_server/src/lsp/constants.dart' as lsp;
import 'package:analysis_server/src/plugin/notification_manager.dart';
import 'package:analysis_server/src/plugin/plugin_manager.dart';
import 'package:analysis_server/src/plugin/plugin_watcher.dart';
@@ -74,7 +74,8 @@ import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:analyzer/src/util/performance/operation_performance.dart';
import 'package:analyzer/src/utilities/extensions/analysis_session.dart';
import 'package:analyzer_plugin/protocol/protocol.dart';
-import 'package:analyzer_plugin/src/protocol/protocol_internal.dart';
+import 'package:analyzer_plugin/src/protocol/protocol_internal.dart'
+ as analyzer_plugin;
import 'package:analyzer_plugin/src/utilities/client_uri_converter.dart';
import 'package:collection/collection.dart';
import 'package:http/http.dart' as http;
@@ -217,10 +218,6 @@ abstract class AnalysisServer {
/// the last idle state.
final Set filesResolvedSinceLastIdle = {};
- /// A converter to change incoming client URIs into analyzer file references
- /// (and back).
- ClientUriConverter uriConverter;
-
/// A mapping of [ProducerGenerator]s to the set of lint names with which they
/// are associated (can fix).
final Map> producerGeneratorsForLintRules;
@@ -240,11 +237,13 @@ abstract class AnalysisServer {
bool enableBlazeWatcher = false,
DartFixPromptManager? dartFixPromptManager,
}) : resourceProvider = OverlayResourceProvider(baseResourceProvider),
- uriConverter =
- ClientUriConverter.noop(baseResourceProvider.pathContext),
pubApi = PubApi(instrumentationService, httpClient,
Platform.environment['PUB_HOSTED_URL']),
producerGeneratorsForLintRules = AssistProcessor.computeLintRuleMap() {
+ // Set the default URI converter. This uses the resource providers path
+ // context (unlike the initialized value) which allows tests to override it.
+ uriConverter = ClientUriConverter.noop(baseResourceProvider.pathContext);
+
// We can only spawn processes (eg. to run pub commands) when backed by
// a real file system, otherwise we may try to run commands in folders that
// don't really exist. If processRunner was supplied, it's likely a mock
@@ -389,6 +388,21 @@ abstract class AnalysisServer {
return DateTime.now().difference(start);
}
+ /// Gets the converter to change incoming client URIs into analyzer file
+ /// references (and back).
+ ///
+ /// Currently backed by a global for use by toJson/fromJson in the legacy
+ /// protocol classes.
+ ClientUriConverter get uriConverter => analyzer_plugin.clientUriConverter;
+
+ /// Sets the converter to change incoming client URIs into analyzer file
+ /// references (and back).
+ ///
+ /// Currently backed by a global for use by toJson/fromJson in the legacy
+ /// protocol classes.
+ set uriConverter(ClientUriConverter converter) =>
+ analyzer_plugin.clientUriConverter = converter;
+
/// Returns the function for sending prompts to the user and collecting button
/// presses.
///
@@ -418,7 +432,8 @@ abstract class AnalysisServer {
/// the plugins have sent a response, or an empty list if no [driver] is
/// provided.
Map> broadcastRequestToPlugins(
- RequestParams requestParams, analysis.AnalysisDriver? driver) {
+ analyzer_plugin.RequestParams requestParams,
+ analysis.AnalysisDriver? driver) {
if (driver == null || !AnalysisServer.supportsPlugins) {
return >{};
}
@@ -828,6 +843,12 @@ abstract class AnalysisServer {
return null;
}
+ /// Sends an LSP notification to the client.
+ ///
+ /// The legacy server will wrap LSP notifications inside an
+ /// 'lsp.notification' notification.
+ void sendLspNotification(lsp.NotificationMessage notification);
+
/// Sends an error notification to the user.
void sendServerErrorNotification(
String message,
@@ -957,6 +978,23 @@ abstract class CommonServerContextManagerCallbacks
analysisServer.filesResolvedSinceLastIdle.add(path);
handleResolvedUnitResult(result);
}
+
+ // If this is a virtual file and the client supports URIs, we need to notify
+ // that it's been updated.
+ var lspUri = analysisServer.uriConverter.toClientUri(result.path);
+ if (!lspUri.isScheme('file')) {
+ // TODO(dantup): Should we do any kind of tracking here to avoid sending
+ // lots of notifications if there aren't actual changes?
+ // TODO(dantup): We may be able to skip sending this if the file is not
+ // open (priority) depending on the response to
+ // https://github.com/microsoft/vscode/issues/202017
+ var message = lsp.NotificationMessage(
+ method: lsp.CustomMethods.dartTextDocumentContentDidChange,
+ params: lsp.DartTextDocumentContentDidChangeParams(uri: lspUri),
+ jsonrpc: lsp.jsonRpcVersion,
+ );
+ analysisServer.sendLspNotification(message);
+ }
}
void handleResolvedUnitResult(ResolvedUnitResult result);
diff --git a/pkg/analysis_server/lib/src/handler/legacy/lsp_over_legacy_handler.dart b/pkg/analysis_server/lib/src/handler/legacy/lsp_over_legacy_handler.dart
index 703ae91c66b..620630bb45e 100644
--- a/pkg/analysis_server/lib/src/handler/legacy/lsp_over_legacy_handler.dart
+++ b/pkg/analysis_server/lib/src/handler/legacy/lsp_over_legacy_handler.dart
@@ -38,6 +38,7 @@ class LspOverLegacyHandler extends LegacyHandler {
@override
Future handle() async {
+ server.sendLspNotifications = true;
final params = LspHandleParams.fromRequest(request);
final lspMessageJson = params.lspMessage;
final reporter = LspJsonReporter();
diff --git a/pkg/analysis_server/lib/src/legacy_analysis_server.dart b/pkg/analysis_server/lib/src/legacy_analysis_server.dart
index 9482c2a7e5b..f22ed840774 100644
--- a/pkg/analysis_server/lib/src/legacy_analysis_server.dart
+++ b/pkg/analysis_server/lib/src/legacy_analysis_server.dart
@@ -107,6 +107,7 @@ import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:analyzer/src/util/performance/operation_performance.dart';
import 'package:analyzer/src/utilities/cancellation.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart' hide Element;
+import 'package:analyzer_plugin/src/utilities/client_uri_converter.dart';
import 'package:analyzer_plugin/src/utilities/navigation/navigation.dart';
import 'package:analyzer_plugin/utilities/navigation/navigation_dart.dart';
import 'package:http/http.dart' as http;
@@ -171,7 +172,7 @@ class LegacyAnalysisServer extends AnalysisServer {
/// handler.
///
/// Requests that don't match anything in this map will be passed to
- /// [_LspOverLegacyHandler] for possible handling before returning an error.
+ /// [LspOverLegacyHandler] for possible handling before returning an error.
static final Map requestHandlerGenerators = {
ANALYSIS_REQUEST_GET_ERRORS: AnalysisGetErrorsHandler.new,
ANALYSIS_REQUEST_GET_HOVER: AnalysisGetHoverHandler.new,
@@ -282,8 +283,10 @@ class LegacyAnalysisServer extends AnalysisServer {
Map> analysisServices = {};
/// The most recently registered set of client capabilities. The default is to
- /// have no registered requests.
- ServerSetClientCapabilitiesParams clientCapabilities =
+ /// have no registered requests and no additional capabilities.
+ ///
+ /// Must be modified through the [clientCapabilities] setter.
+ ServerSetClientCapabilitiesParams _clientCapabilities =
ServerSetClientCapabilitiesParams([]);
@override
@@ -362,6 +365,12 @@ class LegacyAnalysisServer extends AnalysisServer {
/// response when it has been received.
Map> pendingServerRequests = {};
+ /// Whether the server should send LSP notifications.
+ ///
+ /// This is set once the client sends any LSP request or client capability
+ /// that depends on LSP functionality.
+ bool sendLspNotifications = false;
+
/// Initialize a newly created server to receive requests from and send
/// responses to the given [channel].
///
@@ -426,6 +435,26 @@ class LegacyAnalysisServer extends AnalysisServer {
_newRefactoringManager();
}
+ /// The most recently registered set of client capabilities. The default is to
+ /// have no registered requests and no additional capabilities.
+ ServerSetClientCapabilitiesParams get clientCapabilities =>
+ _clientCapabilities;
+
+ /// Updates the current set of client capabilities.
+ set clientCapabilities(ServerSetClientCapabilitiesParams capabilities) {
+ _clientCapabilities = capabilities;
+
+ if (capabilities.supportsUris ?? false) {
+ // URI support implies LSP, as that's the only way to access (and get
+ // change notifications for) custom-scheme files.
+ sendLspNotifications = true;
+ uriConverter = ClientUriConverter.withVirtualFileSupport(
+ resourceProvider.pathContext);
+ } else {
+ uriConverter = ClientUriConverter.noop(resourceProvider.pathContext);
+ }
+ }
+
/// The [Future] that completes when analysis is complete.
Future get onAnalysisComplete {
if (isAnalysisComplete()) {
@@ -627,6 +656,18 @@ class LegacyAnalysisServer extends AnalysisServer {
flutterWidgetDescriptions.flush();
}
+ /// Send the given LSP [notification] to the client.
+ @override
+ void sendLspNotification(lsp.NotificationMessage notification) {
+ if (!sendLspNotifications) {
+ return;
+ }
+
+ channel.sendNotification(
+ LspNotificationParams(notification).toNotification(),
+ );
+ }
+
/// Send the given [notification] to the client.
void sendNotification(Notification notification) {
channel.sendNotification(notification);
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_dart_text_document_content_provider.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_dart_text_document_content_provider.dart
index cdffb83f785..a6877a9f05b 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_dart_text_document_content_provider.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_dart_text_document_content_provider.dart
@@ -30,11 +30,13 @@ class DartTextDocumentContentProviderHandler extends SharedMessageHandler<
var uri = params.uri;
if (!allowedSchemes.contains(uri.scheme)) {
+ var supportedSchemesString = allowedSchemes.isEmpty
+ ? '(none)'
+ : allowedSchemes.map((scheme) => "'$scheme'").join(', ');
return error(
ErrorCodes.InvalidParams,
"Fetching content for scheme '${uri.scheme}' is not supported. "
- 'Supported schemes are '
- '${allowedSchemes.map((scheme) => "'$scheme'").join(', ')}.',
+ 'Supported schemes are $supportedSchemesString.',
);
}
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handlers.dart b/pkg/analysis_server/lib/src/lsp/handlers/handlers.dart
index 93807171997..4e137897bbb 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handlers.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handlers.dart
@@ -124,8 +124,9 @@ mixin HandlerHelperMixin {
var supportedSchemes = server.uriConverter.supportedSchemes;
var isValidScheme = supportedSchemes.contains(uri.scheme);
if (!isValidScheme) {
- var supportedSchemesString =
- supportedSchemes.map((scheme) => "'$scheme'").join(', ');
+ var supportedSchemesString = supportedSchemes.isEmpty
+ ? '(none)'
+ : supportedSchemes.map((scheme) => "'$scheme'").join(', ');
return ErrorOr.error(ResponseError(
code: ServerErrorCodes.InvalidFilePath,
message: "URI scheme '${uri.scheme}' is not supported. "
diff --git a/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart b/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
index 8cd1c0a7620..070bff4d321 100644
--- a/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
+++ b/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
@@ -244,7 +244,7 @@ class LspAnalysisServer extends AnalysisServer {
params: params,
jsonrpc: jsonRpcVersion,
);
- sendNotification(message);
+ sendLspNotification(message);
};
}
@@ -637,7 +637,7 @@ class LspAnalysisServer extends AnalysisServer {
params: params,
jsonrpc: jsonRpcVersion,
);
- sendNotification(message);
+ sendLspNotification(message);
}
void publishDiagnostics(String path, List errors) {
@@ -659,7 +659,7 @@ class LspAnalysisServer extends AnalysisServer {
params: params,
jsonrpc: jsonRpcVersion,
);
- sendNotification(message);
+ sendLspNotification(message);
}
void publishFlutterOutline(String path, FlutterOutline outline) {
@@ -670,7 +670,7 @@ class LspAnalysisServer extends AnalysisServer {
params: params,
jsonrpc: jsonRpcVersion,
);
- sendNotification(message);
+ sendLspNotification(message);
}
void publishOutline(String path, Outline outline) {
@@ -681,7 +681,7 @@ class LspAnalysisServer extends AnalysisServer {
params: params,
jsonrpc: jsonRpcVersion,
);
- sendNotification(message);
+ sendLspNotification(message);
}
Future removePriorityFile(String path) async {
@@ -722,7 +722,8 @@ class LspAnalysisServer extends AnalysisServer {
}
/// Send the given [notification] to the client.
- void sendNotification(NotificationMessage notification) {
+ @override
+ void sendLspNotification(NotificationMessage notification) {
channel.sendNotification(notification);
}
@@ -1159,23 +1160,6 @@ class LspServerContextManagerCallbacks
return;
}
- // If this is a virtual file, we need to notify the client that it's been
- // updated.
- var lspUri = analysisServer.uriConverter.toClientUri(result.path);
- if (!lspUri.isScheme('file')) {
- // TODO(dantup): Should we do any kind of tracking here to avoid sending
- // lots of notifications if there aren't actual changes?
- // TODO(dantup): We may be able to skip sending this if the file is not
- // open (priority) depending on the response to
- // https://github.com/microsoft/vscode/issues/202017
- var message = NotificationMessage(
- method: CustomMethods.dartTextDocumentContentDidChange,
- params: DartTextDocumentContentDidChangeParams(uri: lspUri),
- jsonrpc: jsonRpcVersion,
- );
- analysisServer.sendNotification(message);
- }
-
super.handleFileResult(result);
}
diff --git a/pkg/analysis_server/lib/src/lsp/progress.dart b/pkg/analysis_server/lib/src/lsp/progress.dart
index 17b3e3ab3a1..70599143389 100644
--- a/pkg/analysis_server/lib/src/lsp/progress.dart
+++ b/pkg/analysis_server/lib/src/lsp/progress.dart
@@ -121,7 +121,7 @@ class _TokenProgressReporter extends ProgressReporter {
}
void _sendNotification(ToJsonable value) async {
- _server.sendNotification(NotificationMessage(
+ _server.sendLspNotification(NotificationMessage(
method: Method.progress,
params: ProgressParams(
token: _token,
diff --git a/pkg/analysis_server/test/domain_server_test.dart b/pkg/analysis_server/test/domain_server_test.dart
index 5bc1745a98e..fccd17e8804 100644
--- a/pkg/analysis_server/test/domain_server_test.dart
+++ b/pkg/analysis_server/test/domain_server_test.dart
@@ -92,7 +92,7 @@ class ServerDomainTest extends PubPackageAnalysisServerTest {
await responseFuture;
}
- Future test_setClientCapabilities() async {
+ Future test_setClientCapabilities_requests() async {
var requestId = -1;
Future setCapabilities(
@@ -124,6 +124,151 @@ class ServerDomainTest extends PubPackageAnalysisServerTest {
await setCapabilities(openUrlRequest: false, showMessageRequest: false);
}
+ /// Verify that the server handles URIs once we've enabled the supportsUris
+ /// client capability.
+ Future
+ test_setClientCapabilities_supportsUris_clientToServer_request() async {
+ // Tell the server we support URIs.
+ await handleSuccessfulRequest(
+ ServerSetClientCapabilitiesParams([], supportsUris: true)
+ .toRequest('-1'));
+
+ // Set the roots using a URI. Since the helper methods will to through
+ // toJson() (which will convert paths to URIs) we need to pass the JSON
+ // manually here.
+ await handleSuccessfulRequest(Request('1', 'analysis.setAnalysisRoots', {
+ 'included': [toUri(workspaceRootPath).toString()],
+ 'excluded': [],
+ }));
+ await pumpEventQueue(times: 5000);
+
+ // Ensure the roots were recorded correctly.
+ expect(
+ server.contextManager.includedPaths,
+ [convertPath(workspaceRootPath)],
+ );
+ }
+
+ Future test_setClientCapabilities_supportsUris_defaults() async {
+ // Before request.
+ expect(server.clientCapabilities.supportsUris, isNull);
+ expect(server.uriConverter.supportedNonFileSchemes, isEmpty);
+
+ // If not supplied.
+ await handleSuccessfulRequest(
+ ServerSetClientCapabilitiesParams([]).toRequest('-1'));
+ expect(server.clientCapabilities.supportsUris, isNull);
+ expect(server.uriConverter.supportedNonFileSchemes, isEmpty);
+
+ // If set explicitly to false.
+ await handleSuccessfulRequest(
+ ServerSetClientCapabilitiesParams([], supportsUris: false)
+ .toRequest('-1'));
+ expect(server.clientCapabilities.supportsUris, isFalse);
+ expect(server.uriConverter.supportedNonFileSchemes, isEmpty);
+ }
+
+ Future
+ test_setClientCapabilities_supportsUris_false_rejectsUris() async {
+ // Explicitly tell the server we do not support URIs.
+ await handleSuccessfulRequest(
+ ServerSetClientCapabilitiesParams([], supportsUris: false)
+ .toRequest('-1'));
+
+ // Try to send a URI anyway.
+ var request = Request('1', 'analysis.setAnalysisRoots', {
+ 'included': [toUri(workspaceRootPath).toString()],
+ 'excluded': [],
+ });
+ var response = await handleRequest(request);
+ expect(
+ response,
+ isResponseFailure(
+ request.id, RequestErrorCode.INVALID_FILE_PATH_FORMAT));
+ }
+
+ /// Verify that the server uses URIs in notifications once we've enabled the
+ /// supportsUris client capability.
+ Future
+ test_setClientCapabilities_supportsUris_serverToClient_notification() async {
+ // Add a file with an error for testing.
+ var testFilePath = convertPath('$testPackageLibPath/test.dart');
+ var testFileUriString = toUri(testFilePath).toString();
+ newFile(testFilePath, 'broken');
+
+ // Tell the server we support URIs before analysis starts since we will
+ // verify the analysis.errors notification.
+ await handleSuccessfulRequest(
+ ServerSetClientCapabilitiesParams([], supportsUris: true)
+ .toRequest('10'));
+ await pumpEventQueue(times: 5000);
+
+ // Trigger analysis.
+ await setRoots(
+ // We can use paths here because toJson() will handle the conversion.
+ included: [workspaceRootPath],
+ excluded: [],
+ );
+ await pumpEventQueue(times: 5000);
+
+ // Verify the last error for this file was using a URI.
+ var lastErrorFile = serverChannel.notificationsReceived
+ .where((notification) => notification.event == 'analysis.errors')
+ .map((notification) => notification.params!['file'] as String)
+ .lastWhere((filePath) => filePath.endsWith('test.dart'));
+ expect(
+ lastErrorFile,
+ testFileUriString,
+ );
+ }
+
+ /// Verify that the server returns URIs once we've enabled the supportsUris
+ /// client capability.
+ Future
+ test_setClientCapabilities_supportsUris_serverToClient_response() async {
+ // Add a file with an error for testing.
+ var testFilePath = convertPath('$testPackageLibPath/test.dart');
+ var testFileUriString = toUri(testFilePath).toString();
+ newFile(testFilePath, 'broken');
+
+ await setRoots(included: [workspaceRootPath], excluded: []);
+ await pumpEventQueue(times: 5000);
+
+ // Tell the server we support URIs.
+ await handleSuccessfulRequest(
+ ServerSetClientCapabilitiesParams([], supportsUris: true)
+ .toRequest('10'));
+ await pumpEventQueue(times: 5000);
+
+ // Send a GetErrors request. The response has nested FilePaths inside the
+ // AnalysisErrors so will confirm the server mapped correctly.
+ var response =
+ await handleSuccessfulRequest(Request('1', 'analysis.getErrors', {
+ 'file': testFileUriString,
+ }));
+ // Verify the error location was the expected URI.
+ expect(
+ (response.result as dynamic)['errors'][0]!['location']['file'],
+ testFileUriString,
+ );
+ }
+
+ Future
+ test_setClientCapabilities_supportsUris_unspecified_rejectsUris() async {
+ // Do not tell the server we support URIs.
+
+ // Try to send a URI anyway.
+ var request = Request('1', 'analysis.setAnalysisRoots', {
+ 'included': [toUri(workspaceRootPath).toString()],
+ 'excluded': [],
+ });
+ var response = await handleRequest(request);
+ expect(
+ response,
+ isResponseFailure(
+ request.id, RequestErrorCode.INVALID_FILE_PATH_FORMAT));
+ }
+
Future test_setSubscriptions_invalidServiceName() async {
var request = Request('0', SERVER_REQUEST_SET_SUBSCRIPTIONS, {
SUBSCRIPTIONS: ['noSuchService']
diff --git a/pkg/analysis_server/test/integration/coverage.md b/pkg/analysis_server/test/integration/coverage.md
index 77dabad49ae..0362cb75e37 100644
--- a/pkg/analysis_server/test/integration/coverage.md
+++ b/pkg/analysis_server/test/integration/coverage.md
@@ -105,3 +105,4 @@ server calls. This file is validated by `coverage_test.dart`.
## lsp domain
- [x] lsp.handle
+- [x] lsp.notification
diff --git a/pkg/analysis_server/test/integration/coverage_test.dart b/pkg/analysis_server/test/integration/coverage_test.dart
index d7dbb39ab4c..410275a2b16 100644
--- a/pkg/analysis_server/test/integration/coverage_test.dart
+++ b/pkg/analysis_server/test/integration/coverage_test.dart
@@ -110,9 +110,12 @@ void main() {
// Test that if checked, a test file exists; if not checked, no such
// file exists.
- expect(FileSystemEntity.isFileSync(testPath),
- coveredMembers.contains(fullName),
- reason: '$testName state incorrect');
+ var fileExists = FileSystemEntity.isFileSync(testPath);
+ var isMarkedAsCovered = coveredMembers.contains(fullName);
+ expect(fileExists, isMarkedAsCovered,
+ reason: isMarkedAsCovered
+ ? '$testName marked as covered but has no test at $testPath'
+ : '$testName marked as not covered has test at $testPath');
});
}
});
diff --git a/pkg/analysis_server/test/integration/lsp/abstract_lsp_over_legacy.dart b/pkg/analysis_server/test/integration/lsp/abstract_lsp_over_legacy.dart
new file mode 100644
index 00000000000..7d2a874d0f7
--- /dev/null
+++ b/pkg/analysis_server/test/integration/lsp/abstract_lsp_over_legacy.dart
@@ -0,0 +1,65 @@
+// Copyright (c) 2024, 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:analyzer_plugin/src/utilities/client_uri_converter.dart';
+import 'package:test/test.dart';
+
+import '../../lsp/request_helpers_mixin.dart';
+import '../support/integration_tests.dart';
+
+abstract class AbstractLspOverLegacyTest
+ extends AbstractAnalysisServerIntegrationTest
+ with LspRequestHelpersMixin, LspEditHelpersMixin {
+ late final testFile = sourcePath('lib/test.dart');
+
+ /// A stream of LSP [NotificationMessage]s from the server.
+ @override
+ Stream get notificationsFromServer =>
+ onLspNotification.map((params) => NotificationMessage.fromJson(
+ params.lspNotification as Map));
+
+ /// The URI for the macro-generated content for [testFileUri].
+ Uri get testFileMacroUri =>
+ Uri.file(testFile).replace(scheme: macroClientUriScheme);
+
+ Uri get testFileUri => Uri.file(testFile);
+
+ void expectMarkdown(
+ Either2 contents,
+ String expected,
+ ) {
+ final markup = contents.map(
+ (t1) => t1,
+ (t2) => throw 'Hover contents were String, not MarkupContent',
+ );
+
+ expect(markup.kind, MarkupKind.Markdown);
+ expect(markup.value.trimRight(), expected.trimRight());
+ }
+
+ @override
+ Future expectSuccessfulResponseTo(
+ RequestMessage message,
+ T Function(R) fromJson,
+ ) async {
+ final legacyResult = await sendLspHandle(message.toJson());
+ final lspResponseJson = legacyResult.lspResponse as Map;
+
+ // Unwrap the LSP response.
+ final lspResponse = ResponseMessage.fromJson(lspResponseJson);
+ final error = lspResponse.error;
+ if (error != null) {
+ throw error;
+ } else if (T == Null) {
+ return lspResponse.result == null
+ ? null as T
+ : throw 'Expected Null response but got ${lspResponse.result}';
+ } else {
+ return fromJson(lspResponse.result as R);
+ }
+ }
+}
diff --git a/pkg/analysis_server/test/integration/lsp/handle_test.dart b/pkg/analysis_server/test/integration/lsp/handle_test.dart
index 753df072ca0..1b66121f132 100644
--- a/pkg/analysis_server/test/integration/lsp/handle_test.dart
+++ b/pkg/analysis_server/test/integration/lsp/handle_test.dart
@@ -10,55 +10,28 @@ import 'package:analyzer/src/test_utilities/test_code_format.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
-import '../../lsp/request_helpers_mixin.dart';
import '../../tool/lsp_spec/matchers.dart';
import '../../utils/test_code_extensions.dart';
import '../support/integration_tests.dart';
+import 'abstract_lsp_over_legacy.dart';
void main() {
defineReflectiveSuite(() {
- defineReflectiveTests(LspOverLegacyTest);
+ defineReflectiveTests(LspOverLegacyRequestTest);
});
}
-/// Integration tests for using LSP over the Legacy protocol.
+/// Integration tests for sending LSP requests over the Legacy protocol.
///
/// These tests are slow (each test spawns an out-of-process server) so these
/// tests are intended only to ensure the basic functionality is available and
-/// not to test all handlers/functionality already are covered by LSP tests.
+/// not to test all handlers/functionality already covered by LSP tests.
///
/// Additional tests (to verify each expected LSP handler is available over
/// Legacy) are in `test/lsp_over_legacy/` and tests for all handler
/// functionality are in `test/lsp`.
@reflectiveTest
-class LspOverLegacyTest extends AbstractAnalysisServerIntegrationTest
- with LspRequestHelpersMixin, LspEditHelpersMixin {
- late final testFile = sourcePath('lib/test.dart');
-
- Uri get testFileUri => Uri.file(testFile);
-
- @override
- Future expectSuccessfulResponseTo(
- RequestMessage message,
- T Function(R) fromJson,
- ) async {
- final legacyResult = await sendLspHandle(message.toJson());
- final lspResponseJson = legacyResult.lspResponse as Map;
-
- // Unwrap the LSP response.
- final lspResponse = ResponseMessage.fromJson(lspResponseJson);
- final error = lspResponse.error;
- if (error != null) {
- throw error;
- } else if (T == Null) {
- return lspResponse.result == null
- ? null as T
- : throw 'Expected Null response but got ${lspResponse.result}';
- } else {
- return fromJson(lspResponse.result as R);
- }
- }
-
+class LspOverLegacyRequestTest extends AbstractLspOverLegacyTest {
Future test_error_invalidLspRequest() async {
await standardAnalysisSetup();
await analysisFinished;
@@ -112,7 +85,7 @@ class [!A^aa!] {}
final result = await getHover(testFileUri, code.position.position);
expect(result!.range, code.range.range);
- _expectMarkdown(
+ expectMarkdown(
result.contents,
'''
```dart
@@ -173,17 +146,4 @@ This is my class.''';
}
});
}
-
- void _expectMarkdown(
- Either2 contents,
- String expected,
- ) {
- final markup = contents.map(
- (t1) => t1,
- (t2) => throw 'Hover contents were String, not MarkupContent',
- );
-
- expect(markup.kind, MarkupKind.Markdown);
- expect(markup.value.trimRight(), expected.trimRight());
- }
}
diff --git a/pkg/analysis_server/test/integration/lsp/notification_test.dart b/pkg/analysis_server/test/integration/lsp/notification_test.dart
new file mode 100644
index 00000000000..0a9a2873c78
--- /dev/null
+++ b/pkg/analysis_server/test/integration/lsp/notification_test.dart
@@ -0,0 +1,96 @@
+// Copyright (c) 2024, 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/src/lsp/test_macros.dart';
+import 'package:analyzer/src/util/file_paths.dart' as file_paths;
+import 'package:analyzer_plugin/src/protocol/protocol_internal.dart'
+ as analyzer_plugin;
+import 'package:analyzer_plugin/src/utilities/client_uri_converter.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import '../../analysis_server_base.dart';
+import 'abstract_lsp_over_legacy.dart';
+
+void main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(LspOverLegacyNotificationTest);
+ });
+}
+
+/// Integration tests for receiving LSP notifications over the Legacy protocol.
+///
+/// These tests are slow (each test spawns an out-of-process server) so these
+/// tests are intended only to ensure the basic functionality is available and
+/// not to test all handlers/functionality already covered by LSP tests.
+///
+/// Additional tests (to verify each expected LSP handler is available over
+/// Legacy) are in `test/lsp_over_legacy/` and tests for all handler
+/// functionality are in `test/lsp`.
+@reflectiveTest
+class LspOverLegacyNotificationTest extends AbstractLspOverLegacyTest
+ with TestMacros {
+ /// Tells the server we support custom URIs, otherwise we won't be allowed to
+ /// fetch any content from a URI.
+ Future enableCustomUriSupport() async {
+ // Tell the server we will be using URIs.
+ await sendServerSetClientCapabilities([], supportsUris: true);
+ // Set the (global) encoder for the test process so the JSON we
+ // produce maps files to URIs so the test implementations can just work
+ // with the internal paths.
+ analyzer_plugin.clientUriConverter =
+ ClientUriConverter.withVirtualFileSupport(pathContext);
+ }
+
+ @override
+ tearDown() async {
+ // Reset the converter that some tests set up.
+ analyzer_plugin.clientUriConverter = ClientUriConverter.noop(pathContext);
+ }
+
+ Future test_macroModifiedContentEvent() async {
+ writeTestPackageConfig(macro: true);
+ // TODO(dantup): There are existing methods like
+ // `ResourceProviderMixin.newAnalysisOptionsYamlFile` that would be useful
+ // here, but we'd need to split it up to not be specific to
+ // MemoryResourceProvider.
+ writeFile(
+ pathContext.join(testPackageRootPath, file_paths.analysisOptionsYaml),
+ AnalysisOptionsFileConfig(experiments: ['macros']).toContent(),
+ );
+ writeFile(
+ pathContext.join(testPackageRootPath, file_paths.pubspecYaml),
+ 'name: test',
+ );
+
+ var macroImplementationFilePath =
+ pathContext.join(testPackageRootPath, 'lib', 'with_foo.dart');
+ writeFile(macroImplementationFilePath, withFooMethodMacro);
+
+ var content = '''
+import 'with_foo.dart';
+
+f() {
+ A().foo();
+}
+
+@WithFoo()
+class A {
+ void bar() {}
+}
+''';
+ writeFile(testFile, content);
+
+ await enableCustomUriSupport();
+ await standardAnalysisSetup();
+ await analysisFinished;
+
+ // Modify the macro and expect a change event.
+ writeFile(macroImplementationFilePath,
+ withFooMethodMacro.replaceAll('void foo() {', 'void foo2() {'));
+ await dartTextDocumentContentDidChangeNotifications
+ .firstWhere((notification) => notification.uri == testFileMacroUri);
+ }
+}
diff --git a/pkg/analysis_server/test/integration/lsp/test_all.dart b/pkg/analysis_server/test/integration/lsp/test_all.dart
index c2f7777ddeb..0dc5aaa446b 100644
--- a/pkg/analysis_server/test/integration/lsp/test_all.dart
+++ b/pkg/analysis_server/test/integration/lsp/test_all.dart
@@ -4,10 +4,12 @@
import 'package:test_reflective_loader/test_reflective_loader.dart';
-import 'handle_test.dart' as handle_test;
+import 'handle_test.dart' as handle;
+import 'notification_test.dart' as notification;
void main() {
defineReflectiveSuite(() {
- handle_test.main();
+ handle.main();
+ notification.main();
}, name: 'lsp');
}
diff --git a/pkg/analysis_server/test/integration/support/integration_test_methods.dart b/pkg/analysis_server/test/integration/support/integration_test_methods.dart
index d701c112394..1c46d128d4c 100644
--- a/pkg/analysis_server/test/integration/support/integration_test_methods.dart
+++ b/pkg/analysis_server/test/integration/support/integration_test_methods.dart
@@ -104,8 +104,25 @@ abstract class IntegrationTest {
///
/// - openUrlRequest
/// - showMessageRequest
- Future sendServerSetClientCapabilities(List requests) async {
- var params = ServerSetClientCapabilitiesParams(requests).toJson();
+ ///
+ /// supportsUris: bool (optional)
+ ///
+ /// True if the client supports the server sending URIs in place of file
+ /// paths.
+ ///
+ /// In this mode, the server will use URIs in all protocol fields with the
+ /// type FilePath. Returned URIs may be `file://` URIs or custom schemes.
+ /// The client can fetch the file contents for URIs with custom schemes
+ /// (and receive modification events) through the LSP protocol (see the
+ /// "lsp" domain).
+ ///
+ /// LSP notifications are automatically enabled when the client sets this
+ /// capability.
+ Future sendServerSetClientCapabilities(List requests,
+ {bool? supportsUris}) async {
+ var params =
+ ServerSetClientCapabilitiesParams(requests, supportsUris: supportsUris)
+ .toJson();
var result = await server.send('server.setClientCapabilities', params);
outOfTestExpect(result, isNull);
}
@@ -2565,6 +2582,20 @@ abstract class IntegrationTest {
return LspHandleResult.fromJson(decoder, 'result', result);
}
+ /// Reports an LSP notification from the server.
+ ///
+ /// Parameters
+ ///
+ /// lspNotification: object
+ ///
+ /// The LSP NotificationMessage sent by the server.
+ late final Stream onLspNotification =
+ _onLspNotification.stream.asBroadcastStream();
+
+ /// Stream controller for [onLspNotification].
+ final _onLspNotification =
+ StreamController(sync: true);
+
/// Dispatch the notification named [event], and containing parameters
/// [params], to the appropriate stream.
void dispatchNotification(String event, params) {
@@ -2650,6 +2681,10 @@ abstract class IntegrationTest {
outOfTestExpect(params, isFlutterOutlineParams);
_onFlutterOutline
.add(FlutterOutlineParams.fromJson(decoder, 'params', params));
+ case 'lsp.notification':
+ outOfTestExpect(params, isLspNotificationParams);
+ _onLspNotification
+ .add(LspNotificationParams.fromJson(decoder, 'params', params));
default:
fail('Unexpected notification: $event');
}
diff --git a/pkg/analysis_server/test/integration/support/protocol_matchers.dart b/pkg/analysis_server/test/integration/support/protocol_matchers.dart
index c9e4c1abf18..3d746e87297 100644
--- a/pkg/analysis_server/test/integration/support/protocol_matchers.dart
+++ b/pkg/analysis_server/test/integration/support/protocol_matchers.dart
@@ -2722,6 +2722,14 @@ final Matcher isLspHandleParams = LazyMatcher(
final Matcher isLspHandleResult = LazyMatcher(
() => MatchesJsonObject('lsp.handle result', {'lspResponse': isObject}));
+/// lsp.notification params
+///
+/// {
+/// "lspNotification": object
+/// }
+final Matcher isLspNotificationParams = LazyMatcher(() => MatchesJsonObject(
+ 'lsp.notification params', {'lspNotification': isObject}));
+
/// moveFile feedback
final Matcher isMoveFileFeedback = isNull;
@@ -2956,10 +2964,12 @@ final Matcher isServerOpenUrlRequestResult = isNull;
///
/// {
/// "requests": List
+/// "supportsUris": optional bool
/// }
final Matcher isServerSetClientCapabilitiesParams = LazyMatcher(() =>
- MatchesJsonObject('server.setClientCapabilities params',
- {'requests': isListOf(isString)}));
+ MatchesJsonObject(
+ 'server.setClientCapabilities params', {'requests': isListOf(isString)},
+ optionalFields: {'supportsUris': isBool}));
/// server.setClientCapabilities result
final Matcher isServerSetClientCapabilitiesResult = isNull;
diff --git a/pkg/analysis_server/test/lsp/request_helpers_mixin.dart b/pkg/analysis_server/test/lsp/request_helpers_mixin.dart
index 8827de670ce..2cd3d105a35 100644
--- a/pkg/analysis_server/test/lsp/request_helpers_mixin.dart
+++ b/pkg/analysis_server/test/lsp/request_helpers_mixin.dart
@@ -107,6 +107,19 @@ mixin LspRequestHelpersMixin {
/// Whether to include 'clientRequestTime' fields in outgoing messages.
bool includeClientRequestTime = false;
+ /// A stream of [DartTextDocumentContentDidChangeParams] for any
+ /// `dart/textDocumentContentDidChange` notifications.
+ Stream
+ get dartTextDocumentContentDidChangeNotifications =>
+ notificationsFromServer
+ .where((notification) =>
+ notification.method ==
+ CustomMethods.dartTextDocumentContentDidChange)
+ .map((message) => DartTextDocumentContentDidChangeParams.fromJson(
+ message.params as Map));
+
+ Stream get notificationsFromServer;
+
Future?> callHierarchyIncoming(
CallHierarchyItem item) {
final request = makeRequest(
diff --git a/pkg/analysis_server/test/lsp/server_abstract.dart b/pkg/analysis_server/test/lsp/server_abstract.dart
index f9a78f88c19..974edc714f7 100644
--- a/pkg/analysis_server/test/lsp/server_abstract.dart
+++ b/pkg/analysis_server/test/lsp/server_abstract.dart
@@ -804,16 +804,6 @@ mixin LspAnalysisServerTestMixin
/// list.
final diagnostics = >{};
- /// A stream of [OpenUriParams] for any `dart/openUri` notifications.
- Stream
- get dartTextDocumentContentDidChangeNotifications =>
- notificationsFromServer
- .where((notification) =>
- notification.method ==
- CustomMethods.dartTextDocumentContentDidChange)
- .map((message) => DartTextDocumentContentDidChangeParams.fromJson(
- message.params as Map));
-
/// A stream of [NotificationMessage]s from the server that may be errors.
Stream get errorNotificationsFromServer {
return notificationsFromServer.where(_isErrorNotification);
@@ -833,6 +823,7 @@ mixin LspAnalysisServerTestMixin
Uri get mainFileMacroUri => mainFileUri.replace(scheme: macroClientUriScheme);
/// A stream of [NotificationMessage]s from the server.
+ @override
Stream get notificationsFromServer {
return serverToClient
.where((m) => m is NotificationMessage)
diff --git a/pkg/analysis_server/test/lsp_over_legacy/abstract_lsp_over_legacy.dart b/pkg/analysis_server/test/lsp_over_legacy/abstract_lsp_over_legacy.dart
index fccc0764b14..50e24bfef82 100644
--- a/pkg/analysis_server/test/lsp_over_legacy/abstract_lsp_over_legacy.dart
+++ b/pkg/analysis_server/test/lsp_over_legacy/abstract_lsp_over_legacy.dart
@@ -2,9 +2,11 @@
// 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:convert';
import 'package:analysis_server/lsp_protocol/protocol.dart';
+import 'package:analysis_server/protocol/protocol_constants.dart';
import 'package:analysis_server/src/protocol/protocol_internal.dart';
import 'package:analysis_server/src/protocol_server.dart';
import 'package:analyzer_plugin/src/utilities/client_uri_converter.dart';
@@ -27,12 +29,25 @@ abstract class LspOverLegacyTest extends PubPackageAnalysisServerTest
/// The last ID that was used for a legacy request.
late String lastSentLegacyRequestId;
+ /// A controller for [notificationsFromServer].
+ final StreamController _notificationsFromServer =
+ StreamController.broadcast();
+
+ /// A stream of [NotificationMessage]s from the server.
+ @override
+ Stream get notificationsFromServer =>
+ _notificationsFromServer.stream;
+
@override
path.Context get pathContext => resourceProvider.pathContext;
@override
String get projectFolderPath => convertPath(testPackageRootPath);
+ /// The URI for the macro-generated content for [testFileUri].
+ Uri get testFileMacroUri =>
+ toUri(convertPath(testFilePath)).replace(scheme: macroClientUriScheme);
+
Uri get testFileUri => toUri(convertPath(testFilePath));
@override
@@ -109,6 +124,22 @@ abstract class LspOverLegacyTest extends PubPackageAnalysisServerTest
.valueCount;
}
+ @override
+ void processNotification(Notification notification) {
+ super.processNotification(notification);
+ if (notification.event == LSP_NOTIFICATION_NOTIFICATION) {
+ var params = LspNotificationParams.fromNotification(notification);
+ // Round-trip response via JSON because this doesn't happen automatically
+ // when we're bypassing the streams (running in-process) and we want to
+ // validate everything.
+ final lspNotificationJson = jsonDecode(jsonEncode(params.lspNotification))
+ as Map;
+ var lspNotificationMessage =
+ NotificationMessage.fromJson(lspNotificationJson);
+ _notificationsFromServer.add(lspNotificationMessage);
+ }
+ }
+
@override
Future setUp() async {
super.setUp();
diff --git a/pkg/analysis_server/test/lsp_over_legacy/dart_text_document_content_provider_test.dart b/pkg/analysis_server/test/lsp_over_legacy/dart_text_document_content_provider_test.dart
new file mode 100644
index 00000000000..bca52196188
--- /dev/null
+++ b/pkg/analysis_server/test/lsp_over_legacy/dart_text_document_content_provider_test.dart
@@ -0,0 +1,104 @@
+// Copyright (c) 2024, 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/protocol/protocol_generated.dart';
+import 'package:analysis_server/src/lsp/test_macros.dart';
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'abstract_lsp_over_legacy.dart';
+
+void main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(DartTextDocumentContentProviderTest);
+ });
+}
+
+@reflectiveTest
+class DartTextDocumentContentProviderTest extends LspOverLegacyTest
+ with TestMacros {
+ /// Tells the server we support custom URIs, otherwise we won't be allowed to
+ /// fetch any content from a URI.
+ Future enableCustomUriSupport() async {
+ var request = createLegacyRequest(
+ ServerSetClientCapabilitiesParams([], supportsUris: true));
+ await handleRequest(request);
+ }
+
+ Future test_valid_content() async {
+ writePackageConfig(projectFolderPath, macro: true);
+
+ newFile(
+ join(projectFolderPath, 'lib', 'with_foo.dart'), withFooMethodMacro);
+
+ var content = '''
+import 'with_foo.dart';
+
+f() {
+ A().foo();
+}
+
+@WithFoo()
+class A {
+ void bar() {}
+}
+''';
+ newFile(testFilePath, content);
+ await waitForTasksFinished();
+ await enableCustomUriSupport();
+
+ // Fetch the content for the custom URI scheme.
+ var macroGeneratedContent =
+ await getDartTextDocumentContent(testFileMacroUri);
+
+ // Verify the contents appear correct without doing an exact string
+ // check that might make this text fragile.
+ expect(
+ macroGeneratedContent!.content,
+ allOf([
+ contains('augment class A'),
+ contains('void foo() {'),
+ ]),
+ );
+ }
+
+ Future test_valid_eventAndModifiedContent() async {
+ writePackageConfig(projectFolderPath, macro: true);
+
+ var macroImplementationFilePath =
+ join(projectFolderPath, 'lib', 'with_foo.dart');
+ newFile(macroImplementationFilePath, withFooMethodMacro);
+
+ var content = '''
+import 'with_foo.dart';
+
+f() {
+ A().foo();
+}
+
+@WithFoo()
+class A {
+ void bar() {}
+}
+''';
+ newFile(testFilePath, content);
+ await waitForTasksFinished();
+ await enableCustomUriSupport();
+
+ // Verify initial contents of the macro.
+ var macroGeneratedContent =
+ await getDartTextDocumentContent(testFileMacroUri);
+ expect(macroGeneratedContent!.content, contains('void foo() {'));
+
+ // Modify the macro and expect a change event.
+ newFile(macroImplementationFilePath,
+ withFooMethodMacro.replaceAll('void foo() {', 'void foo2() {'));
+ await dartTextDocumentContentDidChangeNotifications
+ .firstWhere((notification) => notification.uri == testFileMacroUri);
+
+ // Verify updated contents of the macro.
+ macroGeneratedContent = await getDartTextDocumentContent(testFileMacroUri);
+ expect(macroGeneratedContent!.content, contains('void foo2() {'));
+ }
+}
diff --git a/pkg/analysis_server/test/lsp_over_legacy/test_all.dart b/pkg/analysis_server/test/lsp_over_legacy/test_all.dart
index 52461313c5a..488b610d88a 100644
--- a/pkg/analysis_server/test/lsp_over_legacy/test_all.dart
+++ b/pkg/analysis_server/test/lsp_over_legacy/test_all.dart
@@ -5,6 +5,8 @@
import 'package:test_reflective_loader/test_reflective_loader.dart';
import 'call_hierarchy_test.dart' as call_hierarchy;
+import 'dart_text_document_content_provider_test.dart'
+ as dart_text_document_content_provider;
import 'document_color_test.dart' as document_color;
import 'document_highlights_test.dart' as document_highlights;
import 'document_symbols_test.dart' as document_symbols;
@@ -20,6 +22,7 @@ import 'workspace_symbols_test.dart' as workspace_symbols;
void main() {
defineReflectiveSuite(() {
call_hierarchy.main();
+ dart_text_document_content_provider.main();
document_color.main;
document_highlights.main();
document_symbols.main();
diff --git a/pkg/analysis_server/tool/spec/generated/java/AnalysisServer.java b/pkg/analysis_server/tool/spec/generated/java/AnalysisServer.java
index 738ea60864e..8aab58061e3 100644
--- a/pkg/analysis_server/tool/spec/generated/java/AnalysisServer.java
+++ b/pkg/analysis_server/tool/spec/generated/java/AnalysisServer.java
@@ -1010,8 +1010,14 @@ public interface AnalysisServer {
* requests that can be specified:
* - openUrlRequest
* - showMessageRequest
+ * @param supportsUris True if the client supports the server sending URIs in place of file paths.
+ * In this mode, the server will use URIs in all protocol fields with the type FilePath.
+ * Returned URIs may be `file://` URIs or custom schemes. The client can fetch the file
+ * contents for URIs with custom schemes (and receive modification events) through the LSP
+ * protocol (see the "lsp" domain). LSP notifications are automatically enabled when the
+ * client sets this capability.
*/
- public void server_setClientCapabilities(List requests);
+ public void server_setClientCapabilities(List requests, boolean supportsUris);
/**
* {@code server.setSubscriptions}
diff --git a/pkg/analysis_server/tool/spec/spec_input.html b/pkg/analysis_server/tool/spec/spec_input.html
index ad581c9657b..538192f7dd7 100644
--- a/pkg/analysis_server/tool/spec/spec_input.html
+++ b/pkg/analysis_server/tool/spec/spec_input.html
@@ -7,7 +7,7 @@
Analysis Server API Specification
Version
- 1.35.0
+ 1.36.0
This document contains a specification of the API provided by the
@@ -142,6 +142,18 @@
ignoring the item or treating it with some default/fallback handling.
Changelog
+1.36.0
+
+ -
+ Added a new supportsUris client capability to indicate that FilePaths should be URIs instead of file paths.
+
+ -
+ Added an experimental "lsp.handle" request to allow sending LSP requests through the protocol.
+
+ -
+ Added an experimental "lsp.notification" notification to allow receiving LSP notifications.
+
+
1.35.0
-
@@ -393,6 +405,18 @@
+
+ bool
+
+ True if the client supports the server sending URIs in place of file paths.
+
+
+ In this mode, the server will use URIs in all protocol fields with the type FilePath. Returned URIs may be `file://` URIs or custom schemes. The client can fetch the file contents for URIs with custom schemes (and receive modification events) through the LSP protocol (see the "lsp" domain).
+
+
+ LSP notifications are automatically enabled when the client sets this capability.
+
+
@@ -3415,6 +3439,19 @@
+
+
+ Reports an LSP notification from the server.
+
+
+
+
+ The LSP NotificationMessage sent by the server.
+
+ object
+
+
+
Types
diff --git a/pkg/analysis_server_client/lib/handler/notification_handler.dart b/pkg/analysis_server_client/lib/handler/notification_handler.dart
index 4fbdf27df82..eb2e78c5421 100644
--- a/pkg/analysis_server_client/lib/handler/notification_handler.dart
+++ b/pkg/analysis_server_client/lib/handler/notification_handler.dart
@@ -79,6 +79,10 @@ mixin NotificationHandler {
onFlutterOutline(
FlutterOutlineParams.fromJson(decoder, 'params', params));
break;
+ case LSP_NOTIFICATION_NOTIFICATION:
+ onLspNotification(
+ LspNotificationParams.fromJson(decoder, 'params', params));
+ break;
case SEARCH_NOTIFICATION_RESULTS:
onSearchResults(
SearchResultsParams.fromJson(decoder, 'params', params));
@@ -227,6 +231,9 @@ mixin NotificationHandler {
/// request.
void onFlutterOutline(FlutterOutlineParams params) {}
+ /// Reports an LSP notification from the server.
+ void onLspNotification(LspNotificationParams params) {}
+
/// Reports some or all of the results of performing a requested
/// search. Unlike other notifications, this notification
/// contains search results that should be added to any
diff --git a/pkg/analysis_server_client/lib/src/protocol/protocol_constants.dart b/pkg/analysis_server_client/lib/src/protocol/protocol_constants.dart
index e15a9142ce3..b27fb1ef191 100644
--- a/pkg/analysis_server_client/lib/src/protocol/protocol_constants.dart
+++ b/pkg/analysis_server_client/lib/src/protocol/protocol_constants.dart
@@ -6,7 +6,7 @@
// To regenerate the file, use the script
// "pkg/analysis_server/tool/spec/generate_files".
-const String PROTOCOL_VERSION = '1.35.0';
+const String PROTOCOL_VERSION = '1.36.0';
const String ANALYSIS_NOTIFICATION_ANALYZED_FILES = 'analysis.analyzedFiles';
const String ANALYSIS_NOTIFICATION_ANALYZED_FILES_DIRECTORIES = 'directories';
@@ -273,6 +273,8 @@ const String FLUTTER_REQUEST_SET_WIDGET_PROPERTY_VALUE_ID = 'id';
const String FLUTTER_REQUEST_SET_WIDGET_PROPERTY_VALUE_VALUE = 'value';
const String FLUTTER_RESPONSE_GET_WIDGET_DESCRIPTION_PROPERTIES = 'properties';
const String FLUTTER_RESPONSE_SET_WIDGET_PROPERTY_VALUE_CHANGE = 'change';
+const String LSP_NOTIFICATION_NOTIFICATION = 'lsp.notification';
+const String LSP_NOTIFICATION_NOTIFICATION_LSP_NOTIFICATION = 'lspNotification';
const String LSP_REQUEST_HANDLE = 'lsp.handle';
const String LSP_REQUEST_HANDLE_LSP_MESSAGE = 'lspMessage';
const String LSP_RESPONSE_HANDLE_LSP_RESPONSE = 'lspResponse';
@@ -334,6 +336,8 @@ const String SERVER_REQUEST_OPEN_URL_REQUEST_URL = 'url';
const String SERVER_REQUEST_SET_CLIENT_CAPABILITIES =
'server.setClientCapabilities';
const String SERVER_REQUEST_SET_CLIENT_CAPABILITIES_REQUESTS = 'requests';
+const String SERVER_REQUEST_SET_CLIENT_CAPABILITIES_SUPPORTS_URIS =
+ 'supportsUris';
const String SERVER_REQUEST_SET_SUBSCRIPTIONS = 'server.setSubscriptions';
const String SERVER_REQUEST_SET_SUBSCRIPTIONS_SUBSCRIPTIONS = 'subscriptions';
const String SERVER_REQUEST_SHOW_MESSAGE_REQUEST = 'server.showMessageRequest';
diff --git a/pkg/analysis_server_client/lib/src/protocol/protocol_generated.dart b/pkg/analysis_server_client/lib/src/protocol/protocol_generated.dart
index e894e513c3b..badfb1b3d5d 100644
--- a/pkg/analysis_server_client/lib/src/protocol/protocol_generated.dart
+++ b/pkg/analysis_server_client/lib/src/protocol/protocol_generated.dart
@@ -11889,6 +11889,66 @@ class LspHandleResult implements ResponseResult {
int get hashCode => lspResponse.hashCode;
}
+/// lsp.notification params
+///
+/// {
+/// "lspNotification": object
+/// }
+///
+/// Clients may not extend, implement or mix-in this class.
+class LspNotificationParams implements HasToJson {
+ /// The LSP NotificationMessage sent by the server.
+ Object lspNotification;
+
+ LspNotificationParams(this.lspNotification);
+
+ factory LspNotificationParams.fromJson(
+ JsonDecoder jsonDecoder, String jsonPath, Object? json) {
+ json ??= {};
+ if (json is Map) {
+ Object lspNotification;
+ if (json.containsKey('lspNotification')) {
+ lspNotification = json['lspNotification'] as Object;
+ } else {
+ throw jsonDecoder.mismatch(jsonPath, 'lspNotification');
+ }
+ return LspNotificationParams(lspNotification);
+ } else {
+ throw jsonDecoder.mismatch(jsonPath, 'lsp.notification params', json);
+ }
+ }
+
+ factory LspNotificationParams.fromNotification(Notification notification) {
+ return LspNotificationParams.fromJson(
+ ResponseDecoder(null), 'params', notification.params);
+ }
+
+ @override
+ Map toJson() {
+ var result = {};
+ result['lspNotification'] = lspNotification;
+ return result;
+ }
+
+ Notification toNotification() {
+ return Notification('lsp.notification', toJson());
+ }
+
+ @override
+ String toString() => json.encode(toJson());
+
+ @override
+ bool operator ==(other) {
+ if (other is LspNotificationParams) {
+ return lspNotification == other.lspNotification;
+ }
+ return false;
+ }
+
+ @override
+ int get hashCode => lspNotification.hashCode;
+}
+
/// MessageAction
///
/// {
@@ -15335,6 +15395,7 @@ class ServerService implements Enum {
///
/// {
/// "requests": List
+/// "supportsUris": optional bool
/// }
///
/// Clients may not extend, implement or mix-in this class.
@@ -15354,7 +15415,20 @@ class ServerSetClientCapabilitiesParams implements RequestParams {
/// - showMessageRequest
List requests;
- ServerSetClientCapabilitiesParams(this.requests);
+ /// True if the client supports the server sending URIs in place of file
+ /// paths.
+ ///
+ /// In this mode, the server will use URIs in all protocol fields with the
+ /// type FilePath. Returned URIs may be `file://` URIs or custom schemes. The
+ /// client can fetch the file contents for URIs with custom schemes (and
+ /// receive modification events) through the LSP protocol (see the "lsp"
+ /// domain).
+ ///
+ /// LSP notifications are automatically enabled when the client sets this
+ /// capability.
+ bool? supportsUris;
+
+ ServerSetClientCapabilitiesParams(this.requests, {this.supportsUris});
factory ServerSetClientCapabilitiesParams.fromJson(
JsonDecoder jsonDecoder, String jsonPath, Object? json) {
@@ -15367,7 +15441,13 @@ class ServerSetClientCapabilitiesParams implements RequestParams {
} else {
throw jsonDecoder.mismatch(jsonPath, 'requests');
}
- return ServerSetClientCapabilitiesParams(requests);
+ bool? supportsUris;
+ if (json.containsKey('supportsUris')) {
+ supportsUris = jsonDecoder.decodeBool(
+ '$jsonPath.supportsUris', json['supportsUris']);
+ }
+ return ServerSetClientCapabilitiesParams(requests,
+ supportsUris: supportsUris);
} else {
throw jsonDecoder.mismatch(
jsonPath, 'server.setClientCapabilities params', json);
@@ -15383,6 +15463,10 @@ class ServerSetClientCapabilitiesParams implements RequestParams {
Map toJson() {
var result = {};
result['requests'] = requests;
+ var supportsUris = this.supportsUris;
+ if (supportsUris != null) {
+ result['supportsUris'] = supportsUris;
+ }
return result;
}
@@ -15398,13 +15482,17 @@ class ServerSetClientCapabilitiesParams implements RequestParams {
bool operator ==(other) {
if (other is ServerSetClientCapabilitiesParams) {
return listEqual(
- requests, other.requests, (String a, String b) => a == b);
+ requests, other.requests, (String a, String b) => a == b) &&
+ supportsUris == other.supportsUris;
}
return false;
}
@override
- int get hashCode => Object.hashAll(requests);
+ int get hashCode => Object.hash(
+ Object.hashAll(requests),
+ supportsUris,
+ );
}
/// server.setClientCapabilities result
diff --git a/pkg/analyzer_plugin/lib/src/utilities/client_uri_converter.dart b/pkg/analyzer_plugin/lib/src/utilities/client_uri_converter.dart
index 4260e23ff7e..c075b205028 100644
--- a/pkg/analyzer_plugin/lib/src/utilities/client_uri_converter.dart
+++ b/pkg/analyzer_plugin/lib/src/utilities/client_uri_converter.dart
@@ -95,14 +95,16 @@ class _VirtualFileClientUriConverter extends ClientUriConverter {
// For URIs with no scheme, assume it was a relative path and provide a
// better message than "scheme '' is not supported".
if (uri.scheme.isEmpty) {
- throw ArgumentError.value(uri, 'uri', 'URI is not a valid file:// URI');
+ throw ArgumentError.value(
+ uri.toString(), 'uri', 'URI is not a valid file:// URI');
}
if (!supportedSchemes.contains(uri.scheme)) {
- var supportedSchemesString =
- supportedSchemes.map((scheme) => "'$scheme'").join(', ');
+ var supportedSchemesString = supportedSchemes.isEmpty
+ ? '(none)'
+ : supportedSchemes.map((scheme) => "'$scheme'").join(', ');
throw ArgumentError.value(
- uri,
+ uri.toString(),
'uri',
"URI scheme '${uri.scheme}' is not supported. "
'Allowed schemes are $supportedSchemesString.',