1use ls_types::Uri;
2use std::collections::{HashMap, HashSet};
3use std::sync::Arc;
4use tokio::sync::RwLock;
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<Uri, 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<ls_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: &Uri) {
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: &Uri) -> 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: &Uri) -> 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: &Uri) -> 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: Uri, 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: &Uri) -> 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 pub async fn set_config(&self, config: Config) {
174 let mut stored = self.config.write().await;
175 *stored = config;
176 }
177}
178
179fn extract_var_reference(value: &str) -> Option<String> {
180 let trimmed = value.trim();
181 let start = trimmed.find("var(")?;
182 let mut idx = start + 4;
183 let bytes = trimmed.as_bytes();
184 let mut depth = 1i32;
185 while idx < bytes.len() {
186 match bytes[idx] {
187 b'(' => depth += 1,
188 b')' => {
189 depth -= 1;
190 if depth == 0 {
191 break;
192 }
193 }
194 _ => {}
195 }
196 idx += 1;
197 }
198 if depth != 0 {
199 return None;
200 }
201
202 let inner = trimmed[start + 4..idx].trim_start();
203 let inner = inner.strip_prefix("--")?;
204 let mut name_len = 0usize;
205 for ch in inner.chars() {
206 if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
207 name_len += ch.len_utf8();
208 } else {
209 break;
210 }
211 }
212 if name_len == 0 {
213 return None;
214 }
215 Some(format!("--{}", &inner[..name_len]))
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use ls_types::{Position, Range, Uri};
222 use std::str::FromStr;
223
224 fn create_test_variable(name: &str, value: &str, selector: &str, uri: &str) -> CssVariable {
225 CssVariable {
226 name: name.to_string(),
227 value: value.to_string(),
228 selector: selector.to_string(),
229 range: Range::new(Position::new(0, 0), Position::new(0, 10)),
230 name_range: None,
231 value_range: None,
232 uri: Uri::from_str(uri).unwrap(),
233 important: false,
234 inline: false,
235 source_position: 0,
236 }
237 }
238
239 fn create_test_usage(name: &str, context: &str, uri: &str) -> CssVariableUsage {
240 CssVariableUsage {
241 name: name.to_string(),
242 range: Range::new(Position::new(0, 0), Position::new(0, 10)),
243 name_range: None,
244 uri: Uri::from_str(uri).unwrap(),
245 usage_context: context.to_string(),
246 dom_node: None,
247 }
248 }
249
250 #[test]
251 fn extract_var_reference_allows_fallbacks_and_trailing_tokens() {
252 assert_eq!(
253 extract_var_reference("var(--primary, #fff)"),
254 Some("--primary".to_string())
255 );
256 assert_eq!(
257 extract_var_reference("var(--primary) !important"),
258 Some("--primary".to_string())
259 );
260 assert_eq!(
261 extract_var_reference("calc(1px + var(--spacing))"),
262 Some("--spacing".to_string())
263 );
264 }
265
266 #[tokio::test]
267 async fn test_manager_add_and_get_variables() {
268 let manager = CssVariableManager::new(Config::default());
269 let var = create_test_variable("--primary", "#3b82f6", ":root", "file:///test.css");
270
271 manager.add_variable(var.clone()).await;
272
273 let variables = manager.get_variables("--primary").await;
274 assert_eq!(variables.len(), 1);
275 assert_eq!(variables[0].name, "--primary");
276 assert_eq!(variables[0].value, "#3b82f6");
277 }
278
279 #[tokio::test]
280 async fn test_manager_multiple_definitions() {
281 let manager = CssVariableManager::new(Config::default());
282
283 let var1 = create_test_variable("--color", "red", ":root", "file:///test.css");
284 let var2 = create_test_variable("--color", "blue", ".class", "file:///test.css");
285
286 manager.add_variable(var1).await;
287 manager.add_variable(var2).await;
288
289 let variables = manager.get_variables("--color").await;
290 assert_eq!(variables.len(), 2);
291 }
292
293 #[tokio::test]
294 async fn test_manager_add_and_get_usages() {
295 let manager = CssVariableManager::new(Config::default());
296 let usage = create_test_usage("--primary", ".button", "file:///test.css");
297
298 manager.add_usage(usage.clone()).await;
299
300 let usages = manager.get_usages("--primary").await;
301 assert_eq!(usages.len(), 1);
302 assert_eq!(usages[0].name, "--primary");
303 assert_eq!(usages[0].usage_context, ".button");
304 }
305
306 #[tokio::test]
307 async fn test_manager_get_references() {
308 let manager = CssVariableManager::new(Config::default());
309
310 let var = create_test_variable("--spacing", "1rem", ":root", "file:///test.css");
311 let usage = create_test_usage("--spacing", ".card", "file:///test.css");
312
313 manager.add_variable(var).await;
314 manager.add_usage(usage).await;
315
316 let (defs, usages) = manager.get_references("--spacing").await;
317 assert_eq!(defs.len(), 1);
318 assert_eq!(usages.len(), 1);
319 }
320
321 #[tokio::test]
322 async fn test_manager_remove_document() {
323 let manager = CssVariableManager::new(Config::default());
324 let uri = Uri::from_str("file:///test.css").unwrap();
325
326 let var = create_test_variable("--primary", "blue", ":root", "file:///test.css");
327 let usage = create_test_usage("--primary", ".button", "file:///test.css");
328
329 manager.add_variable(var).await;
330 manager.add_usage(usage).await;
331
332 assert_eq!(manager.get_variables("--primary").await.len(), 1);
334 assert_eq!(manager.get_usages("--primary").await.len(), 1);
335
336 manager.remove_document(&uri).await;
338
339 assert_eq!(manager.get_variables("--primary").await.len(), 0);
341 assert_eq!(manager.get_usages("--primary").await.len(), 0);
342 }
343
344 #[tokio::test]
345 async fn test_manager_get_all_variables() {
346 let manager = CssVariableManager::new(Config::default());
347
348 manager
349 .add_variable(create_test_variable(
350 "--primary",
351 "blue",
352 ":root",
353 "file:///test.css",
354 ))
355 .await;
356 manager
357 .add_variable(create_test_variable(
358 "--secondary",
359 "red",
360 ":root",
361 "file:///test.css",
362 ))
363 .await;
364 manager
365 .add_variable(create_test_variable(
366 "--spacing",
367 "1rem",
368 ":root",
369 "file:///test.css",
370 ))
371 .await;
372
373 let all_vars = manager.get_all_variables().await;
374 assert_eq!(all_vars.len(), 3);
375 }
376
377 #[tokio::test]
378 async fn test_manager_resolve_variable_color() {
379 let manager = CssVariableManager::new(Config::default());
380
381 let var = create_test_variable("--primary-color", "#3b82f6", ":root", "file:///test.css");
382 manager.add_variable(var).await;
383
384 let color = manager.resolve_variable_color("--primary-color").await;
385 assert!(color.is_some());
386 }
387
388 #[tokio::test]
389 async fn test_manager_cross_file_references() {
390 let manager = CssVariableManager::new(Config::default());
391
392 let var = create_test_variable("--theme", "dark", ":root", "file:///variables.css");
394 manager.add_variable(var).await;
395
396 let usage = create_test_usage("--theme", ".app", "file:///app.css");
398 manager.add_usage(usage).await;
399
400 let (defs, usages) = manager.get_references("--theme").await;
401 assert_eq!(defs.len(), 1);
402 assert_eq!(usages.len(), 1);
403 assert_ne!(defs[0].uri, usages[0].uri);
404 }
405
406 #[tokio::test]
407 async fn test_manager_document_isolation() {
408 let manager = CssVariableManager::new(Config::default());
409 let uri1 = Uri::from_str("file:///file1.css").unwrap();
410 let _uri2 = Uri::from_str("file:///file2.css").unwrap();
411
412 manager
413 .add_variable(create_test_variable(
414 "--color",
415 "red",
416 ":root",
417 "file:///file1.css",
418 ))
419 .await;
420 manager
421 .add_variable(create_test_variable(
422 "--color",
423 "blue",
424 ":root",
425 "file:///file2.css",
426 ))
427 .await;
428
429 assert_eq!(manager.get_variables("--color").await.len(), 2);
431
432 manager.remove_document(&uri1).await;
434
435 let vars = manager.get_variables("--color").await;
437 assert_eq!(vars.len(), 1);
438 assert_eq!(vars[0].value, "blue");
439 }
440
441 #[tokio::test]
444 async fn test_manager_important_flag() {
445 let manager = CssVariableManager::new(Config::default());
446
447 let mut var = create_test_variable("--color", "red", ":root", "file:///test.css");
448 var.important = true;
449
450 manager.add_variable(var).await;
451
452 let vars = manager.get_variables("--color").await;
453 assert_eq!(vars.len(), 1);
454 assert!(vars[0].important);
455 }
456
457 #[tokio::test]
458 async fn test_manager_inline_flag() {
459 let manager = CssVariableManager::new(Config::default());
460
461 let mut var = create_test_variable(
462 "--inline-color",
463 "green",
464 "inline-style",
465 "file:///test.html",
466 );
467 var.inline = true;
468
469 manager.add_variable(var).await;
470
471 let vars = manager.get_variables("--inline-color").await;
472 assert_eq!(vars.len(), 1);
473 assert!(vars[0].inline);
474 }
475
476 #[tokio::test]
477 async fn test_manager_empty_queries() {
478 let manager = CssVariableManager::new(Config::default());
479
480 let vars = manager.get_variables("--does-not-exist").await;
482 assert_eq!(vars.len(), 0);
483
484 let usages = manager.get_usages("--does-not-exist").await;
485 assert_eq!(usages.len(), 0);
486
487 let (defs, usages) = manager.get_references("--does-not-exist").await;
488 assert_eq!(defs.len(), 0);
489 assert_eq!(usages.len(), 0);
490 }
491}