on master help sign in
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

commit5c41f22c2a20a1b8a91ddd6397686bd752591ffc authorRyan Fowler <ryan.fowler19@gmail.com> dateFri 21 Jul 2023, 15:21 (3 years ago) parent8977123
8 files changed+325−29
Cargo.lock+12−18 helix-lsp/Cargo.toml+1 helix-lsp/src/client.rs+13 helix-lsp/src/file_event.rs+193 helix-lsp/src/lib.rs+11 helix-term/src/application.rs+61−9 helix-term/src/commands/typed.rs+14−1 helix-view/src/editor.rs+20−1
Cargo.lock+12−18
@@ -51,9 +51,9 @@ dependencies = [
5151
5252[[package]]
5353name = "aho-corasick"
54−version = "1.0.1"
54+version = "1.0.2"
5555source = "registry+https://github.com/rust-lang/crates.io-index"
56−checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04"
56+checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
5757dependencies = [
5858 "memchr",
5959]
@@ -126,13 +126,12 @@ checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42"
126126
127127[[package]]
128128name = "bstr"
129−version = "1.4.0"
129+version = "1.6.0"
130130source = "registry+https://github.com/rust-lang/crates.io-index"
131−checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09"
131+checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05"
132132dependencies = [
133133 "memchr",
134− "once_cell",
135− "regex-automata 0.1.10",
134+ "regex-automata",
136135 "serde",
137136]
138137
@@ -1149,11 +1148,11 @@ dependencies = [
11491148
11501149[[package]]
11511150name = "globset"
1152−version = "0.4.10"
1151+version = "0.4.11"
11531152source = "registry+https://github.com/rust-lang/crates.io-index"
1154−checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc"
1153+checksum = "1391ab1f92ffcc08911957149833e682aa3fe252b9f45f966d2ef972274c97df"
11551154dependencies = [
1156− "aho-corasick 0.7.20",
1155+ "aho-corasick 1.0.2",
11571156 "bstr",
11581157 "fnv",
11591158 "log",
@@ -1292,6 +1291,7 @@ dependencies = [
12921291 "anyhow",
12931292 "futures-executor",
12941293 "futures-util",
1294+ "globset",
12951295 "helix-core",
12961296 "helix-loader",
12971297 "helix-parsec",
@@ -1878,25 +1878,19 @@ version = "1.9.1"
18781878source = "registry+https://github.com/rust-lang/crates.io-index"
18791879checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575"
18801880dependencies = [
1881− "aho-corasick 1.0.1",
1881+ "aho-corasick 1.0.2",
18821882 "memchr",
1883− "regex-automata 0.3.2",
1883+ "regex-automata",
18841884 "regex-syntax 0.7.3",
18851885]
18861886
1887−[[package]]
1888−name = "regex-automata"
1889−version = "0.1.10"
1890−source = "registry+https://github.com/rust-lang/crates.io-index"
1891−checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
1892−
18931887[[package]]
18941888name = "regex-automata"
18951889version = "0.3.2"
18961890source = "registry+https://github.com/rust-lang/crates.io-index"
18971891checksum = "83d3daa6976cffb758ec878f108ba0e062a45b2d6ca3a2cca965338855476caf"
18981892dependencies = [
1899− "aho-corasick 1.0.1",
1893+ "aho-corasick 1.0.2",
19001894 "memchr",
19011895 "regex-syntax 0.7.3",
19021896]
helix-lsp/Cargo.toml+1
@@ -19,6 +19,7 @@ helix-parsec = { version = "0.6", path = "../helix-parsec" }
1919anyhow = "1.0"
2020futures-executor = "0.3"
2121futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
22+globset = "0.4.11"
2223log = "0.4"
2324lsp-types = { version = "0.94" }
2425serde = { version = "1.0", features = ["derive"] }
helix-lsp/src/client.rs+13
@@ -544,6 +544,10 @@ impl Client {
544544 normalizes_line_endings: Some(false),
545545 change_annotation_support: None,
546546 }),
547+ did_change_watched_files: Some(lsp::DidChangeWatchedFilesClientCapabilities {
548+ dynamic_registration: Some(true),
549+ relative_pattern_support: Some(false),
550+ }),
547551 ..Default::default()
548552 }),
549553 text_document: Some(lsp::TextDocumentClientCapabilities {
@@ -1453,4 +1457,13 @@ impl Client {
14531457
14541458 Some(self.call::<lsp::request::ExecuteCommand>(params))
14551459 }
1460+
1461+ pub fn did_change_watched_files(
1462+ &self,
1463+ changes: Vec<lsp::FileEvent>,
1464+ ) -> impl Future<Output = std::result::Result<(), Error>> {
1465+ self.notify::<lsp::notification::DidChangeWatchedFiles>(lsp::DidChangeWatchedFilesParams {
1466+ changes,
1467+ })
1468+ }
14561469}
helix-lsp/src/file_event.rsnew file+193
@@ -0,0 +1,193 @@
1+use std::{collections::HashMap, path::PathBuf, sync::Weak};
2+
3+use globset::{GlobBuilder, GlobSetBuilder};
4+use tokio::sync::mpsc;
5+
6+use crate::{lsp, Client};
7+
8+enum Event {
9+ FileChanged {
10+ path: PathBuf,
11+ },
12+ Register {
13+ client_id: usize,
14+ client: Weak<Client>,
15+ registration_id: String,
16+ options: lsp::DidChangeWatchedFilesRegistrationOptions,
17+ },
18+ Unregister {
19+ client_id: usize,
20+ registration_id: String,
21+ },
22+ RemoveClient {
23+ client_id: usize,
24+ },
25+}
26+
27+#[derive(Default)]
28+struct ClientState {
29+ client: Weak<Client>,
30+ registered: HashMap<String, globset::GlobSet>,
31+}
32+
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
35+/// matching glob.
36+///
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)]
43+pub struct Handler {
44+ tx: mpsc::UnboundedSender<Event>,
45+}
46+
47+impl Default for Handler {
48+ fn default() -> Self {
49+ Self::new()
50+ }
51+}
52+
53+impl Handler {
54+ pub fn new() -> Self {
55+ let (tx, rx) = mpsc::unbounded_channel();
56+ tokio::spawn(Self::run(rx));
57+ Self { tx }
58+ }
59+
60+ pub fn register(
61+ &self,
62+ client_id: usize,
63+ client: Weak<Client>,
64+ registration_id: String,
65+ options: lsp::DidChangeWatchedFilesRegistrationOptions,
66+ ) {
67+ let _ = self.tx.send(Event::Register {
68+ client_id,
69+ client,
70+ registration_id,
71+ options,
72+ });
73+ }
74+
75+ pub fn unregister(&self, client_id: usize, registration_id: String) {
76+ let _ = self.tx.send(Event::Unregister {
77+ client_id,
78+ registration_id,
79+ });
80+ }
81+
82+ pub fn file_changed(&self, path: PathBuf) {
83+ let _ = self.tx.send(Event::FileChanged { path });
84+ }
85+
86+ pub fn remove_client(&self, client_id: usize) {
87+ let _ = self.tx.send(Event::RemoveClient { client_id });
88+ }
89+
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 {
93+ match event {
94+ Event::FileChanged { path } => {
95+ log::debug!("Received file event for {:?}", &path);
96+
97+ state.retain(|id, client_state| {
98+ if !client_state
99+ .registered
100+ .values()
101+ .any(|glob| glob.is_match(&path))
102+ {
103+ return true;
104+ }
105+ let Some(client) = client_state.client.upgrade() else {
106+ log::warn!("LSP client was dropped: {id}");
107+ return false;
108+ };
109+ let Ok(uri) = lsp::Url::from_file_path(&path) else {
110+ return true;
111+ };
112+ log::debug!(
113+ "Sending didChangeWatchedFiles notification to client '{}'",
114+ client.name()
115+ );
116+ if let Err(err) = crate::block_on(client
117+ .did_change_watched_files(vec![lsp::FileEvent {
118+ uri,
119+ // We currently always send the CHANGED state
120+ // since we don't actually have more context at
121+ // the moment.
122+ typ: lsp::FileChangeType::CHANGED,
123+ }]))
124+ {
125+ log::warn!("Failed to send didChangeWatchedFiles notification to client: {err}");
126+ }
127+ true
128+ });
129+ }
130+ Event::Register {
131+ client_id,
132+ client,
133+ registration_id,
134+ options: ops,
135+ } => {
136+ log::debug!(
137+ "Registering didChangeWatchedFiles for client '{}' with id '{}'",
138+ client_id,
139+ registration_id
140+ );
141+
142+ let mut entry = state.entry(client_id).or_insert_with(ClientState::default);
143+ entry.client = client;
144+
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() {
149+ builder.add(glob);
150+ }
151+ }
152+ }
153+ match builder.build() {
154+ Ok(globset) => {
155+ entry.registered.insert(registration_id, globset);
156+ }
157+ Err(err) => {
158+ // Remove any old state for that registration id and
159+ // remove the entire client if it's now empty.
160+ entry.registered.remove(&registration_id);
161+ if entry.registered.is_empty() {
162+ state.remove(&client_id);
163+ }
164+ log::warn!(
165+ "Unable to build globset for LSP didChangeWatchedFiles {err}"
166+ )
167+ }
168+ }
169+ }
170+ Event::Unregister {
171+ client_id,
172+ registration_id,
173+ } => {
174+ log::debug!(
175+ "Unregistering didChangeWatchedFiles with id '{}' for client '{}'",
176+ registration_id,
177+ client_id
178+ );
179+ if let Some(client_state) = state.get_mut(&client_id) {
180+ client_state.registered.remove(&registration_id);
181+ if client_state.registered.is_empty() {
182+ state.remove(&client_id);
183+ }
184+ }
185+ }
186+ Event::RemoveClient { client_id } => {
187+ log::debug!("Removing LSP client: {client_id}");
188+ state.remove(&client_id);
189+ }
190+ }
191+ }
192+ }
193+}
helix-lsp/src/lib.rs+11
@@ -1,4 +1,5 @@
11mod client;
2+pub mod file_event;
23pub mod jsonrpc;
34pub mod snippet;
45mod transport;
@@ -547,6 +548,7 @@ pub enum MethodCall {
547548 WorkspaceFolders,
548549 WorkspaceConfiguration(lsp::ConfigurationParams),
549550 RegisterCapability(lsp::RegistrationParams),
551+ UnregisterCapability(lsp::UnregistrationParams),
550552}
551553
552554impl MethodCall {
@@ -570,6 +572,10 @@ impl MethodCall {
570572 let params: lsp::RegistrationParams = params.parse()?;
571573 Self::RegisterCapability(params)
572574 }
575+ lsp::request::UnregisterCapability::METHOD => {
576+ let params: lsp::UnregistrationParams = params.parse()?;
577+ Self::UnregisterCapability(params)
578+ }
573579 _ => {
574580 return Err(Error::Unhandled);
575581 }
@@ -629,6 +635,7 @@ pub struct Registry {
629635 syn_loader: Arc<helix_core::syntax::Loader>,
630636 counter: usize,
631637 pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
638+ pub file_event_handler: file_event::Handler,
632639}
633640
634641impl Registry {
@@ -638,6 +645,7 @@ impl Registry {
638645 syn_loader,
639646 counter: 0,
640647 incoming: SelectAll::new(),
648+ file_event_handler: file_event::Handler::new(),
641649 }
642650 }
643651
@@ -650,6 +658,7 @@ impl Registry {
650658 }
651659
652660 pub fn remove_by_id(&mut self, id: usize) {
661+ self.file_event_handler.remove_client(id);
653662 self.inner.retain(|_, language_servers| {
654663 language_servers.retain(|ls| id != ls.id());
655664 !language_servers.is_empty()
@@ -715,6 +724,7 @@ impl Registry {
715724 .unwrap();
716725
717726 for old_client in old_clients {
727+ self.file_event_handler.remove_client(old_client.id());
718728 tokio::spawn(async move {
719729 let _ = old_client.force_shutdown().await;
720730 });
@@ -731,6 +741,7 @@ impl Registry {
731741 pub fn stop(&mut self, name: &str) {
732742 if let Some(clients) = self.inner.remove(name) {
733743 for client in clients {
744+ self.file_event_handler.remove_client(client.id());
734745 tokio::spawn(async move {
735746 let _ = client.force_shutdown().await;
736747 });
helix-term/src/application.rs+61−9
@@ -5,7 +5,11 @@ use helix_core::{
55 path::get_relative_path,
66 pos_at_coords, syntax, Selection,
77};
8−use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap};
8+use helix_lsp::{
9+ lsp::{self, notification::Notification},
10+ util::lsp_pos_to_pos,
11+ LspProgressMap,
12+};
913use helix_view::{
1014 align_view,
1115 document::DocumentSavedEventResult,
@@ -1080,17 +1084,65 @@ impl Application {
10801084 .collect();
10811085 Ok(json!(result))
10821086 }
1083− Ok(MethodCall::RegisterCapability(_params)) => {
1084− log::warn!("Ignoring a client/registerCapability request because dynamic capability registration is not enabled. Please report this upstream to the language server");
1085− // Language Servers based on the `vscode-languageserver-node` library often send
1086− // client/registerCapability even though we do not enable dynamic registration
1087− // for any capabilities. We should send a MethodNotFound JSONRPC error in this
1088− // case but that rejects the registration promise in the server which causes an
1089− // exit. So we work around this by ignoring the request and sending back an OK
1090− // response.
1087+ Ok(MethodCall::RegisterCapability(params)) => {
1088+ if let Some(client) = self
1089+ .editor
1090+ .language_servers
1091+ .iter_clients()
1092+ .find(|client| client.id() == server_id)
1093+ {
1094+ for reg in params.registrations {
1095+ match reg.method.as_str() {
1096+ lsp::notification::DidChangeWatchedFiles::METHOD => {
1097+ let Some(options) = reg.register_options else {
1098+ continue;
1099+ };
1100+ let ops: lsp::DidChangeWatchedFilesRegistrationOptions =
1101+ match serde_json::from_value(options) {
1102+ Ok(ops) => ops,
1103+ Err(err) => {
1104+ log::warn!("Failed to deserialize DidChangeWatchedFilesRegistrationOptions: {err}");
1105+ continue;
1106+ }
1107+ };
1108+ self.editor.language_servers.file_event_handler.register(
1109+ client.id(),
1110+ Arc::downgrade(client),
1111+ reg.id,
1112+ ops,
1113+ )
1114+ }
1115+ _ => {
1116+ // Language Servers based on the `vscode-languageserver-node` library often send
1117+ // client/registerCapability even though we do not enable dynamic registration
1118+ // for most capabilities. We should send a MethodNotFound JSONRPC error in this
1119+ // case but that rejects the registration promise in the server which causes an
1120+ // exit. So we work around this by ignoring the request and sending back an OK
1121+ // response.
1122+ log::warn!("Ignoring a client/registerCapability request because dynamic capability registration is not enabled. Please report this upstream to the language server");
1123+ }
1124+ }
1125+ }
1126+ }
10911127
10921128 Ok(serde_json::Value::Null)
10931129 }
1130+ Ok(MethodCall::UnregisterCapability(params)) => {
1131+ for unreg in params.unregisterations {
1132+ match unreg.method.as_str() {
1133+ lsp::notification::DidChangeWatchedFiles::METHOD => {
1134+ self.editor
1135+ .language_servers
1136+ .file_event_handler
1137+ .unregister(server_id, unreg.id);
1138+ }
1139+ _ => {
1140+ log::warn!("Received unregistration request for unsupported method: {}", unreg.method);
1141+ }
1142+ }
1143+ }
1144+ Ok(serde_json::Value::Null)
1145+ }
10941146 };
10951147
10961148 tokio::spawn(language_server!().reply(id, reply));
helix-term/src/commands/typed.rs+14−1
@@ -1283,7 +1283,14 @@ fn reload(
12831283 doc.reload(view, &cx.editor.diff_providers, redraw_handle)
12841284 .map(|_| {
12851285 view.ensure_cursor_in_view(doc, scrolloff);
1286− })
1286+ })?;
1287+ if let Some(path) = doc.path() {
1288+ cx.editor
1289+ .language_servers
1290+ .file_event_handler
1291+ .file_changed(path.clone());
1292+ }
1293+ Ok(())
12871294}
12881295
12891296fn reload_all(
@@ -1324,6 +1331,12 @@ fn reload_all(
13241331
13251332 let redraw_handle = cx.editor.redraw_handle.clone();
13261333 doc.reload(view, &cx.editor.diff_providers, redraw_handle)?;
1334+ if let Some(path) = doc.path() {
1335+ cx.editor
1336+ .language_servers
1337+ .file_event_handler
1338+ .file_changed(path.clone());
1339+ }
13271340
13281341 for view_id in view_ids {
13291342 let view = view_mut!(cx.editor, view_id);
helix-view/src/editor.rs+20−1
@@ -1535,7 +1535,18 @@ impl Editor {
15351535
15361536 let path = path.map(|path| path.into());
15371537 let doc = doc_mut!(self, &doc_id);
1538− let future = doc.save(path, force)?;
1538+ let doc_save_future = doc.save(path, force)?;
1539+
1540+ // When a file is written to, notify the file event handler.
1541+ // Note: This can be removed once proper file watching is implemented.
1542+ let handler = self.language_servers.file_event_handler.clone();
1543+ let future = async move {
1544+ let res = doc_save_future.await;
1545+ if let Ok(event) = &res {
1546+ handler.file_changed(event.path.clone());
1547+ }
1548+ res
1549+ };
15391550
15401551 use futures_util::stream;
15411552
@@ -1671,6 +1682,14 @@ impl Editor {
16711682 &self,
16721683 timeout: Option<u64>,
16731684 ) -> Result<(), tokio::time::error::Elapsed> {
1685+ // Remove all language servers from the file event handler.
1686+ // Note: this is non-blocking.
1687+ for client in self.language_servers.iter_clients() {
1688+ self.language_servers
1689+ .file_event_handler
1690+ .remove_client(client.id());
1691+ }
1692+
16741693 tokio::time::timeout(
16751694 Duration::from_millis(timeout.unwrap_or(3000)),
16761695 future::join_all(