@@ -0,0 +1,193 @@
1+use std::{collections::HashMap, path::PathBuf, sync::Weak};
3+use globset::{GlobBuilder, GlobSetBuilder};
6+use crate::{lsp, Client};
14+ client: Weak<Client>,
15+ registration_id: String,
16+ options: lsp::DidChangeWatchedFilesRegistrationOptions,
20+ registration_id: String,
29+ client: Weak<Client>,
30+ registered: HashMap<String, globset::GlobSet>,
33+/// The Handler uses a dedicated tokio task to respond to file change events by
34+/// forwarding changes to LSPs that have registered for notifications with a
37+/// When an LSP registers for the DidChangeWatchedFiles notification, the
38+/// Handler is notified by sending the registration details in addition to a
39+/// weak reference to the LSP client. This is done so that the Handler can have
40+/// access to the client without preventing the client from being dropped if it
41+/// is closed and the Handler isn't properly notified.
42+#[derive(Clone, Debug)]
44+ tx: mpsc::UnboundedSender<Event>,
47+impl Default for Handler {
48+ fn default() -> Self {
54+ pub fn new() -> Self {
55+ let (tx, rx) = mpsc::unbounded_channel();
56+ tokio::spawn(Self::run(rx));
63+ client: Weak<Client>,
64+ registration_id: String,
65+ options: lsp::DidChangeWatchedFilesRegistrationOptions,
67+ let _ = self.tx.send(Event::Register {
75+ pub fn unregister(&self, client_id: usize, registration_id: String) {
76+ let _ = self.tx.send(Event::Unregister {
82+ pub fn file_changed(&self, path: PathBuf) {
83+ let _ = self.tx.send(Event::FileChanged { path });
86+ pub fn remove_client(&self, client_id: usize) {
87+ let _ = self.tx.send(Event::RemoveClient { client_id });
90+ async fn run(mut rx: mpsc::UnboundedReceiver<Event>) {
91+ let mut state: HashMap<usize, ClientState> = HashMap::new();
92+ while let Some(event) = rx.recv().await {
94+ Event::FileChanged { path } => {
95+ log::debug!("Received file event for {:?}", &path);
97+ state.retain(|id, client_state| {
101+ .any(|glob| glob.is_match(&path))
105+ let Some(client) = client_state.client.upgrade() else {
106+ log::warn!("LSP client was dropped: {id}");
109+ let Ok(uri) = lsp::Url::from_file_path(&path) else {
113+ "Sending didChangeWatchedFiles notification to client '{}'",
116+ if let Err(err) = crate::block_on(client
117+ .did_change_watched_files(vec![lsp::FileEvent {
119+ // We currently always send the CHANGED state
120+ // since we don't actually have more context at
122+ typ: lsp::FileChangeType::CHANGED,
125+ log::warn!("Failed to send didChangeWatchedFiles notification to client: {err}");
137+ "Registering didChangeWatchedFiles for client '{}' with id '{}'",
142+ let mut entry = state.entry(client_id).or_insert_with(ClientState::default);
143+ entry.client = client;
145+ let mut builder = GlobSetBuilder::new();
146+ for watcher in ops.watchers {
147+ if let lsp::GlobPattern::String(pattern) = watcher.glob_pattern {
148+ if let Ok(glob) = GlobBuilder::new(&pattern).build() {
153+ match builder.build() {
155+ entry.registered.insert(registration_id, globset);
158+ // Remove any old state for that registration id and
159+ // remove the entire client if it's now empty.
160+ entry.registered.remove(®istration_id);
161+ if entry.registered.is_empty() {
162+ state.remove(&client_id);
165+ "Unable to build globset for LSP didChangeWatchedFiles {err}"
175+ "Unregistering didChangeWatchedFiles with id '{}' for client '{}'",
179+ if let Some(client_state) = state.get_mut(&client_id) {
180+ client_state.registered.remove(®istration_id);
181+ if client_state.registered.is_empty() {
182+ state.remove(&client_id);
186+ Event::RemoveClient { client_id } => {
187+ log::debug!("Removing LSP client: {client_id}");
188+ state.remove(&client_id);