[analysis_server] Change LSP-over-Legacy to be wrapped with the original protocol

Originally we didn't use the LSP Request/Response classes, and just exposed the handlers through the legacy request/response.

However there were some mismatches (such as legacy protocol always returns Map<String, Object?> but some LSP requests return Lists, LSP using int|String IDs, and LSP having numeric error codes that don't match legacy string error codes).

This change uses LSP's request and Response by wrapping them inside a standard (legacy) handler. The LSP-over-Legacy handler has become a standard handler, and the params contain an "lspMessage" field that holds an LSP message, and the result contains an "lspResponse" field that contains an LSP response.

If an LSP handler returns an error, it will be returned as an error inside the LSP response, which will be in a _successful_ legacy request (since that's how we can return an LSP response - as the result).

Change-Id: I67973590ab32f3543d1a6e1b7279974e5e8832bc
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/315201
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
Danny Tuppeny 2023-08-06 14:32:18 +00:00 committed by Commit Queue
parent 678e207a6d
commit 76dc2c4cfa
29 changed files with 684 additions and 195 deletions

View file

@ -3375,6 +3375,7 @@ a:focus, a:hover {
</p>
</dd></dl></dd></dl>
<h2 class="domain"><a name="types">Types</a></h2>
<p>
This section contains descriptions of the data types referenced

View file

@ -311,6 +311,9 @@ 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_REQUEST_HANDLE = 'lsp.handle';
const String LSP_REQUEST_HANDLE_LSP_MESSAGE = 'lspMessage';
const String LSP_RESPONSE_HANDLE_LSP_RESPONSE = 'lspResponse';
const String SEARCH_NOTIFICATION_RESULTS = 'search.results';
const String SEARCH_NOTIFICATION_RESULTS_ID = 'id';
const String SEARCH_NOTIFICATION_RESULTS_IS_LAST = 'isLast';

View file

@ -12992,6 +12992,130 @@ class LibraryPathSet implements HasToJson {
);
}
/// lsp.handle params
///
/// {
/// "lspMessage": object
/// }
///
/// Clients may not extend, implement or mix-in this class.
class LspHandleParams implements RequestParams {
/// The LSP RequestMessage.
Object lspMessage;
LspHandleParams(this.lspMessage);
factory LspHandleParams.fromJson(
JsonDecoder jsonDecoder, String jsonPath, Object? json) {
json ??= {};
if (json is Map) {
Object lspMessage;
if (json.containsKey('lspMessage')) {
lspMessage = json['lspMessage'] as Object;
} else {
throw jsonDecoder.mismatch(jsonPath, 'lspMessage');
}
return LspHandleParams(lspMessage);
} else {
throw jsonDecoder.mismatch(jsonPath, 'lsp.handle params', json);
}
}
factory LspHandleParams.fromRequest(Request request) {
return LspHandleParams.fromJson(
RequestDecoder(request), 'params', request.params);
}
@override
Map<String, Object> toJson() {
var result = <String, Object>{};
result['lspMessage'] = lspMessage;
return result;
}
@override
Request toRequest(String id) {
return Request(id, 'lsp.handle', toJson());
}
@override
String toString() => json.encode(toJson());
@override
bool operator ==(other) {
if (other is LspHandleParams) {
return lspMessage == other.lspMessage;
}
return false;
}
@override
int get hashCode => lspMessage.hashCode;
}
/// lsp.handle result
///
/// {
/// "lspResponse": object
/// }
///
/// Clients may not extend, implement or mix-in this class.
class LspHandleResult implements ResponseResult {
/// The LSP ResponseMessage returned by the handler.
Object lspResponse;
LspHandleResult(this.lspResponse);
factory LspHandleResult.fromJson(
JsonDecoder jsonDecoder, String jsonPath, Object? json) {
json ??= {};
if (json is Map) {
Object lspResponse;
if (json.containsKey('lspResponse')) {
lspResponse = json['lspResponse'] as Object;
} else {
throw jsonDecoder.mismatch(jsonPath, 'lspResponse');
}
return LspHandleResult(lspResponse);
} else {
throw jsonDecoder.mismatch(jsonPath, 'lsp.handle result', json);
}
}
factory LspHandleResult.fromResponse(Response response) {
return LspHandleResult.fromJson(
ResponseDecoder(REQUEST_ID_REFACTORING_KINDS.remove(response.id)),
'result',
response.result);
}
@override
Map<String, Object> toJson() {
var result = <String, Object>{};
result['lspResponse'] = lspResponse;
return result;
}
@override
Response toResponse(String id) {
return Response(id, result: toJson());
}
@override
String toString() => json.encode(toJson());
@override
bool operator ==(other) {
if (other is LspHandleResult) {
return lspResponse == other.lspResponse;
}
return false;
}
@override
int get hashCode => lspResponse.hashCode;
}
/// MessageAction
///
/// {

View file

@ -0,0 +1,79 @@
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:analysis_server/lsp_protocol/protocol_custom_generated.dart';
import 'package:analysis_server/lsp_protocol/protocol_special.dart';
import 'package:analysis_server/protocol/protocol.dart';
import 'package:analysis_server/protocol/protocol_generated.dart';
import 'package:analysis_server/src/handler/legacy/legacy_handler.dart';
import 'package:analysis_server/src/lsp/constants.dart';
import 'package:analysis_server/src/lsp/handlers/handler_states.dart';
import 'package:analysis_server/src/lsp/handlers/handlers.dart' as lsp;
import 'package:analysis_server/src/lsp/json_parsing.dart';
/// The handler for the `lsp.handle` request.
class LspOverLegacyHandler extends LegacyHandler {
/// To match behaviour of the LSP server where only one
/// InitializedStateMessageHandler exists for a server (and handlers can be
/// stateful), we hand the handler off the server.
///
/// Using a static causes issues for in-process tests, so this ensures a new
/// server always gets a new handler.
final _handlers = Expando<InitializedStateMessageHandler>();
LspOverLegacyHandler(
super.server, super.request, super.cancellationToken, super.performance) {
_handlers[server] ??= InitializedStateMessageHandler(server);
}
InitializedStateMessageHandler get handler => _handlers[server]!;
@override
Future<void> handle() async {
final params = LspHandleParams.fromRequest(request);
final lspMessageJson = params.lspMessage;
final reporter = LspJsonReporter();
final lspMessage = lspMessageJson is Map<String, Object?> &&
RequestMessage.canParse(lspMessageJson, reporter)
? RequestMessage.fromJson(lspMessageJson)
: null;
if (lspMessage != null) {
await handleRequest(lspMessage);
} else {
final message =
"The 'lspMessage' parameter was not a valid LSP request:\n"
"${reporter.errors.join('\n')}";
final error = RequestError(RequestErrorCode.INVALID_PARAMETER, message);
sendResponse(Response(request.id, error: error));
}
}
Future<void> handleRequest(RequestMessage message) async {
final messageInfo = lsp.MessageInfo(
performance: performance,
timeSinceRequest: request.timeSinceRequest,
);
ErrorOr<Object?> result;
try {
result = await handler.handleMessage(message, messageInfo);
} catch (e) {
final errorMessage =
'An error occurred while handling ${message.method} request: $e';
result = error(ServerErrorCodes.UnhandledError, errorMessage);
}
final lspResponse = ResponseMessage(
id: message.id,
error: result.isError ? result.error : null,
result: !result.isError ? result.result : null,
jsonrpc: jsonRpcVersion,
);
sendResult(LspHandleResult(lspResponse.toJson()));
}
}

View file

@ -64,6 +64,7 @@ import 'package:analysis_server/src/handler/legacy/flutter_get_widget_descriptio
import 'package:analysis_server/src/handler/legacy/flutter_set_subscriptions.dart';
import 'package:analysis_server/src/handler/legacy/flutter_set_widget_property_value.dart';
import 'package:analysis_server/src/handler/legacy/legacy_handler.dart';
import 'package:analysis_server/src/handler/legacy/lsp_over_legacy_handler.dart';
import 'package:analysis_server/src/handler/legacy/search_find_element_references.dart';
import 'package:analysis_server/src/handler/legacy/search_find_member_declarations.dart';
import 'package:analysis_server/src/handler/legacy/search_find_member_references.dart';
@ -78,8 +79,6 @@ import 'package:analysis_server/src/handler/legacy/server_shutdown.dart';
import 'package:analysis_server/src/handler/legacy/unsupported_request.dart';
import 'package:analysis_server/src/lsp/client_capabilities.dart' as lsp;
import 'package:analysis_server/src/lsp/client_configuration.dart' as lsp;
import 'package:analysis_server/src/lsp/handlers/handler_states.dart' as lsp;
import 'package:analysis_server/src/lsp/handlers/handlers.dart' as lsp;
import 'package:analysis_server/src/operation/operation_analysis.dart';
import 'package:analysis_server/src/plugin/notification_manager.dart';
import 'package:analysis_server/src/protocol_server.dart' as server;
@ -260,6 +259,9 @@ class LegacyAnalysisServer extends AnalysisServer {
ServerSetClientCapabilitiesHandler.new,
SERVER_REQUEST_SET_SUBSCRIPTIONS: ServerSetSubscriptionsHandler.new,
SERVER_REQUEST_SHUTDOWN: ServerShutdownHandler.new,
//
LSP_REQUEST_HANDLE: LspOverLegacyHandler.new,
};
/// The channel from which requests are received and to which responses should
@ -366,10 +368,6 @@ class LegacyAnalysisServer extends AnalysisServer {
/// response when it has been received.
Map<String, Completer<Response>> pendingServerRequests = {};
/// A lazy-initialized handler for LSP requests through the legacy-protocol
/// server.
late final _LspOverLegacyHandler _lspHandler = _LspOverLegacyHandler(this);
/// Initialize a newly created server to receive requests from and send
/// responses to the given [channel].
///
@ -548,8 +546,7 @@ class LegacyAnalysisServer extends AnalysisServer {
var handler =
generator(this, request, cancellationToken, performance);
await handler.handle();
} else if (!(await _lspHandler.handle(
request, cancellationToken, performance))) {
} else {
sendResponse(Response.unknownRequest(request));
}
});
@ -1163,49 +1160,3 @@ class ServerPerformance {
}
}
}
/// A request handler for the legacy server that can delegate to LSP handlers
/// that are written to support either kind of server.
class _LspOverLegacyHandler {
final LegacyAnalysisServer server;
final Map<String, lsp.SharedMessageHandler<Object?, Object?>>
_messageHandlers = {};
_LspOverLegacyHandler(this.server) {
final generators =
lsp.InitializedStateMessageHandler.sharedHandlerGenerators;
for (final generator in generators) {
final handler = generator(server);
_messageHandlers[handler.handlesMessage.toString()] = handler;
}
}
Future<bool> handle(
Request request,
CancellationToken cancellationToken,
OperationPerformanceImpl performance,
) async {
final handler = _messageHandlers[request.method];
if (handler == null) {
return false;
}
final message = lsp.MessageInfo(
performance: performance,
timeSinceRequest: request.timeSinceRequest,
);
final params = handler.jsonHandler.convertParams(request.params);
final result = await handler.handle(params, message, cancellationToken);
server.sendResponse(Response(
request.id,
result: lsp.specToJson(result.resultOrNull) as Map<String, Object?>?,
error: result.isError
? RequestError(RequestErrorCode.INVALID_REQUEST, result.error.message)
: null,
));
return true;
}
}

