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 <devoncarew@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
Devon Carew 2021-01-06 18:14:51 +00:00 committed by commit-bot@chromium.org
parent 3bef6cf639
commit ad3c3644a4
9 changed files with 305 additions and 93 deletions

View file

@ -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

View file

@ -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<String> arguments, [SendPort sendPort]) {
var parser = _createArgParser();
void start(
List<String> 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)`.

View file

@ -31,5 +31,6 @@ abstract class ServerStarter {
set instrumentationService(InstrumentationService service);
/// Use the given command-line [arguments] to start this server.
void start(List<String> arguments, [SendPort sendPort]);
void start(List<String> arguments,
{SendPort sendPort, bool defaultToLsp = false});
}

View file

@ -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<int> {
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));

View file

@ -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<int> 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;
}
}

View file

@ -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

View file

@ -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<String> args) async {
project = utils.project();
process = await project.start(args);
final Stream<String> inStream =
process.stdout.transform<String>(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<String> 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<String> inStream = process.stdout
.transform<String>(utf8.decoder)
.transform<String>(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;
});
}

View file

@ -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();

View file

@ -94,6 +94,20 @@ dev_dependencies:
environment: {if (logAnalytics) '_DARTDEV_LOG_ANALYTICS': 'true'});
}
Future<Process> start(
List<String> 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.