Skip to content

Commit

Permalink
Automatic configuration reloading for ruff server (#10404)
Browse files Browse the repository at this point in the history
## 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
  • Loading branch information
snowsignal committed Mar 21, 2024
1 parent 5062572 commit 4f06d59
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 13 deletions.
65 changes: 63 additions & 2 deletions crates/ruff_server/src/server.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -31,6 +36,7 @@ pub struct Server {
threads: lsp::IoThreads,
worker_threads: NonZeroUsize,
session: Session,
next_request_id: AtomicI32,
}

impl Server {
Expand All @@ -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())
Expand All @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions crates/ruff_server/src/server/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ pub(super) fn notification<'a>(notif: server::Notification) -> Task<'a> {
notification::DidChangeConfiguration::METHOD => {
local_notification_task::<notification::DidChangeConfiguration>(notif)
}
notification::DidChangeWatchedFiles::METHOD => {
local_notification_task::<notification::DidChangeWatchedFiles>(notif)
}
notification::DidChangeWorkspace::METHOD => {
local_notification_task::<notification::DidChangeWorkspace>(notif)
}
Expand Down
2 changes: 2 additions & 0 deletions crates/ruff_server/src/server/api/notifications.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Original file line number Diff line number Diff line change
@@ -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(())
}
}
47 changes: 36 additions & 11 deletions crates/ruff_server/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
Expand Down Expand Up @@ -231,23 +235,32 @@ impl Workspaces {
}

fn snapshot(&self, document_url: &Url) -> Option<DocumentRef> {
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<RuffConfiguration>> {
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);
}
}

Expand All @@ -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))
}
}

Expand All @@ -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());
Expand Down

0 comments on commit 4f06d59

Please sign in to comment.