on master help sign in
Add support for LSP DidChangeWatchedFiles (#7665)
5c41f22Ryan Fowler
Cargo.lock+12−18
@@ -51,9 +51,9 @@ dependencies = [
51
51
52[[package]]
52[[package]]
53name = "aho-corasick"
53name = "aho-corasick"
54−version = "1.0.1"
54+version = "1.0.2"
55source = "registry+https://github.com/rust-lang/crates.io-index"
55source = "registry+https://github.com/rust-lang/crates.io-index"
56−checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04"
56+checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
57dependencies = [
57dependencies = [
58 "memchr",
58 "memchr",
59]
59]
@@ -126,13 +126,12 @@ checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42"
126
126
127[[package]]
127[[package]]
128name = "bstr"
128name = "bstr"
129−version = "1.4.0"
129+version = "1.6.0"
130source = "registry+https://github.com/rust-lang/crates.io-index"
130source = "registry+https://github.com/rust-lang/crates.io-index"
131−checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09"
131+checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05"
132dependencies = [
132dependencies = [
133 "memchr",
133 "memchr",
134− "once_cell",
135− "regex-automata 0.1.10",
134+ "regex-automata",
136 "serde",
135 "serde",
137]
136]
138
137
@@ -1149,11 +1148,11 @@ dependencies = [
1149
1148
1150[[package]]
1149[[package]]
1151name = "globset"
1150name = "globset"
1152−version = "0.4.10"
1151+version = "0.4.11"
1153source = "registry+https://github.com/rust-lang/crates.io-index"
1152source = "registry+https://github.com/rust-lang/crates.io-index"
1154−checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc"
1153+checksum = "1391ab1f92ffcc08911957149833e682aa3fe252b9f45f966d2ef972274c97df"
1155dependencies = [
1154dependencies = [
1156− "aho-corasick 0.7.20",
1155+ "aho-corasick 1.0.2",
1157 "bstr",
1156 "bstr",
1158 "fnv",
1157 "fnv",
1159 "log",
1158 "log",
@@ -1292,6 +1291,7 @@ dependencies = [
1292 "anyhow",
1291 "anyhow",
1293 "futures-executor",
1292 "futures-executor",
1294 "futures-util",
1293 "futures-util",
1294+ "globset",
1295 "helix-core",
1295 "helix-core",
1296 "helix-loader",
1296 "helix-loader",
1297 "helix-parsec",
1297 "helix-parsec",
@@ -1878,25 +1878,19 @@ version = "1.9.1"
1878source = "registry+https://github.com/rust-lang/crates.io-index"
1878source = "registry+https://github.com/rust-lang/crates.io-index"
1879checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575"
1879checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575"
1880dependencies = [
1880dependencies = [
1881− "aho-corasick 1.0.1",
1881+ "aho-corasick 1.0.2",
1882 "memchr",
1882 "memchr",
1883− "regex-automata 0.3.2",
1883+ "regex-automata",
1884 "regex-syntax 0.7.3",
1884 "regex-syntax 0.7.3",
1885]
1885]
1886
1886
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−
1893[[package]]
1887[[package]]
1894name = "regex-automata"
1888name = "regex-automata"
1895version = "0.3.2"
1889version = "0.3.2"
1896source = "registry+https://github.com/rust-lang/crates.io-index"
1890source = "registry+https://github.com/rust-lang/crates.io-index"
1897checksum = "83d3daa6976cffb758ec878f108ba0e062a45b2d6ca3a2cca965338855476caf"
1891checksum = "83d3daa6976cffb758ec878f108ba0e062a45b2d6ca3a2cca965338855476caf"
1898dependencies = [
1892dependencies = [
1899− "aho-corasick 1.0.1",
1893+ "aho-corasick 1.0.2",
1900 "memchr",
1894 "memchr",
1901 "regex-syntax 0.7.3",
1895 "regex-syntax 0.7.3",
1902]
1896]
helix-lsp/Cargo.toml+1
@@ -19,6 +19,7 @@ helix-parsec = { version = "0.6", path = "../helix-parsec" }
19anyhow = "1.0"
19anyhow = "1.0"
20futures-executor = "0.3"
20futures-executor = "0.3"
21futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
21futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
22+globset = "0.4.11"
22log = "0.4"
23log = "0.4"
23lsp-types = { version = "0.94" }
24lsp-types = { version = "0.94" }
24serde = { version = "1.0", features = ["derive"] }
25serde = { version = "1.0", features = ["derive"] }
helix-lsp/src/client.rs+13
@@ -544,6 +544,10 @@ impl Client {
544 normalizes_line_endings: Some(false),
544 normalizes_line_endings: Some(false),
545 change_annotation_support: None,
545 change_annotation_support: None,
546 }),
546 }),
547+ did_change_watched_files: Some(lsp::DidChangeWatchedFilesClientCapabilities {
548+ dynamic_registration: Some(true),
549+ relative_pattern_support: Some(false),
550+ }),
547 ..Default::default()
551 ..Default::default()
548 }),
552 }),
549 text_document: Some(lsp::TextDocumentClientCapabilities {
553 text_document: Some(lsp::TextDocumentClientCapabilities {
@@ -1453,4 +1457,13 @@ impl Client {
1453
1457
1454 Some(self.call::<lsp::request::ExecuteCommand>(params))
1458 Some(self.call::<lsp::request::ExecuteCommand>(params))
1455 }
1459 }
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+ }
1456}
1469}
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 @@
1mod client;
1mod client;
2+pub mod file_event;
2pub mod jsonrpc;
3pub mod jsonrpc;
3pub mod snippet;
4pub mod snippet;
4mod transport;
5mod transport;
@@ -547,6 +548,7 @@ pub enum MethodCall {
547 WorkspaceFolders,
548 WorkspaceFolders,
548 WorkspaceConfiguration(lsp::ConfigurationParams),
549 WorkspaceConfiguration(lsp::ConfigurationParams),
549 RegisterCapability(lsp::RegistrationParams),
550 RegisterCapability(lsp::RegistrationParams),
551+ UnregisterCapability(lsp::UnregistrationParams),
550}
552}
551
553
552impl MethodCall {
554impl MethodCall {
@@ -570,6 +572,10 @@ impl MethodCall {
570 let params: lsp::RegistrationParams = params.parse()?;
572 let params: lsp::RegistrationParams = params.parse()?;
571 Self::RegisterCapability(params)
573 Self::RegisterCapability(params)
572 }
574 }
575+ lsp::request::UnregisterCapability::METHOD => {
576+ let params: lsp::UnregistrationParams = params.parse()?;
577+ Self::UnregisterCapability(params)
578+ }
573 _ => {
579 _ => {
574 return Err(Error::Unhandled);
580 return Err(Error::Unhandled);
575 }
581 }
@@ -629,6 +635,7 @@ pub struct Registry {
629 syn_loader: Arc<helix_core::syntax::Loader>,
635 syn_loader: Arc<helix_core::syntax::Loader>,
630 counter: usize,
636 counter: usize,
631 pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
637 pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
638+ pub file_event_handler: file_event::Handler,
632}
639}
633
640
634impl Registry {
641impl Registry {
@@ -638,6 +645,7 @@ impl Registry {
638 syn_loader,
645 syn_loader,
639 counter: 0,
646 counter: 0,
640 incoming: SelectAll::new(),
647 incoming: SelectAll::new(),
648+ file_event_handler: file_event::Handler::new(),
641 }
649 }
642 }
650 }
643
651
@@ -650,6 +658,7 @@ impl Registry {
650 }
658 }
651
659
652 pub fn remove_by_id(&mut self, id: usize) {
660 pub fn remove_by_id(&mut self, id: usize) {
661+ self.file_event_handler.remove_client(id);
653 self.inner.retain(|_, language_servers| {
662 self.inner.retain(|_, language_servers| {
654 language_servers.retain(|ls| id != ls.id());
663 language_servers.retain(|ls| id != ls.id());
655 !language_servers.is_empty()
664 !language_servers.is_empty()
@@ -715,6 +724,7 @@ impl Registry {
715 .unwrap();
724 .unwrap();
716
725
717 for old_client in old_clients {
726 for old_client in old_clients {
727+ self.file_event_handler.remove_client(old_client.id());
718 tokio::spawn(async move {
728 tokio::spawn(async move {
719 let _ = old_client.force_shutdown().await;
729 let _ = old_client.force_shutdown().await;
720 });
730 });
@@ -731,6 +741,7 @@ impl Registry {
731 pub fn stop(&mut self, name: &str) {
741 pub fn stop(&mut self, name: &str) {
732 if let Some(clients) = self.inner.remove(name) {
742 if let Some(clients) = self.inner.remove(name) {
733 for client in clients {
743 for client in clients {
744+ self.file_event_handler.remove_client(client.id());
734 tokio::spawn(async move {
745 tokio::spawn(async move {
735 let _ = client.force_shutdown().await;
746 let _ = client.force_shutdown().await;
736 });
747 });
helix-term/src/application.rs+61−9
@@ -5,7 +5,11 @@ use helix_core::{
5 path::get_relative_path,
5 path::get_relative_path,
6 pos_at_coords, syntax, Selection,
6 pos_at_coords, syntax, Selection,
7};
7};
8+use helix_lsp::{
9+ lsp::{self, notification::Notification},
8−use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap};
10+ util::lsp_pos_to_pos,
11+ LspProgressMap,
12+};
9use helix_view::{
13use helix_view::{
10 align_view,
14 align_view,
11 document::DocumentSavedEventResult,
15 document::DocumentSavedEventResult,
@@ -1080,17 +1084,65 @@ impl Application {
1080 .collect();
1084 .collect();
1081 Ok(json!(result))
1085 Ok(json!(result))
1082 }
1086 }
1083− Ok(MethodCall::RegisterCapability(_params)) => {
1087+ 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");
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+ _ => {
1085− // Language Servers based on the `vscode-languageserver-node` library often send
1116+ // Language Servers based on the `vscode-languageserver-node` library often send
1086− // client/registerCapability even though we do not enable dynamic registration
1117+ // client/registerCapability even though we do not enable dynamic registration
1087− // for any capabilities. We should send a MethodNotFound JSONRPC error in this
1118+ // for most capabilities. We should send a MethodNotFound JSONRPC error in this
1088− // case but that rejects the registration promise in the server which causes an
1119+ // 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
1120+ // exit. So we work around this by ignoring the request and sending back an OK
1090− // response.
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+ }
1091
1127
1092 Ok(serde_json::Value::Null)
1128 Ok(serde_json::Value::Null)
1093 }
1129 }
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+ }
1094 };
1146 };
1095
1147
1096 tokio::spawn(language_server!().reply(id, reply));
1148 tokio::spawn(language_server!().reply(id, reply));
helix-term/src/commands/typed.rs+14−1
@@ -1283,7 +1283,14 @@ fn reload(
1283 doc.reload(view, &cx.editor.diff_providers, redraw_handle)
1283 doc.reload(view, &cx.editor.diff_providers, redraw_handle)
1284 .map(|_| {
1284 .map(|_| {
1285 view.ensure_cursor_in_view(doc, scrolloff);
1285 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(())
1287}
1294}
1288
1295
1289fn reload_all(
1296fn reload_all(
@@ -1324,6 +1331,12 @@ fn reload_all(
1324
1331
1325 let redraw_handle = cx.editor.redraw_handle.clone();
1332 let redraw_handle = cx.editor.redraw_handle.clone();
1326 doc.reload(view, &cx.editor.diff_providers, redraw_handle)?;
1333 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+ }
1327
1340
1328 for view_id in view_ids {
1341 for view_id in view_ids {
1329 let view = view_mut!(cx.editor, view_id);
1342 let view = view_mut!(cx.editor, view_id);
helix-view/src/editor.rs+20−1
@@ -1535,7 +1535,18 @@ impl Editor {
1535
1535
1536 let path = path.map(|path| path.into());
1536 let path = path.map(|path| path.into());
1537 let doc = doc_mut!(self, &doc_id);
1537 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+ };
1539
1550
1540 use futures_util::stream;
1551 use futures_util::stream;
1541
1552
@@ -1671,6 +1682,14 @@ impl Editor {
1671 &self,
1682 &self,
1672 timeout: Option<u64>,
1683 timeout: Option<u64>,
1673 ) -> Result<(), tokio::time::error::Elapsed> {
1684 ) -> 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+
1674 tokio::time::timeout(
1693 tokio::time::timeout(
1675 Duration::from_millis(timeout.unwrap_or(3000)),
1694 Duration::from_millis(timeout.unwrap_or(3000)),
1676 future::join_all(
1695 future::join_all(