Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-lsp/src/file_event.rs')
| -rw-r--r-- | helix-lsp/src/file_event.rs | 300 |
1 files changed, 226 insertions, 74 deletions
diff --git a/helix-lsp/src/file_event.rs b/helix-lsp/src/file_event.rs index 5e7f8ca6..c07bfe80 100644 --- a/helix-lsp/src/file_event.rs +++ b/helix-lsp/src/file_event.rs @@ -1,16 +1,23 @@ +use std::collections::hash_map; +use std::mem::take; +use std::path::{is_separator, Path}; use std::{collections::HashMap, path::PathBuf, sync::Weak}; -use globset::{GlobBuilder, GlobSetBuilder}; +use globset::{Glob, GlobSet}; +use helix_core::file_watcher::{EventType, Events, FileSystemDidChange}; +use helix_event::register_hook; +use helix_lsp_types::WatchKind; use tokio::sync::mpsc; use crate::{lsp, Client, LanguageServerId}; enum Event { - FileChanged { + /// file written by helix, special cased to not wait on FS + FileWritten { path: PathBuf, }, + FileWatcher(Events), Register { - client_id: LanguageServerId, client: Weak<Client>, registration_id: String, options: lsp::DidChangeWatchedFilesRegistrationOptions, @@ -24,10 +31,108 @@ enum Event { }, } -#[derive(Default)] struct ClientState { client: Weak<Client>, - registered: HashMap<String, globset::GlobSet>, + registerations: HashMap<String, u32>, + pending: Vec<lsp::FileEvent>, +} + +#[derive(Debug, Clone)] +struct Interest { + glob: Glob, + server: LanguageServerId, + id: u32, + flags: WatchKind, +} + +struct State { + clients: HashMap<LanguageServerId, ClientState>, + glob_matcher: GlobSet, + interest: Vec<Interest>, + // used for matching to avoid reallocation + candidates: Vec<usize>, +} +impl State { + fn notify<'a>(&mut self, events: impl Iterator<Item = (&'a Path, EventType)> + Clone) { + for (path, ty) in events { + let (interest_kind, notification_type) = match ty { + EventType::Create => (WatchKind::Create, lsp::FileChangeType::CREATED), + EventType::Delete => (WatchKind::Delete, lsp::FileChangeType::DELETED), + EventType::Modified => (WatchKind::Change, lsp::FileChangeType::CHANGED), + EventType::Tempfile => continue, + }; + self.glob_matcher.matches_into(path, &mut self.candidates); + for interest in self.candidates.drain(..) { + let interest = &self.interest[interest]; + if !interest.flags.contains(interest_kind) { + continue; + } + let Ok(uri) = lsp::Url::from_file_path(path) else { + continue; + }; + let event = lsp::FileEvent { + uri, + typ: notification_type, + }; + self.clients + .get_mut(&interest.server) + .unwrap() + .pending + .push(event); + } + } + for client_state in self.clients.values_mut() { + if client_state.pending.is_empty() { + continue; + } + let Some(client) = client_state.client.upgrade() else { + continue; + }; + log::debug!( + "Sending didChangeWatchedFiles notification to client '{}'", + client.name() + ); + client.did_change_watched_files(take(&mut client_state.pending)); + } + } + + fn purge_client(&mut self, id: LanguageServerId) { + self.clients.remove(&id); + let interest = self + .interest + .iter() + .filter(|it| it.server != id) + .cloned() + .collect(); + self.rebuild_globmatcher(interest); + } + + fn rebuild_globmatcher(&mut self, interest: Vec<Interest>) { + let mut builder = GlobSet::builder(); + for interest in &interest { + builder.add(interest.glob.clone()); + } + match builder.build() { + Ok(glob_matcher) => { + self.glob_matcher = glob_matcher; + self.interest = interest; + } + Err(err) => { + log::error!("failde to build glob matcher for file watching: ({err})",); + } + } + } +} + +impl Default for State { + fn default() -> State { + State { + clients: Default::default(), + glob_matcher: Default::default(), + interest: Default::default(), + candidates: Vec::with_capacity(32), + } + } } /// The Handler uses a dedicated tokio task to respond to file change events by @@ -54,18 +159,21 @@ impl Handler { pub fn new() -> Self { let (tx, rx) = mpsc::unbounded_channel(); tokio::spawn(Self::run(rx)); + let tx_ = tx.clone(); + register_hook!(move |event: &mut FileSystemDidChange| { + let _ = tx_.send(Event::FileWatcher(event.fs_events.clone())); + Ok(()) + }); Self { tx } } pub fn register( &self, - client_id: LanguageServerId, client: Weak<Client>, registration_id: String, options: lsp::DidChangeWatchedFilesRegistrationOptions, ) { let _ = self.tx.send(Event::Register { - client_id, client, registration_id, options, @@ -80,7 +188,7 @@ impl Handler { } pub fn file_changed(&self, path: PathBuf) { - let _ = self.tx.send(Event::FileChanged { path }); + let _ = self.tx.send(Event::FileWritten { path }); } pub fn remove_client(&self, client_id: LanguageServerId) { @@ -88,100 +196,144 @@ impl Handler { } async fn run(mut rx: mpsc::UnboundedReceiver<Event>) { - let mut state: HashMap<LanguageServerId, ClientState> = HashMap::new(); + let mut state = State::default(); while let Some(event) = rx.recv().await { match event { - Event::FileChanged { path } => { + Event::FileWatcher(events) => { + let events = events + .iter() + .map(|event| (event.path.as_std_path(), event.ty)); + state.notify(events); + } + Event::FileWritten { 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() - ); - 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, - }]); - true - }); + state.notify([(&*path, EventType::Modified)].iter().cloned()); } Event::Register { - client_id, client, registration_id, options: ops, } => { + let Some(client_) = client.upgrade() else { + continue; + }; log::debug!( "Registering didChangeWatchedFiles for client '{}' with id '{}'", - client_id, + client_.name(), registration_id ); - let entry = state.entry(client_id).or_default(); + if !state + .clients + .get(&client_.id()) + .is_some_and(|state| !state.client.ptr_eq(&client)) + { + state.purge_client(client_.id()); + } + let entry = state + .clients + .entry(client_.id()) + .or_insert_with(|| ClientState { + client: client.clone(), + registerations: HashMap::with_capacity(8), + pending: Vec::with_capacity(32), + }); 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); - } + let next_id = u32::try_from(entry.registerations.len()).unwrap(); + let (mut interest, id) = match entry.registerations.entry(registration_id) { + hash_map::Entry::Occupied(entry) => { + let id = *entry.get(); + let mut interest = Vec::with_capacity(state.interest.len()); + interest.extend( + state + .interest + .iter() + .filter(|it| it.server != client_.id() || it.id != id) + .cloned(), + ); + (interest, id) } - } - match builder.build() { - Ok(globset) => { - entry.registered.insert(registration_id, globset); + hash_map::Entry::Vacant(entry) => { + entry.insert(next_id); + (state.interest.clone(), next_id) + } + }; + for watcher in ops.watchers { + if watcher.kind.is_some_and(|flags| flags.is_empty()) { + continue; } - 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); + let glob = match watcher.glob_pattern { + helix_lsp_types::GlobPattern::String(pattern) => pattern, + helix_lsp_types::GlobPattern::Relative(relative_pattern) => { + let base_url = match relative_pattern.base_uri { + helix_lsp_types::OneOf::Left(folder) => folder.uri, + helix_lsp_types::OneOf::Right(url) => url, + }; + let Ok(mut base_dir) = base_url.to_file_path() else { + log::error!( + "{} provided invalid URL for watching '{base_url}'", + client_.name(), + ); + continue; + }; + if let Ok(dir) = base_dir.canonicalize() { + base_dir = dir + } + let Ok(mut base_dir) = base_dir.into_os_string().into_string() + else { + log::error!( + "{} provided invalid URL for watching '{base_url}' (must be valid utf-8)", + client_.name(), + ); + continue; + }; + if !base_dir.chars().next_back().is_some_and(is_separator) { + base_dir.push('/'); + } + base_dir.push_str(&relative_pattern.pattern); + base_dir + } + }; + match Glob::new(&glob) { + Ok(glob) => { + interest.push(Interest { + glob, + server: client_.id(), + id, + flags: watcher.kind.unwrap_or(WatchKind::all()), + }); + } + Err(err) => { + log::error!( + "{} provided invalid glob for watching '{glob}': ({err})", + client_.name(), + ); } - log::warn!( - "Unable to build globset for LSP didChangeWatchedFiles {err}" - ) } } + state.rebuild_globmatcher(interest); } 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); - } - } + let Some(client_state) = state.clients.get_mut(&client_id) else { + return; + }; + let Some(id) = client_state.registerations.remove(&*registration_id) else { + return; + }; + let interest = state + .interest + .iter() + .filter(|it| it.server != client_id || it.id != id) + .cloned() + .collect(); + state.rebuild_globmatcher(interest); } Event::RemoveClient { client_id } => { log::debug!("Removing LSP client: {client_id}"); - state.remove(&client_id); + state.purge_client(client_id); } } } |