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 /// /// {