mirror of
https://github.com/dart-lang/sdk
synced 2024-09-05 00:13:50 +00:00
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:
parent
3bef6cf639
commit
ad3c3644a4
|
@ -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
|
||||
|
||||
|
|
|
@ -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)`.
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
53
pkg/dartdev/lib/src/commands/language_server.dart
Normal file
53
pkg/dartdev/lib/src/commands/language_server.dart
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
101
pkg/dartdev/test/commands/language_server_test.dart
Normal file
101
pkg/dartdev/test/commands/language_server_test.dart
Normal 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;
|
||||
});
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue