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