From 76dc2c4cfac98eac6b0327cb07a0c6f99232aade Mon Sep 17 00:00:00 2001 From: Danny Tuppeny Date: Sun, 6 Aug 2023 14:32:18 +0000 Subject: [PATCH] [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 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 Reviewed-by: Brian Wilkerson --- pkg/analysis_server/doc/api.html | 1 + .../lib/protocol/protocol_constants.dart | 3 + .../lib/protocol/protocol_generated.dart | 124 +++++++++++++ .../legacy/lsp_over_legacy_handler.dart | 79 +++++++++ .../lib/src/legacy_analysis_server.dart | 59 +------ .../lsp/handlers/handler_cancel_request.dart | 2 +- .../src/lsp/handlers/handler_initialized.dart | 2 +- .../lib/src/lsp/handlers/handler_reject.dart | 2 +- .../lib/src/lsp/handlers/handler_states.dart | 34 ++-- .../lib/src/lsp/handlers/handlers.dart | 2 +- .../test/integration/coverage.md | 3 + .../test/integration/coverage_test.dart | 9 +- .../test/integration/lsp/handle_test.dart | 167 ++++++++++++++++++ .../test/integration/lsp/readme.md | 3 + .../test/integration/lsp/test_all.dart | 13 ++ .../server/lsp_over_legacy_test.dart | 90 ---------- .../test/integration/server/test_all.dart | 2 - .../support/integration_test_methods.dart | 20 +++ .../support/integration_tests.dart | 2 + .../support/protocol_matchers.dart | 16 ++ .../test/integration/test_all.dart | 2 + .../abstract_lsp_over_legacy.dart | 56 ++++-- .../test/lsp_over_legacy/hover_test.dart | 11 +- .../tool/spec/codegen_dart.dart | 2 +- .../tool/spec/codegen_dart_protocol.dart | 13 +- .../spec/generated/java/AnalysisServer.java | 9 + pkg/analysis_server/tool/spec/spec_input.html | 26 +++ .../lib/src/protocol/protocol_constants.dart | 3 + .../lib/src/protocol/protocol_generated.dart | 124 +++++++++++++ 29 files changed, 684 insertions(+), 195 deletions(-) create mode 100644 pkg/analysis_server/lib/src/handler/legacy/lsp_over_legacy_handler.dart create mode 100644 pkg/analysis_server/test/integration/lsp/handle_test.dart create mode 100644 pkg/analysis_server/test/integration/lsp/readme.md create mode 100644 pkg/analysis_server/test/integration/lsp/test_all.dart delete mode 100644 pkg/analysis_server/test/integration/server/lsp_over_legacy_test.dart diff --git a/pkg/analysis_server/doc/api.html b/pkg/analysis_server/doc/api.html index b792411dbfe..d86890ce38a 100644 --- a/pkg/analysis_server/doc/api.html +++ b/pkg/analysis_server/doc/api.html @@ -3375,6 +3375,7 @@ a:focus, a:hover {

+

Types

This section contains descriptions of the data types referenced diff --git a/pkg/analysis_server/lib/protocol/protocol_constants.dart b/pkg/analysis_server/lib/protocol/protocol_constants.dart index e0f4b6e199b..065a1d7d05e 100644 --- a/pkg/analysis_server/lib/protocol/protocol_constants.dart +++ b/pkg/analysis_server/lib/protocol/protocol_constants.dart @@ -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'; diff --git a/pkg/analysis_server/lib/protocol/protocol_generated.dart b/pkg/analysis_server/lib/protocol/protocol_generated.dart index ce86acd1449..e5c436d0283 100644 --- a/pkg/analysis_server/lib/protocol/protocol_generated.dart +++ b/pkg/analysis_server/lib/protocol/protocol_generated.dart @@ -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 toJson() { + var result = {}; + 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 toJson() { + var result = {}; + 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 /// /// { 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 new file mode 100644 index 00000000000..8f6de76bb21 --- /dev/null +++ b/pkg/analysis_server/lib/src/handler/legacy/lsp_over_legacy_handler.dart @@ -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(); + + LspOverLegacyHandler( + super.server, super.request, super.cancellationToken, super.performance) { + _handlers[server] ??= InitializedStateMessageHandler(server); + } + + InitializedStateMessageHandler get handler => _handlers[server]!; + + @override + Future handle() async { + final params = LspHandleParams.fromRequest(request); + final lspMessageJson = params.lspMessage; + final reporter = LspJsonReporter(); + final lspMessage = lspMessageJson is Map && + 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 handleRequest(RequestMessage message) async { + final messageInfo = lsp.MessageInfo( + performance: performance, + timeSinceRequest: request.timeSinceRequest, + ); + + ErrorOr 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())); + } +} diff --git a/pkg/analysis_server/lib/src/legacy_analysis_server.dart b/pkg/analysis_server/lib/src/legacy_analysis_server.dart index fbfba9159d0..a4816d76ac1 100644 --- a/pkg/analysis_server/lib/src/legacy_analysis_server.dart +++ b/pkg/analysis_server/lib/src/legacy_analysis_server.dart @@ -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> 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> - _messageHandlers = {}; - - _LspOverLegacyHandler(this.server) { - final generators = - lsp.InitializedStateMessageHandler.sharedHandlerGenerators; - for (final generator in generators) { - final handler = generator(server); - _messageHandlers[handler.handlesMessage.toString()] = handler; - } - } - - Future 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?, - error: result.isError - ? RequestError(RequestErrorCode.INVALID_REQUEST, result.error.message) - : null, - )); - - return true; - } -} diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_cancel_request.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_cancel_request.dart index 9bfcf79a5e9..4e24e7e95a0 100644 --- a/pkg/analysis_server/lib/src/lsp/handlers/handler_cancel_request.dart +++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_cancel_request.dart @@ -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 { +class CancelRequestHandler extends SharedMessageHandler { final Map _tokens = {}; CancelRequestHandler(super.server); diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_initialized.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_initialized.dart index 9ab962774cd..0dd5fab5543 100644 --- a/pkg/analysis_server/lib/src/lsp/handlers/handler_initialized.dart +++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_initialized.dart @@ -23,7 +23,7 @@ class InitializedMessageHandler @override Future> handle(InitializedParams params, MessageInfo message, CancellationToken token) async { - server.messageHandler = InitializedStateMessageHandler( + server.messageHandler = InitializedLspStateMessageHandler( server, ); diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_reject.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_reject.dart index b0a03017d7e..0d7e019cf3d 100644 --- a/pkg/analysis_server/lib/src/lsp/handlers/handler_reject.dart +++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_reject.dart @@ -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 { +class RejectMessageHandler extends SharedMessageHandler { @override final Method handlesMessage; final ErrorCodes errorCode; diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_states.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_states.dart index 6110a83b70c..85962a45120 100644 --- a/pkg/analysis_server/lib/src/lsp/handlers/handler_states.dart +++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_states.dart @@ -64,13 +64,7 @@ class FailureStateMessageHandler extends ServerStateMessageHandler { } } -class InitializedStateMessageHandler extends ServerStateMessageHandler { - /// Generators for handlers that work with any [AnalysisServer]. - static const sharedHandlerGenerators = - <_RequestHandlerGenerator>[ - HoverHandler.new, - ]; - +class InitializedLspStateMessageHandler extends InitializedStateMessageHandler { /// Generators for handlers that require an [LspAnalysisServer]. static const lspHandlerGenerators = <_RequestHandlerGenerator>[ @@ -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>[ + 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)); - } } } diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handlers.dart b/pkg/analysis_server/lib/src/lsp/handlers/handlers.dart index 3b2ac4ebf74..33289f58374 100644 --- a/pkg/analysis_server/lib/src/lsp/handlers/handlers.dart +++ b/pkg/analysis_server/lib/src/lsp/handlers/handlers.dart @@ -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> _messageHandlers = {}; final CancelRequestHandler _cancelHandler; diff --git a/pkg/analysis_server/test/integration/coverage.md b/pkg/analysis_server/test/integration/coverage.md index 8d2ded59279..77dabad49ae 100644 --- a/pkg/analysis_server/test/integration/coverage.md +++ b/pkg/analysis_server/test/integration/coverage.md @@ -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 diff --git a/pkg/analysis_server/test/integration/coverage_test.dart b/pkg/analysis_server/test/integration/coverage_test.dart index 73caa7ad31a..d7dbb39ab4c 100644 --- a/pkg/analysis_server/test/integration/coverage_test.dart +++ b/pkg/analysis_server/test/integration/coverage_test.dart @@ -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'); }); } }); diff --git a/pkg/analysis_server/test/integration/lsp/handle_test.dart b/pkg/analysis_server/test/integration/lsp/handle_test.dart new file mode 100644 index 00000000000..1bbf7516169 --- /dev/null +++ b/pkg/analysis_server/test/integration/lsp/handle_test.dart @@ -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 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); + } + } + + Future 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 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 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 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 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/readme.md b/pkg/analysis_server/test/integration/lsp/readme.md new file mode 100644 index 00000000000..2f2c0a16a1f --- /dev/null +++ b/pkg/analysis_server/test/integration/lsp/readme.md @@ -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. diff --git a/pkg/analysis_server/test/integration/lsp/test_all.dart b/pkg/analysis_server/test/integration/lsp/test_all.dart new file mode 100644 index 00000000000..c2f7777ddeb --- /dev/null +++ b/pkg/analysis_server/test/integration/lsp/test_all.dart @@ -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'); +} diff --git a/pkg/analysis_server/test/integration/server/lsp_over_legacy_test.dart b/pkg/analysis_server/test/integration/server/lsp_over_legacy_test.dart deleted file mode 100644 index 1112a815622..00000000000 --- a/pkg/analysis_server/test/integration/server/lsp_over_legacy_test.dart +++ /dev/null @@ -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 expectSuccessfulResponseTo( - RequestMessage request, T Function(R) fromJson) async { - final resp = await server.send( - request.method.toString(), - specToJson(request.params) as Map, - ); - return fromJson(resp as R); - } - - Future 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 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 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/server/test_all.dart b/pkg/analysis_server/test/integration/server/test_all.dart index 465e26fe001..d96283a3ab1 100644 --- a/pkg/analysis_server/test/integration/server/test_all.dart +++ b/pkg/analysis_server/test/integration/server/test_all.dart @@ -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(); 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 076eab7d1a4..d0170765872 100644 --- a/pkg/analysis_server/test/integration/support/integration_test_methods.dart +++ b/pkg/analysis_server/test/integration/support/integration_test_methods.dart @@ -2731,6 +2731,26 @@ abstract class IntegrationTest { /// Stream controller for [onFlutterOutline]. final _onFlutterOutline = StreamController(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 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) { diff --git a/pkg/analysis_server/test/integration/support/integration_tests.dart b/pkg/analysis_server/test/integration/support/integration_tests.dart index 80913b8c5ed..37a6986c758 100644 --- a/pkg/analysis_server/test/integration/support/integration_tests.dart +++ b/pkg/analysis_server/test/integration/support/integration_tests.dart @@ -28,6 +28,8 @@ const Matcher isNotification = MatchesJsonObject( 'notification', {'event': isString}, optionalFields: {'params': isMap}); +const Matcher isObject = TypeMatcher(); + const Matcher isString = TypeMatcher(); final Matcher isResponse = MatchesJsonObject('response', {'id': isString}, diff --git a/pkg/analysis_server/test/integration/support/protocol_matchers.dart b/pkg/analysis_server/test/integration/support/protocol_matchers.dart index 224b886f43d..1b8e87ade28 100644 --- a/pkg/analysis_server/test/integration/support/protocol_matchers.dart +++ b/pkg/analysis_server/test/integration/support/protocol_matchers.dart @@ -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; diff --git a/pkg/analysis_server/test/integration/test_all.dart b/pkg/analysis_server/test/integration/test_all.dart index 0581afe600b..bdfca33a816 100644 --- a/pkg/analysis_server/test/integration/test_all.dart +++ b/pkg/analysis_server/test/integration/test_all.dart @@ -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(); 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 42b30801517..264ced66e51 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 @@ -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); - } + @override + Future expectSuccessfulResponseTo( + 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; - Future sendRequest( - Request request, T Function(Map) 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); + // 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; + + // 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 diff --git a/pkg/analysis_server/test/lsp_over_legacy/hover_test.dart b/pkg/analysis_server/test/lsp_over_legacy/hover_test.dart index da8411924d7..1f6f347c537 100644 --- a/pkg/analysis_server/test/lsp_over_legacy/hover_test.dart +++ b/pkg/analysis_server/test/lsp_over_legacy/hover_test.dart @@ -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); diff --git a/pkg/analysis_server/tool/spec/codegen_dart.dart b/pkg/analysis_server/tool/spec/codegen_dart.dart index fe679a9bb50..158ff7d1b48 100644 --- a/pkg/analysis_server/tool/spec/codegen_dart.dart +++ b/pkg/analysis_server/tool/spec/codegen_dart.dart @@ -9,7 +9,7 @@ class DartCodegenVisitor extends HierarchicalApiVisitor { /// Type references in the spec that are named something else in Dart. static const Map _typeRenames = { 'long': 'int', - 'object': 'Map', + 'object': 'Object', }; DartCodegenVisitor(super.api); diff --git a/pkg/analysis_server/tool/spec/codegen_dart_protocol.dart b/pkg/analysis_server/tool/spec/codegen_dart_protocol.dart index 75b24298d86..81415aa852e 100644 --- a/pkg/analysis_server/tool/spec/codegen_dart_protocol.dart +++ b/pkg/analysis_server/tool/spec/codegen_dart_protocol.dart @@ -836,17 +836,20 @@ class CodegenProtocolVisitor extends DartCodegenVisitor with CodeGenerator { writeln('@override'); writeln('Map toJson() {'); indent(() { - writeln('var result = {};'); + var resultMapName = type.fields.any((field) => field.name == 'result') + ? 'result_' + : 'result'; + writeln('var $resultMapName = {};'); 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}'); } diff --git a/pkg/analysis_server/tool/spec/generated/java/AnalysisServer.java b/pkg/analysis_server/tool/spec/generated/java/AnalysisServer.java index f8757343fa3..300b6dd5a69 100644 --- a/pkg/analysis_server/tool/spec/generated/java/AnalysisServer.java +++ b/pkg/analysis_server/tool/spec/generated/java/AnalysisServer.java @@ -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. diff --git a/pkg/analysis_server/tool/spec/spec_input.html b/pkg/analysis_server/tool/spec/spec_input.html index d24388425d2..ccf40334776 100644 --- a/pkg/analysis_server/tool/spec/spec_input.html +++ b/pkg/analysis_server/tool/spec/spec_input.html @@ -3607,6 +3607,32 @@ + +

+ The LSP domain contains API's for interacting with LSP handlers. +

+ +

+ Call an LSP handler. Message can be requests or notifications. +

+ + +

+ The LSP RequestMessage. +

+ object +
+
+ + +

+ The LSP ResponseMessage returned by the handler. +

+ object +
+
+
+

Types

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 e0f4b6e199b..065a1d7d05e 100644 --- a/pkg/analysis_server_client/lib/src/protocol/protocol_constants.dart +++ b/pkg/analysis_server_client/lib/src/protocol/protocol_constants.dart @@ -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'; 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 c099d3b58fb..7d641078975 100644 --- a/pkg/analysis_server_client/lib/src/protocol/protocol_generated.dart +++ b/pkg/analysis_server_client/lib/src/protocol/protocol_generated.dart @@ -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 toJson() { + var result = {}; + 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 toJson() { + var result = {}; + 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 /// /// {