1use ls_types::{Position, Uri};
2use std::collections::{HashMap, HashSet};
3use std::sync::Arc;
4use tokio::sync::RwLock;
5
6use crate::color::{color_from_key, normalize_color, parse_color, NormalizedColorKey};
7use crate::dom_tree::DomTree;
8use crate::specificity::sort_by_cascade;
9use crate::types::{Config, CssVariable, CssVariableUsage, LiteralColorOccurrence};
10
11type LiteralColorMap = HashMap<Uri, HashMap<u32, Vec<LiteralColorOccurrence>>>;
12
13#[derive(Clone)]
15pub struct CssVariableManager {
16 variables: Arc<RwLock<HashMap<String, Vec<CssVariable>>>>,
18
19 usages: Arc<RwLock<HashMap<String, Vec<CssVariableUsage>>>>,
21
22 literal_colors: Arc<RwLock<LiteralColorMap>>,
25
26 color_variables: Arc<RwLock<HashMap<NormalizedColorKey, HashSet<String>>>>,
28
29 config: Arc<RwLock<Config>>,
31
32 dom_trees: Arc<RwLock<HashMap<Uri, DomTree>>>,
34
35 tracked_documents: Arc<RwLock<HashSet<Uri>>>,
37}
38
39impl CssVariableManager {
40 pub fn new(config: Config) -> Self {
41 Self {
42 variables: Arc::new(RwLock::new(HashMap::new())),
43 usages: Arc::new(RwLock::new(HashMap::new())),
44 literal_colors: Arc::new(RwLock::new(HashMap::new())),
45 color_variables: Arc::new(RwLock::new(HashMap::new())),
46 config: Arc::new(RwLock::new(config)),
47 dom_trees: Arc::new(RwLock::new(HashMap::new())),
48 tracked_documents: Arc::new(RwLock::new(HashSet::new())),
49 }
50 }
51
52 pub async fn add_variable(&self, variable: CssVariable) -> Result<(), String> {
54 let max_documents = self.config.read().await.max_documents;
55 let mut tracked = self.tracked_documents.write().await;
56
57 if max_documents > 0 && !tracked.contains(&variable.uri) && tracked.len() >= max_documents {
59 return Err(format!(
60 "Maximum document limit ({}) reached. Cannot add more documents.",
61 max_documents
62 ));
63 }
64 tracked.insert(variable.uri.clone());
65
66 let mut vars = self.variables.write().await;
68 vars.entry(variable.name.clone())
69 .or_insert_with(Vec::new)
70 .push(variable);
71
72 Ok(())
73 }
74
75 pub async fn add_usage(&self, usage: CssVariableUsage) {
77 let mut usages = self.usages.write().await;
78 usages
79 .entry(usage.name.clone())
80 .or_insert_with(Vec::new)
81 .push(usage);
82 }
83
84 pub async fn add_literal_color(&self, occurrence: LiteralColorOccurrence) {
86 let mut literal_colors = self.literal_colors.write().await;
87 let line = occurrence.range.start.line;
88 literal_colors
89 .entry(occurrence.uri.clone())
90 .or_default()
91 .entry(line)
92 .or_default()
93 .push(occurrence);
94 }
95
96 pub async fn get_variables(&self, name: &str) -> Vec<CssVariable> {
98 let vars = self.variables.read().await;
99 vars.get(name).cloned().unwrap_or_default()
100 }
101
102 pub async fn get_usages(&self, name: &str) -> Vec<CssVariableUsage> {
104 let usages = self.usages.read().await;
105 usages.get(name).cloned().unwrap_or_default()
106 }
107
108 pub async fn resolve_variable_color(&self, name: &str) -> Option<ls_types::Color> {
110 self.resolve_variable_color_key(name)
111 .await
112 .map(color_from_key)
113 }
114
115 pub async fn resolve_variable_color_key(&self, name: &str) -> Option<NormalizedColorKey> {
117 let vars = self.variables.read().await;
118 resolve_variable_color_key_from_map(name, &vars)
119 }
120
121 pub async fn get_all_variables(&self) -> Vec<CssVariable> {
123 let vars = self.variables.read().await;
124 vars.values().flatten().cloned().collect()
125 }
126
127 pub async fn get_references(&self, name: &str) -> (Vec<CssVariable>, Vec<CssVariableUsage>) {
129 let definitions = self.get_variables(name).await;
130 let usages = self.get_usages(name).await;
131 (definitions, usages)
132 }
133
134 pub async fn get_document_literal_colors(&self, uri: &Uri) -> Vec<LiteralColorOccurrence> {
136 let literal_colors = self.literal_colors.read().await;
137 literal_colors
138 .get(uri)
139 .map(|by_line| by_line.values().flatten().cloned().collect())
140 .unwrap_or_default()
141 }
142
143 pub async fn get_literal_colors_at_position(
145 &self,
146 uri: &Uri,
147 position: Position,
148 ) -> Vec<LiteralColorOccurrence> {
149 let literal_colors = self.literal_colors.read().await;
150 literal_colors
151 .get(uri)
152 .and_then(|by_line| by_line.get(&position.line).cloned())
153 .unwrap_or_default()
154 }
155
156 pub async fn get_variables_by_color_key(&self, key: &NormalizedColorKey) -> Vec<CssVariable> {
158 let names = {
159 let index = self.color_variables.read().await;
160 index.get(key).cloned().unwrap_or_default()
161 };
162 let vars = self.variables.read().await;
163 let mut matches = Vec::new();
164 for name in names {
165 if let Some(definitions) = vars.get(&name) {
166 let mut definitions = definitions.clone();
167 sort_by_cascade(&mut definitions);
168 if let Some(variable) = definitions.into_iter().next() {
169 matches.push(variable);
170 }
171 }
172 }
173 matches.sort_by(|a, b| a.name.cmp(&b.name));
174 matches
175 }
176
177 pub async fn get_document_resolved_color_keys(&self, uri: &Uri) -> HashSet<NormalizedColorKey> {
179 let names = self.get_document_variable_names(uri).await;
180 let vars = self.variables.read().await;
181 names
182 .into_iter()
183 .filter_map(|name| resolve_variable_color_key_from_map(&name, &vars))
184 .collect()
185 }
186
187 pub async fn remove_document(&self, uri: &Uri) {
189 self.remove_documents(&HashSet::from([uri.clone()])).await;
190 }
191
192 pub async fn remove_documents(&self, uris: &HashSet<Uri>) {
194 if uris.is_empty() {
195 return;
196 }
197
198 let mut tracked = self.tracked_documents.write().await;
200 let mut vars = self.variables.write().await;
201 let mut usages = self.usages.write().await;
202 let mut literal_colors = self.literal_colors.write().await;
203 let mut dom_trees = self.dom_trees.write().await;
204
205 tracked.retain(|uri| !uris.contains(uri));
206
207 for var_list in vars.values_mut() {
208 var_list.retain(|variable| !uris.contains(&variable.uri));
209 }
210 vars.retain(|_, var_list| !var_list.is_empty());
211
212 for usage_list in usages.values_mut() {
213 usage_list.retain(|usage| !uris.contains(&usage.uri));
214 }
215 usages.retain(|_, usage_list| !usage_list.is_empty());
216
217 literal_colors.retain(|uri, _| !uris.contains(uri));
218 dom_trees.retain(|uri, _| !uris.contains(uri));
219
220 drop(tracked);
222 drop(vars);
223 drop(usages);
224 drop(literal_colors);
225 drop(dom_trees);
226 self.rebuild_color_index().await;
227 }
228
229 pub async fn get_document_uris(&self) -> HashSet<Uri> {
231 let mut uris = self.tracked_documents.read().await.clone();
232
233 {
234 let usages = self.usages.read().await;
235 uris.extend(usages.values().flatten().map(|usage| usage.uri.clone()));
236 }
237 {
238 let literal_colors = self.literal_colors.read().await;
239 uris.extend(literal_colors.keys().cloned());
240 }
241 {
242 let dom_trees = self.dom_trees.read().await;
243 uris.extend(dom_trees.keys().cloned());
244 }
245
246 uris
247 }
248
249 pub async fn get_document_variables(&self, uri: &Uri) -> Vec<CssVariable> {
251 let vars = self.variables.read().await;
252 vars.values()
253 .flatten()
254 .filter(|v| &v.uri == uri)
255 .cloned()
256 .collect()
257 }
258
259 pub async fn get_document_variable_names(&self, uri: &Uri) -> HashSet<String> {
261 let vars = self.get_document_variables(uri).await;
262 vars.into_iter().map(|v| v.name).collect()
263 }
264
265 pub async fn get_document_usages(&self, uri: &Uri) -> Vec<CssVariableUsage> {
267 let usages = self.usages.read().await;
268 usages
269 .values()
270 .flatten()
271 .filter(|u| &u.uri == uri)
272 .cloned()
273 .collect()
274 }
275
276 pub async fn set_dom_tree(&self, uri: Uri, dom_tree: DomTree) {
278 let mut dom_trees = self.dom_trees.write().await;
279 dom_trees.insert(uri, dom_tree);
280 }
281
282 pub async fn get_dom_tree(&self, uri: &Uri) -> Option<DomTree> {
284 let dom_trees = self.dom_trees.read().await;
285 dom_trees.get(uri).cloned()
286 }
287
288 pub async fn get_config(&self) -> Config {
290 self.config.read().await.clone()
291 }
292
293 pub async fn set_config(&self, config: Config) {
295 let mut stored = self.config.write().await;
296 *stored = config;
297 }
298
299 pub async fn rebuild_color_index(&self) {
301 let snapshot = {
302 let vars = self.variables.read().await;
303 vars.clone()
304 };
305
306 let mut color_variables: HashMap<NormalizedColorKey, HashSet<String>> = HashMap::new();
307 for name in snapshot.keys() {
308 if let Some(key) = resolve_variable_color_key_from_map(name, &snapshot) {
309 color_variables.entry(key).or_default().insert(name.clone());
310 }
311 }
312
313 let mut stored = self.color_variables.write().await;
314 *stored = color_variables;
315 }
316}
317
318fn extract_var_reference(value: &str) -> Option<String> {
319 let trimmed = value.trim();
320 let start = trimmed.find("var(")?;
321 let mut idx = start + 4;
322 let bytes = trimmed.as_bytes();
323 let mut depth = 1i32;
324 while idx < bytes.len() {
325 match bytes[idx] {
326 b'(' => depth += 1,
327 b')' => {
328 depth -= 1;
329 if depth == 0 {
330 break;
331 }
332 }
333 _ => {}
334 }
335 idx += 1;
336 }
337 if depth != 0 {
338 return None;
339 }
340
341 let inner = trimmed[start + 4..idx].trim_start();
342 let inner = inner.strip_prefix("--")?;
343 let mut name_len = 0usize;
344 for ch in inner.chars() {
345 if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
346 name_len += ch.len_utf8();
347 } else {
348 break;
349 }
350 }
351 if name_len == 0 {
352 return None;
353 }
354 Some(format!("--{}", &inner[..name_len]))
355}
356
357fn resolve_variable_color_key_from_map(
358 name: &str,
359 variables: &HashMap<String, Vec<CssVariable>>,
360) -> Option<NormalizedColorKey> {
361 let mut seen = HashSet::new();
362 let mut current = name.to_string();
363
364 loop {
365 if seen.contains(¤t) {
366 return None;
367 }
368 seen.insert(current.clone());
369
370 let mut definitions = variables.get(¤t)?.clone();
371 if definitions.is_empty() {
372 return None;
373 }
374
375 sort_by_cascade(&mut definitions);
376 let variable = &definitions[0];
377
378 if let Some(next_name) = extract_var_reference(&variable.value) {
379 current = next_name;
380 continue;
381 }
382
383 return parse_color(&variable.value).map(normalize_color);
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390 use ls_types::{Position, Range, Uri};
391 use std::str::FromStr;
392
393 fn create_test_variable(name: &str, value: &str, selector: &str, uri: &str) -> CssVariable {
394 CssVariable {
395 name: name.to_string(),
396 value: value.to_string(),
397 selector: selector.to_string(),
398 range: Range::new(Position::new(0, 0), Position::new(0, 10)),
399 name_range: None,
400 value_range: None,
401 uri: Uri::from_str(uri).unwrap(),
402 important: false,
403 inline: false,
404 source_position: 0,
405 }
406 }
407
408 fn create_test_usage(name: &str, context: &str, uri: &str) -> CssVariableUsage {
409 CssVariableUsage {
410 name: name.to_string(),
411 range: Range::new(Position::new(0, 0), Position::new(0, 10)),
412 name_range: None,
413 uri: Uri::from_str(uri).unwrap(),
414 usage_context: context.to_string(),
415 dom_node: None,
416 }
417 }
418
419 fn create_literal_color(
420 text: &str,
421 uri: &str,
422 color: &str,
423 context: &str,
424 ) -> LiteralColorOccurrence {
425 LiteralColorOccurrence {
426 text: text.to_string(),
427 uri: Uri::from_str(uri).unwrap(),
428 range: Range::new(Position::new(0, 0), Position::new(0, text.len() as u32)),
429 usage_context: context.to_string(),
430 normalized_color: crate::color::normalized_color_key(color).unwrap(),
431 }
432 }
433
434 #[test]
435 fn extract_var_reference_allows_fallbacks_and_trailing_tokens() {
436 assert_eq!(
437 extract_var_reference("var(--primary, #fff)"),
438 Some("--primary".to_string())
439 );
440 assert_eq!(
441 extract_var_reference("var(--primary) !important"),
442 Some("--primary".to_string())
443 );
444 assert_eq!(
445 extract_var_reference("calc(1px + var(--spacing))"),
446 Some("--spacing".to_string())
447 );
448 }
449
450 #[tokio::test]
451 async fn test_manager_add_and_get_variables() {
452 let manager = CssVariableManager::new(Config::default());
453 let var = create_test_variable("--primary", "#3b82f6", ":root", "file:///test.css");
454
455 manager
456 .add_variable(var.clone())
457 .await
458 .expect("add_variable failed");
459
460 let variables = manager.get_variables("--primary").await;
461 assert_eq!(variables.len(), 1);
462 assert_eq!(variables[0].name, "--primary");
463 assert_eq!(variables[0].value, "#3b82f6");
464 }
465
466 #[tokio::test]
467 async fn test_manager_multiple_definitions() {
468 let manager = CssVariableManager::new(Config::default());
469
470 let var1 = create_test_variable("--color", "red", ":root", "file:///test.css");
471 let var2 = create_test_variable("--color", "blue", ".class", "file:///test.css");
472
473 manager
474 .add_variable(var1)
475 .await
476 .expect("add_variable failed");
477 manager
478 .add_variable(var2)
479 .await
480 .expect("add_variable failed");
481
482 let variables = manager.get_variables("--color").await;
483 assert_eq!(variables.len(), 2);
484 }
485
486 #[tokio::test]
487 async fn test_manager_add_and_get_usages() {
488 let manager = CssVariableManager::new(Config::default());
489 let usage = create_test_usage("--primary", ".button", "file:///test.css");
490
491 manager.add_usage(usage.clone()).await;
492
493 let usages = manager.get_usages("--primary").await;
494 assert_eq!(usages.len(), 1);
495 assert_eq!(usages[0].name, "--primary");
496 assert_eq!(usages[0].usage_context, ".button");
497 }
498
499 #[tokio::test]
500 async fn test_manager_get_references() {
501 let manager = CssVariableManager::new(Config::default());
502
503 let var = create_test_variable("--spacing", "1rem", ":root", "file:///test.css");
504 let usage = create_test_usage("--spacing", ".card", "file:///test.css");
505
506 manager
507 .add_variable(var)
508 .await
509 .expect("add_variable failed");
510 manager.add_usage(usage).await;
511
512 let (defs, usages) = manager.get_references("--spacing").await;
513 assert_eq!(defs.len(), 1);
514 assert_eq!(usages.len(), 1);
515 }
516
517 #[tokio::test]
518 async fn test_manager_remove_document() {
519 let manager = CssVariableManager::new(Config::default());
520 let uri = Uri::from_str("file:///test.css").unwrap();
521
522 let var = create_test_variable("--primary", "blue", ":root", "file:///test.css");
523 let usage = create_test_usage("--primary", ".button", "file:///test.css");
524 let literal = create_literal_color("blue", "file:///test.css", "blue", ".button");
525
526 manager
527 .add_variable(var)
528 .await
529 .expect("add_variable failed");
530 manager.add_usage(usage).await;
531 manager.add_literal_color(literal).await;
532
533 assert_eq!(manager.get_variables("--primary").await.len(), 1);
535 assert_eq!(manager.get_usages("--primary").await.len(), 1);
536 assert_eq!(manager.get_document_literal_colors(&uri).await.len(), 1);
537
538 manager.remove_document(&uri).await;
540
541 assert_eq!(manager.get_variables("--primary").await.len(), 0);
543 assert_eq!(manager.get_usages("--primary").await.len(), 0);
544 assert_eq!(manager.get_document_literal_colors(&uri).await.len(), 0);
545 }
546
547 #[tokio::test]
548 async fn test_manager_get_all_variables() {
549 let manager = CssVariableManager::new(Config::default());
550
551 manager
552 .add_variable(create_test_variable(
553 "--primary",
554 "blue",
555 ":root",
556 "file:///test.css",
557 ))
558 .await
559 .expect("add_variable failed");
560 manager
561 .add_variable(create_test_variable(
562 "--secondary",
563 "red",
564 ":root",
565 "file:///test.css",
566 ))
567 .await
568 .expect("add_variable failed");
569 manager
570 .add_variable(create_test_variable(
571 "--spacing",
572 "1rem",
573 ":root",
574 "file:///test.css",
575 ))
576 .await
577 .expect("add_variable failed");
578
579 let all_vars = manager.get_all_variables().await;
580 assert_eq!(all_vars.len(), 3);
581 }
582
583 #[tokio::test]
584 async fn test_manager_resolve_variable_color() {
585 let manager = CssVariableManager::new(Config::default());
586
587 let var = create_test_variable("--primary-color", "#3b82f6", ":root", "file:///test.css");
588 manager
589 .add_variable(var)
590 .await
591 .expect("add_variable failed");
592
593 let color = manager.resolve_variable_color("--primary-color").await;
594 assert!(color.is_some());
595 }
596
597 #[tokio::test]
598 async fn test_manager_resolve_variable_color_key_chain() {
599 let manager = CssVariableManager::new(Config::default());
600
601 manager
602 .add_variable(create_test_variable(
603 "--base-color",
604 "#fff",
605 ":root",
606 "file:///test.css",
607 ))
608 .await
609 .expect("add_variable failed");
610 manager
611 .add_variable(create_test_variable(
612 "--alias-color",
613 "var(--base-color)",
614 ":root",
615 "file:///test.css",
616 ))
617 .await
618 .expect("add_variable failed");
619
620 let key = manager.resolve_variable_color_key("--alias-color").await;
621 assert_eq!(key, crate::color::normalized_color_key("white"));
622 }
623
624 #[tokio::test]
625 async fn test_manager_get_variables_by_color_key_excludes_non_colors() {
626 let manager = CssVariableManager::new(Config::default());
627
628 manager
629 .add_variable(create_test_variable(
630 "--spacing",
631 "1rem",
632 ":root",
633 "file:///test.css",
634 ))
635 .await
636 .expect("add_variable failed");
637 manager
638 .add_variable(create_test_variable(
639 "--text-color",
640 "#fff",
641 ":root",
642 "file:///test.css",
643 ))
644 .await
645 .expect("add_variable failed");
646
647 manager.rebuild_color_index().await;
648
649 let matches = manager
650 .get_variables_by_color_key(&crate::color::normalized_color_key("white").unwrap())
651 .await;
652 assert_eq!(matches.len(), 1);
653 assert_eq!(matches[0].name, "--text-color");
654 }
655
656 #[tokio::test]
657 async fn test_manager_get_variables_by_color_key_multiple_names() {
658 let manager = CssVariableManager::new(Config::default());
659
660 manager
661 .add_variable(create_test_variable(
662 "--text-color",
663 "#fff",
664 ":root",
665 "file:///test.css",
666 ))
667 .await
668 .expect("add_variable failed");
669 manager
670 .add_variable(create_test_variable(
671 "--surface",
672 "rgb(255 255 255)",
673 ":root",
674 "file:///test.css",
675 ))
676 .await
677 .expect("add_variable failed");
678
679 manager.rebuild_color_index().await;
680
681 let matches = manager
682 .get_variables_by_color_key(&crate::color::normalized_color_key("white").unwrap())
683 .await;
684 assert_eq!(matches.len(), 2);
685 assert!(matches.iter().any(|var| var.name == "--surface"));
686 assert!(matches.iter().any(|var| var.name == "--text-color"));
687 }
688
689 #[tokio::test]
690 async fn test_manager_cross_file_references() {
691 let manager = CssVariableManager::new(Config::default());
692
693 let var = create_test_variable("--theme", "dark", ":root", "file:///variables.css");
695 manager
696 .add_variable(var)
697 .await
698 .expect("add_variable failed");
699
700 let usage = create_test_usage("--theme", ".app", "file:///app.css");
702 manager.add_usage(usage).await;
703
704 let (defs, usages) = manager.get_references("--theme").await;
705 assert_eq!(defs.len(), 1);
706 assert_eq!(usages.len(), 1);
707 assert_ne!(defs[0].uri, usages[0].uri);
708 }
709
710 #[tokio::test]
711 async fn test_manager_document_isolation() {
712 let manager = CssVariableManager::new(Config::default());
713 let uri1 = Uri::from_str("file:///file1.css").unwrap();
714 let _uri2 = Uri::from_str("file:///file2.css").unwrap();
715
716 manager
717 .add_variable(create_test_variable(
718 "--color",
719 "red",
720 ":root",
721 "file:///file1.css",
722 ))
723 .await
724 .expect("add_variable failed");
725 manager
726 .add_variable(create_test_variable(
727 "--color",
728 "blue",
729 ":root",
730 "file:///file2.css",
731 ))
732 .await
733 .expect("add_variable failed");
734
735 assert_eq!(manager.get_variables("--color").await.len(), 2);
737
738 manager.remove_document(&uri1).await;
740
741 let vars = manager.get_variables("--color").await;
743 assert_eq!(vars.len(), 1);
744 assert_eq!(vars[0].value, "blue");
745 }
746
747 #[tokio::test]
748 async fn test_manager_color_index_stale_after_remove() {
749 let manager = CssVariableManager::new(Config::default());
752 let uri = Uri::from_str("file:///test.css").unwrap();
753 let white_key = crate::color::normalized_color_key("white").unwrap();
754
755 let var = create_test_variable("--bg", "#ffffff", ":root", "file:///test.css");
757 manager
758 .add_variable(var)
759 .await
760 .expect("add_variable failed");
761 manager.rebuild_color_index().await;
762
763 assert_eq!(
765 manager.get_variables_by_color_key(&white_key).await.len(),
766 1,
767 "Variable should be indexed by color"
768 );
769
770 manager.remove_document(&uri).await;
772
773 assert_eq!(
775 manager.get_variables("--bg").await.len(),
776 0,
777 "Variable should be removed from variables map"
778 );
779
780 let color_matches = manager.get_variables_by_color_key(&white_key).await;
784 assert_eq!(
785 color_matches.len(),
786 0,
787 "BUG: color index is stale, returns 0 instead of correctly handling removal"
788 );
789
790 manager.rebuild_color_index().await;
792 assert_eq!(
793 manager.get_variables_by_color_key(&white_key).await.len(),
794 0,
795 "After rebuild, color index is correct"
796 );
797 }
798
799 #[tokio::test]
802 async fn test_manager_important_flag() {
803 let manager = CssVariableManager::new(Config::default());
804
805 let mut var = create_test_variable("--color", "red", ":root", "file:///test.css");
806 var.important = true;
807
808 manager
809 .add_variable(var)
810 .await
811 .expect("add_variable failed");
812
813 let vars = manager.get_variables("--color").await;
814 assert_eq!(vars.len(), 1);
815 assert!(vars[0].important);
816 }
817
818 #[tokio::test]
819 async fn test_manager_inline_flag() {
820 let manager = CssVariableManager::new(Config::default());
821
822 let mut var = create_test_variable(
823 "--inline-color",
824 "green",
825 "inline-style",
826 "file:///test.html",
827 );
828 var.inline = true;
829
830 manager
831 .add_variable(var)
832 .await
833 .expect("add_variable failed");
834
835 let vars = manager.get_variables("--inline-color").await;
836 assert_eq!(vars.len(), 1);
837 assert!(vars[0].inline);
838 }
839
840 #[tokio::test]
841 async fn test_manager_empty_queries() {
842 let manager = CssVariableManager::new(Config::default());
843
844 let vars = manager.get_variables("--does-not-exist").await;
846 assert_eq!(vars.len(), 0);
847
848 let usages = manager.get_usages("--does-not-exist").await;
849 assert_eq!(usages.len(), 0);
850
851 let (defs, usages) = manager.get_references("--does-not-exist").await;
852 assert_eq!(defs.len(), 0);
853 assert_eq!(usages.len(), 0);
854 }
855
856 #[tokio::test]
863 async fn test_manager_has_memory_limits() {
864 use ls_types::{Position, Range};
865 use std::str::FromStr;
866
867 let config = Config {
868 max_documents: 100,
869 ..Default::default()
870 };
871
872 let manager = CssVariableManager::new(config);
873
874 let mut success_count = 0;
876 let mut failure_count = 0;
877
878 for i in 0..200 {
879 let var = CssVariable {
880 name: format!("--var-{}", i),
881 value: "red".to_string(),
882 selector: ":root".to_string(),
883 range: Range::new(Position::new(0, 0), Position::new(0, 10)),
884 name_range: None,
885 value_range: None,
886 uri: Uri::from_str(&format!("file:///test/doc_{}.css", i)).unwrap(),
887 important: false,
888 inline: false,
889 source_position: 0,
890 };
891
892 match manager.add_variable(var).await {
893 Ok(()) => success_count += 1,
894 Err(_) => failure_count += 1,
895 }
896 }
897
898 assert_eq!(
900 success_count, 100,
901 "Should successfully add exactly 100 documents (the limit)"
902 );
903 assert_eq!(
904 failure_count, 100,
905 "Should fail to add the remaining 100 documents beyond the limit"
906 );
907
908 let vars = manager.variables.read().await;
910 assert!(
911 vars.len() <= 100,
912 "Manager should not have more than 100 documents, but has {}",
913 vars.len()
914 );
915 }
916
917 #[tokio::test]
918 async fn test_add_and_remove_use_consistent_lock_order() {
919 use tokio::time::{timeout, Duration};
920
921 let manager = CssVariableManager::new(Config::default());
922 let existing_uri = Uri::from_str("file:///existing.css").unwrap();
923 manager
924 .add_variable(create_test_variable(
925 "--existing",
926 "red",
927 ":root",
928 existing_uri.as_str(),
929 ))
930 .await
931 .unwrap();
932
933 let variables_guard = manager.variables.write().await;
936 let remove_manager = manager.clone();
937 let remove_task = tokio::spawn(async move {
938 remove_manager.remove_document(&existing_uri).await;
939 });
940 tokio::task::yield_now().await;
941
942 let add_manager = manager.clone();
943 let add_task = tokio::spawn(async move {
944 add_manager
945 .add_variable(create_test_variable(
946 "--added",
947 "blue",
948 ":root",
949 "file:///added.css",
950 ))
951 .await
952 });
953 tokio::task::yield_now().await;
954 drop(variables_guard);
955
956 timeout(Duration::from_secs(1), async {
957 remove_task.await.unwrap();
958 add_task.await.unwrap().unwrap();
959 })
960 .await
961 .expect("concurrent add/remove should not deadlock");
962 }
963
964 #[tokio::test]
973 async fn test_manager_color_index_concurrent_consistency() {
974 use ls_types::{Position, Range};
975 use std::str::FromStr;
976 use std::sync::atomic::{AtomicUsize, Ordering};
977 use std::sync::Arc;
978
979 let config = Config::default();
980 let manager = CssVariableManager::new(config);
981
982 let inconsistencies = Arc::new(AtomicUsize::new(0));
984
985 let var = CssVariable {
987 name: "--color-primary".to_string(),
988 value: "#ff0000".to_string(),
989 selector: ":root".to_string(),
990 range: Range::new(Position::new(0, 0), Position::new(0, 10)),
991 name_range: None,
992 value_range: None,
993 uri: Uri::from_str("file:///test/colors.css").unwrap(),
994 important: false,
995 inline: false,
996 source_position: 0,
997 };
998 manager
999 .add_variable(var)
1000 .await
1001 .expect("add_variable failed");
1002 manager.rebuild_color_index().await;
1003
1004 let mut handles = vec![];
1006
1007 for i in 0..100 {
1009 let manager_clone = manager.clone();
1010 handles.push(tokio::spawn(async move {
1011 let var = CssVariable {
1012 name: format!("--color-{}", i),
1013 value: format!("hsl({}, 100%, 50%)", i * 3),
1014 selector: ":root".to_string(),
1015 range: Range::new(Position::new(0, 0), Position::new(0, 10)),
1016 name_range: None,
1017 value_range: None,
1018 uri: Uri::from_str(&format!("file:///test/color_{}.css", i)).unwrap(),
1019 important: false,
1020 inline: false,
1021 source_position: 0,
1022 };
1023 manager_clone
1024 .add_variable(var)
1025 .await
1026 .expect("add_variable failed");
1027 manager_clone.rebuild_color_index().await;
1029 }));
1030 }
1031
1032 let inconsistencies_clone = inconsistencies.clone();
1034 let manager_reader = manager.clone();
1035 handles.push(tokio::spawn(async move {
1036 for _ in 0..50 {
1037 let all_vars = manager_reader.get_all_variables().await;
1039
1040 let color_count = all_vars
1042 .iter()
1043 .filter(|v| crate::color::parse_color(&v.value).is_some())
1044 .count();
1045
1046 manager_reader.rebuild_color_index().await;
1048
1049 let white_key = crate::color::normalized_color_key("white").unwrap();
1051 let white_matches = manager_reader.get_variables_by_color_key(&white_key).await;
1052
1053 if white_matches.len() > color_count + 10 {
1056 inconsistencies_clone.fetch_add(1, Ordering::SeqCst);
1057 }
1058
1059 tokio::time::sleep(tokio::time::Duration::from_micros(1)).await;
1060 }
1061 }));
1062
1063 for handle in handles {
1065 let _ = handle.await;
1066 }
1067
1068 assert_eq!(
1072 inconsistencies.load(Ordering::SeqCst),
1073 0,
1074 "Color index had {} inconsistencies during concurrent access",
1075 inconsistencies.load(Ordering::SeqCst)
1076 );
1077 }
1078}