diff --git a/crates/ruff_server/src/server.rs b/crates/ruff_server/src/server.rs index ae1f9a20ed..d5282560b0 100644 --- a/crates/ruff_server/src/server.rs +++ b/crates/ruff_server/src/server.rs @@ -1,6 +1,9 @@ //! Scheduling, I/O, and API endpoints. use std::num::NonZeroUsize; +use std::sync::atomic::AtomicI32; +use std::sync::atomic::Ordering; +use std::time::Duration; use lsp::Connection; use lsp_server as lsp; @@ -9,6 +12,8 @@ use types::ClientCapabilities; use types::CodeActionKind; use types::CodeActionOptions; use types::DiagnosticOptions; +use types::DidChangeWatchedFilesRegistrationOptions; +use types::FileSystemWatcher; use types::OneOf; use types::TextDocumentSyncCapability; use types::TextDocumentSyncKind; @@ -31,6 +36,7 @@ pub struct Server { threads: lsp::IoThreads, worker_threads: NonZeroUsize, session: Session, + next_request_id: AtomicI32, } impl Server { @@ -44,6 +50,12 @@ impl Server { let client_capabilities = init_params.capabilities; let server_capabilities = Self::server_capabilities(&client_capabilities); + let dynamic_registration = client_capabilities + .workspace + .and_then(|workspace| workspace.did_change_watched_files) + .and_then(|watched_files| watched_files.dynamic_registration) + .unwrap_or_default(); + let workspaces = init_params .workspace_folders .map(|folders| folders.into_iter().map(|folder| folder.uri).collect()) @@ -64,31 +76,80 @@ impl Server { } }); + let next_request_id = AtomicI32::from(1); + conn.initialize_finish(id, initialize_data)?; + if dynamic_registration { + // Register capabilities + conn.sender + .send(lsp_server::Message::Request(lsp_server::Request { + id: next_request_id.fetch_add(1, Ordering::Relaxed).into(), + method: "client/registerCapability".into(), + params: serde_json::to_value(lsp_types::RegistrationParams { + registrations: vec![lsp_types::Registration { + id: "ruff-server-watch".into(), + method: "workspace/didChangeWatchedFiles".into(), + register_options: Some(serde_json::to_value( + DidChangeWatchedFilesRegistrationOptions { + watchers: vec![ + FileSystemWatcher { + glob_pattern: types::GlobPattern::String( + "**/.?ruff.toml".into(), + ), + kind: None, + }, + FileSystemWatcher { + glob_pattern: types::GlobPattern::String( + "**/pyproject.toml".into(), + ), + kind: None, + }, + ], + }, + )?), + }], + })?, + }))?; + + // Flush response from the client (to avoid an unexpected response appearing in the event loop) + let _ = conn.receiver.recv_timeout(Duration::from_secs(5)).map_err(|_| { + tracing::error!("Timed out while waiting for client to acknowledge registration of dynamic capabilities"); + }); + } else { + tracing::warn!("LSP client does not support dynamic file watcher registration - automatic configuration reloading will not be available."); + } + Ok(Self { conn, threads, worker_threads, session: Session::new(&server_capabilities, &workspaces)?, + next_request_id, }) } pub fn run(self) -> crate::Result<()> { let result = event_loop_thread(move || { - Self::event_loop(&self.conn, self.session, self.worker_threads) + Self::event_loop( + &self.conn, + self.session, + self.worker_threads, + self.next_request_id, + ) })? .join(); self.threads.join()?; result } + #[allow(clippy::needless_pass_by_value)] // this is because we aren't using `next_request_id` yet. fn event_loop( connection: &Connection, session: Session, worker_threads: NonZeroUsize, + _next_request_id: AtomicI32, ) -> crate::Result<()> { - // TODO(jane): Make thread count configurable let mut scheduler = schedule::Scheduler::new(session, worker_threads, &connection.sender); for msg in &connection.receiver { let task = match msg { diff --git a/crates/ruff_server/src/server/api.rs b/crates/ruff_server/src/server/api.rs index a957ca24c4..21f9a26a42 100644 --- a/crates/ruff_server/src/server/api.rs +++ b/crates/ruff_server/src/server/api.rs @@ -65,6 +65,9 @@ pub(super) fn notification<'a>(notif: server::Notification) -> Task<'a> { notification::DidChangeConfiguration::METHOD => { local_notification_task::(notif) } + notification::DidChangeWatchedFiles::METHOD => { + local_notification_task::(notif) + } notification::DidChangeWorkspace::METHOD => { local_notification_task::(notif) } diff --git a/crates/ruff_server/src/server/api/notifications.rs b/crates/ruff_server/src/server/api/notifications.rs index bb7b52bc70..231f8f2dc3 100644 --- a/crates/ruff_server/src/server/api/notifications.rs +++ b/crates/ruff_server/src/server/api/notifications.rs @@ -1,6 +1,7 @@ mod cancel; mod did_change; mod did_change_configuration; +mod did_change_watched_files; mod did_change_workspace; mod did_close; mod did_open; @@ -9,6 +10,7 @@ use super::traits::{NotificationHandler, SyncNotificationHandler}; pub(super) use cancel::Cancel; pub(super) use did_change::DidChange; pub(super) use did_change_configuration::DidChangeConfiguration; +pub(super) use did_change_watched_files::DidChangeWatchedFiles; pub(super) use did_change_workspace::DidChangeWorkspace; pub(super) use did_close::DidClose; pub(super) use did_open::DidOpen; diff --git a/crates/ruff_server/src/server/api/notifications/did_change_watched_files.rs b/crates/ruff_server/src/server/api/notifications/did_change_watched_files.rs new file mode 100644 index 0000000000..75547ff73a --- /dev/null +++ b/crates/ruff_server/src/server/api/notifications/did_change_watched_files.rs @@ -0,0 +1,27 @@ +use crate::server::api::LSPResult; +use crate::server::client::Notifier; +use crate::server::Result; +use crate::session::Session; +use lsp_types as types; +use lsp_types::notification as notif; + +pub(crate) struct DidChangeWatchedFiles; + +impl super::NotificationHandler for DidChangeWatchedFiles { + type NotificationType = notif::DidChangeWatchedFiles; +} + +impl super::SyncNotificationHandler for DidChangeWatchedFiles { + fn run( + session: &mut Session, + _notifier: Notifier, + params: types::DidChangeWatchedFilesParams, + ) -> Result<()> { + for change in params.changes { + session + .reload_configuration(&change.uri) + .with_failure_code(lsp_server::ErrorCode::InternalError)?; + } + Ok(()) + } +} diff --git a/crates/ruff_server/src/session.rs b/crates/ruff_server/src/session.rs index a64c4844ee..aa039b2836 100644 --- a/crates/ruff_server/src/session.rs +++ b/crates/ruff_server/src/session.rs @@ -111,6 +111,10 @@ impl Session { .ok_or_else(|| anyhow!("Tried to open unavailable document `{url}`")) } + pub(crate) fn reload_configuration(&mut self, url: &Url) -> crate::Result<()> { + self.workspaces.reload_configuration(url) + } + pub(crate) fn open_workspace_folder(&mut self, url: &Url) -> crate::Result<()> { self.workspaces.open_workspace_folder(url)?; Ok(()) @@ -231,23 +235,32 @@ impl Workspaces { } fn snapshot(&self, document_url: &Url) -> Option { - self.workspace_for_url(document_url) - .and_then(|w| w.open_documents.snapshot(document_url)) + self.workspace_for_url(document_url)? + .open_documents + .snapshot(document_url) } fn controller(&mut self, document_url: &Url) -> Option<&mut DocumentController> { - self.workspace_for_url_mut(document_url) - .and_then(|w| w.open_documents.controller(document_url)) + self.workspace_for_url_mut(document_url)? + .open_documents + .controller(document_url) } fn configuration(&self, document_url: &Url) -> Option<&Arc> { - self.workspace_for_url(document_url) - .map(|w| &w.configuration) + Some(&self.workspace_for_url(document_url)?.configuration) + } + + fn reload_configuration(&mut self, changed_url: &Url) -> crate::Result<()> { + let (path, workspace) = self + .entry_for_url_mut(changed_url) + .ok_or_else(|| anyhow!("Workspace not found for {changed_url}"))?; + workspace.reload_configuration(path); + Ok(()) } fn open(&mut self, url: &Url, contents: String, version: DocumentVersion) { - if let Some(w) = self.workspace_for_url_mut(url) { - w.open_documents.open(url, contents, version); + if let Some(workspace) = self.workspace_for_url_mut(url) { + workspace.open_documents.open(url, contents, version); } } @@ -259,19 +272,27 @@ impl Workspaces { } fn workspace_for_url(&self, url: &Url) -> Option<&Workspace> { + Some(self.entry_for_url(url)?.1) + } + + fn workspace_for_url_mut(&mut self, url: &Url) -> Option<&mut Workspace> { + Some(self.entry_for_url_mut(url)?.1) + } + + fn entry_for_url(&self, url: &Url) -> Option<(&Path, &Workspace)> { let path = url.to_file_path().ok()?; self.0 .range(..path) .next_back() - .map(|(_, workspace)| workspace) + .map(|(path, workspace)| (path.as_path(), workspace)) } - fn workspace_for_url_mut(&mut self, url: &Url) -> Option<&mut Workspace> { + fn entry_for_url_mut(&mut self, url: &Url) -> Option<(&Path, &mut Workspace)> { let path = url.to_file_path().ok()?; self.0 .range_mut(..path) .next_back() - .map(|(_, workspace)| workspace) + .map(|(path, workspace)| (path.as_path(), workspace)) } } @@ -292,6 +313,10 @@ impl Workspace { )) } + fn reload_configuration(&mut self, path: &Path) { + self.configuration = Arc::new(Self::find_configuration_or_fallback(path)); + } + fn find_configuration_or_fallback(root: &Path) -> RuffConfiguration { find_configuration_from_root(root).unwrap_or_else(|err| { tracing::error!("The following error occurred when trying to find a configuration file at `{}`:\n{err}", root.display());