mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-03 07:04:53 +00:00
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:
parent
5062572aca
commit
4f06d59ff6
5 changed files with 131 additions and 13 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue