css_variable_lsp/
workspace.rs1use globset::{Glob, GlobSetBuilder};
2use ls_types::Uri;
3use std::collections::HashSet;
4use tokio::fs;
5use walkdir::WalkDir;
6
7use crate::manager::CssVariableManager;
8use crate::parsers::{parse_css_document, parse_html_document};
9
10pub async fn scan_workspace(
17 folders: Vec<Uri>,
18 manager: &CssVariableManager,
19 mut on_progress: impl FnMut(usize, usize),
20) -> Result<(), String> {
21 let config = manager.get_config().await;
22
23 let mut lookup_builder = GlobSetBuilder::new();
25 for pattern in &config.lookup_files {
26 if let Ok(glob) = Glob::new(pattern) {
27 lookup_builder.add(glob);
28 }
29 }
30 let lookup_set = lookup_builder
31 .build()
32 .map_err(|e| format!("Failed to build lookup glob set: {}", e))?;
33
34 let mut ignore_builder = GlobSetBuilder::new();
36 for pattern in &config.ignore_globs {
37 if let Ok(glob) = Glob::new(pattern) {
38 ignore_builder.add(glob);
39 }
40 }
41 let ignore_set = ignore_builder
42 .build()
43 .map_err(|e| format!("Failed to build ignore glob set: {}", e))?;
44
45 let mut all_files = HashSet::new();
47
48 for folder_uri in folders {
49 let folder_path = match crate::path_display::to_normalized_fs_path(&folder_uri) {
50 Some(path) => path,
51 None => continue,
52 };
53
54 for entry in WalkDir::new(&folder_path)
55 .follow_links(false)
56 .into_iter()
57 .filter_map(|e| e.ok())
58 {
59 let path = entry.path();
60
61 if !path.is_file() {
63 continue;
64 }
65
66 let relative = match path.strip_prefix(&folder_path) {
68 Ok(rel) => rel,
69 Err(_) => continue,
70 };
71
72 let path_str = relative.to_string_lossy();
74
75 if ignore_set.is_match(&*path_str) {
77 continue;
78 }
79
80 if lookup_set.is_match(&*path_str) {
82 all_files.insert(path.to_path_buf());
83 }
84 }
85 }
86
87 let mut all_files: Vec<_> = all_files.into_iter().collect();
88 all_files.sort();
89 let total = all_files.len();
90
91 let file_uris: HashSet<Uri> = all_files.iter().filter_map(Uri::from_file_path).collect();
94 manager.remove_documents(&file_uris).await;
95
96 let lookup_map = crate::document_kind::build_lookup_extension_map(&config.lookup_files);
99
100 for (i, file_path) in all_files.iter().enumerate() {
102 on_progress(i + 1, total);
104
105 let content = match fs::read_to_string(file_path).await {
107 Ok(c) => c,
108 Err(_) => continue,
109 };
110
111 let file_uri = match Uri::from_file_path(file_path) {
113 Some(u) => u,
114 None => continue,
115 };
116
117 let path_str = file_path.to_string_lossy();
119 let kind = match crate::document_kind::resolve_document_kind(&path_str, None, &lookup_map) {
120 Some(kind) => kind,
121 None => continue,
122 };
123 let result = match kind {
124 crate::document_kind::DocumentKind::Html => {
125 parse_html_document(&content, &file_uri, manager).await
126 }
127 crate::document_kind::DocumentKind::Css => {
128 parse_css_document(&content, &file_uri, manager).await
129 }
130 crate::document_kind::DocumentKind::Js => continue,
134 };
135
136 if let Err(e) = result {
140 tracing::debug!(file = %file_path.display(), error = %e, "workspace scan: parse error");
141 }
142 }
143
144 manager.rebuild_color_index().await;
145
146 Ok(())
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::types::Config;
153
154 #[tokio::test]
155 async fn repeated_scans_replace_existing_document_state() {
156 let root = std::env::temp_dir().join(format!(
157 "css-variable-lsp-repeat-scan-{}",
158 std::process::id()
159 ));
160 std::fs::create_dir_all(&root).unwrap();
161 std::fs::write(root.join("variables.css"), ":root { --primary: red; }").unwrap();
162
163 let manager = CssVariableManager::new(Config::default());
164 let root_uri = Uri::from_file_path(&root).unwrap();
165 scan_workspace(vec![root_uri.clone()], &manager, |_, _| {})
166 .await
167 .unwrap();
168 scan_workspace(vec![root_uri], &manager, |_, _| {})
169 .await
170 .unwrap();
171
172 assert_eq!(manager.get_variables("--primary").await.len(), 1);
173 std::fs::remove_dir_all(root).unwrap();
174 }
175}