From 5c41f22c2a20a1b8a91ddd6397686bd752591ffc Mon Sep 17 00:00:00 2001 From: Ryan Fowler Date: Fri, 21 Jul 2023 15:21:21 -0700 Subject: Add support for LSP DidChangeWatchedFiles (#7665) * Add initial support for LSP DidChangeWatchedFiles * Move file event Handler to helix-lsp * Simplify file event handling * Refactor file event handling * Block on future within LSP file event handler * Fully qualify uses of the file_event::Handler type * Rename ops field to options * Revert newline removal from helix-view/Cargo.toml * Ensure file event Handler is cleaned up when lsp client is shutdown--- helix-lsp/Cargo.toml | 1 + helix-lsp/src/client.rs | 13 +++ helix-lsp/src/file_event.rs | 193 ++++++++++++++++++++++++++++++++++++++++++++ helix-lsp/src/lib.rs | 11 +++ 4 files changed, 218 insertions(+) create mode 100644 helix-lsp/src/file_event.rs (limited to 'helix-lsp') diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index b4abeb8a..33c63e0a 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -19,6 +19,7 @@ helix-parsec = { version = "0.6", path = "../helix-parsec" } anyhow = "1.0" futures-executor = "0.3" futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false } +globset = "0.4.11" log = "0.4" lsp-types = { version = "0.94" } serde = { version = "1.0", features = ["derive"] } diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 92ab03db..ed9a2f83 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -544,6 +544,10 @@ impl Client { normalizes_line_endings: Some(false), change_annotation_support: None, }), + did_change_watched_files: Some(lsp::DidChangeWatchedFilesClientCapabilities { + dynamic_registration: Some(true), + relative_pattern_support: Some(false), + }), ..Default::default() }), text_document: Some(lsp::TextDocumentClientCapabilities { @@ -1453,4 +1457,13 @@ impl Client { Some(self.call::(params)) } + + pub fn did_change_watched_files( + &self, + changes: Vec, + ) -> impl Future> { + self.notify::(lsp::DidChangeWatchedFilesParams { + changes, + }) + } } diff --git a/helix-lsp/src/file_event.rs b/helix-lsp/src/file_event.rs new file mode 100644 index 00000000..26a27c98 --- /dev/null +++ b/helix-lsp/src/file_event.rs @@ -0,0 +1,193 @@ +use std::{collections::HashMap, path::PathBuf, sync::Weak}; + +use globset::{GlobBuilder, GlobSetBuilder}; +use tokio::sync::mpsc; + +use crate::{lsp, Client}; + +enum Event { + FileChanged { + path: PathBuf, + }, + Register { + client_id: usize, + client: Weak, + registration_id: String, + options: lsp::DidChangeWatchedFilesRegistrationOptions, + }, + Unregister { + client_id: usize, + registration_id: String, + }, + RemoveClient { + client_id: usize, + }, +} + +#[derive(Default)] +struct ClientState { + client: Weak, + registered: HashMap, +} + +/// The Handler uses a dedicated tokio task to respond to file change events by +/// forwarding changes to LSPs that have registered for notifications with a +/// matching glob. +/// +/// When an LSP registers for the DidChangeWatchedFiles notification, the +/// Handler is notified by sending the registration details in addition to a +/// weak reference to the LSP client. This is done so that the Handler can have +/// access to the client without preventing the client from being dropped if it +/// is closed and the Handler isn't properly notified. +#[derive(Clone, Debug)] +pub struct Handler { + tx: mpsc::UnboundedSender, +} + +impl Default for Handler { + fn default() -> Self { + Self::new() + } +} + +impl Handler { + pub fn new() -> Self { + let (tx, rx) = mpsc::unbounded_channel(); + tokio::spawn(Self::run(rx)); + Self { tx } + } + + pub fn register( + &self, + client_id: usize, + client: Weak, + registration_id: String, + options: lsp::DidChangeWatchedFilesRegistrationOptions, + ) { + let _ = self.tx.send(Event::Register { + client_id, + client, + registration_id, + options, + }); + } + + pub fn unregister(&self, client_id: usize, registration_id: String) { + let _ = self.tx.send(Event::Unregister { + client_id, + registration_id, + }); + } + + pub fn file_changed(&self, path: PathBuf) { + let _ = self.tx.send(Event::FileChanged { path }); + } + + pub fn remove_client(&self, client_id: usize) { + let _ = self.tx.send(Event::RemoveClient { client_id }); + } + + async fn run(mut rx: mpsc::UnboundedReceiver) { + let mut state: HashMap = HashMap::new(); + while let Some(event) = rx.recv().await { + match event { + Event::FileChanged { path } => { + log::debug!("Received file event for {:?}", &path); + + state.retain(|id, client_state| { + if !client_state + .registered + .values() + .any(|glob| glob.is_match(&path)) + { + return true; + } + let Some(client) = client_state.client.upgrade() else { + log::warn!("LSP client was dropped: {id}"); + return false; + }; + let Ok(uri) = lsp::Url::from_file_path(&path) else { + return true; + }; + log::debug!( + "Sending didChangeWatchedFiles notification to client '{}'", + client.name() + ); + if let Err(err) = crate::block_on(client + .did_change_watched_files(vec![lsp::FileEvent { + uri, + // We currently always send the CHANGED state + // since we don't actually have more context at + // the moment. + typ: lsp::FileChangeType::CHANGED, + }])) + { + log::warn!("Failed to send didChangeWatchedFiles notification to client: {err}"); + } + true + }); + } + Event::Register { + client_id, + client, + registration_id, + options: ops, + } => { + log::debug!( + "Registering didChangeWatchedFiles for client '{}' with id '{}'", + client_id, + registration_id + ); + + let mut entry = state.entry(client_id).or_insert_with(ClientState::default); + entry.client = client; + + let mut builder = GlobSetBuilder::new(); + for watcher in ops.watchers { + if let lsp::GlobPattern::String(pattern) = watcher.glob_pattern { + if let Ok(glob) = GlobBuilder::new(&pattern).build() { + builder.add(glob); + } + } + } + match builder.build() { + Ok(globset) => { + entry.registered.insert(registration_id, globset); + } + Err(err) => { + // Remove any old state for that registration id and + // remove the entire client if it's now empty. + entry.registered.remove(®istration_id); + if entry.registered.is_empty() { + state.remove(&client_id); + } + log::warn!( + "Unable to build globset for LSP didChangeWatchedFiles {err}" + ) + } + } + } + Event::Unregister { + client_id, + registration_id, + } => { + log::debug!( + "Unregistering didChangeWatchedFiles with id '{}' for client '{}'", + registration_id, + client_id + ); + if let Some(client_state) = state.get_mut(&client_id) { + client_state.registered.remove(®istration_id); + if client_state.registered.is_empty() { + state.remove(&client_id); + } + } + } + Event::RemoveClient { client_id } => { + log::debug!("Removing LSP client: {client_id}"); + state.remove(&client_id); + } + } + } + } +} diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 95c61086..90f0c3fd 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -1,4 +1,5 @@ mod client; +pub mod file_event; pub mod jsonrpc; pub mod snippet; mod transport; @@ -547,6 +548,7 @@ pub enum MethodCall { WorkspaceFolders, WorkspaceConfiguration(lsp::ConfigurationParams), RegisterCapability(lsp::RegistrationParams), + UnregisterCapability(lsp::UnregistrationParams), } impl MethodCall { @@ -570,6 +572,10 @@ impl MethodCall { let params: lsp::RegistrationParams = params.parse()?; Self::RegisterCapability(params) } + lsp::request::UnregisterCapability::METHOD => { + let params: lsp::UnregistrationParams = params.parse()?; + Self::UnregisterCapability(params) + } _ => { return Err(Error::Unhandled); } @@ -629,6 +635,7 @@ pub struct Registry { syn_loader: Arc, counter: usize, pub incoming: SelectAll>, + pub file_event_handler: file_event::Handler, } impl Registry { @@ -638,6 +645,7 @@ impl Registry { syn_loader, counter: 0, incoming: SelectAll::new(), + file_event_handler: file_event::Handler::new(), } } @@ -650,6 +658,7 @@ impl Registry { } pub fn remove_by_id(&mut self, id: usize) { + self.file_event_handler.remove_client(id); self.inner.retain(|_, language_servers| { language_servers.retain(|ls| id != ls.id()); !language_servers.is_empty() @@ -715,6 +724,7 @@ impl Registry { .unwrap(); for old_client in old_clients { + self.file_event_handler.remove_client(old_client.id()); tokio::spawn(async move { let _ = old_client.force_shutdown().await; }); @@ -731,6 +741,7 @@ impl Registry { pub fn stop(&mut self, name: &str) { if let Some(clients) = self.inner.remove(name) { for client in clients { + self.file_event_handler.remove_client(client.id()); tokio::spawn(async move { let _ = client.force_shutdown().await; }); -- cgit v1.2.3-70-g09d2