diff --git a/crates/ty_server/src/server.rs b/crates/ty_server/src/server.rs index 1e2dcd8707..ceb7d58a19 100644 --- a/crates/ty_server/src/server.rs +++ b/crates/ty_server/src/server.rs @@ -7,7 +7,8 @@ use std::panic::PanicInfo; use lsp_server::Message; use lsp_types::{ - ClientCapabilities, DiagnosticOptions, DiagnosticServerCapabilities, HoverProviderCapability, + ClientCapabilities, DiagnosticOptions, DiagnosticServerCapabilities, + DidChangeWatchedFilesRegistrationOptions, FileSystemWatcher, HoverProviderCapability, InlayHintOptions, InlayHintServerCapabilities, MessageType, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, TypeDefinitionProviderCapability, Url, @@ -24,6 +25,7 @@ mod connection; mod schedule; use crate::message::try_show_message; +use crate::server::schedule::Task; pub(crate) use connection::ClientSender; pub(crate) type Result = std::result::Result; @@ -170,13 +172,80 @@ impl Server { fn event_loop( connection: &Connection, - _client_capabilities: &ClientCapabilities, + client_capabilities: &ClientCapabilities, mut session: Session, worker_threads: NonZeroUsize, ) -> crate::Result<()> { let mut scheduler = 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::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() { if connection.handle_shutdown(&msg)? { break; diff --git a/crates/ty_server/src/server/api.rs b/crates/ty_server/src/server/api.rs index 2c274dfc29..1dcae1dd7a 100644 --- a/crates/ty_server/src/server/api.rs +++ b/crates/ty_server/src/server/api.rs @@ -74,6 +74,9 @@ pub(super) fn notification<'a>(notif: server::Notification) -> Task<'a> { notification::DidCloseNotebookHandler::METHOD => { local_notification_task::(notif) } + notification::DidChangeWatchedFiles::METHOD => { + local_notification_task::(notif) + } lsp_types::notification::SetTrace::METHOD => { tracing::trace!("Ignoring `setTrace` notification"); return Task::nothing(); diff --git a/crates/ty_server/src/server/api/notifications.rs b/crates/ty_server/src/server/api/notifications.rs index 9482c44cf8..5e4f54eb94 100644 --- a/crates/ty_server/src/server/api/notifications.rs +++ b/crates/ty_server/src/server/api/notifications.rs @@ -1,10 +1,12 @@ mod did_change; +mod did_change_watched_files; mod did_close; mod did_close_notebook; mod did_open; mod did_open_notebook; 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_notebook::DidCloseNotebookHandler; pub(super) use did_open::DidOpenTextDocumentHandler; diff --git a/crates/ty_server/src/server/api/notifications/did_change_watched_files.rs b/crates/ty_server/src/server/api/notifications/did_change_watched_files.rs new file mode 100644 index 0000000000..2e470efedf --- /dev/null +++ b/crates/ty_server/src/server/api/notifications/did_change_watched_files.rs @@ -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> = 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::((), |()| Task::nothing()) + .with_failure_code(lsp_server::ErrorCode::InternalError)?; + } + + if client_capabilities.inlay_refresh { + requester + .request::((), |()| Task::nothing()) + .with_failure_code(lsp_server::ErrorCode::InternalError)?; + } + + Ok(()) + } +} diff --git a/crates/ty_server/src/server/schedule.rs b/crates/ty_server/src/server/schedule.rs index c33edd78f2..d051fcb141 100644 --- a/crates/ty_server/src/server/schedule.rs +++ b/crates/ty_server/src/server/schedule.rs @@ -57,7 +57,6 @@ impl<'s> Scheduler<'s> { /// 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 /// comes back from the client. - #[expect(dead_code)] pub(super) fn request( &mut self, params: R::Params, diff --git a/crates/ty_server/src/session.rs b/crates/ty_server/src/session.rs index c636a9b28d..987b7f0f91 100644 --- a/crates/ty_server/src/session.rs +++ b/crates/ty_server/src/session.rs @@ -210,6 +210,10 @@ impl Session { 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. diff --git a/crates/ty_server/src/session/capabilities.rs b/crates/ty_server/src/session/capabilities.rs index 6b1e51c53a..36ebf0368d 100644 --- a/crates/ty_server/src/session/capabilities.rs +++ b/crates/ty_server/src/session/capabilities.rs @@ -6,7 +6,8 @@ pub(crate) struct ResolvedClientCapabilities { pub(crate) code_action_deferred_edit_resolution: bool, pub(crate) apply_edit: 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, /// Whether `textDocument.typeDefinition.linkSupport` is `true` pub(crate) type_definition_link_support: bool, @@ -47,18 +48,17 @@ impl ResolvedClientCapabilities { .and_then(|document| document.type_definition?.link_support) .unwrap_or_default(); - let workspace_refresh = true; - - // TODO(jane): Once the bug involving workspace.diagnostic(s) deserialization has been fixed, - // uncomment this. - /* - let workspace_refresh = client_capabilities + let diagnostics_refresh = client_capabilities .workspace .as_ref() - .and_then(|workspace| workspace.diagnostic.as_ref()) - .and_then(|diagnostic| diagnostic.refresh_support) + .and_then(|workspace| workspace.diagnostics.as_ref()?.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(); - */ let pull_diagnostics = client_capabilities .text_document @@ -86,7 +86,8 @@ impl ResolvedClientCapabilities { && code_action_edit_resolution, apply_edit, document_changes, - workspace_refresh, + diagnostics_refresh, + inlay_refresh, pull_diagnostics, type_definition_link_support: declaration_link_support, hover_prefer_markdown,