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
This commit is contained in:
Jane Lewis 2024-03-21 13:17:07 -07:00 committed by GitHub
parent 5062572aca
commit 4f06d59ff6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 131 additions and 13 deletions

View file

@ -1,6 +1,9 @@
//! Scheduling, I/O, and API endpoints. //! Scheduling, I/O, and API endpoints.
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::Ordering;
use std::time::Duration;
use lsp::Connection; use lsp::Connection;
use lsp_server as lsp; use lsp_server as lsp;
@ -9,6 +12,8 @@ use types::ClientCapabilities;
use types::CodeActionKind; use types::CodeActionKind;
use types::CodeActionOptions; use types::CodeActionOptions;
use types::DiagnosticOptions; use types::DiagnosticOptions;
use types::DidChangeWatchedFilesRegistrationOptions;
use types::FileSystemWatcher;
use types::OneOf; use types::OneOf;
use types::TextDocumentSyncCapability; use types::TextDocumentSyncCapability;
use types::TextDocumentSyncKind; use types::TextDocumentSyncKind;
@ -31,6 +36,7 @@ pub struct Server {
threads: lsp::IoThreads, threads: lsp::IoThreads,
worker_threads: NonZeroUsize, worker_threads: NonZeroUsize,
session: Session, session: Session,
next_request_id: AtomicI32,
} }
impl Server { impl Server {
@ -44,6 +50,12 @@ impl Server {
let client_capabilities = init_params.capabilities; let client_capabilities = init_params.capabilities;
let server_capabilities = Self::server_capabilities(&client_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 let workspaces = init_params
.workspace_folders .workspace_folders
.map(|folders| folders.into_iter().map(|folder| folder.uri).collect()) .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)?; 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 { Ok(Self {
conn, conn,
threads, threads,
worker_threads, worker_threads,
session: Session::new(&server_capabilities, &workspaces)?, session: Session::new(&server_capabilities, &workspaces)?,
next_request_id,
}) })
} }
pub fn run(self) -> crate::Result<()> { pub fn run(self) -> crate::Result<()> {
let result = event_loop_thread(move || { 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(); .join();
self.threads.join()?; self.threads.join()?;
result result
} }
#[allow(clippy::needless_pass_by_value)] // this is because we aren't using `next_request_id` yet.
fn event_loop( fn event_loop(
connection: &Connection, connection: &Connection,
session: Session, session: Session,
worker_threads: NonZeroUsize, worker_threads: NonZeroUsize,
_next_request_id: AtomicI32,
) -> crate::Result<()> { ) -> crate::Result<()> {
// TODO(jane): Make thread count configurable
let mut scheduler = schedule::Scheduler::new(session, worker_threads, &connection.sender); let mut scheduler = schedule::Scheduler::new(session, worker_threads, &connection.sender);
for msg in &connection.receiver { for msg in &connection.receiver {
let task = match msg { let task = match msg {

View file

@ -65,6 +65,9 @@ pub(super) fn notification<'a>(notif: server::Notification) -> Task<'a> {
notification::DidChangeConfiguration::METHOD => { notification::DidChangeConfiguration::METHOD => {
local_notification_task::<notification::DidChangeConfiguration>(notif) local_notification_task::<notification::DidChangeConfiguration>(notif)
} }
notification::DidChangeWatchedFiles::METHOD => {
local_notification_task::<notification::DidChangeWatchedFiles>(notif)
}
notification::DidChangeWorkspace::METHOD => { notification::DidChangeWorkspace::METHOD => {
local_notification_task::<notification::DidChangeWorkspace>(notif) local_notification_task::<notification::DidChangeWorkspace>(notif)
} }

View file

@ -1,6 +1,7 @@
mod cancel; mod cancel;
mod did_change; mod did_change;
mod did_change_configuration; mod did_change_configuration;
mod did_change_watched_files;
mod did_change_workspace; mod did_change_workspace;
mod did_close; mod did_close;
mod did_open; mod did_open;
@ -9,6 +10,7 @@ use super::traits::{NotificationHandler, SyncNotificationHandler};
pub(super) use cancel::Cancel; pub(super) use cancel::Cancel;
pub(super) use did_change::DidChange; pub(super) use did_change::DidChange;
pub(super) use did_change_configuration::DidChangeConfiguration; 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_change_workspace::DidChangeWorkspace;
pub(super) use did_close::DidClose; pub(super) use did_close::DidClose;
pub(super) use did_open::DidOpen; pub(super) use did_open::DidOpen;

View file

@ -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(())
}
}

View file

@ -111,6 +111,10 @@ impl Session {
.ok_or_else(|| anyhow!("Tried to open unavailable document `{url}`")) .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<()> { pub(crate) fn open_workspace_folder(&mut self, url: &Url) -> crate::Result<()> {
self.workspaces.open_workspace_folder(url)?; self.workspaces.open_workspace_folder(url)?;
Ok(()) Ok(())
@ -231,23 +235,32 @@ impl Workspaces {
} }
fn snapshot(&self, document_url: &Url) -> Option<DocumentRef> { fn snapshot(&self, document_url: &Url) -> Option<DocumentRef> {
self.workspace_for_url(document_url) self.workspace_for_url(document_url)?
.and_then(|w| w.open_documents.snapshot(document_url)) .open_documents
.snapshot(document_url)
} }
fn controller(&mut self, document_url: &Url) -> Option<&mut DocumentController> { fn controller(&mut self, document_url: &Url) -> Option<&mut DocumentController> {
self.workspace_for_url_mut(document_url) self.workspace_for_url_mut(document_url)?
.and_then(|w| w.open_documents.controller(document_url)) .open_documents
.controller(document_url)
} }
fn configuration(&self, document_url: &Url) -> Option<&Arc<RuffConfiguration>> { fn configuration(&self, document_url: &Url) -> Option<&Arc<RuffConfiguration>> {
self.workspace_for_url(document_url) Some(&self.workspace_for_url(document_url)?.configuration)
.map(|w| &w.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) { fn open(&mut self, url: &Url, contents: String, version: DocumentVersion) {
if let Some(w) = self.workspace_for_url_mut(url) { if let Some(workspace) = self.workspace_for_url_mut(url) {
w.open_documents.open(url, contents, version); workspace.open_documents.open(url, contents, version);
} }
} }
@ -259,19 +272,27 @@ impl Workspaces {
} }
fn workspace_for_url(&self, url: &Url) -> Option<&Workspace> { 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()?; let path = url.to_file_path().ok()?;
self.0 self.0
.range(..path) .range(..path)
.next_back() .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()?; let path = url.to_file_path().ok()?;
self.0 self.0
.range_mut(..path) .range_mut(..path)
.next_back() .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 { fn find_configuration_or_fallback(root: &Path) -> RuffConfiguration {
find_configuration_from_root(root).unwrap_or_else(|err| { 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()); tracing::error!("The following error occurred when trying to find a configuration file at `{}`:\n{err}", root.display());