mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-01 14:21:53 +00:00
[ty] Add basic file watching to server (#17912)
This commit is contained in:
parent
51e2effd2d
commit
51386b3c7a
7 changed files with 203 additions and 14 deletions
|
@ -7,7 +7,8 @@ use std::panic::PanicInfo;
|
||||||
|
|
||||||
use lsp_server::Message;
|
use lsp_server::Message;
|
||||||
use lsp_types::{
|
use lsp_types::{
|
||||||
ClientCapabilities, DiagnosticOptions, DiagnosticServerCapabilities, HoverProviderCapability,
|
ClientCapabilities, DiagnosticOptions, DiagnosticServerCapabilities,
|
||||||
|
DidChangeWatchedFilesRegistrationOptions, FileSystemWatcher, HoverProviderCapability,
|
||||||
InlayHintOptions, InlayHintServerCapabilities, MessageType, ServerCapabilities,
|
InlayHintOptions, InlayHintServerCapabilities, MessageType, ServerCapabilities,
|
||||||
TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,
|
TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,
|
||||||
TypeDefinitionProviderCapability, Url,
|
TypeDefinitionProviderCapability, Url,
|
||||||
|
@ -24,6 +25,7 @@ mod connection;
|
||||||
mod schedule;
|
mod schedule;
|
||||||
|
|
||||||
use crate::message::try_show_message;
|
use crate::message::try_show_message;
|
||||||
|
use crate::server::schedule::Task;
|
||||||
pub(crate) use connection::ClientSender;
|
pub(crate) use connection::ClientSender;
|
||||||
|
|
||||||
pub(crate) type Result<T> = std::result::Result<T, api::Error>;
|
pub(crate) type Result<T> = std::result::Result<T, api::Error>;
|
||||||
|
@ -170,13 +172,80 @@ impl Server {
|
||||||
|
|
||||||
fn event_loop(
|
fn event_loop(
|
||||||
connection: &Connection,
|
connection: &Connection,
|
||||||
_client_capabilities: &ClientCapabilities,
|
client_capabilities: &ClientCapabilities,
|
||||||
mut session: Session,
|
mut session: Session,
|
||||||
worker_threads: NonZeroUsize,
|
worker_threads: NonZeroUsize,
|
||||||
) -> crate::Result<()> {
|
) -> crate::Result<()> {
|
||||||
let mut scheduler =
|
let mut scheduler =
|
||||||
schedule::Scheduler::new(&mut session, worker_threads, connection.make_sender());
|
schedule::Scheduler::new(&mut session, worker_threads, connection.make_sender());
|
||||||
|
|
||||||
|
let fs_watcher = client_capabilities
|
||||||
|
.workspace
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|workspace| workspace.did_change_watched_files?.dynamic_registration)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if fs_watcher {
|
||||||
|
let registration = lsp_types::Registration {
|
||||||
|
id: "workspace/didChangeWatchedFiles".to_owned(),
|
||||||
|
method: "workspace/didChangeWatchedFiles".to_owned(),
|
||||||
|
register_options: Some(
|
||||||
|
serde_json::to_value(DidChangeWatchedFilesRegistrationOptions {
|
||||||
|
watchers: vec![
|
||||||
|
FileSystemWatcher {
|
||||||
|
glob_pattern: lsp_types::GlobPattern::String("**/ty.toml".into()),
|
||||||
|
kind: None,
|
||||||
|
},
|
||||||
|
FileSystemWatcher {
|
||||||
|
glob_pattern: lsp_types::GlobPattern::String(
|
||||||
|
"**/.gitignore".into(),
|
||||||
|
),
|
||||||
|
kind: None,
|
||||||
|
},
|
||||||
|
FileSystemWatcher {
|
||||||
|
glob_pattern: lsp_types::GlobPattern::String("**/.ignore".into()),
|
||||||
|
kind: None,
|
||||||
|
},
|
||||||
|
FileSystemWatcher {
|
||||||
|
glob_pattern: lsp_types::GlobPattern::String(
|
||||||
|
"**/pyproject.toml".into(),
|
||||||
|
),
|
||||||
|
kind: None,
|
||||||
|
},
|
||||||
|
FileSystemWatcher {
|
||||||
|
glob_pattern: lsp_types::GlobPattern::String("**/*.py".into()),
|
||||||
|
kind: None,
|
||||||
|
},
|
||||||
|
FileSystemWatcher {
|
||||||
|
glob_pattern: lsp_types::GlobPattern::String("**/*.pyi".into()),
|
||||||
|
kind: None,
|
||||||
|
},
|
||||||
|
FileSystemWatcher {
|
||||||
|
glob_pattern: lsp_types::GlobPattern::String("**/*.ipynb".into()),
|
||||||
|
kind: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let response_handler = |()| {
|
||||||
|
tracing::info!("File watcher successfully registered");
|
||||||
|
Task::nothing()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = scheduler.request::<lsp_types::request::RegisterCapability>(
|
||||||
|
lsp_types::RegistrationParams {
|
||||||
|
registrations: vec![registration],
|
||||||
|
},
|
||||||
|
response_handler,
|
||||||
|
) {
|
||||||
|
tracing::error!("An error occurred when trying to register the configuration file watcher: {err}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tracing::warn!("The client does not support file system watching.");
|
||||||
|
}
|
||||||
|
|
||||||
for msg in connection.incoming() {
|
for msg in connection.incoming() {
|
||||||
if connection.handle_shutdown(&msg)? {
|
if connection.handle_shutdown(&msg)? {
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -74,6 +74,9 @@ pub(super) fn notification<'a>(notif: server::Notification) -> Task<'a> {
|
||||||
notification::DidCloseNotebookHandler::METHOD => {
|
notification::DidCloseNotebookHandler::METHOD => {
|
||||||
local_notification_task::<notification::DidCloseNotebookHandler>(notif)
|
local_notification_task::<notification::DidCloseNotebookHandler>(notif)
|
||||||
}
|
}
|
||||||
|
notification::DidChangeWatchedFiles::METHOD => {
|
||||||
|
local_notification_task::<notification::DidChangeWatchedFiles>(notif)
|
||||||
|
}
|
||||||
lsp_types::notification::SetTrace::METHOD => {
|
lsp_types::notification::SetTrace::METHOD => {
|
||||||
tracing::trace!("Ignoring `setTrace` notification");
|
tracing::trace!("Ignoring `setTrace` notification");
|
||||||
return Task::nothing();
|
return Task::nothing();
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
mod did_change;
|
mod did_change;
|
||||||
|
mod did_change_watched_files;
|
||||||
mod did_close;
|
mod did_close;
|
||||||
mod did_close_notebook;
|
mod did_close_notebook;
|
||||||
mod did_open;
|
mod did_open;
|
||||||
mod did_open_notebook;
|
mod did_open_notebook;
|
||||||
|
|
||||||
pub(super) use did_change::DidChangeTextDocumentHandler;
|
pub(super) use did_change::DidChangeTextDocumentHandler;
|
||||||
|
pub(super) use did_change_watched_files::DidChangeWatchedFiles;
|
||||||
pub(super) use did_close::DidCloseTextDocumentHandler;
|
pub(super) use did_close::DidCloseTextDocumentHandler;
|
||||||
pub(super) use did_close_notebook::DidCloseNotebookHandler;
|
pub(super) use did_close_notebook::DidCloseNotebookHandler;
|
||||||
pub(super) use did_open::DidOpenTextDocumentHandler;
|
pub(super) use did_open::DidOpenTextDocumentHandler;
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
|
||||||
|
use crate::server::api::LSPResult;
|
||||||
|
use crate::server::client::{Notifier, Requester};
|
||||||
|
use crate::server::schedule::Task;
|
||||||
|
use crate::server::Result;
|
||||||
|
use crate::session::Session;
|
||||||
|
use crate::system::{url_to_any_system_path, AnySystemPath};
|
||||||
|
use lsp_types as types;
|
||||||
|
use lsp_types::{notification as notif, FileChangeType};
|
||||||
|
use rustc_hash::FxHashMap;
|
||||||
|
use ty_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind};
|
||||||
|
use ty_project::Db;
|
||||||
|
|
||||||
|
pub(crate) struct DidChangeWatchedFiles;
|
||||||
|
|
||||||
|
impl NotificationHandler for DidChangeWatchedFiles {
|
||||||
|
type NotificationType = notif::DidChangeWatchedFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SyncNotificationHandler for DidChangeWatchedFiles {
|
||||||
|
fn run(
|
||||||
|
session: &mut Session,
|
||||||
|
_notifier: Notifier,
|
||||||
|
requester: &mut Requester,
|
||||||
|
params: types::DidChangeWatchedFilesParams,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut events_by_db: FxHashMap<_, Vec<ChangeEvent>> = FxHashMap::default();
|
||||||
|
|
||||||
|
for change in params.changes {
|
||||||
|
let path = match url_to_any_system_path(&change.uri) {
|
||||||
|
Ok(path) => path,
|
||||||
|
Err(err) => {
|
||||||
|
tracing::warn!(
|
||||||
|
"Failed to convert URI '{}` to system path: {err:?}",
|
||||||
|
change.uri
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let system_path = match path {
|
||||||
|
AnySystemPath::System(system) => system,
|
||||||
|
AnySystemPath::SystemVirtual(path) => {
|
||||||
|
tracing::debug!("Ignoring virtual path from change event: `{path}`");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(db) = session.project_db_for_path(system_path.as_std_path()) else {
|
||||||
|
tracing::trace!(
|
||||||
|
"Ignoring change event for `{system_path}` because it's not in any workspace"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let change_event = match change.typ {
|
||||||
|
FileChangeType::CREATED => ChangeEvent::Created {
|
||||||
|
path: system_path,
|
||||||
|
kind: CreatedKind::Any,
|
||||||
|
},
|
||||||
|
FileChangeType::CHANGED => ChangeEvent::Changed {
|
||||||
|
path: system_path,
|
||||||
|
kind: ChangedKind::Any,
|
||||||
|
},
|
||||||
|
FileChangeType::DELETED => ChangeEvent::Deleted {
|
||||||
|
path: system_path,
|
||||||
|
kind: DeletedKind::Any,
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
tracing::debug!(
|
||||||
|
"Ignoring unsupported change event type: `{:?}` for {system_path}",
|
||||||
|
change.typ
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
events_by_db
|
||||||
|
.entry(db.project().root(db).to_path_buf())
|
||||||
|
.or_default()
|
||||||
|
.push(change_event);
|
||||||
|
}
|
||||||
|
|
||||||
|
if events_by_db.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (root, changes) in events_by_db {
|
||||||
|
tracing::debug!("Applying changes to `{root}`");
|
||||||
|
let db = session.project_db_for_path_mut(&*root).unwrap();
|
||||||
|
|
||||||
|
db.apply_changes(changes, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let client_capabilities = session.client_capabilities();
|
||||||
|
|
||||||
|
if client_capabilities.diagnostics_refresh {
|
||||||
|
requester
|
||||||
|
.request::<types::request::WorkspaceDiagnosticRefresh>((), |()| Task::nothing())
|
||||||
|
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if client_capabilities.inlay_refresh {
|
||||||
|
requester
|
||||||
|
.request::<types::request::InlayHintRefreshRequest>((), |()| Task::nothing())
|
||||||
|
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -57,7 +57,6 @@ impl<'s> Scheduler<'s> {
|
||||||
/// Immediately sends a request of kind `R` to the client, with associated parameters.
|
/// Immediately sends a request of kind `R` to the client, with associated parameters.
|
||||||
/// The task provided by `response_handler` will be dispatched as soon as the response
|
/// The task provided by `response_handler` will be dispatched as soon as the response
|
||||||
/// comes back from the client.
|
/// comes back from the client.
|
||||||
#[expect(dead_code)]
|
|
||||||
pub(super) fn request<R>(
|
pub(super) fn request<R>(
|
||||||
&mut self,
|
&mut self,
|
||||||
params: R::Params,
|
params: R::Params,
|
||||||
|
|
|
@ -210,6 +210,10 @@ impl Session {
|
||||||
index,
|
index,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn client_capabilities(&self) -> &ResolvedClientCapabilities {
|
||||||
|
&self.resolved_client_capabilities
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A guard that holds the only reference to the index and allows modifying it.
|
/// A guard that holds the only reference to the index and allows modifying it.
|
||||||
|
|
|
@ -6,7 +6,8 @@ pub(crate) struct ResolvedClientCapabilities {
|
||||||
pub(crate) code_action_deferred_edit_resolution: bool,
|
pub(crate) code_action_deferred_edit_resolution: bool,
|
||||||
pub(crate) apply_edit: bool,
|
pub(crate) apply_edit: bool,
|
||||||
pub(crate) document_changes: bool,
|
pub(crate) document_changes: bool,
|
||||||
pub(crate) workspace_refresh: bool,
|
pub(crate) diagnostics_refresh: bool,
|
||||||
|
pub(crate) inlay_refresh: bool,
|
||||||
pub(crate) pull_diagnostics: bool,
|
pub(crate) pull_diagnostics: bool,
|
||||||
/// Whether `textDocument.typeDefinition.linkSupport` is `true`
|
/// Whether `textDocument.typeDefinition.linkSupport` is `true`
|
||||||
pub(crate) type_definition_link_support: bool,
|
pub(crate) type_definition_link_support: bool,
|
||||||
|
@ -47,18 +48,17 @@ impl ResolvedClientCapabilities {
|
||||||
.and_then(|document| document.type_definition?.link_support)
|
.and_then(|document| document.type_definition?.link_support)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let workspace_refresh = true;
|
let diagnostics_refresh = client_capabilities
|
||||||
|
|
||||||
// TODO(jane): Once the bug involving workspace.diagnostic(s) deserialization has been fixed,
|
|
||||||
// uncomment this.
|
|
||||||
/*
|
|
||||||
let workspace_refresh = client_capabilities
|
|
||||||
.workspace
|
.workspace
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|workspace| workspace.diagnostic.as_ref())
|
.and_then(|workspace| workspace.diagnostics.as_ref()?.refresh_support)
|
||||||
.and_then(|diagnostic| diagnostic.refresh_support)
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let inlay_refresh = client_capabilities
|
||||||
|
.workspace
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|workspace| workspace.inlay_hint.as_ref()?.refresh_support)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
*/
|
|
||||||
|
|
||||||
let pull_diagnostics = client_capabilities
|
let pull_diagnostics = client_capabilities
|
||||||
.text_document
|
.text_document
|
||||||
|
@ -86,7 +86,8 @@ impl ResolvedClientCapabilities {
|
||||||
&& code_action_edit_resolution,
|
&& code_action_edit_resolution,
|
||||||
apply_edit,
|
apply_edit,
|
||||||
document_changes,
|
document_changes,
|
||||||
workspace_refresh,
|
diagnostics_refresh,
|
||||||
|
inlay_refresh,
|
||||||
pull_diagnostics,
|
pull_diagnostics,
|
||||||
type_definition_link_support: declaration_link_support,
|
type_definition_link_support: declaration_link_support,
|
||||||
hover_prefer_markdown,
|
hover_prefer_markdown,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue