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