From ad3c3644a4c6aecfb2d30ffd5251f6a1e165cf49 Mon Sep 17 00:00:00 2001 From: Devon Carew Date: Wed, 6 Jan 2021 18:14:51 +0000 Subject: [PATCH] Add an initial version of a 'dart language-server' command. Change-Id: Iffb8dedf7419a421e5282e09b4584e768c47e53f Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/176485 Commit-Queue: Devon Carew Reviewed-by: Brian Wilkerson --- pkg/analysis_server/README.md | 4 +- .../lib/src/server/driver.dart | 214 +++++++++++------- pkg/analysis_server/lib/starter.dart | 3 +- pkg/dartdev/lib/dartdev.dart | 2 + .../lib/src/commands/language_server.dart | 53 +++++ pkg/dartdev/pubspec.yaml | 5 +- .../test/commands/language_server_test.dart | 101 +++++++++ pkg/dartdev/test/test_all.dart | 2 + pkg/dartdev/test/utils.dart | 14 ++ 9 files changed, 305 insertions(+), 93 deletions(-) create mode 100644 pkg/dartdev/lib/src/commands/language_server.dart create mode 100644 pkg/dartdev/test/commands/language_server_test.dart diff --git a/pkg/analysis_server/README.md b/pkg/analysis_server/README.md index d93666c73b0..c6a1813a8eb 100644 --- a/pkg/analysis_server/README.md +++ b/pkg/analysis_server/README.md @@ -13,8 +13,8 @@ not have a human-friendly user interface. Clients (typically tools, such as an editor) are expected to run the analysis server in a separate process and communicate with it using a JSON protocol. The original protocol is specified in the file [`analysis_server/doc/api.html`][api] -and (less complete) [Language Server Protocol][lsp_spec] support is documented -in [`tool/lsp_spec/README.md`](tool/lsp_spec/README.md). +and [Language Server Protocol][lsp_spec] support is documented in +[`tool/lsp_spec/README.md`](tool/lsp_spec/README.md). ## Features and bugs diff --git a/pkg/analysis_server/lib/src/server/driver.dart b/pkg/analysis_server/lib/src/server/driver.dart index 183c6698b80..d7efddf0be6 100644 --- a/pkg/analysis_server/lib/src/server/driver.dart +++ b/pkg/analysis_server/lib/src/server/driver.dart @@ -98,6 +98,11 @@ class Driver implements ServerStarter { /// The path to the data cache. static const String CACHE_FOLDER = 'cache'; + /// The name of the flag specifying the server protocol to use. + static const String SERVER_PROTOCOL = 'protocol'; + static const String PROTOCOL_ANALYZER = 'analyzer'; + static const String PROTOCOL_LSP = 'lsp'; + /// The name of the flag to use the Language Server Protocol (LSP). static const String USE_LSP = 'lsp'; @@ -124,17 +129,26 @@ class Driver implements ServerStarter { /// If [sendPort] is not null, assumes this is launched in an isolate and will /// connect to the original isolate via an [IsolateChannel]. @override - void start(List arguments, [SendPort sendPort]) { - var parser = _createArgParser(); + void start( + List arguments, { + SendPort sendPort, + bool defaultToLsp = false, + }) { + var parser = createArgParser(defaultToLsp: defaultToLsp); var results = parser.parse(arguments); var analysisServerOptions = AnalysisServerOptions(); analysisServerOptions.newAnalysisDriverLog = results[ANALYSIS_DRIVER_LOG] ?? results[ANALYSIS_DRIVER_LOG_ALIAS]; analysisServerOptions.clientId = results[CLIENT_ID]; - analysisServerOptions.useLanguageServerProtocol = results[USE_LSP]; - // For clients that don't supply their own identifier, use a default based on - // whether the server will run in LSP mode or not. + if (results.wasParsed(USE_LSP)) { + analysisServerOptions.useLanguageServerProtocol = results[USE_LSP]; + } else { + analysisServerOptions.useLanguageServerProtocol = + results[SERVER_PROTOCOL] == PROTOCOL_LSP; + } + // For clients that don't supply their own identifier, use a default based + // on whether the server will run in LSP mode or not. analysisServerOptions.clientId ??= analysisServerOptions.useLanguageServerProtocol ? 'unknown.client.lsp' @@ -478,11 +492,95 @@ class Driver implements ServerStarter { return runZoned(callback, zoneSpecification: zoneSpecification); } + DartSdk _createDefaultSdk(String defaultSdkPath) { + var resourceProvider = PhysicalResourceProvider.INSTANCE; + return FolderBasedDartSdk( + resourceProvider, + resourceProvider.getFolder(defaultSdkPath), + ); + } + + /// Constructs a uuid combining the current date and a random integer. + String _generateUuidString() { + var millisecondsSinceEpoch = DateTime.now().millisecondsSinceEpoch; + var random = Random().nextInt(0x3fffffff); + return '$millisecondsSinceEpoch$random'; + } + + String _getSdkPath(ArgResults args) { + if (args[DART_SDK] != null) { + return args[DART_SDK]; + } else if (args[DART_SDK_ALIAS] != null) { + return args[DART_SDK_ALIAS]; + } else { + return getSdkPath(); + } + } + + /// Print information about how to use the server. + void _printUsage( + ArgParser parser, + telemetry.Analytics analytics, { + bool fromHelp = false, + }) { + print('Usage: $BINARY_NAME [flags]'); + print(''); + print('Supported flags are:'); + print(parser.usage); + + if (telemetry.SHOW_ANALYTICS_UI) { + // Print analytics status and information. + if (fromHelp) { + print(''); + print(telemetry.analyticsNotice); + } + print(''); + print(telemetry.createAnalyticsStatusMessage(analytics.enabled, + command: ANALYTICS_FLAG)); + } + } + + /// Read the UUID from disk, generating and storing a new one if necessary. + String _readUuid(InstrumentationService service) { + final instrumentationLocation = + PhysicalResourceProvider.INSTANCE.getStateLocation('.instrumentation'); + if (instrumentationLocation == null) { + return _generateUuidString(); + } + var uuidFile = File(instrumentationLocation.getChild('uuid.txt').path); + try { + if (uuidFile.existsSync()) { + var uuid = uuidFile.readAsStringSync(); + if (uuid != null && uuid.length > 5) { + return uuid; + } + } + } catch (exception, stackTrace) { + service.logException(exception, stackTrace); + } + var uuid = _generateUuidString(); + try { + uuidFile.parent.createSync(recursive: true); + uuidFile.writeAsStringSync(uuid); + } catch (exception, stackTrace) { + service.logException(exception, stackTrace); + // Slightly alter the uuid to indicate it was not persisted + uuid = 'temp-$uuid'; + } + return uuid; + } + /// Create and return the parser used to parse the command-line arguments. - ArgParser _createArgParser() { - var parser = ArgParser(); - parser.addFlag(HELP_OPTION, - abbr: 'h', negatable: false, help: 'Print this usage information.'); + static ArgParser createArgParser({ + int usageLineLength, + bool includeHelpFlag = true, + bool defaultToLsp = false, + }) { + var parser = ArgParser(usageLineLength: usageLineLength); + if (includeHelpFlag) { + parser.addFlag(HELP_OPTION, + abbr: 'h', negatable: false, help: 'Print this usage information.'); + } parser.addOption(CLIENT_ID, valueHelp: 'name', help: 'An identifier for the analysis server client.'); @@ -495,10 +593,28 @@ class Driver implements ServerStarter { parser.addOption(CACHE_FOLDER, valueHelp: 'path', help: 'Override the location of the analysis server\'s cache.'); + + parser.addOption( + SERVER_PROTOCOL, + defaultsTo: defaultToLsp ? PROTOCOL_LSP : PROTOCOL_ANALYZER, + valueHelp: 'protocol', + allowed: [PROTOCOL_LSP, PROTOCOL_ANALYZER], + allowedHelp: { + PROTOCOL_LSP: 'The Language Server Protocol ' + '(https://microsoft.github.io/language-server-protocol)', + PROTOCOL_ANALYZER: 'Dart\'s analysis server protocol ' + '(https://dart.dev/go/analysis-server-protocol)', + }, + help: + 'Specify the protocol to use to communicate with the analysis server.', + ); + // This option is hidden but still accepted; it's effectively translated to + // the 'protocol' option above. parser.addFlag(USE_LSP, defaultsTo: false, negatable: false, - help: 'Whether to use the Language Server Protocol (LSP).'); + help: 'Whether to use the Language Server Protocol (LSP).', + hide: true); parser.addSeparator('Server diagnostics:'); @@ -584,84 +700,6 @@ class Driver implements ServerStarter { return parser; } - DartSdk _createDefaultSdk(String defaultSdkPath) { - var resourceProvider = PhysicalResourceProvider.INSTANCE; - return FolderBasedDartSdk( - resourceProvider, - resourceProvider.getFolder(defaultSdkPath), - ); - } - - /// Constructs a uuid combining the current date and a random integer. - String _generateUuidString() { - var millisecondsSinceEpoch = DateTime.now().millisecondsSinceEpoch; - var random = Random().nextInt(0x3fffffff); - return '$millisecondsSinceEpoch$random'; - } - - String _getSdkPath(ArgResults args) { - if (args[DART_SDK] != null) { - return args[DART_SDK]; - } else if (args[DART_SDK_ALIAS] != null) { - return args[DART_SDK_ALIAS]; - } else { - return getSdkPath(); - } - } - - /// Print information about how to use the server. - void _printUsage( - ArgParser parser, - telemetry.Analytics analytics, { - bool fromHelp = false, - }) { - print('Usage: $BINARY_NAME [flags]'); - print(''); - print('Supported flags are:'); - print(parser.usage); - - if (telemetry.SHOW_ANALYTICS_UI) { - // Print analytics status and information. - if (fromHelp) { - print(''); - print(telemetry.analyticsNotice); - } - print(''); - print(telemetry.createAnalyticsStatusMessage(analytics.enabled, - command: ANALYTICS_FLAG)); - } - } - - /// Read the UUID from disk, generating and storing a new one if necessary. - String _readUuid(InstrumentationService service) { - final instrumentationLocation = - PhysicalResourceProvider.INSTANCE.getStateLocation('.instrumentation'); - if (instrumentationLocation == null) { - return _generateUuidString(); - } - var uuidFile = File(instrumentationLocation.getChild('uuid.txt').path); - try { - if (uuidFile.existsSync()) { - var uuid = uuidFile.readAsStringSync(); - if (uuid != null && uuid.length > 5) { - return uuid; - } - } - } catch (exception, stackTrace) { - service.logException(exception, stackTrace); - } - var uuid = _generateUuidString(); - try { - uuidFile.parent.createSync(recursive: true); - uuidFile.writeAsStringSync(uuid); - } catch (exception, stackTrace) { - service.logException(exception, stackTrace); - // Slightly alter the uuid to indicate it was not persisted - uuid = 'temp-$uuid'; - } - return uuid; - } - /// Perform log files rolling. /// /// Rename existing files with names `[path].(x)` to `[path].(x+1)`. diff --git a/pkg/analysis_server/lib/starter.dart b/pkg/analysis_server/lib/starter.dart index 5877e34435b..78fb7736f3a 100644 --- a/pkg/analysis_server/lib/starter.dart +++ b/pkg/analysis_server/lib/starter.dart @@ -31,5 +31,6 @@ abstract class ServerStarter { set instrumentationService(InstrumentationService service); /// Use the given command-line [arguments] to start this server. - void start(List arguments, [SendPort sendPort]); + void start(List arguments, + {SendPort sendPort, bool defaultToLsp = false}); } diff --git a/pkg/dartdev/lib/dartdev.dart b/pkg/dartdev/lib/dartdev.dart index 5e8f2a9283b..b194847c61c 100644 --- a/pkg/dartdev/lib/dartdev.dart +++ b/pkg/dartdev/lib/dartdev.dart @@ -20,6 +20,7 @@ import 'src/commands/analyze.dart'; import 'src/commands/compile.dart'; import 'src/commands/create.dart'; import 'src/commands/fix.dart'; +import 'src/commands/language_server.dart'; import 'src/commands/run.dart'; import 'src/commands/test.dart'; import 'src/core.dart'; @@ -96,6 +97,7 @@ class DartdevRunner extends CommandRunner { addCommand(CompileCommand(verbose: verbose)); addCommand(FixCommand(verbose: verbose)); addCommand(FormatCommand(verbose: verbose)); + addCommand(LanguageServerCommand(verbose: verbose)); addCommand(MigrateCommand(verbose: verbose)); addCommand(pubCommand()); addCommand(RunCommand(verbose: verbose)); diff --git a/pkg/dartdev/lib/src/commands/language_server.dart b/pkg/dartdev/lib/src/commands/language_server.dart new file mode 100644 index 00000000000..924ae19a6af --- /dev/null +++ b/pkg/dartdev/lib/src/commands/language_server.dart @@ -0,0 +1,53 @@ +// Copyright (c) 2021, 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:io' as io; + +import 'package:analysis_server/src/server/driver.dart' as server_driver; +import 'package:args/args.dart'; + +import '../core.dart'; +import '../utils.dart'; + +class LanguageServerCommand extends DartdevCommand { + static const String commandName = 'language-server'; + + static const String commandDescription = ''' +Start Dart's analysis server. + +This is a long-running process used to provide language services to IDEs and other tooling clients. + +It communicates over stdin and stdout and provides services like code completion, errors and warnings, and refactorings. This command is generally not user-facing but consumed by higher level tools. + +For more information about the server's capabilities and configuration, see: + + https://github.com/dart-lang/sdk/tree/master/pkg/analysis_server'''; + + LanguageServerCommand({bool verbose = false}) + : super(commandName, commandDescription, hidden: !verbose); + + @override + ArgParser createArgParser() { + return server_driver.Driver.createArgParser( + usageLineLength: dartdevUsageLineLength, + includeHelpFlag: false, + defaultToLsp: true, + ); + } + + @override + Future run() async { + final driver = server_driver.Driver(); + driver.start( + argResults.arguments, + defaultToLsp: true, + ); + + // The server will continue to run past the return from this method. + // + // On an error on startup, the server will set the dart:io exitCode value + // (or, call exit() directly). + return io.exitCode; + } +} diff --git a/pkg/dartdev/pubspec.yaml b/pkg/dartdev/pubspec.yaml index d6347db5c53..a8fef55dd94 100644 --- a/pkg/dartdev/pubspec.yaml +++ b/pkg/dartdev/pubspec.yaml @@ -6,6 +6,8 @@ publish_to: none environment: sdk: '>=2.6.0 <3.0.0' dependencies: + analysis_server: + path: ../analysis_server analysis_server_client: path: ../analysis_server_client analyzer: @@ -22,8 +24,7 @@ dependencies: path: ../nnbd_migration path: ^1.0.0 pedantic: ^1.9.0 - pub: - path: ../../third_party/pkg/pub + pub: any stagehand: any telemetry: path: ../telemetry diff --git a/pkg/dartdev/test/commands/language_server_test.dart b/pkg/dartdev/test/commands/language_server_test.dart new file mode 100644 index 00000000000..fcc637ff4ef --- /dev/null +++ b/pkg/dartdev/test/commands/language_server_test.dart @@ -0,0 +1,101 @@ +// Copyright (c) 2021, 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:convert'; +import 'dart:io'; + +import 'package:test/test.dart'; + +import '../utils.dart' as utils; + +void main() { + group( + 'language-server', + defineLanguageServerTests, + timeout: utils.longTimeout, + ); +} + +void defineLanguageServerTests() { + utils.TestProject project; + Process process; + + tearDown(() { + project?.dispose(); + process?.kill(); + }); + + Future runWithLsp(List args) async { + project = utils.project(); + + process = await project.start(args); + + final Stream inStream = + process.stdout.transform(utf8.decoder); + + // Send an LSP init. + final String message = jsonEncode({ + 'jsonrpc': '2.0', + 'id': 1, + 'method': 'initialize', + 'params': { + 'processId': pid, + 'clientInfo': {'name': 'dart-cli-tester'}, + 'capabilities': {}, + 'rootUri': project.dir.uri.toString(), + }, + }); + + process.stdin.write('Content-Length: ${message.length}\r\n'); + process.stdin.write('\r\n'); + process.stdin.write(message); + + List responses = await inStream.take(2).toList(); + expect(responses, hasLength(2)); + + expect(responses[0], startsWith('Content-Length: ')); + + final json = jsonDecode(responses[1]); + expect(json['id'], 1); + expect(json['result'], isNotNull); + final result = json['result']; + expect(result['capabilities'], isNotNull); + expect(result['serverInfo'], isNotNull); + final serverInfo = result['serverInfo']; + expect(serverInfo['name'], isNotEmpty); + + process.kill(); + process = null; + } + + test('protocol default', () async { + return runWithLsp(['language-server']); + }); + + test('protocol lsp', () async { + return runWithLsp(['language-server', '--protocol=lsp']); + }); + + test('protocol analyzer', () async { + project = utils.project(); + + process = await project.start(['language-server', '--protocol=analyzer']); + + final Stream inStream = process.stdout + .transform(utf8.decoder) + .transform(const LineSplitter()); + + final line = await inStream.first; + final json = jsonDecode(line); + + expect(json['event'], 'server.connected'); + expect(json['params'], isNotNull); + final params = json['params']; + expect(params['version'], isNotEmpty); + expect(params['pid'], isNot(0)); + + process.kill(); + process = null; + }); +} diff --git a/pkg/dartdev/test/test_all.dart b/pkg/dartdev/test/test_all.dart index edd644bddc1..f2329486faf 100644 --- a/pkg/dartdev/test/test_all.dart +++ b/pkg/dartdev/test/test_all.dart @@ -12,6 +12,7 @@ import 'commands/fix_test.dart' as fix; import 'commands/flag_test.dart' as flag; import 'commands/format_test.dart' as format; import 'commands/help_test.dart' as help; +import 'commands/language_server_test.dart' as language_server; import 'commands/migrate_test.dart' as migrate; import 'commands/pub_test.dart' as pub; import 'commands/run_test.dart' as run; @@ -39,6 +40,7 @@ void main() { help.main(); implicit_smoke.main(); invalid_smoke.main(); + language_server.main(); migrate.main(); no_such_file.main(); pub.main(); diff --git a/pkg/dartdev/test/utils.dart b/pkg/dartdev/test/utils.dart index 0ef90d38fdb..de507c1fd9a 100644 --- a/pkg/dartdev/test/utils.dart +++ b/pkg/dartdev/test/utils.dart @@ -94,6 +94,20 @@ dev_dependencies: environment: {if (logAnalytics) '_DARTDEV_LOG_ANALYTICS': 'true'}); } + Future start( + List arguments, { + String workingDir, + }) { + return Process.start( + Platform.resolvedExecutable, + [ + '--no-analytics', + ...arguments, + ], + workingDirectory: workingDir ?? dir.path, + environment: {if (logAnalytics) '_DARTDEV_LOG_ANALYTICS': 'true'}); + } + String _sdkRootPath; /// Return the root of the SDK.