From 4f06d59ff6f58f2d13c2f8fcfe375b81058f87c3 Mon Sep 17 00:00:00 2001 From: Jane Lewis Date: Thu, 21 Mar 2024 13:17:07 -0700 Subject: [PATCH] Automatic configuration reloading for `ruff server` (#10404) ## Summary Fixes #10366. `ruff server` now registers a file watcher on the client side using the LSP protocol, and listen for events on configuration files. On such an event, it reloads the configuration in the 'nearest' workspace to the file that was changed. ## Test Plan N/A --- crates/ruff_server/src/server.rs | 65 ++++++++++++++++++- crates/ruff_server/src/server/api.rs | 3 + .../src/server/api/notifications.rs | 2 + .../notifications/did_change_watched_files.rs | 27 ++++++++ crates/ruff_server/src/session.rs | 47 ++++++++++---- 5 files changed, 131 insertions(+), 13 deletions(-) create mode 100644 crates/ruff_server/src/server/api/notifications/did_change_watched_files.rs diff --git a/crates/ruff_server/src/server.rs b/crates/ruff_server/src/server.rs index ae1f9a20edb8d..d5282560b0ed2 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 a957ca24c42b8..21f9a26a42f82 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 bb7b52bc70422..231f8f2dc3f52 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 0000000000000..75547ff73a50b --- /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 a64c4844ee25d..aa039b28363d4 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());