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 {

Analysis Server API Specification

Version - 1.35.0 + 1.36.0

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.

Changelog

+

1.36.0

+

1.35.0

+
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. +

server.openUrlRequest
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.',