Skip to main content

css_variable_lsp/
workspace.rs

1use globset::{Glob, GlobSetBuilder};
2use ls_types::Uri;
3use tokio::fs;
4use walkdir::WalkDir;
5
6use crate::manager::CssVariableManager;
7use crate::parsers::{parse_css_document, parse_html_document};
8
9/// Scan workspace folders for CSS and HTML files.
10///
11/// Uses the configured `lookup_files` glob patterns to discover files and the
12/// `ignore_globs` patterns to exclude them. Document kind is resolved via
13/// `document_kind::resolve_document_kind` so that workspace scanning stays in
14/// sync with completion / hover / goto-definition behavior.
15pub async fn scan_workspace(
16    folders: Vec<Uri>,
17    manager: &CssVariableManager,
18    mut on_progress: impl FnMut(usize, usize),
19) -> Result<(), String> {
20    let config = manager.get_config().await;
21
22    // Build glob matchers for lookup patterns
23    let mut lookup_builder = GlobSetBuilder::new();
24    for pattern in &config.lookup_files {
25        if let Ok(glob) = Glob::new(pattern) {
26            lookup_builder.add(glob);
27        }
28    }
29    let lookup_set = lookup_builder
30        .build()
31        .map_err(|e| format!("Failed to build lookup glob set: {}", e))?;
32
33    // Build glob matchers for ignore patterns
34    let mut ignore_builder = GlobSetBuilder::new();
35    for pattern in &config.ignore_globs {
36        if let Ok(glob) = Glob::new(pattern) {
37            ignore_builder.add(glob);
38        }
39    }
40    let ignore_set = ignore_builder
41        .build()
42        .map_err(|e| format!("Failed to build ignore glob set: {}", e))?;
43
44    // Collect all files from all folders
45    let mut all_files = Vec::new();
46
47    for folder_uri in folders {
48        let folder_path = match crate::path_display::to_normalized_fs_path(&folder_uri) {
49            Some(path) => path,
50            None => continue,
51        };
52
53        for entry in WalkDir::new(&folder_path)
54            .follow_links(false)
55            .into_iter()
56            .filter_map(|e| e.ok())
57        {
58            let path = entry.path();
59
60            // Skip if not a file
61            if !path.is_file() {
62                continue;
63            }
64
65            // Get relative path for glob matching
66            let relative = match path.strip_prefix(&folder_path) {
67                Ok(rel) => rel,
68                Err(_) => continue,
69            };
70
71            // Convert to string for glob matching
72            let path_str = relative.to_string_lossy();
73
74            // Skip if matches ignore pattern
75            if ignore_set.is_match(&*path_str) {
76                continue;
77            }
78
79            // Include if matches lookup pattern
80            if lookup_set.is_match(&*path_str) {
81                all_files.push(path.to_path_buf());
82            }
83        }
84    }
85
86    let total = all_files.len();
87
88    // Build the extension->kind map once for the whole scan so we agree with the
89    // editor on what counts as CSS vs HTML vs JS without re-deriving it per file.
90    let lookup_map = crate::document_kind::build_lookup_extension_map(&config.lookup_files);
91
92    // Parse each file
93    for (i, file_path) in all_files.iter().enumerate() {
94        // Report progress
95        on_progress(i + 1, total);
96
97        // Read file content
98        let content = match fs::read_to_string(file_path).await {
99            Ok(c) => c,
100            Err(_) => continue,
101        };
102
103        // Convert to URI
104        let file_uri = match Uri::from_file_path(file_path) {
105            Some(u) => u,
106            None => continue,
107        };
108
109        // Determine file type and parse using the extension->kind map built once above.
110        let path_str = file_path.to_string_lossy();
111        let kind = match crate::document_kind::resolve_document_kind(&path_str, None, &lookup_map) {
112            Some(kind) => kind,
113            None => continue,
114        };
115        let result = match kind {
116            crate::document_kind::DocumentKind::Html => {
117                parse_html_document(&content, &file_uri, manager).await
118            }
119            crate::document_kind::DocumentKind::Css => {
120                parse_css_document(&content, &file_uri, manager).await
121            }
122            // JS/CSS-in-JS files are not scanned eagerly from disk: their CSS lives
123            // inside string/template literals that we only parse when the editor is
124            // actually showing us the file (did_open/did_change).
125            crate::document_kind::DocumentKind::Js => continue,
126        };
127
128        // Log errors but continue so a single malformed file does not abort the
129        // whole workspace scan. Only logged when the user has opted into tracing
130        // via CSS_LSP_ENABLE_LOGS=1 so we keep LSP stdio clean by default.
131        if let Err(e) = result {
132            tracing::debug!(file = %file_path.display(), error = %e, "workspace scan: parse error");
133        }
134    }
135
136    Ok(())
137}