aboutsummaryrefslogtreecommitdiff
path: root/helix-lsp
diff options
context:
space:
mode:
authorRyan Fowler2023-07-21 22:21:21 +0000
committerGitHub2023-07-21 22:21:21 +0000
commit5c41f22c2a20a1b8a91ddd6397686bd752591ffc (patch)
treef64c0dba72fb6b14a7c0f0f4c98220a25f5b2462 /helix-lsp
parent8977123f25baba838d2d16f8a40e78563c4ebf4a (diff)
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
Diffstat (limited to 'helix-lsp')
-rw-r--r--helix-lsp/Cargo.toml1
-rw-r--r--helix-lsp/src/client.rs13
-rw-r--r--helix-lsp/src/file_event.rs193
-rw-r--r--helix-lsp/src/lib.rs11
4 files changed, 218 insertions, 0 deletions
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::<lsp::request::ExecuteCommand>(params))
}
+
+ pub fn did_change_watched_files(
+ &self,
+ changes: Vec<lsp::FileEvent>,
+ ) -> impl Future<Output = std::result::Result<(), Error>> {
+ self.notify::<lsp::notification::DidChangeWatchedFiles>(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<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(&registration_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(&registration_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<helix_core::syntax::Loader>,
counter: usize,
pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
+ 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;
});