Skip to content

Commit cd94fce

Browse files
DanTupcommit-bot@chromium.org
authored andcommitted
Add ability for LSP server to send requests to (+ receive responses from) client
Change-Id: Iff1f8f63dc58ef2689ecc44f6e705d6811b12667 Reviewed-on: https://dart-review.googlesource.com/c/86420 Commit-Queue: Danny Tuppeny <[email protected]> Reviewed-by: Brian Wilkerson <[email protected]>
1 parent 3e3f31b commit cd94fce

File tree

6 files changed

+199
-34
lines changed

6 files changed

+199
-34
lines changed

pkg/analysis_server/lib/src/lsp/channel/lsp_byte_stream_channel.dart

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class LspByteStreamServerChannel implements LspServerCommunicationChannel {
5555
}
5656

5757
@override
58-
void listen(void onMessage(IncomingMessage message),
58+
void listen(void onMessage(Message message),
5959
{Function onError, void onDone()}) {
6060
_input.transform(new LspPacketTransformer()).listen(
6161
(String data) => _readMessage(data, onMessage),
@@ -71,14 +71,17 @@ class LspByteStreamServerChannel implements LspServerCommunicationChannel {
7171
void sendNotification(NotificationMessage notification) =>
7272
_sendLsp(notification.toJson());
7373

74+
@override
75+
void sendRequest(RequestMessage request) => _sendLsp(request.toJson());
76+
7477
@override
7578
void sendResponse(ResponseMessage response) => _sendLsp(response.toJson());
7679

7780
/**
7881
* Read a request from the given [data] and use the given function to handle
7982
* the message.
8083
*/
81-
void _readMessage(String data, void onMessage(IncomingMessage request)) {
84+
void _readMessage(String data, void onMessage(Message request)) {
8285
// Ignore any further requests after the communication channel is closed.
8386
if (_closed.isCompleted) {
8487
return;
@@ -90,6 +93,8 @@ class LspByteStreamServerChannel implements LspServerCommunicationChannel {
9093
onMessage(RequestMessage.fromJson(json));
9194
} else if (NotificationMessage.canParse(json)) {
9295
onMessage(NotificationMessage.fromJson(json));
96+
} else if (ResponseMessage.canParse(json)) {
97+
onMessage(ResponseMessage.fromJson(json));
9398
} else {
9499
_sendParseError();
95100
}

pkg/analysis_server/lib/src/lsp/channel/lsp_channel.dart

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
6-
import 'package:analysis_server/lsp_protocol/protocol_special.dart';
76
import 'package:analysis_server/src/lsp/lsp_analysis_server.dart';
87

98
/**
@@ -25,14 +24,19 @@ abstract class LspServerCommunicationChannel {
2524
* client, invoke the [onDone] function.
2625
* Only one listener is allowed per channel.
2726
*/
28-
void listen(void onMessage(IncomingMessage message),
27+
void listen(void onMessage(Message message),
2928
{Function onError, void onDone()});
3029

3130
/**
3231
* Send the given [notification] to the client.
3332
*/
3433
void sendNotification(NotificationMessage notification);
3534

35+
/**
36+
* Send the given [request] to the client.
37+
*/
38+
void sendRequest(RequestMessage request);
39+
3640
/**
3741
* Send the given [response] to the client.
3842
*/

pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@ class LspAnalysisServer extends AbstractAnalysisServer {
111111

112112
ServerStateMessageHandler messageHandler;
113113

114+
int nextRequestId = 1;
115+
116+
Map<int, Completer<ResponseMessage>> completers = {};
117+
114118
/**
115119
* Initialize a newly created server to send and receive messages to the given
116120
* [channel].
@@ -197,28 +201,66 @@ class LspAnalysisServer extends AbstractAnalysisServer {
197201
null, new Uri.file(path).toString());
198202
}
199203

204+
/// Handles a response from the client by invoking the completer that the
205+
/// outbound request created.
206+
void handleClientResponse(ResponseMessage message) {
207+
// The ID from the client is an Either2<num, String>, though it's not valid
208+
// for it to be a string because it should match a request we sent to the
209+
// client (and we always use numeric IDs for outgoing requests).
210+
message.id.map(
211+
(id) {
212+
// It's possible that even if we got a numeric ID that it's not valid.
213+
// If it's not in our completers list (which is a list of the outstanding
214+
// requests we've sent) then show an error.
215+
final completer = completers[id];
216+
if (completer == null) {
217+
showError('Response with ID $id was unexpected');
218+
} else {
219+
completers.remove(id);
220+
completer.complete(message);
221+
}
222+
},
223+
(stringID) {
224+
showError('Unexpected String ID for response $stringID');
225+
},
226+
);
227+
}
228+
200229
/**
201230
* Handle a [message] that was read from the communication channel.
202231
*/
203-
void handleMessage(IncomingMessage message) {
232+
void handleMessage(Message message) {
204233
// TODO(dantup): Put in all the things this server is missing, like:
205234
// _performance.logRequest(message);
206235
runZoned(() {
207236
ServerPerformanceStatistics.serverRequests.makeCurrentWhile(() async {
208237
try {
209-
final result = await messageHandler.handleMessage(message);
210-
if (result.isError) {
211-
sendErrorResponse(message, result.error);
238+
if (message is ResponseMessage) {
239+
handleClientResponse(message);
212240
} else if (message is RequestMessage) {
213-
channel.sendResponse(new ResponseMessage(
214-
message.id, result.result, null, jsonRpcVersion));
241+
final result = await messageHandler.handleMessage(message);
242+
if (result.isError) {
243+
sendErrorResponse(message, result.error);
244+
} else {
245+
channel.sendResponse(new ResponseMessage(
246+
message.id, result.result, null, jsonRpcVersion));
247+
}
248+
} else if (message is NotificationMessage) {
249+
final result = await messageHandler.handleMessage(message);
250+
if (result.isError) {
251+
sendErrorResponse(message, result.error);
252+
}
253+
} else {
254+
showError('Unknown message type');
215255
}
216256
} catch (error, stackTrace) {
217-
final errorMessage = message is RequestMessage
218-
? 'An error occurred while handling ${message.method} request'
219-
: message is NotificationMessage
220-
? 'An error occurred while handling ${message.method} notification'
221-
: 'Unknown message type';
257+
final errorMessage = message is ResponseMessage
258+
? 'An error occurred while handling the response to request ${message.id}'
259+
: message is RequestMessage
260+
? 'An error occurred while handling ${message.method} request'
261+
: message is NotificationMessage
262+
? 'An error occurred while handling ${message.method} notification'
263+
: 'Unknown message type';
222264
sendErrorResponse(
223265
message,
224266
new ResponseError(
@@ -243,13 +285,17 @@ class LspAnalysisServer extends AbstractAnalysisServer {
243285
));
244286
}
245287

246-
void sendErrorResponse(IncomingMessage message, ResponseError error) {
288+
void sendErrorResponse(Message message, ResponseError error) {
247289
if (message is RequestMessage) {
248290
channel.sendResponse(
249291
new ResponseMessage(message.id, null, error, jsonRpcVersion));
250292
// Since the LSP client might not show the failed requests to the user,
251293
// also ensure the error is logged to the client.
252294
logError(error.message);
295+
} else if (message is ResponseMessage) {
296+
// For notifications where we couldn't respond with an error, send it as
297+
// show instead of log.
298+
showError(error.message);
253299
} else {
254300
// For notifications where we couldn't respond with an error, send it as
255301
// show instead of log.
@@ -264,6 +310,24 @@ class LspAnalysisServer extends AbstractAnalysisServer {
264310
channel.sendNotification(notification);
265311
}
266312

313+
/**
314+
* Send the given [request] to the client and wait for a response.
315+
*/
316+
Future<ResponseMessage> sendRequest(Method method, Object params) {
317+
final requestId = nextRequestId++;
318+
final completer = new Completer<ResponseMessage>();
319+
completers[requestId] = completer;
320+
321+
channel.sendRequest(new RequestMessage(
322+
Either2<num, String>.t1(requestId),
323+
method,
324+
params,
325+
jsonRpcVersion,
326+
));
327+
328+
return completer.future;
329+
}
330+
267331
/**
268332
* Send the given [response] to the client.
269333
*/

pkg/analysis_server/lib/src/lsp/lsp_socket_server.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class LspSocketServer {
5454
new ShowMessageParams(MessageType.Error, error.message),
5555
jsonRpcVersion,
5656
));
57-
serverChannel.listen((IncomingMessage message) {
57+
serverChannel.listen((Message message) {
5858
if (message is RequestMessage) {
5959
serverChannel.sendResponse(
6060
new ResponseMessage(message.id, null, error, jsonRpcVersion));

pkg/analysis_server/test/lsp/server_abstract.dart

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import 'package:analyzer/source/line_info.dart';
1414
import 'package:analyzer/src/generated/sdk.dart';
1515
import 'package:analyzer/src/test_utilities/mock_sdk.dart';
1616
import 'package:analyzer/src/test_utilities/resource_provider_mixin.dart';
17+
import 'package:meta/meta.dart';
1718
import 'package:test/test.dart';
1819

1920
import '../mocks.dart';
@@ -143,6 +144,22 @@ abstract class AbstractLspAnalysisServerTest with ResourceProviderMixin {
143144
return notificationFromServer.params as T;
144145
}
145146

147+
/// Expects a [method] request from the server after executing [f].
148+
Future<RequestMessage> expectRequest(
149+
Method method,
150+
FutureOr<void> f(), {
151+
Duration timeout = const Duration(seconds: 5),
152+
}) async {
153+
final firstRequest =
154+
channel.requestsFromServer.firstWhere((n) => n.method == method);
155+
await f();
156+
157+
final requestFromServer = await firstRequest.timeout(timeout);
158+
159+
expect(requestFromServer, isNotNull);
160+
return requestFromServer;
161+
}
162+
146163
/// Sends a request to the server and unwraps the result. Throws if the
147164
/// response was not successful or returned an error.
148165
Future<T> expectSuccessfulResponseTo<T>(RequestMessage request) async {
@@ -268,6 +285,49 @@ abstract class AbstractLspAnalysisServerTest with ResourceProviderMixin {
268285
return expectSuccessfulResponseTo<SignatureHelp>(request);
269286
}
270287

288+
/// Executes [f] then waits for a request of type [method] from the server which
289+
/// is passed to [handler] to process, then waits for (and returns) the
290+
/// response to the original request.
291+
///
292+
/// This is used for testing things like code actions, where the client initiates
293+
/// a request but the server does not respond to it until it's sent its own
294+
/// request to the client and it recieved a response.
295+
///
296+
/// Client Server
297+
/// 1. |- Req: textDocument/codeAction ->
298+
/// 1. <- Resp: textDocument/codeAction -|
299+
///
300+
/// 2. |- Req: workspace/executeCommand ->
301+
/// 3. <- Req: textDocument/applyEdits -|
302+
/// 3. |- Resp: textDocument/applyEdits ->
303+
/// 2. <- Resp: workspace/executeCommand -|
304+
///
305+
/// Request 2 from the client is not responded to until the server has its own
306+
/// response to the request it sends (3).
307+
Future<T> handleExpectedRequest<T, R, RR>(
308+
Method method,
309+
Future<T> f(), {
310+
@required FutureOr<RR> handler(R params),
311+
Duration timeout = const Duration(seconds: 5),
312+
}) async {
313+
FutureOr<T> outboundRequest;
314+
315+
// Run [f] and wait for the incoming request from the server.
316+
final incomingRequest = await expectRequest(method, () {
317+
// Don't return/await the response yet, as this may not complete until
318+
// after we have handled the request that comes from the server.
319+
outboundRequest = f();
320+
});
321+
322+
// Handle the request from the server and send the response back.
323+
final clientsResponse = await handler(incomingRequest.params as R);
324+
respondTo(incomingRequest, clientsResponse);
325+
326+
// Return a future that completes when the response to the original request
327+
// (from [f]) returns.
328+
return outboundRequest;
329+
}
330+
271331
/// A helper that initializes the server with common values, since the server
272332
/// will reject any other requests until it is initialized.
273333
/// Capabilities are overridden by providing JSON to avoid having to construct
@@ -368,6 +428,13 @@ abstract class AbstractLspAnalysisServerTest with ResourceProviderMixin {
368428
);
369429
}
370430

431+
/// Sends [responseParams] to the server as a successful response to
432+
/// a server-initiated [request].
433+
void respondTo<T>(RequestMessage request, T responseParams) async {
434+
channel.sendResponseToServer(
435+
new ResponseMessage(request.id, responseParams, null, jsonRpcVersion));
436+
}
437+
371438
void setUp() {
372439
channel = new MockLspServerChannel(debugPrintCommunication);
373440
// Create an SDK in the mock file system.

0 commit comments

Comments
 (0)