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