1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3use tokio::sync::RwLock;
4use tower_lsp::lsp_types::Url;
5
6use crate::color::parse_color;
7use crate::dom_tree::DomTree;
8use crate::specificity::sort_by_cascade;
9use crate::types::{Config, CssVariable, CssVariableUsage};
10
11#[derive(Clone)]
13pub struct CssVariableManager {
14 variables: Arc<RwLock<HashMap<String, Vec<CssVariable>>>>,
16
17 usages: Arc<RwLock<HashMap<String, Vec<CssVariableUsage>>>>,
19
20 config: Arc<RwLock<Config>>,
22
23 dom_trees: Arc<RwLock<HashMap<Url, DomTree>>>,
25}
26
27impl CssVariableManager {
28 pub fn new(config: Config) -> Self {
29 Self {
30 variables: Arc::new(RwLock::new(HashMap::new())),
31 usages: Arc::new(RwLock::new(HashMap::new())),
32 config: Arc::new(RwLock::new(config)),
33 dom_trees: Arc::new(RwLock::new(HashMap::new())),
34 }
35 }
36
37 pub async fn add_variable(&self, variable: CssVariable) {
39 let mut vars = self.variables.write().await;
40 vars.entry(variable.name.clone())
41 .or_insert_with(Vec::new)
42 .push(variable);
43 }
44
45 pub async fn add_usage(&self, usage: CssVariableUsage) {
47 let mut usages = self.usages.write().await;
48 usages
49 .entry(usage.name.clone())
50 .or_insert_with(Vec::new)
51 .push(usage);
52 }
53
54 pub async fn get_variables(&self, name: &str) -> Vec<CssVariable> {
56 let vars = self.variables.read().await;
57 vars.get(name).cloned().unwrap_or_default()
58 }
59
60 pub async fn get_usages(&self, name: &str) -> Vec<CssVariableUsage> {
62 let usages = self.usages.read().await;
63 usages.get(name).cloned().unwrap_or_default()
64 }
65
66 pub async fn resolve_variable_color(&self, name: &str) -> Option<tower_lsp::lsp_types::Color> {
68 let mut seen = std::collections::HashSet::new();
69 let mut current = name.to_string();
70
71 loop {
72 if seen.contains(¤t) {
73 return None;
74 }
75 seen.insert(current.clone());
76
77 let mut variables = self.get_variables(¤t).await;
78 if variables.is_empty() {
79 return None;
80 }
81
82 sort_by_cascade(&mut variables);
83 let variable = &variables[0];
84
85 if let Some(next_name) = extract_var_reference(&variable.value) {
86 current = next_name;
87 continue;
88 }
89
90 return parse_color(&variable.value);
91 }
92 }
93
94 pub async fn get_all_variables(&self) -> Vec<CssVariable> {
96 let vars = self.variables.read().await;
97 vars.values().flatten().cloned().collect()
98 }
99
100 pub async fn get_references(&self, name: &str) -> (Vec<CssVariable>, Vec<CssVariableUsage>) {
102 let definitions = self.get_variables(name).await;
103 let usages = self.get_usages(name).await;
104 (definitions, usages)
105 }
106
107 pub async fn remove_document(&self, uri: &Url) {
109 let mut vars = self.variables.write().await;
110 let mut usages = self.usages.write().await;
111 let mut dom_trees = self.dom_trees.write().await;
112
113 for (_, var_list) in vars.iter_mut() {
115 var_list.retain(|v| &v.uri != uri);
116 }
117 vars.retain(|_, var_list| !var_list.is_empty());
118
119 for (_, usage_list) in usages.iter_mut() {
121 usage_list.retain(|u| &u.uri != uri);
122 }
123 usages.retain(|_, usage_list| !usage_list.is_empty());
124
125 dom_trees.remove(uri);
126 }
127
128 pub async fn get_document_variables(&self, uri: &Url) -> Vec<CssVariable> {
130 let vars = self.variables.read().await;
131 vars.values()
132 .flatten()
133 .filter(|v| &v.uri == uri)
134 .cloned()
135 .collect()
136 }
137
138 pub async fn get_document_variable_names(&self, uri: &Url) -> HashSet<String> {
140 let vars = self.get_document_variables(uri).await;
141 vars.into_iter().map(|v| v.name).collect()
142 }
143
144 pub async fn get_document_usages(&self, uri: &Url) -> Vec<CssVariableUsage> {
146 let usages = self.usages.read().await;
147 usages
148 .values()
149 .flatten()
150 .filter(|u| &u.uri == uri)
151 .cloned()
152 .collect()
153 }
154
155 pub async fn set_dom_tree(&self, uri: Url, dom_tree: DomTree) {
157 let mut dom_trees = self.dom_trees.write().await;
158 dom_trees.insert(uri, dom_tree);
159 }
160
161 pub async fn get_dom_tree(&self, uri: &Url) -> Option<DomTree> {
163 let dom_trees = self.dom_trees.read().await;
164 dom_trees.get(uri).cloned()
165 }
166
167 pub async fn get_config(&self) -> Config {
169 self.config.read().await.clone()
170 }
171}
172
173fn extract_var_reference(value: &str) -> Option<String> {
174 let trimmed = value.trim();
175 let start = trimmed.find("var(")?;
176 let mut idx = start + 4;
177 let bytes = trimmed.as_bytes();
178 let mut depth = 1i32;
179 while idx < bytes.len() {
180 match bytes[idx] {
181 b'(' => depth += 1,
182 b')' => {
183 depth -= 1;
184 if depth == 0 {
185 break;
186 }
187 }
188 _ => {}
189 }
190 idx += 1;
191 }
192 if depth != 0 {
193 return None;
194 }
195
196 let inner = trimmed[start + 4..idx].trim_start();
197 let inner = inner.strip_prefix("--")?;
198 let mut name_len = 0usize;
199 for ch in inner.chars() {
200 if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
201 name_len += ch.len_utf8();
202 } else {
203 break;
204 }
205 }
206 if name_len == 0 {
207 return None;
208 }
209 Some(format!("--{}", &inner[..name_len]))
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use tower_lsp::lsp_types::{Position, Range, Url};
216
217 fn create_test_variable(name: &str, value: &str, selector: &str, uri: &str) -> CssVariable {
218 CssVariable {
219 name: name.to_string(),
220 value: value.to_string(),
221 selector: selector.to_string(),
222 range: Range::new(Position::new(0, 0), Position::new(0, 10)),
223 name_range: None,
224 value_range: None,
225 uri: Url::parse(uri).unwrap(),
226 important: false,
227 inline: false,
228 source_position: 0,
229 }
230 }
231
232 fn create_test_usage(name: &str, context: &str, uri: &str) -> CssVariableUsage {
233 CssVariableUsage {
234 name: name.to_string(),
235 range: Range::new(Position::new(0, 0), Position::new(0, 10)),
236 name_range: None,
237 uri: Url::parse(uri).unwrap(),
238 usage_context: context.to_string(),
239 dom_node: None,
240 }
241 }
242
243 #[test]
244 fn extract_var_reference_allows_fallbacks_and_trailing_tokens() {
245 assert_eq!(
246 extract_var_reference("var(--primary, #fff)"),
247 Some("--primary".to_string())
248 );
249 assert_eq!(
250 extract_var_reference("var(--primary) !important"),
251 Some("--primary".to_string())
252 );
253 assert_eq!(
254 extract_var_reference("calc(1px + var(--spacing))"),
255 Some("--spacing".to_string())
256 );
257 }
258
259 #[tokio::test]
260 async fn test_manager_add_and_get_variables() {
261 let manager = CssVariableManager::new(Config::default());
262 let var = create_test_variable("--primary", "#3b82f6", ":root", "file:///test.css");
263
264 manager.add_variable(var.clone()).await;
265
266 let variables = manager.get_variables("--primary").await;
267 assert_eq!(variables.len(), 1);
268 assert_eq!(variables[0].name, "--primary");
269 assert_eq!(variables[0].value, "#3b82f6");
270 }
271
272 #[tokio::test]
273 async fn test_manager_multiple_definitions() {
274 let manager = CssVariableManager::new(Config::default());
275
276 let var1 = create_test_variable("--color", "red", ":root", "file:///test.css");
277 let var2 = create_test_variable("--color", "blue", ".class", "file:///test.css");
278
279 manager.add_variable(var1).await;
280 manager.add_variable(var2).await;
281
282 let variables = manager.get_variables("--color").await;
283 assert_eq!(variables.len(), 2);
284 }
285
286 #[tokio::test]
287 async fn test_manager_add_and_get_usages() {
288 let manager = CssVariableManager::new(Config::default());
289 let usage = create_test_usage("--primary", ".button", "file:///test.css");
290
291 manager.add_usage(usage.clone()).await;
292
293 let usages = manager.get_usages("--primary").await;
294 assert_eq!(usages.len(), 1);
295 assert_eq!(usages[0].name, "--primary");
296 assert_eq!(usages[0].usage_context, ".button");
297 }
298
299 #[tokio::test]
300 async fn test_manager_get_references() {
301 let manager = CssVariableManager::new(Config::default());
302
303 let var = create_test_variable("--spacing", "1rem", ":root", "file:///test.css");
304 let usage = create_test_usage("--spacing", ".card", "file:///test.css");
305
306 manager.add_variable(var).await;
307 manager.add_usage(usage).await;
308
309 let (defs, usages) = manager.get_references("--spacing").await;
310 assert_eq!(defs.len(), 1);
311 assert_eq!(usages.len(), 1);
312 }
313
314 #[tokio::test]
315 async fn test_manager_remove_document() {
316 let manager = CssVariableManager::new(Config::default());
317 let uri = Url::parse("file:///test.css").unwrap();
318
319 let var = create_test_variable("--primary", "blue", ":root", "file:///test.css");
320 let usage = create_test_usage("--primary", ".button", "file:///test.css");
321
322 manager.add_variable(var).await;
323 manager.add_usage(usage).await;
324
325 assert_eq!(manager.get_variables("--primary").await.len(), 1);
327 assert_eq!(manager.get_usages("--primary").await.len(), 1);
328
329 manager.remove_document(&uri).await;
331
332 assert_eq!(manager.get_variables("--primary").await.len(), 0);
334 assert_eq!(manager.get_usages("--primary").await.len(), 0);
335 }
336
337 #[tokio::test]
338 async fn test_manager_get_all_variables() {
339 let manager = CssVariableManager::new(Config::default());
340
341 manager
342 .add_variable(create_test_variable(
343 "--primary",
344 "blue",
345 ":root",
346 "file:///test.css",
347 ))
348 .await;
349 manager
350 .add_variable(create_test_variable(
351 "--secondary",
352 "red",
353 ":root",
354 "file:///test.css",
355 ))
356 .await;
357 manager
358 .add_variable(create_test_variable(
359 "--spacing",
360 "1rem",
361 ":root",
362 "file:///test.css",
363 ))
364 .await;
365
366 let all_vars = manager.get_all_variables().await;
367 assert_eq!(all_vars.len(), 3);
368 }
369
370 #[tokio::test]
371 async fn test_manager_resolve_variable_color() {
372 let manager = CssVariableManager::new(Config::default());
373
374 let var = create_test_variable("--primary-color", "#3b82f6", ":root", "file:///test.css");
375 manager.add_variable(var).await;
376
377 let color = manager.resolve_variable_color("--primary-color").await;
378 assert!(color.is_some());
379 }
380
381 #[tokio::test]
382 async fn test_manager_cross_file_references() {
383 let manager = CssVariableManager::new(Config::default());
384
385 let var = create_test_variable("--theme", "dark", ":root", "file:///variables.css");
387 manager.add_variable(var).await;
388
389 let usage = create_test_usage("--theme", ".app", "file:///app.css");
391 manager.add_usage(usage).await;
392
393 let (defs, usages) = manager.get_references("--theme").await;
394 assert_eq!(defs.len(), 1);
395 assert_eq!(usages.len(), 1);
396 assert_ne!(defs[0].uri, usages[0].uri);
397 }
398
399 #[tokio::test]
400 async fn test_manager_document_isolation() {
401 let manager = CssVariableManager::new(Config::default());
402 let uri1 = Url::parse("file:///file1.css").unwrap();
403 let _uri2 = Url::parse("file:///file2.css").unwrap();
404
405 manager
406 .add_variable(create_test_variable(
407 "--color",
408 "red",
409 ":root",
410 "file:///file1.css",
411 ))
412 .await;
413 manager
414 .add_variable(create_test_variable(
415 "--color",
416 "blue",
417 ":root",
418 "file:///file2.css",
419 ))
420 .await;
421
422 assert_eq!(manager.get_variables("--color").await.len(), 2);
424
425 manager.remove_document(&uri1).await;
427
428 let vars = manager.get_variables("--color").await;
430 assert_eq!(vars.len(), 1);
431 assert_eq!(vars[0].value, "blue");
432 }
433
434 #[tokio::test]
437 async fn test_manager_important_flag() {
438 let manager = CssVariableManager::new(Config::default());
439
440 let mut var = create_test_variable("--color", "red", ":root", "file:///test.css");
441 var.important = true;
442
443 manager.add_variable(var).await;
444
445 let vars = manager.get_variables("--color").await;
446 assert_eq!(vars.len(), 1);
447 assert!(vars[0].important);
448 }
449
450 #[tokio::test]
451 async fn test_manager_inline_flag() {
452 let manager = CssVariableManager::new(Config::default());
453
454 let mut var = create_test_variable(
455 "--inline-color",
456 "green",
457 "inline-style",
458 "file:///test.html",
459 );
460 var.inline = true;
461
462 manager.add_variable(var).await;
463
464 let vars = manager.get_variables("--inline-color").await;
465 assert_eq!(vars.len(), 1);
466 assert!(vars[0].inline);
467 }
468
469 #[tokio::test]
470 async fn test_manager_empty_queries() {
471 let manager = CssVariableManager::new(Config::default());
472
473 let vars = manager.get_variables("--does-not-exist").await;
475 assert_eq!(vars.len(), 0);
476
477 let usages = manager.get_usages("--does-not-exist").await;
478 assert_eq!(usages.len(), 0);
479
480 let (defs, usages) = manager.get_references("--does-not-exist").await;
481 assert_eq!(defs.len(), 0);
482 assert_eq!(usages.len(), 0);
483 }
484}