View file

@ -5,7 +5,7 @@
import 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:analysis_server/src/lsp/handlers/handlers.dart';
class CancelRequestHandler extends LspMessageHandler<CancelParams, void> {
class CancelRequestHandler extends SharedMessageHandler<CancelParams, void> {
final Map<String, CancelableToken> _tokens = {};
CancelRequestHandler(super.server);

View file

@ -23,7 +23,7 @@ class InitializedMessageHandler
@override
Future<ErrorOr<void>> handle(InitializedParams params, MessageInfo message,
CancellationToken token) async {
server.messageHandler = InitializedStateMessageHandler(
server.messageHandler = InitializedLspStateMessageHandler(
server,
);

View file

@ -7,7 +7,7 @@ import 'package:analysis_server/src/lsp/handlers/handlers.dart';
/// A [MessageHandler] that rejects specific types of messages with a given
/// error code/message.
class RejectMessageHandler extends LspMessageHandler<Object?, void> {
class RejectMessageHandler extends SharedMessageHandler<Object?, void> {
@override
final Method handlesMessage;
final ErrorCodes errorCode;

View file

@ -64,13 +64,7 @@ class FailureStateMessageHandler extends ServerStateMessageHandler {
}
}
class InitializedStateMessageHandler extends ServerStateMessageHandler {
/// Generators for handlers that work with any [AnalysisServer].
static const sharedHandlerGenerators =
<_RequestHandlerGenerator<AnalysisServer>>[
HoverHandler.new,
];
class InitializedLspStateMessageHandler extends InitializedStateMessageHandler {
/// Generators for handlers that require an [LspAnalysisServer].
static const lspHandlerGenerators =
<_RequestHandlerGenerator<LspAnalysisServer>>[
@ -117,8 +111,29 @@ class InitializedStateMessageHandler extends ServerStateMessageHandler {
InlayHintHandler.new,
];
InitializedStateMessageHandler(
InitializedLspStateMessageHandler(
LspAnalysisServer server,
) : super(server) {
for (final generator in lspHandlerGenerators) {
registerHandler(generator(server));
}
}
}
/// A message handler for the initialized state that can be used by either
/// server.
///
/// Only handlers that can work with either server are available. Use
/// [InitializedLspStateMessageHandler] for full LSP support.
class InitializedStateMessageHandler extends ServerStateMessageHandler {
/// Generators for handlers that work with any [AnalysisServer].
static const sharedHandlerGenerators =
<_RequestHandlerGenerator<AnalysisServer>>[
HoverHandler.new,
];
InitializedStateMessageHandler(
AnalysisServer server,
) : super(server) {
reject(Method.initialize, ServerErrorCodes.ServerAlreadyInitialized,
'Server already initialized');
@ -128,9 +143,6 @@ class InitializedStateMessageHandler extends ServerStateMessageHandler {
for (final generator in sharedHandlerGenerators) {
registerHandler(generator(server));
}
for (final generator in lspHandlerGenerators) {
registerHandler(generator(server));
}
}
}

View file

@ -319,7 +319,7 @@ mixin PositionalArgCommandHandler {
/// A message handler that handles all messages for a given server state.
abstract class ServerStateMessageHandler {
final LspAnalysisServer server;
final AnalysisServer server;
final Map<Method, SharedMessageHandler<Object?, Object?>> _messageHandlers =
{};
final CancelRequestHandler _cancelHandler;

View file

@ -102,3 +102,6 @@ server calls. This file is validated by `coverage_test.dart`.
- [ ] flutter.outline
- [ ] flutter.getWidgetDescription
- [ ] flutter.setWidgetPropertyValue
## lsp domain
- [x] lsp.handle

View file

@ -82,9 +82,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');
});
}
});

View file

@ -0,0 +1,167 @@
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:analysis_server/src/lsp/constants.dart';
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';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(LspOverLegacyTest);
});
}
@reflectiveTest
class LspOverLegacyTest extends AbstractAnalysisServerIntegrationTest
with LspRequestHelpersMixin {
@override
Future<T> expectSuccessfulResponseTo<T, R>(
RequestMessage message,
T Function(R) fromJson,
) async {
final legacyResult = await sendLspHandle(message.toJson());
final lspResponseJson = legacyResult.lspResponse as Map<String, Object?>;
// 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);
}
}
Future<void> test_error_invalidLspRequest() async {
await standardAnalysisSetup();
await analysisFinished;
try {
await sendLspHandle({'id': '1'});
fail('expected INVALID_PARAMETER');
} on ServerErrorMessage catch (message) {
expect(message.error['code'], 'INVALID_PARAMETER');
expect(
message.error['message'],
"The 'lspMessage' parameter was not a valid LSP request:\n"
"jsonrpc must not be undefined");
}
}
Future<void> test_error_lspHandlerError() async {
// This file will not be created.
final testFile = sourcePath('lib/test.dart');
await standardAnalysisSetup();
await analysisFinished;
await expectLater(
getHover(Uri.file(testFile), Position(character: 0, line: 0)),
throwsA(isResponseError(ServerErrorCodes.InvalidFilePath,
message: 'File does not exist')),
);
}
Future<void> test_hover() async {
final testFile = sourcePath('lib/test.dart');
final code = TestCode.parse('''
/// This is my class.
class [!A^aa!] {}
''');
writeFile(testFile, code.code);
await standardAnalysisSetup();
await analysisFinished;
final result = await getHover(Uri.file(testFile), code.position.position);
expect(result!.range, code.range.range);
_expectMarkdown(
result.contents,
'''
```dart
class Aaa
```
*package:test/test.dart*
---
This is my class.
''',
);
}
/// Tests the protocol using JSON instead of helpers.
///
/// This is to verify (and document) the exact payloads for `lsp.handle`
/// in a way that is not abstracted by (or affected by refactors to) helper
/// methods to ensure this never changes in a way that will affect clients.
Future<void> test_hover_rawProtocol() async {
final testFile = sourcePath('lib/test.dart');
final code = TestCode.parse('''
/// This is my class.
class [!A^aa!] {}
''');
const expectedHover = '''
```dart
class Aaa
```
*package:test/test.dart*
---
This is my class.''';
writeFile(testFile, code.code);
await standardAnalysisSetup();
await analysisFinished;
final response = await server.send('lsp.handle', {
'lspMessage': {
'jsonrpc': '2.0',
'id': '12345',
'method': Method.textDocument_hover.toString(),
'params': {
"textDocument": {"uri": Uri.file(testFile).toString()},
"position": code.position.position.toJson(),
},
}
});
expect(response, {
'lspResponse': {
'id': '12345',
'jsonrpc': '2.0',
'result': {
'contents': {'kind': 'markdown', 'value': expectedHover},
'range': code.range.range.toJson()
}
}
});
}
void _expectMarkdown(
Either2<MarkupContent, String> 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());
}
}

View file

@ -0,0 +1,3 @@
This folder is for the "lsp" domain in the legacy server.
Tests for the native LSP server are in the lsp_server folder.

View file

@ -0,0 +1,13 @@
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:test_reflective_loader/test_reflective_loader.dart';
import 'handle_test.dart' as handle_test;
void main() {
defineReflectiveSuite(() {
handle_test.main();
}, name: 'lsp');
}

View file

@ -1,90 +0,0 @@
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package: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 '../../utils/test_code_extensions.dart';
import '../support/integration_tests.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(LspOverLegacyTest);
});
}
@reflectiveTest
class LspOverLegacyTest extends AbstractAnalysisServerIntegrationTest
with LspRequestHelpersMixin {
@override
Future<T> expectSuccessfulResponseTo<T, R>(
RequestMessage request, T Function(R) fromJson) async {
final resp = await server.send(
request.method.toString(),
specToJson(request.params) as Map<String, Object?>,
);
return fromJson(resp as R);
}
Future<void> test_error() async {
// This file will not be created.
final testFile = sourcePath('lib/test.dart');
await standardAnalysisSetup();
await analysisFinished;
try {
await getHover(Uri.file(testFile), Position(character: 0, line: 0));
fail('expected INVALID_REQUEST');
} on ServerErrorMessage catch (message) {
expect(message.error['code'], 'INVALID_REQUEST');
expect(message.error['message'], 'File does not exist');
}
}
Future<void> test_hover() async {
final testFile = sourcePath('lib/test.dart');
final code = TestCode.parse('''
/// This is my class.
class [!A^aa!] {}
''');
writeFile(testFile, code.code);
await standardAnalysisSetup();
await analysisFinished;
final result = await getHover(Uri.file(testFile), code.position.position);
expect(result!.range, code.range.range);
_expectMarkdown(
result.contents,
'''
```dart
class Aaa
```
*package:test/test.dart*
---
This is my class.
''',
);
}
void _expectMarkdown(
Either2<MarkupContent, String> 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());
}
}

