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/src/file_event.rs | 193 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 helix-lsp/src/file_event.rs (limited to 'helix-lsp/src/file_event.rs') 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); + } + } + } + } +} -- cgit v1.2.3-70-g09d2