[ty] Add basic file watching to server (#17912)

This commit is contained in:
Micha Reiser 2025-05-07 19:03:30 +02:00 committed by GitHub
parent 51e2effd2d
commit 51386b3c7a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 203 additions and 14 deletions

View file

@ -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<T> = std::result::Result<T, api::Error>;
@ -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::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() {
if connection.handle_shutdown(&msg)? {
break;

View file

@ -74,6 +74,9 @@ pub(super) fn notification<'a>(notif: server::Notification) -> Task<'a> {
notification::DidCloseNotebookHandler::METHOD => {
local_notification_task::<notification::DidCloseNotebookHandler>(notif)
}
notification::DidChangeWatchedFiles::METHOD => {
local_notification_task::<notification::DidChangeWatchedFiles>(notif)
}
lsp_types::notification::SetTrace::METHOD => {
tracing::trace!("Ignoring `setTrace` notification");
return Task::nothing();

View file

@ -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;

View file

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

View file

@ -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<R>(
&mut self,
params: R::Params,

View file

@ -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.

View file

@ -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,