Ensure LSP server clears diagnostics for removed files/analysis roots

Change-Id: I5eaa4141da499c5193253440e6913a0eb92ed533
Reviewed-on: https://dart-review.googlesource.com/c/88836
Commit-Queue: Danny Tuppeny <dantup@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
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 d53c2f7..782ecad 100644
--- a/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
+++ b/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
@@ -404,6 +404,17 @@
     return contextManager.isInAnalysisRoot(file);
   }
 
+  void publishDiagnostics(String path, List<Diagnostic> errors) {
+    final params =
+        new PublishDiagnosticsParams(Uri.file(path).toString(), errors);
+    final message = new NotificationMessage(
+      Method.textDocument_publishDiagnostics,
+      params,
+      jsonRpcVersion,
+    );
+    sendNotification(message);
+  }
+
   void showError(String message) {
     channel.sendNotification(new NotificationMessage(
       Method.window_showMessage,
@@ -479,14 +490,7 @@
             result.errors,
             toDiagnostic);
 
-        final params = new PublishDiagnosticsParams(
-            Uri.file(result.path).toString(), serverErrors);
-        final message = new NotificationMessage(
-          Method.textDocument_publishDiagnostics,
-          params,
-          jsonRpcVersion,
-        );
-        analysisServer.sendNotification(message);
+        analysisServer.publishDiagnostics(result.path, serverErrors);
       }
     });
     analysisDriver.exceptions.listen((nd.ExceptionResult result) {
@@ -524,7 +528,7 @@
   @override
   void applyFileRemoved(nd.AnalysisDriver driver, String file) {
     driver.removeFile(file);
-    // sendAnalysisNotificationFlushResults(analysisServer, [file]);
+    analysisServer.publishDiagnostics(file, []);
   }
 
   @override
@@ -567,8 +571,10 @@
 
   @override
   void removeContext(Folder folder, List<String> flushedFiles) {
-    // sendAnalysisNotificationFlushResults(analysisServer, flushedFiles);
     nd.AnalysisDriver driver = analysisServer.driverMap.remove(folder);
+    // Flush any errors for these files that the client may be displaying.
+    flushedFiles
+        ?.forEach((path) => analysisServer.publishDiagnostics(path, const []));
     driver.dispose();
   }
 }
diff --git a/pkg/analysis_server/test/lsp/change_workspace_folders_test.dart b/pkg/analysis_server/test/lsp/change_workspace_folders_test.dart
index 78c038d..fd0e3c0 100644
--- a/pkg/analysis_server/test/lsp/change_workspace_folders_test.dart
+++ b/pkg/analysis_server/test/lsp/change_workspace_folders_test.dart
@@ -72,4 +72,21 @@
       unorderedEquals([workspaceFolder1Path]),
     );
   }
+
+  test_changeWorkspaceFolders_removeFlushesDiagnostics() async {
+    // Add our standard test project as well as a dummy project.
+    await initialize(workspaceFolders: [projectFolderUri, workspaceFolder1Uri]);
+
+    // Generate an error in the test project.
+    final firstDiagnosticsUpdate = waitForDiagnostics(mainFileUri);
+    await openFile(mainFileUri, 'String a = 1;');
+    final initialDiagnostics = await firstDiagnosticsUpdate;
+    expect(initialDiagnostics, hasLength(1));
+
+    // Ensure the error is removed if we removed the workspace folder.
+    final secondDiagnosticsUpdate = waitForDiagnostics(mainFileUri);
+    await changeWorkspaceFolders(remove: [projectFolderUri]);
+    final updatedDiagnostics = await secondDiagnosticsUpdate;
+    expect(updatedDiagnostics, hasLength(0));
+  }
 }
diff --git a/pkg/analysis_server/test/lsp/diagnostic_test.dart b/pkg/analysis_server/test/lsp/diagnostic_test.dart
index bbddc00..802d3be 100644
--- a/pkg/analysis_server/test/lsp/diagnostic_test.dart
+++ b/pkg/analysis_server/test/lsp/diagnostic_test.dart
@@ -32,6 +32,21 @@
     expect(updatedDiagnostics, hasLength(1));
   }
 
+  test_deletedFile() async {
+    newFile(mainFilePath, content: 'String a = 1;');
+
+    final firstDiagnosticsUpdate = waitForDiagnostics(mainFileUri);
+    await initialize();
+    final originalDiagnostics = await firstDiagnosticsUpdate;
+    expect(originalDiagnostics, hasLength(1));
+
+    // Deleting the file should result in an update to remove the diagnostics.
+    final secondDiagnosticsUpdate = waitForDiagnostics(mainFileUri);
+    await deleteFile(mainFilePath);
+    final updatedDiagnostics = await secondDiagnosticsUpdate;
+    expect(updatedDiagnostics, hasLength(0));
+  }
+
   test_initialAnalysis() async {
     newFile(mainFilePath, content: 'String a = 1;');