diff --git a/crates/oxc_language_server/src/backend.rs b/crates/oxc_language_server/src/backend.rs index fc3f39f9d5eaa..2459c8a3f046c 100644 --- a/crates/oxc_language_server/src/backend.rs +++ b/crates/oxc_language_server/src/backend.rs @@ -13,7 +13,7 @@ use tower_lsp_server::{ DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentFormattingParams, ExecuteCommandParams, InitializeParams, InitializeResult, InitializedParams, Registration, ServerInfo, TextEdit, - Unregistration, Uri, + Uri, }, }; @@ -218,7 +218,21 @@ impl LanguageServer for Backend { /// /// See: async fn shutdown(&self) -> Result<()> { - self.clear_all_diagnostics().await; + let mut clearing_diagnostics = Vec::new(); + let mut removed_registrations = Vec::new(); + + for worker in &*self.workspace_workers.read().await { + let (uris, unregistrations) = worker.shutdown().await; + clearing_diagnostics.extend(uris); + removed_registrations.extend(unregistrations); + } + self.clear_diagnostics(clearing_diagnostics).await; + if !removed_registrations.is_empty() + && let Err(err) = self.client.unregister_capability(removed_registrations).await + { + warn!("sending unregisterCapability.didChangeWatchedFiles failed: {err}"); + } + if self.capabilities.get().is_some_and(|option| option.dynamic_formatting) { self.file_system.write().await.clear(); } @@ -387,15 +401,13 @@ impl LanguageServer for Backend { else { continue; }; - cleared_diagnostics.extend(worker.get_clear_diagnostics().await); - removed_registrations.push(Unregistration { - id: format!("watcher-{}", worker.get_root_uri().as_str()), - method: "workspace/didChangeWatchedFiles".to_string(), - }); + let (uris, unregistrations) = worker.shutdown().await; + cleared_diagnostics.extend(uris); + removed_registrations.extend(unregistrations); workers.remove(index); } - self.publish_all_diagnostics(&cleared_diagnostics).await; + self.clear_diagnostics(cleared_diagnostics).await; // client support `workspace/configuration` request if self.capabilities.get().is_some_and(|capabilities| capabilities.workspace_configuration) @@ -632,14 +644,10 @@ impl Backend { configs } - /// Clears all diagnostics for workspace folders - async fn clear_all_diagnostics(&self) { - let mut cleared_diagnostics = vec![]; - let workers = &*self.workspace_workers.read().await; - for worker in workers { - cleared_diagnostics.extend(worker.get_clear_diagnostics().await); - } - self.publish_all_diagnostics(&cleared_diagnostics).await; + async fn clear_diagnostics(&self, uris: Vec) { + let diagnostics: Vec<(String, Vec)> = + uris.into_iter().map(|uri| (uri.to_string(), vec![])).collect(); + self.publish_all_diagnostics(&diagnostics).await; } /// Publish diagnostics for all files. diff --git a/crates/oxc_language_server/src/linter/server_linter.rs b/crates/oxc_language_server/src/linter/server_linter.rs index 069c6d65ae858..4dc6742cccaac 100644 --- a/crates/oxc_language_server/src/linter/server_linter.rs +++ b/crates/oxc_language_server/src/linter/server_linter.rs @@ -19,7 +19,7 @@ use oxc_linter::{ LintIgnoreMatcher, LintOptions, Oxlintrc, }; -use crate::tool::{Tool, ToolBuilder}; +use crate::tool::{Tool, ToolBuilder, ToolShutdownChanges}; use crate::{ ConcurrentHashMap, linter::{ @@ -245,6 +245,12 @@ pub struct ServerLinter { } impl Tool for ServerLinter { + fn shutdown(&self) -> ToolShutdownChanges { + ToolShutdownChanges { + uris_to_clear_diagnostics: Some(self.get_cached_files_of_diagnostics()), + } + } + /// # Panics /// Panics if the root URI cannot be converted to a file path. async fn handle_configuration_change( @@ -522,7 +528,7 @@ impl ServerLinter { None } - pub fn get_cached_files_of_diagnostics(&self) -> Vec { + fn get_cached_files_of_diagnostics(&self) -> Vec { self.diagnostics.pin().keys().filter_map(|s| Uri::from_str(s).ok()).collect() } diff --git a/crates/oxc_language_server/src/tool.rs b/crates/oxc_language_server/src/tool.rs index ca975837d1a85..8d272305c9bca 100644 --- a/crates/oxc_language_server/src/tool.rs +++ b/crates/oxc_language_server/src/tool.rs @@ -113,6 +113,11 @@ pub trait Tool: Sized { fn remove_diagnostics(&self, _uri: &Uri) { // Default implementation does nothing. } + + /// Shutdown the tool and return any necessary changes to be made after shutdown. + fn shutdown(&self) -> ToolShutdownChanges { + ToolShutdownChanges { uris_to_clear_diagnostics: None } + } } pub struct ToolRestartChanges { @@ -125,3 +130,8 @@ pub struct ToolRestartChanges { /// Old patterns will be automatically unregistered pub watch_patterns: Option>, } + +pub struct ToolShutdownChanges { + /// The URIs that need to have their diagnostics removed after the tool shutdown + pub uris_to_clear_diagnostics: Option>, +} diff --git a/crates/oxc_language_server/src/worker.rs b/crates/oxc_language_server/src/worker.rs index 34f117ff9f284..cdef4195d76a4 100644 --- a/crates/oxc_language_server/src/worker.rs +++ b/crates/oxc_language_server/src/worker.rs @@ -184,26 +184,31 @@ impl WorkspaceWorker { server_formatter.run_format(uri, content) } - /// Get all clear diagnostics for the current workspace - /// This should be called when: - /// - The linter is disabled (not currently implemented) - /// - The workspace is closed - /// - The server is shut down - /// - /// This will return a list of URIs that had diagnostics before, each with an empty diagnostics list - pub async fn get_clear_diagnostics(&self) -> Vec<(String, Vec)> { - self.server_linter - .read() - .await - .as_ref() - .map(|server_linter| { - server_linter - .get_cached_files_of_diagnostics() - .iter() - .map(|uri| (uri.to_string(), vec![])) - .collect::>() - }) - .unwrap_or_default() + /// Shutdown the worker and return any necessary changes to be made after shutdown. + /// This includes clearing diagnostics and unregistering file watchers. + pub async fn shutdown( + &self, + ) -> ( + // The URIs that need to have their diagnostics removed after shutdown + Vec, + // Watchers that need to be unregistered + Vec, + ) { + let mut uris_to_clear_diagnostics = Vec::new(); + + if let Some(server_linter) = &*self.server_linter.read().await + && let Some(uris) = server_linter.shutdown().uris_to_clear_diagnostics + { + uris_to_clear_diagnostics.extend(uris); + } + + ( + uris_to_clear_diagnostics, + vec![ + unregistration_tool_watcher_id("linter", &self.root_uri), + unregistration_tool_watcher_id("formatter", &self.root_uri), + ], + ) } /// Get code actions or commands for the given range