Add LSP custom notifications for analyzing status
Change-Id: I0d26d274c9ccc472da81e45d0c9170104bbd9414
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/102370
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Danny Tuppeny <dantup@google.com>
diff --git a/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart b/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart
index fb15b97..3f03e09 100644
--- a/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart
+++ b/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart
@@ -21,6 +21,54 @@
const jsonEncoder = const JsonEncoder.withIndent(' ');
+class AnalyzerStatusParams implements ToJsonable {
+ static const jsonHandler = const LspJsonHandler(
+ AnalyzerStatusParams.canParse, AnalyzerStatusParams.fromJson);
+
+ AnalyzerStatusParams(this.isAnalyzing) {
+ if (isAnalyzing == null) {
+ throw 'isAnalyzing is required but was not provided';
+ }
+ }
+ static AnalyzerStatusParams fromJson(Map<String, dynamic> json) {
+ final isAnalyzing = json['isAnalyzing'];
+ return new AnalyzerStatusParams(isAnalyzing);
+ }
+
+ final bool isAnalyzing;
+
+ Map<String, dynamic> toJson() {
+ Map<String, dynamic> __result = {};
+ __result['isAnalyzing'] =
+ isAnalyzing ?? (throw 'isAnalyzing is required but was not set');
+ return __result;
+ }
+
+ static bool canParse(Object obj) {
+ return obj is Map<String, dynamic> &&
+ obj.containsKey('isAnalyzing') &&
+ obj['isAnalyzing'] is bool;
+ }
+
+ @override
+ bool operator ==(other) {
+ if (other is AnalyzerStatusParams) {
+ return isAnalyzing == other.isAnalyzing && true;
+ }
+ return false;
+ }
+
+ @override
+ int get hashCode {
+ int hash = 0;
+ hash = JenkinsSmiHash.combine(hash, isAnalyzing.hashCode);
+ return JenkinsSmiHash.finish(hash);
+ }
+
+ @override
+ String toString() => jsonEncoder.convert(toJson());
+}
+
class DartDiagnosticServer implements ToJsonable {
static const jsonHandler = const LspJsonHandler(
DartDiagnosticServer.canParse, DartDiagnosticServer.fromJson);
diff --git a/pkg/analysis_server/lib/src/lsp/constants.dart b/pkg/analysis_server/lib/src/lsp/constants.dart
index aaf762f..b5922b0 100644
--- a/pkg/analysis_server/lib/src/lsp/constants.dart
+++ b/pkg/analysis_server/lib/src/lsp/constants.dart
@@ -59,4 +59,5 @@
abstract class CustomMethods {
static const DiagnosticServer = const Method('dart/diagnosticServer');
+ static const AnalyzerStatus = const Method(r'$/analyzerStatus');
}
diff --git a/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart b/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
index 031ba5d..d543a25 100644
--- a/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
+++ b/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
@@ -6,6 +6,7 @@
import 'dart:collection';
import 'dart:io' as io;
+import 'package:analysis_server/lsp_protocol/protocol_custom_generated.dart';
import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
import 'package:analysis_server/lsp_protocol/protocol_special.dart';
import 'package:analysis_server/protocol/protocol_generated.dart' as protocol;
@@ -169,6 +170,7 @@
byteStore = createByteStore(resourceProvider);
analysisDriverScheduler =
new nd.AnalysisDriverScheduler(_analysisPerformanceLogger);
+ analysisDriverScheduler.status.listen(sendStatusNotification);
analysisDriverScheduler.start();
contextManager = new ContextManagerImpl(
@@ -430,6 +432,16 @@
));
}
+ /// Send status notification to the client. The state of analysis is given by
+ /// the [status] information.
+ void sendStatusNotification(nd.AnalysisStatus status) {
+ channel.sendNotification(new NotificationMessage(
+ CustomMethods.AnalyzerStatus,
+ new AnalyzerStatusParams(status.isAnalyzing),
+ jsonRpcVersion,
+ ));
+ }
+
void setAnalysisRoots(List<String> includedPaths) {
final uniquePaths = HashSet<String>.of(includedPaths ?? const []);
contextManager.setRoots(uniquePaths.toList(), [], {});
diff --git a/pkg/analysis_server/test/integration/lsp_server/analyzer_status_test.dart b/pkg/analysis_server/test/integration/lsp_server/analyzer_status_test.dart
new file mode 100644
index 0000000..bff7062
--- /dev/null
+++ b/pkg/analysis_server/test/integration/lsp_server/analyzer_status_test.dart
@@ -0,0 +1,59 @@
+// Copyright (c) 2019, 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/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'integration_tests.dart';
+
+main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(AnalyzerStatusTest);
+ });
+}
+
+@reflectiveTest
+class AnalyzerStatusTest extends AbstractLspAnalysisServerIntegrationTest {
+ test_afterInitialize() async {
+ const initialContents = 'int a = 1;';
+ newFile(mainFilePath, content: initialContents);
+
+ // To avoid races, set up listeners for the notifications before we initialise
+ // and track which event came first to ensure they arrived in the expected
+ // order.
+ bool firstNotificationWasAnalyzing = null;
+ final startNotification = waitForAnalysisStart()
+ .then((_) => firstNotificationWasAnalyzing ??= true);
+ final completeNotification = waitForAnalysisComplete()
+ .then((_) => firstNotificationWasAnalyzing ??= false);
+
+ await initialize();
+ await startNotification;
+ await completeNotification;
+
+ expect(firstNotificationWasAnalyzing, isTrue);
+ }
+
+ test_afterDocumentEdits() async {
+ const initialContents = 'int a = 1;';
+ newFile(mainFilePath, content: initialContents);
+
+ final initialAnalysis = waitForAnalysisComplete();
+
+ await initialize();
+ await initialAnalysis;
+
+ // Set up futures to wait for the new events.
+ final startNotification = waitForAnalysisStart();
+ final completeNotification = waitForAnalysisComplete();
+
+ // Send a modification
+ await openFile(mainFileUri, initialContents);
+ await replaceFile(222, mainFileUri, 'String a = 1;');
+
+ // Ensure the notifications come through again.
+ await startNotification;
+ await completeNotification;
+ }
+}
diff --git a/pkg/analysis_server/test/integration/lsp_server/test_all.dart b/pkg/analysis_server/test/integration/lsp_server/test_all.dart
index 1297cf4..b171d0a 100644
--- a/pkg/analysis_server/test/integration/lsp_server/test_all.dart
+++ b/pkg/analysis_server/test/integration/lsp_server/test_all.dart
@@ -4,12 +4,14 @@
import 'package:test_reflective_loader/test_reflective_loader.dart';
+import 'analyzer_status_test.dart' as analyzer_status;
import 'diagnostic_test.dart' as diagnostic_test;
import 'initialization_test.dart' as initialization_test;
import 'server_test.dart' as server_test;
main() {
defineReflectiveSuite(() {
+ analyzer_status.main();
diagnostic_test.main();
initialization_test.main();
server_test.main();
diff --git a/pkg/analysis_server/test/lsp/analyzer_status_test.dart b/pkg/analysis_server/test/lsp/analyzer_status_test.dart
new file mode 100644
index 0000000..1f5859c
--- /dev/null
+++ b/pkg/analysis_server/test/lsp/analyzer_status_test.dart
@@ -0,0 +1,59 @@
+// Copyright (c) 2019, 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/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'server_abstract.dart';
+
+main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(AnalyzerStatusTest);
+ });
+}
+
+@reflectiveTest
+class AnalyzerStatusTest extends AbstractLspAnalysisServerTest {
+ test_afterInitialize() async {
+ const initialContents = 'int a = 1;';
+ newFile(mainFilePath, content: initialContents);
+
+ // To avoid races, set up listeners for the notifications before we initialise
+ // and track which event came first to ensure they arrived in the expected
+ // order.
+ bool firstNotificationWasAnalyzing = null;
+ final startNotification = waitForAnalysisStart()
+ .then((_) => firstNotificationWasAnalyzing ??= true);
+ final completeNotification = waitForAnalysisComplete()
+ .then((_) => firstNotificationWasAnalyzing ??= false);
+
+ await initialize();
+ await startNotification;
+ await completeNotification;
+
+ expect(firstNotificationWasAnalyzing, isTrue);
+ }
+
+ test_afterDocumentEdits() async {
+ const initialContents = 'int a = 1;';
+ newFile(mainFilePath, content: initialContents);
+
+ final initialAnalysis = waitForAnalysisComplete();
+
+ await initialize();
+ await initialAnalysis;
+
+ // Set up futures to wait for the new events.
+ final startNotification = waitForAnalysisStart();
+ final completeNotification = waitForAnalysisComplete();
+
+ // Send a modification
+ await openFile(mainFileUri, initialContents);
+ await replaceFile(222, mainFileUri, 'String a = 1;');
+
+ // Ensure the notifications come through again.
+ await startNotification;
+ await completeNotification;
+ }
+}
diff --git a/pkg/analysis_server/test/lsp/server_abstract.dart b/pkg/analysis_server/test/lsp/server_abstract.dart
index ba85297..85796c9a 100644
--- a/pkg/analysis_server/test/lsp/server_abstract.dart
+++ b/pkg/analysis_server/test/lsp/server_abstract.dart
@@ -730,6 +730,31 @@
return diagnosticParams.diagnostics;
}
+ Future<AnalyzerStatusParams> waitForAnalysisStart() =>
+ waitForAnalysisStatus(true);
+
+ Future<AnalyzerStatusParams> waitForAnalysisComplete() =>
+ waitForAnalysisStatus(false);
+
+ Future<AnalyzerStatusParams> waitForAnalysisStatus(bool analyzing) async {
+ AnalyzerStatusParams params;
+ await serverToClient.firstWhere((message) {
+ if (message is NotificationMessage &&
+ message.method == CustomMethods.AnalyzerStatus) {
+ // This helper method is used both in in-process tests where we'll get
+ // the real type back, and out-of-process integration tests where
+ // params is `Map<String, dynamic>` so for convenience just
+ // handle either here.
+ params = message.params is AnalyzerStatusParams
+ ? message.params
+ : AnalyzerStatusParams.fromJson(message.params);
+ return params.isAnalyzing == analyzing;
+ }
+ return false;
+ });
+ return params;
+ }
+
/// Removes markers like `[[` and `]]` and `^` that are used for marking
/// positions/ranges in strings to avoid hard-coding positions in tests.
String withoutMarkers(String contents) =>
diff --git a/pkg/analysis_server/test/lsp/test_all.dart b/pkg/analysis_server/test/lsp/test_all.dart
index 7c6ab34..1f29976 100644
--- a/pkg/analysis_server/test/lsp/test_all.dart
+++ b/pkg/analysis_server/test/lsp/test_all.dart
@@ -5,6 +5,7 @@
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../src/lsp/lsp_packet_transformer_test.dart' as lsp_packet_transformer;
+import 'analyzer_status_test.dart' as analyzer_status;
import 'change_workspace_folders_test.dart' as change_workspace_folders;
import 'code_actions_assists_test.dart' as code_actions_assists;
import 'code_actions_fixes_test.dart' as code_actions_fixes;
@@ -28,6 +29,7 @@
main() {
defineReflectiveSuite(() {
+ analyzer_status.main();
change_workspace_folders.main();
code_actions_assists.main();
code_actions_fixes.main();
diff --git a/pkg/analysis_server/tool/lsp_spec/README.md b/pkg/analysis_server/tool/lsp_spec/README.md
index f5420ac..46ad00d 100644
--- a/pkg/analysis_server/tool/lsp_spec/README.md
+++ b/pkg/analysis_server/tool/lsp_spec/README.md
@@ -16,7 +16,7 @@
Note: In LSP the client makes the first request so there is no obvious confirmation that the server is working correctly until the client sends an `initialize` request. Unlike standard JSON RPC, [LSP requires that headers are sent](https://microsoft.github.io/language-server-protocol/specification).
-## Message Status
+## Method Status
Below is a list of LSP methods and their implementation status.
@@ -81,4 +81,21 @@
| textDocument/prepareRename | | | | |
| textDocument/foldingRange | ✅ | ✅ | ✅ | ✅ |
+## Custom Methods
+The following custom methods are also provided by the Dart LSP server:
+
+### dart/diagnosticServer Method
+
+Direction: Client -> Server
+Params: None
+Returns: `{ port: number }`
+
+Starts the analzyer diagnostics server (if not already running) and returns the port number it's listening on.
+
+### $/analyzerStatus Notification
+
+Direction: Server -> Client
+Params: `{ isAnalyzing: boolean }`
+
+Notifies the client when analysis starts/completes.
diff --git a/pkg/analysis_server/tool/lsp_spec/generate_all.dart b/pkg/analysis_server/tool/lsp_spec/generate_all.dart
index 23fb23f..7d6f52d 100644
--- a/pkg/analysis_server/tool/lsp_spec/generate_all.dart
+++ b/pkg/analysis_server/tool/lsp_spec/generate_all.dart
@@ -83,6 +83,7 @@
final List<AstNode> customTypes = [
interface('DartDiagnosticServer', [field('port', type: 'number')]),
+ interface('AnalyzerStatusParams', [field('isAnalyzing', type: 'boolean')]),
];
final String output = generateDartForTypes(customTypes);