View file

@ -7,7 +7,6 @@ import 'package:test_reflective_loader/test_reflective_loader.dart';
import 'blaze_changes_test.dart' as blaze_changes_test;
import 'command_line_options_test.dart' as command_line_options_test;
import 'get_version_test.dart' as get_version_test;
import 'lsp_over_legacy_test.dart' as lsp_over_legacy;
import 'set_subscriptions_invalid_service_test.dart'
as set_subscriptions_invalid_service_test;
import 'set_subscriptions_test.dart' as set_subscriptions_test;
@ -19,7 +18,6 @@ void main() {
blaze_changes_test.main();
command_line_options_test.main();
get_version_test.main();
lsp_over_legacy.main();
set_subscriptions_test.main();
set_subscriptions_invalid_service_test.main();
shutdown_test.main();

View file

@ -2731,6 +2731,26 @@ abstract class IntegrationTest {
/// Stream controller for [onFlutterOutline].
final _onFlutterOutline = StreamController<FlutterOutlineParams>(sync: true);
/// Call an LSP handler. Message can be requests or notifications.
///
/// Parameters
///
/// lspMessage: object
///
/// The LSP RequestMessage.
///
/// Returns
///
/// lspResponse: object
///
/// The LSP ResponseMessage returned by the handler.
Future<LspHandleResult> sendLspHandle(Object lspMessage) async {
var params = LspHandleParams(lspMessage).toJson();
var result = await server.send('lsp.handle', params);
var decoder = ResponseDecoder(null);
return LspHandleResult.fromJson(decoder, 'result', result);
}
/// Dispatch the notification named [event], and containing parameters
/// [params], to the appropriate stream.
void dispatchNotification(String event, params) {

View file

@ -28,6 +28,8 @@ const Matcher isNotification = MatchesJsonObject(
'notification', {'event': isString},
optionalFields: {'params': isMap});
const Matcher isObject = TypeMatcher<Object>();
const Matcher isString = TypeMatcher<String>();
final Matcher isResponse = MatchesJsonObject('response', {'id': isString},

View file

@ -2868,6 +2868,22 @@ final Matcher isInlineMethodFeedback = LazyMatcher(() => MatchesJsonObject(
final Matcher isInlineMethodOptions = LazyMatcher(() => MatchesJsonObject(
'inlineMethod options', {'deleteSource': isBool, 'inlineAll': isBool}));
/// lsp.handle params
///
/// {
/// "lspMessage": object
/// }
final Matcher isLspHandleParams = LazyMatcher(
() => MatchesJsonObject('lsp.handle params', {'lspMessage': isObject}));
/// lsp.handle result
///
/// {
/// "lspResponse": object
/// }
final Matcher isLspHandleResult = LazyMatcher(
() => MatchesJsonObject('lsp.handle result', {'lspResponse': isObject}));
/// moveFile feedback
final Matcher isMoveFileFeedback = isNull;

View file

@ -12,6 +12,7 @@ import 'diagnostic/test_all.dart' as diagnostic;
import 'edit/test_all.dart' as edit;
import 'execution/test_all.dart' as execution;
import 'linter/test_all.dart' as linter;
import 'lsp/test_all.dart' as lsp;
import 'lsp_server/test_all.dart' as lsp_server;
import 'search/test_all.dart' as search;
import 'server/test_all.dart' as server;
@ -26,6 +27,7 @@ void main() {
edit.main();
execution.main();
linter.main();
lsp.main();
lsp_server.main();
search.main();
server.main();

View file

@ -8,28 +8,52 @@ import 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:analysis_server/src/protocol_server.dart';
import '../analysis_server_base.dart';
import '../lsp/request_helpers_mixin.dart';
abstract class LspOverLegacyTest extends PubPackageAnalysisServerTest {
abstract class LspOverLegacyTest extends PubPackageAnalysisServerTest
with LspRequestHelpersMixin {
var _requestId = 0;
TextDocumentIdentifier get testFileIdentifier => TextDocumentIdentifier(
uri:
server.resourceProvider.pathContext.toUri(convertPath(testFilePath)));
Uri get testFileUri =>
server.resourceProvider.pathContext.toUri(convertPath(testFilePath));
Request createRequest(Method method, ToJsonable params) {
return Request('${_requestId++}', method.toString(),
params.toJson() as Map<String, Object?>);
}
@override
Future<T> expectSuccessfulResponseTo<T, R>(
RequestMessage message,
T Function(R) fromJson,
) async {
// Round-trip request via JSON because this doesn't happen automatically
// when we're bypassing the streams (running in-process) and we want to
// validate everything.
final messageJson =
jsonDecode(jsonEncode(message.toJson())) as Map<String, Object?>;
Future<T> sendRequest<T>(
Request request, T Function(Map<String, Object?>) fromJson) async {
final response = await handleSuccessfulRequest(request);
final legacyRequest = Request(
'${_requestId++}',
'lsp.handle',
LspHandleParams(messageJson).toJson(),
);
final legacyResponse = await handleSuccessfulRequest(legacyRequest);
final legacyResult = LspHandleResult.fromResponse(legacyResponse);
// Round-trip via JSON because this doesn't happen automatically when
// we're bypassing the streams (running in-process) and we want to ensure
// everything is valid.
final jsonResult = jsonDecode(jsonEncode(response.result));
return fromJson(jsonResult as Map<String, Object?>);
// 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 lspResponseJson = jsonDecode(jsonEncode(legacyResult.lspResponse))
as Map<String, Object?>;
// 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);
}
}
@override

View file

@ -23,15 +23,8 @@ class HoverTest extends LspOverLegacyTest {
newFile(testFilePath, code.code);
await waitForTasksFinished();
final request = createRequest(
Method.textDocument_hover,
HoverParams(
position: code.position.position,
textDocument: testFileIdentifier,
),
);
final result = await sendRequest(request, Hover.fromJson);
final markup = _getMarkupContents(result);
final result = await getHover(testFileUri, code.position.position);
final markup = _getMarkupContents(result!);
expect(markup.kind, MarkupKind.Markdown);
expect(markup.value.trimRight(), expected.trimRight());
expect(result.range, code.range.range);

View file

@ -9,7 +9,7 @@ class DartCodegenVisitor extends HierarchicalApiVisitor {
/// Type references in the spec that are named something else in Dart.
static const Map<String, String> _typeRenames = {
'long': 'int',
'object': 'Map',
'object': 'Object',
};
DartCodegenVisitor(super.api);

View file

@ -836,17 +836,20 @@ class CodegenProtocolVisitor extends DartCodegenVisitor with CodeGenerator {
writeln('@override');
writeln('Map<String, Object> toJson() {');
indent(() {
writeln('var result = <String, Object>{};');
var resultMapName = type.fields.any((field) => field.name == 'result')
? 'result_'
: 'result';
writeln('var $resultMapName = <String, Object>{};');
for (var field in type.fields) {
var fieldNameString = literalString(field.name);
var fieldValue = field.value;
if (fieldValue is String) {
var valueString = literalString(fieldValue);
writeln('result[$fieldNameString] = $valueString;');
writeln('$resultMapName[$fieldNameString] = $valueString;');
continue;
}
var fieldToJson = toJsonCode(field.type).asSnippet(field.name);
var populateField = 'result[$fieldNameString] = $fieldToJson;';
var populateField = '$resultMapName[$fieldNameString] = $fieldToJson;';
if (field.optional) {
var name = field.name;
writeln('var $name = this.$name;');
@ -859,7 +862,7 @@ class CodegenProtocolVisitor extends DartCodegenVisitor with CodeGenerator {
writeln(populateField);
}
}
writeln('return result;');
writeln('return $resultMapName;');
});
writeln('}');
}
@ -956,7 +959,7 @@ class CodegenProtocolVisitor extends DartCodegenVisitor with CodeGenerator {
case 'long':
return FromJsonFunction('jsonDecoder.decodeInt');
case 'object':
return FromJsonIdentity();
return FromJsonSnippet((jsonPath, json) => '$json as Object');
default:
throw Exception('Unexpected type name ${type.typeName}');
}

View file

@ -881,6 +881,15 @@ public interface AnalysisServer {
*/
public boolean isSocketOpen();
/**
* {@code lsp.handle}
*
* Call an LSP handler. Message can be requests or notifications.
*
* @param lspMessage The LSP RequestMessage.
*/
public void lsp_handle(Object lspMessage, HandleConsumer consumer);
/**
* Remove the given listener from the list of listeners that will receive notification when new
* analysis results become available.

View file

@ -3607,6 +3607,32 @@
</params>
</notification>
</domain>
<domain name="lsp" experimental="true">
<p>
The LSP domain contains API's for interacting with LSP handlers.
</p>
<request method="handle" experimental="true">
<p>
Call an LSP handler. Message can be requests or notifications.
</p>
<params>
<field name="lspMessage">
<p>
The LSP RequestMessage.
</p>
<ref>object</ref>
</field>
</params>
<result>
<field name="lspResponse">
<p>
The LSP ResponseMessage returned by the handler.
</p>
<ref>object</ref>
</field>
</result>
</request>
</domain>
<types>
<h2 class="domain"><a name="types">Types</a></h2>
<p>

View file

@ -311,6 +311,9 @@ 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_REQUEST_HANDLE = 'lsp.handle';
const String LSP_REQUEST_HANDLE_LSP_MESSAGE = 'lspMessage';
const String LSP_RESPONSE_HANDLE_LSP_RESPONSE = 'lspResponse';
const String SEARCH_NOTIFICATION_RESULTS = 'search.results';
const String SEARCH_NOTIFICATION_RESULTS_ID = 'id';
const String SEARCH_NOTIFICATION_RESULTS_IS_LAST = 'isLast';

View file

@ -12992,6 +12992,130 @@ class LibraryPathSet implements HasToJson {
);
}
/// lsp.handle params
///
/// {
/// "lspMessage": object
/// }
///
/// Clients may not extend, implement or mix-in this class.
class LspHandleParams implements RequestParams {
/// The LSP RequestMessage.
Object lspMessage;
LspHandleParams(this.lspMessage);
factory LspHandleParams.fromJson(
JsonDecoder jsonDecoder, String jsonPath, Object? json) {
json ??= {};
if (json is Map) {
Object lspMessage;
if (json.containsKey('lspMessage')) {
lspMessage = json['lspMessage'] as Object;
} else {
throw jsonDecoder.mismatch(jsonPath, 'lspMessage');
}
return LspHandleParams(lspMessage);
} else {
throw jsonDecoder.mismatch(jsonPath, 'lsp.handle params', json);
}
}
factory LspHandleParams.fromRequest(Request request) {
return LspHandleParams.fromJson(
RequestDecoder(request), 'params', request.params);
}
@override
Map<String, Object> toJson() {
var result = <String, Object>{};
result['lspMessage'] = lspMessage;
return result;
}
@override
Request toRequest(String id) {
return Request(id, 'lsp.handle', toJson());
}
@override
String toString() => json.encode(toJson());
@override
bool operator ==(other) {
if (other is LspHandleParams) {
return lspMessage == other.lspMessage;
}
return false;
}
@override
int get hashCode => lspMessage.hashCode;
}
/// lsp.handle result
///
/// {
/// "lspResponse": object
/// }
///
/// Clients may not extend, implement or mix-in this class.
class LspHandleResult implements ResponseResult {
/// The LSP ResponseMessage returned by the handler.
Object lspResponse;
LspHandleResult(this.lspResponse);
factory LspHandleResult.fromJson(
JsonDecoder jsonDecoder, String jsonPath, Object? json) {
json ??= {};
if (json is Map) {
Object lspResponse;
if (json.containsKey('lspResponse')) {
lspResponse = json['lspResponse'] as Object;
} else {
throw jsonDecoder.mismatch(jsonPath, 'lspResponse');
}
return LspHandleResult(lspResponse);
} else {
throw jsonDecoder.mismatch(jsonPath, 'lsp.handle result', json);
}
}
factory LspHandleResult.fromResponse(Response response) {
return LspHandleResult.fromJson(
ResponseDecoder(REQUEST_ID_REFACTORING_KINDS.remove(response.id)),
'result',
response.result);
}
@override
Map<String, Object> toJson() {
var result = <String, Object>{};
result['lspResponse'] = lspResponse;
return result;
}
@override
Response toResponse(String id) {
return Response(id, result: toJson());
}
@override
String toString() => json.encode(toJson());
@override
bool operator ==(other) {
if (other is LspHandleResult) {
return lspResponse == other.lspResponse;
}
return false;
}
@override
int get hashCode => lspResponse.hashCode;
}
/// MessageAction
///
/// {