deps_lsp/
file_watcher.rs

1//! Lock file watching infrastructure.
2//!
3//! Provides file system watcher registration for lock files.
4//! Lock file patterns are provided by individual ecosystem implementations.
5
6use std::path::Path;
7use tower_lsp_server::Client;
8use tower_lsp_server::ls_types::{
9    DidChangeWatchedFilesRegistrationOptions, FileSystemWatcher, GlobPattern, Registration,
10    WatchKind,
11};
12
13/// Registers file system watchers for lock files from all registered ecosystems.
14///
15/// Uses dynamic registration to request the client to watch lock file patterns.
16/// Patterns are collected from all registered ecosystems via `EcosystemRegistry::all_lockfile_patterns()`.
17///
18/// # Arguments
19///
20/// * `client` - LSP client for registration requests
21/// * `patterns` - Lock file glob patterns (e.g., "**/Cargo.lock")
22///
23/// # Errors
24///
25/// Returns an error if the client doesn't support dynamic registration
26/// or if the registration request fails.
27pub 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
61/// Determines the ecosystem type from a lock file path.
62///
63/// This is a convenience function that extracts the filename and can be used
64/// in conjunction with `EcosystemRegistry::get_for_lockfile()`.
65///
66/// # Arguments
67///
68/// * `lockfile_path` - Path to the lock file
69///
70/// # Returns
71///
72/// * `Some(&str)` - Lock file name
73/// * `None` - Path has no filename component
74///
75/// # Examples
76///
77/// ```
78/// use std::path::Path;
79/// use deps_lsp::file_watcher::extract_lockfile_name;
80///
81/// let path = Path::new("/project/Cargo.lock");
82/// assert_eq!(extract_lockfile_name(path), Some("Cargo.lock"));
83/// ```
84pub 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}