55import 'dart:async' ;
66
77import 'package:analysis_server/lsp_protocol/protocol.dart' ;
8+ import 'package:analysis_server/src/lsp/client_capabilities.dart' ;
89import 'package:analysis_server/src/lsp/constants.dart' ;
910import 'package:analysis_server/src/lsp/mapping.dart' ;
1011import 'package:analysis_server/src/services/completion/dart/feature_computer.dart' ;
@@ -14,7 +15,7 @@ import 'package:collection/collection.dart';
1415import 'package:language_server_protocol/json_parsing.dart' ;
1516import 'package:path/path.dart' as path;
1617import 'package:test/test.dart' as test show expect;
17- import 'package:test/test.dart' hide expect ;
18+ import 'package:test/test.dart' ;
1819
1920import 'change_verifier.dart' ;
2021
@@ -169,6 +170,8 @@ mixin LspRequestHelpersMixin {
169170
170171 Stream <NotificationMessage > get notificationsFromServer;
171172
173+ Stream <RequestMessage > get requestsFromServer;
174+
172175 Future <List <CallHierarchyIncomingCall >?> callHierarchyIncoming (
173176 CallHierarchyItem item,
174177 ) {
@@ -225,6 +228,29 @@ mixin LspRequestHelpersMixin {
225228 void expect (Object ? actual, Matcher matcher, {String ? reason}) =>
226229 test.expect (actual, matcher, reason: reason);
227230
231+ /// Expects a [method] request from the server after executing [f] .
232+ Future <RequestMessage > expectRequest (
233+ Method method,
234+ FutureOr <void > Function () f, {
235+ Duration timeout = const Duration (seconds: 5 ),
236+ }) async {
237+ var firstRequest = requestsFromServer.firstWhere ((n) => n.method == method);
238+ await f ();
239+
240+ var requestFromServer = await firstRequest.timeout (
241+ timeout,
242+ onTimeout:
243+ () =>
244+ throw TimeoutException (
245+ 'Did not receive the expected $method request from the server in the timeout period' ,
246+ timeout,
247+ ),
248+ );
249+
250+ expect (requestFromServer, isNotNull);
251+ return requestFromServer;
252+ }
253+
228254 Future <T > expectSuccessfulResponseTo <T , R >(
229255 RequestMessage request,
230256 T Function (R ) fromJson,
@@ -777,6 +803,71 @@ mixin LspRequestHelpersMixin {
777803 );
778804 }
779805
806+ /// Executes [f] then waits for a request of type [method] from the server which
807+ /// is passed to [handler] to process, then waits for (and returns) the
808+ /// response to the original request.
809+ ///
810+ /// This is used for testing things like code actions, where the client initiates
811+ /// a request but the server does not respond to it until it's sent its own
812+ /// request to the client and it received a response.
813+ ///
814+ /// Client Server
815+ /// 1. |- Req: textDocument/codeAction ->
816+ /// 1. <- Resp: textDocument/codeAction -|
817+ ///
818+ /// 2. |- Req: workspace/executeCommand ->
819+ /// 3. <- Req: textDocument/applyEdits -|
820+ /// 3. |- Resp: textDocument/applyEdits ->
821+ /// 2. <- Resp: workspace/executeCommand -|
822+ ///
823+ /// Request 2 from the client is not responded to until the server has its own
824+ /// response to the request it sends (3).
825+ Future <T > handleExpectedRequest <T , R , RR >(
826+ Method method,
827+ R Function (Map <String , dynamic >) fromJson,
828+ Future <T > Function () f, {
829+ required FutureOr <RR > Function (R ) handler,
830+ Duration timeout = const Duration (seconds: 5 ),
831+ }) async {
832+ late Future <T > outboundRequest;
833+ Object ? outboundRequestError;
834+
835+ // Run [f] and wait for the incoming request from the server.
836+ var incomingRequest = await expectRequest (method, () {
837+ // Don't return/await the response yet, as this may not complete until
838+ // after we have handled the request that comes from the server.
839+ outboundRequest = f ();
840+
841+ // Because we don't await this future until "later", if it throws the
842+ // error is treated as unhandled and will fail the test even if expected.
843+ // Instead, capture the error and suppress it. But if we time out (in
844+ // which case we will never return outboundRequest), then we'll raise this
845+ // error.
846+ outboundRequest.then (
847+ (_) {},
848+ onError: (e) {
849+ outboundRequestError = e;
850+ return null ;
851+ },
852+ );
853+ }, timeout: timeout).catchError ((Object timeoutException) {
854+ // We timed out waiting for the request from the server. Probably this is
855+ // because our outbound request for some reason, so if we have an error
856+ // for that, then throw it. Otherwise, propogate the timeout.
857+ throw outboundRequestError ?? timeoutException;
858+ }, test: (e) => e is TimeoutException );
859+
860+ // Handle the request from the server and send the response back.
861+ var clientsResponse = await handler (
862+ fromJson (incomingRequest.params as Map <String , Object ?>),
863+ );
864+ respondTo (incomingRequest, clientsResponse);
865+
866+ // Return a future that completes when the response to the original request
867+ // (from [f]) returns.
868+ return outboundRequest;
869+ }
870+
780871 RequestMessage makeRequest (Method method, ToJsonable ? params) {
781872 var id = Either2 <int , String >.t1 (_id++ );
782873 return RequestMessage (
@@ -851,6 +942,22 @@ mixin LspRequestHelpersMixin {
851942 return expectSuccessfulResponseTo (request, CompletionItem .fromJson);
852943 }
853944
945+ /// Sends [responseParams] to the server as a successful response to
946+ /// a server-initiated [request] .
947+ void respondTo <T >(RequestMessage request, T responseParams) {
948+ sendResponseToServer (
949+ ResponseMessage (
950+ id: request.id,
951+ result: responseParams,
952+ jsonrpc: jsonRpcVersion,
953+ ),
954+ );
955+ }
956+
957+ /// Sends a ResponseMessage to the server, completing a reverse
958+ /// (server-to-client) request.
959+ void sendResponseToServer (ResponseMessage response);
960+
854961 Future <List <TypeHierarchyItem >?> typeHierarchySubtypes (
855962 TypeHierarchyItem item,
856963 ) {
@@ -1079,15 +1186,64 @@ mixin LspRequestHelpersMixin {
10791186/// Extends [LspEditHelpersMixin] with methods for accessing file state and
10801187/// information about the project to build paths.
10811188mixin LspVerifyEditHelpersMixin on LspEditHelpersMixin {
1189+ LspClientCapabilities get editorClientCapabilities;
1190+
10821191 path.Context get pathContext;
10831192
10841193 String get projectFolderPath;
10851194
10861195 ClientUriConverter get uriConverter;
10871196
1197+ /// Executes a function which is expected to call back to the client to apply
1198+ /// a [WorkspaceEdit] .
1199+ ///
1200+ /// Returns a [LspChangeVerifier] that can be used to verify changes.
1201+ Future <LspChangeVerifier > executeForEdits (
1202+ Future <Object ?> Function () function, {
1203+ ApplyWorkspaceEditResult ? applyEditResult,
1204+ }) async {
1205+ ApplyWorkspaceEditParams ? editParams;
1206+
1207+ var commandResponse = await handleExpectedRequest<
1208+ Object ? ,
1209+ ApplyWorkspaceEditParams ,
1210+ ApplyWorkspaceEditResult
1211+ > (
1212+ Method .workspace_applyEdit,
1213+ ApplyWorkspaceEditParams .fromJson,
1214+ function,
1215+ handler: (edit) {
1216+ // When the server sends the edit back, just keep a copy and say we
1217+ // applied successfully (it'll be verified by the caller).
1218+ editParams = edit;
1219+ return applyEditResult ?? ApplyWorkspaceEditResult (applied: true );
1220+ },
1221+ );
1222+ // Successful edits return an empty success() response.
1223+ expect (commandResponse, isNull);
1224+
1225+ // Ensure the edit came back, and using the expected change type.
1226+ expect (editParams, isNotNull);
1227+ var edit = editParams! .edit;
1228+
1229+ var expectDocumentChanges = editorClientCapabilities.documentChanges;
1230+ expect (edit.documentChanges, expectDocumentChanges ? isNotNull : isNull);
1231+ expect (edit.changes, expectDocumentChanges ? isNull : isNotNull);
1232+
1233+ return LspChangeVerifier (this , edit);
1234+ }
1235+
10881236 /// A function to get the current contents of a file to apply edits.
10891237 String ? getCurrentFileContent (Uri uri);
10901238
1239+ Future <T > handleExpectedRequest <T , R , RR >(
1240+ Method method,
1241+ R Function (Map <String , dynamic >) fromJson,
1242+ Future <T > Function () f, {
1243+ required FutureOr <RR > Function (R ) handler,
1244+ Duration timeout = const Duration (seconds: 5 ),
1245+ });
1246+
10911247 /// Formats a path relative to the project root always using forward slashes.
10921248 ///
10931249 /// This is used in the text format for comparing edits.
0 commit comments