From c6846a74f7ba46b26d22ad7e9f4602cdc3b1b8a8 Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Wed, 9 Jul 2025 14:36:03 +0200 Subject: [PATCH 1/6] Make registrations respect config changes Signed-off-by: Jonatan Waern --- src/actions/mod.rs | 60 ++++++++++++++++++++++++++++++++++-- src/actions/notifications.rs | 14 +-------- src/actions/requests.rs | 10 ++++++ 3 files changed, 68 insertions(+), 16 deletions(-) diff --git a/src/actions/mod.rs b/src/actions/mod.rs index 577cece9..89f798cb 100644 --- a/src/actions/mod.rs +++ b/src/actions/mod.rs @@ -15,6 +15,10 @@ use std::fs; use std::sync::atomic::AtomicBool; use std::sync::{Arc, Mutex}; +use lsp_types::notification::{DidChangeWatchedFiles}; +use lsp_types::request::{RegisterCapability, UnregisterCapability}; +use lsp_types::Unregistration; + use crate::actions::analysis_storage::AnalysisStorage; use crate::actions::analysis_queue::AnalysisQueue; use crate::actions::progress::{AnalysisProgressNotifier, @@ -31,6 +35,7 @@ use crate::lint::{LintCfg, maybe_parse_lint_cfg}; use crate::lsp_data; use crate::lsp_data::*; use crate::lsp_data::ls_util::{dls_to_range, dls_to_location}; + use crate::server::{Output, ServerToHandle, error_message, Request, RequestId, SentRequest}; use crate::server::message::RawResponse; @@ -285,6 +290,7 @@ pub struct InitActionContext { pub config: Arc>, pub lint_config: Arc>, + pub active_watch: Arc>>, pub sent_warnings: Arc>>, jobs: Arc>>, pub client_capabilities: Arc, @@ -392,6 +398,7 @@ impl InitActionContext { quiescent: Arc::new(AtomicBool::new(false)), prev_changes: Arc::default(), client_capabilities: Arc::new(client_capabilities), + active_watch: Arc::default(), has_notified_missing_builtins: false, //client_supports_cmd_run, active_waits: Arc::default(), @@ -438,6 +445,8 @@ impl InitActionContext { has_notified_missing_builtins: false, shut_down, pid: std::process::id(), + active_watch: Arc::new(Mutex::new(None)), + } } @@ -773,11 +782,53 @@ impl InitActionContext { self.report_errors(out); }, } - // Re-update log level if let Some(level) = self.config.lock().unwrap().server_debug_level { crate::logging::set_global_log_level(level); } + self.update_file_watchers(out); + } + + + const WATCH_ID: &str = "dls-watch"; + pub fn update_file_watchers(&self, out: &O) { + if self.active_watch.lock().unwrap().take().is_some() { + self.send_request::( + UnregistrationParams { + unregisterations: vec![Unregistration { + id: Self::WATCH_ID.to_string(), + method: ::METHOD + .to_string(), + }] + }, + out); + } else { + self.register_new_watchers(out); + } + } + + pub fn register_new_watchers(&self, out: &O) { + let mut watchers = self.active_watch.lock().unwrap(); + if let Some(previous) = watchers.take() { + error!("Wanted to register new watchers, but the previous ones \ + were not cleared. (were: {:?})", previous); + } + *watchers = FileWatch::new(self); + if let Some(watchers_spec) = watchers.as_ref() { + let reg_params = RegistrationParams { + registrations: vec![Registration { + id: Self::WATCH_ID.to_string(), + method: + ::METHOD.to_string(), + register_options: Some(watchers_spec.watchers_config()), + }], + }; + self.send_request::(reg_params, out); + } else { + error!("Failed to register file watchers with config: {:?}", + self.config.lock().unwrap()); + } } // Call before adding new analysis @@ -1453,6 +1504,7 @@ fn find_word_at_pos(line: &str, pos: Column) -> (Column, Column) { } // /// Client file-watching request / filtering logic +#[derive(Debug, Clone)] pub struct FileWatch { file_path: PathBuf, } @@ -1511,7 +1563,9 @@ impl FileWatch { let watchers = vec![watcher( self.file_path.to_string_lossy().to_string())]; - - json!({ "watchers": watchers }) + let watchers = DidChangeWatchedFilesRegistrationOptions { + watchers, + }; + json!(watchers) } } diff --git a/src/actions/notifications.rs b/src/actions/notifications.rs index 0594cb28..2bbd4ee7 100644 --- a/src/actions/notifications.rs +++ b/src/actions/notifications.rs @@ -55,19 +55,7 @@ impl BlockingNotificationAction for Initialized { }; ctx.send_request::(reg_params, &out); } - - // Register files we watch for changes based on config - const WATCH_ID: &str = "dls-watch"; - let reg_params = RegistrationParams { - registrations: vec![Registration { - id: WATCH_ID.to_owned(), - method: - ::METHOD.to_owned(), - register_options: FileWatch::new(ctx).map( - |fw|fw.watchers_config()), - }], - }; - ctx.send_request::(reg_params, &out); + ctx.update_file_watchers(&out); Ok(()) } } diff --git a/src/actions/requests.rs b/src/actions/requests.rs index 6fb4b50c..d257c00a 100644 --- a/src/actions/requests.rs +++ b/src/actions/requests.rs @@ -36,6 +36,7 @@ pub use crate::lsp_data::request::{ RangeFormatting, References, RegisterCapability, + UnregisterCapability, Rename, ResolveCompletionItem as ResolveCompletion, WorkspaceConfiguration, @@ -968,6 +969,15 @@ impl SentRequest for RegisterCapability { } } +impl SentRequest for UnregisterCapability { + type Response = ::Result; + fn on_response + (ctx: &InitActionContext, _response: Self::Response, out: &O) { + ctx.register_new_watchers(out) + } +} + + impl SentRequest for WorkspaceConfiguration { type Response = ::Result; fn on_response From d7097898ed290c6b605c81e64521e408f92d374d Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Wed, 9 Jul 2025 14:41:41 +0200 Subject: [PATCH 2/6] Add lint config to watched files Signed-off-by: Jonatan Waern --- src/actions/mod.rs | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/actions/mod.rs b/src/actions/mod.rs index 89f798cb..6e0f5ffb 100644 --- a/src/actions/mod.rs +++ b/src/actions/mod.rs @@ -1506,18 +1506,24 @@ fn find_word_at_pos(line: &str, pos: Column) -> (Column, Column) { // /// Client file-watching request / filtering logic #[derive(Debug, Clone)] pub struct FileWatch { - file_path: PathBuf, + file_paths: Vec, } impl FileWatch { /// Construct a new `FileWatch`. pub fn new(ctx: &InitActionContext) -> Option { + let mut file_paths = vec![]; match ctx.config.lock() { Ok(config) => { - config.compile_info_path.as_ref().map( - |c| FileWatch { - file_path: c.clone() - }) + if let Some(compile_info) = config.compile_info_path.as_ref() { + file_paths.push(compile_info.clone()); + } + if let Some(lint_cfg_path) = config.lint_cfg_path.as_ref() { + file_paths.push(lint_cfg_path.clone()); + } + Some(FileWatch { + file_paths, + }) }, Err(e) => { error!("Unable to access configuration: {:?}", e); @@ -1534,7 +1540,9 @@ impl FileWatch { fn relevant_change_kind(&self, change_uri: &Uri, _kind: FileChangeType) -> bool { let path = change_uri.as_str(); - self.file_path.to_str().is_some_and(|fp|fp == path) + self.file_paths.iter() + .filter_map(|p|p.to_str()) + .any(|our_path|our_path == path) } #[inline] @@ -1561,8 +1569,10 @@ impl FileWatch { kind: Some(kind) } } - let watchers = vec![watcher( - self.file_path.to_string_lossy().to_string())]; + let watchers: Vec<_> = self.file_paths.iter() + .map(|p|p.to_string_lossy().to_string()) + .map(watcher) + .collect(); let watchers = DidChangeWatchedFilesRegistrationOptions { watchers, }; From 8f52a6e0e0dffb358f3d19832202238400a8b238 Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Mon, 15 Jun 2026 10:05:45 +0200 Subject: [PATCH 3/6] Canonicalize watch paths before watching them The note on implementation details for relevant_change_kind are kind of outdated, we will only really be calling this on change notifications for specific files that rarely change. Signed-off-by: Jonatan Waern --- src/actions/mod.rs | 99 ++++++++++++++++++++++++++++++++++-------- src/file_management.rs | 3 ++ 2 files changed, 84 insertions(+), 18 deletions(-) diff --git a/src/actions/mod.rs b/src/actions/mod.rs index 6e0f5ffb..72fb2d9e 100644 --- a/src/actions/mod.rs +++ b/src/actions/mod.rs @@ -1503,26 +1503,90 @@ fn find_word_at_pos(line: &str, pos: Column) -> (Column, Column) { (span::Column::new_zero_indexed(start), span::Column::new_zero_indexed(end)) } +#[derive(Debug, Clone)] +pub struct FileWatchSpec { + full: CanonPath, + base: WorkspaceFolder, + relative: String, +} + // /// Client file-watching request / filtering logic #[derive(Debug, Clone)] pub struct FileWatch { - file_paths: Vec, + file_paths: Vec, } impl FileWatch { /// Construct a new `FileWatch`. pub fn new(ctx: &InitActionContext) -> Option { - let mut file_paths = vec![]; + let mut file_paths: HashMap = HashMap::default(); match ctx.config.lock() { Ok(config) => { if let Some(compile_info) = config.compile_info_path.as_ref() { - file_paths.push(compile_info.clone()); + if let Some(canon_path) = + CanonPath::from_path_buf(compile_info.clone()) { + file_paths.insert(canon_path, false); + } else { + error!("Could not watch compilation info {:?}, \ + not a canonizable path", compile_info); + } } if let Some(lint_cfg_path) = config.lint_cfg_path.as_ref() { - file_paths.push(lint_cfg_path.clone()); + if let Some(canon_path) = + CanonPath::from_path_buf(lint_cfg_path.clone()) { + file_paths.insert(canon_path, false); + } else { + error!("Could not watch lint config path {:?}, \ + not a canonizable path", lint_cfg_path); + } + } + fn path_to_relative(path: CanonPath, + roots: &Vec, + hit_paths: &mut HashMap) + -> Option> { + let mut globs = vec![]; + for root in roots { + let root_path = + parse_file_path!(&root.uri, "workspace").ok()?; + let root_canon_path = + CanonPath::from_path_buf(root_path)?; + info!("watch {:?} under {:?}", path, root); + if let Ok(relative_path) = path + .strip_prefix(root_canon_path.as_path()) { + hit_paths.insert(path.clone(), true); + globs.push( + FileWatchSpec { + full: root_canon_path, + base: root.clone(), + relative: relative_path + .to_string_lossy().to_string(), + } + ); + } + } + Some(globs) + } + + let watch_paths: Vec<_> = { + let lock_workspaces = ctx.workspace_roots.lock().unwrap(); + file_paths.keys().cloned() + .collect::>().into_iter() + .flat_map(|post_path|path_to_relative( + post_path, + &lock_workspaces, + &mut file_paths)) + .flatten() + .collect() + }; + for (path, watched) in &file_paths { + if !watched { + error!("Could not watch {:?}, not under any \ + workspace root", path); + } } Some(FileWatch { - file_paths, + file_paths: watch_paths, }) }, Err(e) => { @@ -1534,14 +1598,12 @@ impl FileWatch { /// Returns if a file change is relevant to the files we /// actually wanted to watch - /// Implementation note: This is expected to be called a - /// large number of times in a loop so should be fast / avoid allocation. #[inline] fn relevant_change_kind(&self, change_uri: &Uri, _kind: FileChangeType) -> bool { let path = change_uri.as_str(); self.file_paths.iter() - .filter_map(|p|p.to_str()) + .filter_map(|ws|ws.full.to_str()) .any(|our_path|our_path == path) } @@ -1559,19 +1621,20 @@ impl FileWatch { /// Returns json config for desired file watches pub fn watchers_config(&self) -> serde_json::Value { - fn watcher(pat: String) -> FileSystemWatcher { - FileSystemWatcher { glob_pattern: GlobPattern::String(pat), - kind: None } - } - fn _watcher_with_kind(pat: String, kind: WatchKind) - -> FileSystemWatcher { - FileSystemWatcher { glob_pattern: GlobPattern::String(pat), - kind: Some(kind) } + fn watcher(base: WorkspaceFolder, pat: String) -> FileSystemWatcher { + FileSystemWatcher { + glob_pattern: GlobPattern::Relative( + RelativePattern { + base_uri: OneOf::Left(base), + pattern: pat, + }), + kind: Some(WatchKind::all()), + } } let watchers: Vec<_> = self.file_paths.iter() - .map(|p|p.to_string_lossy().to_string()) - .map(watcher) + .map(|ws|watcher(ws.base.clone(), + ws.relative.clone())) .collect(); let watchers = DidChangeWatchedFilesRegistrationOptions { watchers, diff --git a/src/file_management.rs b/src/file_management.rs index e0747f88..eb0940ce 100644 --- a/src/file_management.rs +++ b/src/file_management.rs @@ -50,6 +50,9 @@ impl CanonPath { pub fn as_path(&self) -> &Path { self.0.as_path() } + pub fn as_path_buf(self) -> PathBuf { + self.0 + } } /// This is how we resolve relative paths to in-workspace full paths From d99ae9c27eec6b2ec0b40081c9c0570053c940ef Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Mon, 15 Jun 2026 10:06:15 +0200 Subject: [PATCH 4/6] Fix comparison in relevant_change_kind Signed-off-by: Jonatan Waern --- src/actions/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/actions/mod.rs b/src/actions/mod.rs index 72fb2d9e..ead2f8a6 100644 --- a/src/actions/mod.rs +++ b/src/actions/mod.rs @@ -1601,10 +1601,10 @@ impl FileWatch { #[inline] fn relevant_change_kind(&self, change_uri: &Uri, _kind: FileChangeType) -> bool { - let path = change_uri.as_str(); + let path = change_uri.path().to_string(); self.file_paths.iter() .filter_map(|ws|ws.full.to_str()) - .any(|our_path|our_path == path) + .any(|our_path|our_path == path.as_str()) } #[inline] From 79b994730614ff55ce230feb53d09d0c8acfadb2 Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Mon, 15 Jun 2026 10:20:32 +0200 Subject: [PATCH 5/6] Fix misc errors in filewatch creation Signed-off-by: Jonatan Waern --- src/actions/mod.rs | 99 ++++++++++++++++++++++------------------------ 1 file changed, 48 insertions(+), 51 deletions(-) diff --git a/src/actions/mod.rs b/src/actions/mod.rs index ead2f8a6..84d5f00b 100644 --- a/src/actions/mod.rs +++ b/src/actions/mod.rs @@ -803,9 +803,8 @@ impl InitActionContext { }] }, out); - } else { - self.register_new_watchers(out); } + self.register_new_watchers(out); } pub fn register_new_watchers(&self, out: &O) { @@ -824,6 +823,7 @@ impl InitActionContext { register_options: Some(watchers_spec.watchers_config()), }], }; + info!("Registered as {:?}", reg_params); self.send_request::(reg_params, out); } else { error!("Failed to register file watchers with config: {:?}", @@ -1506,7 +1506,7 @@ fn find_word_at_pos(line: &str, pos: Column) -> (Column, Column) { #[derive(Debug, Clone)] pub struct FileWatchSpec { full: CanonPath, - base: WorkspaceFolder, + base: Uri, relative: String, } @@ -1540,54 +1540,50 @@ impl FileWatch { not a canonizable path", lint_cfg_path); } } - fn path_to_relative(path: CanonPath, - roots: &Vec, - hit_paths: &mut HashMap) - -> Option> { - let mut globs = vec![]; - for root in roots { - let root_path = - parse_file_path!(&root.uri, "workspace").ok()?; - let root_canon_path = - CanonPath::from_path_buf(root_path)?; - info!("watch {:?} under {:?}", path, root); - if let Ok(relative_path) = path - .strip_prefix(root_canon_path.as_path()) { - hit_paths.insert(path.clone(), true); - globs.push( - FileWatchSpec { - full: root_canon_path, - base: root.clone(), - relative: relative_path - .to_string_lossy().to_string(), - } - ); - } - } - Some(globs) + // Anchor each watcher at the file's immediate parent directory + // and use just the file name as the glob pattern. This avoids + // VS Code's "non-recursive unless the pattern starts with **/" + // behaviour for RelativePattern bases that aren't (recognised + // as) workspace folders, which previously caused us to never + // receive change events for files in subdirectories. + fn path_to_spec(path: CanonPath, + hit_paths: &mut HashMap) + -> Option { + let parent = path.as_path().parent()?; + let file_name = path.as_path().file_name()? + .to_string_lossy().into_owned(); + let base = match parse_uri(&parent.to_string_lossy()) { + Ok(uri) => uri, + Err(e) => { + error!("Could not build URI for watch parent \ + dir {:?}: {}", parent, e); + return None; + } + }; + hit_paths.insert(path.clone(), true); + Some(FileWatchSpec { + full: path, + base, + relative: file_name, + }) } - let watch_paths: Vec<_> = { - let lock_workspaces = ctx.workspace_roots.lock().unwrap(); - file_paths.keys().cloned() - .collect::>().into_iter() - .flat_map(|post_path|path_to_relative( - post_path, - &lock_workspaces, - &mut file_paths)) - .flatten() - .collect() - }; + let watch_paths: Vec<_> = file_paths.keys().cloned() + .collect::>().into_iter() + .filter_map(|p|path_to_spec(p, &mut file_paths)) + .collect(); for (path, watched) in &file_paths { if !watched { - error!("Could not watch {:?}, not under any \ - workspace root", path); + error!("Could not register watcher for {:?}", path); } } - Some(FileWatch { - file_paths: watch_paths, - }) + if !watch_paths.is_empty() { + Some(FileWatch { + file_paths: watch_paths, + }) + } else { + None + } }, Err(e) => { error!("Unable to access configuration: {:?}", e); @@ -1601,10 +1597,11 @@ impl FileWatch { #[inline] fn relevant_change_kind(&self, change_uri: &Uri, _kind: FileChangeType) -> bool { - let path = change_uri.path().to_string(); - self.file_paths.iter() - .filter_map(|ws|ws.full.to_str()) - .any(|our_path|our_path == path.as_str()) + let Ok(changed_path) = parse_file_path!(change_uri, "watched_change") + else { return false; }; + let Some(changed_canon) = CanonPath::from_path_buf(changed_path) + else { return false; }; + self.file_paths.iter().any(|ws|ws.full == changed_canon) } #[inline] @@ -1621,11 +1618,11 @@ impl FileWatch { /// Returns json config for desired file watches pub fn watchers_config(&self) -> serde_json::Value { - fn watcher(base: WorkspaceFolder, pat: String) -> FileSystemWatcher { + fn watcher(base: Uri, pat: String) -> FileSystemWatcher { FileSystemWatcher { glob_pattern: GlobPattern::Relative( RelativePattern { - base_uri: OneOf::Left(base), + base_uri: OneOf::Right(base), pattern: pat, }), kind: Some(WatchKind::all()), From 031565d5feec817f5244c8cb33929da24289fa69 Mon Sep 17 00:00:00 2001 From: Jonatan Waern Date: Mon, 15 Jun 2026 11:46:08 +0200 Subject: [PATCH 6/6] Fix error in parse_uri Signed-off-by: Jonatan Waern --- src/lsp_data.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lsp_data.rs b/src/lsp_data.rs index eaa92900..c37d1bb9 100644 --- a/src/lsp_data.rs +++ b/src/lsp_data.rs @@ -93,14 +93,16 @@ pub fn parse_uri(pathb: &str) -> Result { // Replace windows slashes with unix-style let fixed_path = path.replace('\\', "/"); - // Add an extra slash on windows, on unix it is implicit + // A file URI is `file://` (scheme + empty authority) followed by an + // absolute path. On unix the canonical path already starts with `/`, on + // windows it doesn't (e.g. `C:/foo`) so we add one. let extra_slash = if !fixed_path.starts_with('/') { "/" } else { "" }; - let to_parse = format!("file:{}{}", extra_slash, fixed_path); + let to_parse = format!("file://{}{}", extra_slash, fixed_path); Uri::from_str(to_parse.as_str()) .map_err(|e|UriGenerationError( format!("Invalid URI '{}'; {}", to_parse, e)))