mirror of https://github.com/helix-editor/helix
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 shutdownpull/7704/head
parent
8977123f25
commit
5c41f22c2a
@ -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<Client>,
|
||||
registration_id: String,
|
||||
options: lsp::DidChangeWatchedFilesRegistrationOptions,
|
||||
},
|
||||
Unregister {
|
||||
client_id: usize,
|
||||
registration_id: String,
|
||||
},
|
||||
RemoveClient {
|
||||
client_id: usize,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ClientState {
|
||||
client: Weak<Client>,
|
||||
registered: HashMap<String, globset::GlobSet>,
|
||||
}
|
||||
|
||||
/// 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<Event>,
|
||||
}
|
||||
|
||||
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<Client>,
|
||||
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<Event>) {
|
||||
let mut state: HashMap<usize, ClientState> = 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue