1use std::path::Path;
7use tower_lsp_server::Client;
8use tower_lsp_server::ls_types::{
9 DidChangeWatchedFilesRegistrationOptions, FileSystemWatcher, GlobPattern, Registration,
10 WatchKind,
11};
12
13pub async fn register_lock_file_watchers(
28 client: &Client,
29 patterns: &[String],
30) -> Result<(), String> {
31 if patterns.is_empty() {
32 tracing::debug!("No lock file patterns to watch");
33 return Ok(());
34 }
35
36 let watchers: Vec<FileSystemWatcher> = patterns
37 .iter()
38 .map(|pattern| FileSystemWatcher {
39 glob_pattern: GlobPattern::String(pattern.clone()),
40 kind: Some(WatchKind::Create | WatchKind::Change | WatchKind::Delete),
41 })
42 .collect();
43
44 let options = DidChangeWatchedFilesRegistrationOptions { watchers };
45
46 let registration = Registration {
47 id: "deps-lsp-lockfile-watcher".to_string(),
48 method: "workspace/didChangeWatchedFiles".to_string(),
49 register_options: Some(serde_json::to_value(options).map_err(|e| e.to_string())?),
50 };
51
52 client
53 .register_capability(vec![registration])
54 .await
55 .map_err(|e| format!("Failed to register file watchers: {e}"))?;
56
57 tracing::info!("Registered {} lock file watchers", patterns.len());
58 Ok(())
59}
60
61pub fn extract_lockfile_name(lockfile_path: &Path) -> Option<&str> {
85 lockfile_path.file_name()?.to_str()
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use std::path::PathBuf;
92
93 #[test]
94 fn test_extract_lockfile_name_cargo() {
95 let path = PathBuf::from("/project/Cargo.lock");
96 assert_eq!(extract_lockfile_name(&path), Some("Cargo.lock"));
97 }
98
99 #[test]
100 fn test_extract_lockfile_name_npm() {
101 let path = PathBuf::from("/project/package-lock.json");
102 assert_eq!(extract_lockfile_name(&path), Some("package-lock.json"));
103 }
104
105 #[test]
106 fn test_extract_lockfile_name_poetry() {
107 let path = PathBuf::from("/project/poetry.lock");
108 assert_eq!(extract_lockfile_name(&path), Some("poetry.lock"));
109 }
110
111 #[test]
112 fn test_extract_lockfile_name_uv() {
113 let path = PathBuf::from("/project/uv.lock");
114 assert_eq!(extract_lockfile_name(&path), Some("uv.lock"));
115 }
116
117 #[test]
118 fn test_extract_lockfile_name_nested() {
119 let path = PathBuf::from("/workspace/member/Cargo.lock");
120 assert_eq!(extract_lockfile_name(&path), Some("Cargo.lock"));
121 }
122
123 #[test]
124 fn test_extract_lockfile_name_no_filename() {
125 let path = PathBuf::from("/");
126 assert_eq!(extract_lockfile_name(&path), None);
127 }
128
129 #[test]
130 #[cfg(windows)]
131 fn test_extract_lockfile_name_windows_path() {
132 let path = PathBuf::from(r"C:\Users\project\Cargo.lock");
133 assert_eq!(extract_lockfile_name(&path), Some("Cargo.lock"));
134 }
135
136 #[test]
137 #[cfg(not(windows))]
138 fn test_extract_lockfile_name_windows_style_string() {
139 let path = PathBuf::from("/Users/project/Cargo.lock");
140 assert_eq!(extract_lockfile_name(&path), Some("Cargo.lock"));
141 }
142
143 #[test]
144 fn test_extract_lockfile_name_relative_path() {
145 let path = PathBuf::from("./Cargo.lock");
146 assert_eq!(extract_lockfile_name(&path), Some("Cargo.lock"));
147 }
148
149 #[test]
150 fn test_extract_lockfile_name_parent_directory() {
151 let path = PathBuf::from("../project/package-lock.json");
152 assert_eq!(extract_lockfile_name(&path), Some("package-lock.json"));
153 }
154
155 #[test]
156 fn test_extract_lockfile_name_current_directory_only() {
157 let path = PathBuf::from("Cargo.lock");
158 assert_eq!(extract_lockfile_name(&path), Some("Cargo.lock"));
159 }
160
161 #[test]
162 fn test_extract_lockfile_name_empty_path() {
163 let path = PathBuf::from("");
164 assert_eq!(extract_lockfile_name(&path), None);
165 }
166
167 #[test]
168 fn test_extract_lockfile_name_yarn_lock() {
169 let path = PathBuf::from("/project/yarn.lock");
170 assert_eq!(extract_lockfile_name(&path), Some("yarn.lock"));
171 }
172
173 #[test]
174 fn test_extract_lockfile_name_pnpm_lock() {
175 let path = PathBuf::from("/project/pnpm-lock.yaml");
176 assert_eq!(extract_lockfile_name(&path), Some("pnpm-lock.yaml"));
177 }
178
179 #[test]
180 fn test_extract_lockfile_name_pipfile_lock() {
181 let path = PathBuf::from("/project/Pipfile.lock");
182 assert_eq!(extract_lockfile_name(&path), Some("Pipfile.lock"));
183 }
184
185 #[test]
186 fn test_extract_lockfile_name_deeply_nested() {
187 let path = PathBuf::from("/workspace/apps/backend/services/api/Cargo.lock");
188 assert_eq!(extract_lockfile_name(&path), Some("Cargo.lock"));
189 }
190
191 #[test]
192 fn test_extract_lockfile_name_with_dots_in_path() {
193 let path = PathBuf::from("/project/my.app.v1.0/Cargo.lock");
194 assert_eq!(extract_lockfile_name(&path), Some("Cargo.lock"));
195 }
196
197 #[test]
198 fn test_extract_lockfile_name_non_utf8_safe() {
199 let path = PathBuf::from("/project/Cargo.lock");
200 let result = extract_lockfile_name(&path);
201 assert!(result.is_some());
202 assert!(result.unwrap().is_ascii());
203 }
204}