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 document_versions: Arc<RwLock<HashMap<Uri, i64>>>,
40}
41
42impl CssVariableManager {
43 pub fn new(config: Config) -> Self {
44 Self {
45 variables: Arc::new(RwLock::new(HashMap::new())),
46 usages: Arc::new(RwLock::new(HashMap::new())),
47 literal_colors: Arc::new(RwLock::new(HashMap::new())),
48 color_variables: Arc::new(RwLock::new(HashMap::new())),
49 config: Arc::new(RwLock::new(config)),
50 dom_trees: Arc::new(RwLock::new(HashMap::new())),
51 tracked_documents: Arc::new(RwLock::new(HashSet::new())),
52 document_versions: Arc::new(RwLock::new(HashMap::new())),
53 }
54 }
55
56 pub async fn accept_document_version(&self, uri: &Uri, version: i64) -> bool {
61 let mut versions = self.document_versions.write().await;
62 if versions.get(uri).is_some_and(|current| version <= *current) {
63 return false;
64 }
65 versions.insert(uri.clone(), version);
66 true
67 }
68
69 pub async fn clear_document_version(&self, uri: &Uri) {
71 let mut versions = self.document_versions.write().await;
72 versions.remove(uri);
73 }
74
75 pub async fn get_document_version(&self, uri: &Uri) -> Option<i64> {
77 let versions = self.document_versions.read().await;
78 versions.get(uri).copied()
79 }
80
81 pub async fn add_variable(&self, variable: CssVariable) -> Result<(), String> {
83 self.add_variables(vec![variable]).await
84 }
85
86 pub async fn add_variables(&self, variables: Vec<CssVariable>) -> Result<(), String> {
88 if variables.is_empty() {
89 return Ok(());
90 }
91
92 let max_documents = self.config.read().await.max_documents;
93 let mut tracked = self.tracked_documents.write().await;
94
95 let new_documents: HashSet<_> = variables
96 .iter()
97 .map(|variable| variable.uri.clone())
98 .filter(|uri| !tracked.contains(uri))
99 .collect();
100 if max_documents > 0 && tracked.len() + new_documents.len() > max_documents {
101 return Err(format!(
102 "Maximum document limit ({max_documents}) reached. Cannot add more documents."
103 ));
104 }
105 tracked.extend(new_documents);
106
107 let mut vars = self.variables.write().await;
109 for variable in variables {
110 vars.entry(variable.name.clone())
111 .or_default()
112 .push(variable);
113 }
114
115 Ok(())
116 }
117
118 pub async fn replace_document_variables(
120 &self,
121 uri: &Uri,
122 variables: Vec<CssVariable>,
123 ) -> Result<(), String> {
124 if variables.iter().any(|variable| &variable.uri != uri) {
125 return Err("Replacement variables must belong to the target document".to_string());
126 }
127
128 let max_documents = self.config.read().await.max_documents;
129 let mut tracked = self.tracked_documents.write().await;
130 let introduces_document = !variables.is_empty() && !tracked.contains(uri);
131 if max_documents > 0 && introduces_document && tracked.len() >= max_documents {
132 return Err(format!(
133 "Maximum document limit ({max_documents}) reached. Cannot add more documents."
134 ));
135 }
136
137 let mut vars = self.variables.write().await;
138 for definitions in vars.values_mut() {
139 definitions.retain(|variable| &variable.uri != uri);
140 }
141 vars.retain(|_, definitions| !definitions.is_empty());
142
143 if variables.is_empty() {
144 tracked.remove(uri);
145 } else {
146 tracked.insert(uri.clone());
147 }
148 for variable in variables {
149 vars.entry(variable.name.clone())
150 .or_default()
151 .push(variable);
152 }
153
154 Ok(())
155 }
156
157 pub async fn replace_document_analysis(
159 &self,
160 uri: &Uri,
161 variables: Vec<CssVariable>,
162 usages: Vec<CssVariableUsage>,
163 ) -> Result<(), String> {
164 self.replace_document_analysis_inner(uri, variables, usages)
165 .await
166 }
167
168 pub async fn replace_document_analysis_at_version(
174 &self,
175 uri: &Uri,
176 version: i64,
177 variables: Vec<CssVariable>,
178 usages: Vec<CssVariableUsage>,
179 ) -> Result<bool, String> {
180 let versions = self.document_versions.write().await;
181 if versions.get(uri).copied() != Some(version) {
182 return Ok(false);
183 }
184
185 self.replace_document_analysis_inner(uri, variables, usages)
186 .await?;
187 Ok(true)
188 }
189
190 async fn replace_document_analysis_inner(
191 &self,
192 uri: &Uri,
193 variables: Vec<CssVariable>,
194 usages: Vec<CssVariableUsage>,
195 ) -> Result<(), String> {
196 if variables.iter().any(|variable| &variable.uri != uri)
197 || usages.iter().any(|usage| &usage.uri != uri)
198 {
199 return Err("Replacement analysis must belong to the target document".to_string());
200 }
201
202 let max_documents = self.config.read().await.max_documents;
203 let mut tracked = self.tracked_documents.write().await;
204 let has_analysis = !variables.is_empty() || !usages.is_empty();
205 let introduces_document = has_analysis && !tracked.contains(uri);
206 if max_documents > 0 && introduces_document && tracked.len() >= max_documents {
207 return Err(format!(
208 "Maximum document limit ({max_documents}) reached. Cannot add more documents."
209 ));
210 }
211
212 let mut vars = self.variables.write().await;
214 let mut stored_usages = self.usages.write().await;
215
216 for definitions in vars.values_mut() {
217 definitions.retain(|variable| &variable.uri != uri);
218 }
219 vars.retain(|_, definitions| !definitions.is_empty());
220
221 for document_usages in stored_usages.values_mut() {
222 document_usages.retain(|usage| &usage.uri != uri);
223 }
224 stored_usages.retain(|_, document_usages| !document_usages.is_empty());
225
226 if !has_analysis {
227 tracked.remove(uri);
228 } else {
229 tracked.insert(uri.clone());
230 }
231 for variable in variables {
232 vars.entry(variable.name.clone())
233 .or_default()
234 .push(variable);
235 }
236
237 for usage in usages {
238 stored_usages
239 .entry(usage.name.clone())
240 .or_default()
241 .push(usage);
242 }
243
244 Ok(())
245 }
246
247 pub async fn add_usage(&self, usage: CssVariableUsage) {
249 let max_documents = self.config.read().await.max_documents;
250 let mut tracked = self.tracked_documents.write().await;
251 if !tracked.contains(&usage.uri) {
252 if max_documents > 0 && tracked.len() >= max_documents {
253 return;
254 }
255 tracked.insert(usage.uri.clone());
256 }
257
258 let mut usages = self.usages.write().await;
259 usages
260 .entry(usage.name.clone())
261 .or_insert_with(Vec::new)
262 .push(usage);
263 }
264
265 pub async fn add_literal_color(&self, occurrence: LiteralColorOccurrence) {
267 let mut literal_colors = self.literal_colors.write().await;
268 let line = occurrence.range.start.line;
269 literal_colors
270 .entry(occurrence.uri.clone())
271 .or_default()
272 .entry(line)
273 .or_default()
274 .push(occurrence);
275 }
276
277 pub async fn get_variables(&self, name: &str) -> Vec<CssVariable> {
279 let vars = self.variables.read().await;
280 vars.get(name).cloned().unwrap_or_default()
281 }
282
283 pub async fn get_usages(&self, name: &str) -> Vec<CssVariableUsage> {
285 let usages = self.usages.read().await;
286 usages.get(name).cloned().unwrap_or_default()
287 }
288
289 pub async fn resolve_variable_color(&self, name: &str) -> Option<ls_types::Color> {
291 self.resolve_variable_color_key(name)
292 .await
293 .map(color_from_key)
294 }
295
296 pub async fn resolve_variable_color_key(&self, name: &str) -> Option<NormalizedColorKey> {
298 let vars = self.variables.read().await;
299 resolve_variable_color_key_from_map(name, &vars)
300 }
301
302 pub async fn get_all_variables(&self) -> Vec<CssVariable> {
304 let vars = self.variables.read().await;
305 vars.values().flatten().cloned().collect()
306 }
307
308 pub async fn get_references(&self, name: &str) -> (Vec<CssVariable>, Vec<CssVariableUsage>) {
310 let definitions = self.get_variables(name).await;
311 let usages = self.get_usages(name).await;
312 (definitions, usages)
313 }
314
315 pub async fn get_document_literal_colors(&self, uri: &Uri) -> Vec<LiteralColorOccurrence> {
317 let literal_colors = self.literal_colors.read().await;
318 literal_colors
319 .get(uri)
320 .map(|by_line| by_line.values().flatten().cloned().collect())
321 .unwrap_or_default()
322 }
323
324 pub async fn get_literal_colors_at_position(
326 &self,
327 uri: &Uri,
328 position: Position,
329 ) -> Vec<LiteralColorOccurrence> {
330 let literal_colors = self.literal_colors.read().await;
331 literal_colors
332 .get(uri)
333 .and_then(|by_line| by_line.get(&position.line).cloned())
334 .unwrap_or_default()
335 }
336
337 pub async fn get_variables_by_color_key(&self, key: &NormalizedColorKey) -> Vec<CssVariable> {
339 let names = {
340 let index = self.color_variables.read().await;
341 index.get(key).cloned().unwrap_or_default()
342 };
343 let vars = self.variables.read().await;
344 let mut matches = Vec::new();
345 for name in names {
346 if let Some(definitions) = vars.get(&name) {
347 let mut definitions = definitions.clone();
348 sort_by_cascade(&mut definitions);
349 if let Some(variable) = definitions.into_iter().next() {
350 matches.push(variable);
351 }
352 }
353 }
354 matches.sort_by(|a, b| a.name.cmp(&b.name));
355 matches
356 }
357
358 pub async fn get_document_resolved_color_keys(&self, uri: &Uri) -> HashSet<NormalizedColorKey> {
360 let names = self.get_document_variable_names(uri).await;
361 let vars = self.variables.read().await;
362 names
363 .into_iter()
364 .filter_map(|name| resolve_variable_color_key_from_map(&name, &vars))
365 .collect()
366 }
367
368 pub async fn remove_document(&self, uri: &Uri) {
370 self.remove_documents(&HashSet::from([uri.clone()])).await;
371 }
372
373 pub async fn remove_documents(&self, uris: &HashSet<Uri>) {
375 if uris.is_empty() {
376 return;
377 }
378
379 let mut tracked = self.tracked_documents.write().await;
381 let mut vars = self.variables.write().await;
382 let mut usages = self.usages.write().await;
383 let mut literal_colors = self.literal_colors.write().await;
384 let mut dom_trees = self.dom_trees.write().await;
385
386 tracked.retain(|uri| !uris.contains(uri));
387
388 for var_list in vars.values_mut() {
389 var_list.retain(|variable| !uris.contains(&variable.uri));
390 }
391 vars.retain(|_, var_list| !var_list.is_empty());
392
393 for usage_list in usages.values_mut() {
394 usage_list.retain(|usage| !uris.contains(&usage.uri));
395 }
396 usages.retain(|_, usage_list| !usage_list.is_empty());
397
398 literal_colors.retain(|uri, _| !uris.contains(uri));
399 dom_trees.retain(|uri, _| !uris.contains(uri));
400
401 drop(tracked);
403 drop(vars);
404 drop(usages);
405 drop(literal_colors);
406 drop(dom_trees);
407 self.rebuild_color_index().await;
408 }
409
410 pub async fn get_document_uris(&self) -> HashSet<Uri> {
412 let mut uris = self.tracked_documents.read().await.clone();
413
414 {
415 let usages = self.usages.read().await;
416 uris.extend(usages.values().flatten().map(|usage| usage.uri.clone()));
417 }
418 {
419 let literal_colors = self.literal_colors.read().await;
420 uris.extend(literal_colors.keys().cloned());
421 }
422 {
423 let dom_trees = self.dom_trees.read().await;
424 uris.extend(dom_trees.keys().cloned());
425 }
426
427 uris
428 }
429
430 pub async fn get_document_variables(&self, uri: &Uri) -> Vec<CssVariable> {
432 let vars = self.variables.read().await;
433 vars.values()
434 .flatten()
435 .filter(|v| &v.uri == uri)
436 .cloned()
437 .collect()
438 }
439
440 pub async fn get_document_variable_names(&self, uri: &Uri) -> HashSet<String> {
442 let vars = self.get_document_variables(uri).await;
443 vars.into_iter().map(|v| v.name).collect()
444 }
445
446 pub async fn get_document_usages(&self, uri: &Uri) -> Vec<CssVariableUsage> {
448 let usages = self.usages.read().await;
449 usages
450 .values()
451 .flatten()
452 .filter(|u| &u.uri == uri)
453 .cloned()
454 .collect()
455 }
456
457 pub async fn set_dom_tree(&self, uri: Uri, dom_tree: DomTree) {
459 let mut dom_trees = self.dom_trees.write().await;
460 dom_trees.insert(uri, dom_tree);
461 }
462
463 pub async fn get_dom_tree(&self, uri: &Uri) -> Option<DomTree> {
465 let dom_trees = self.dom_trees.read().await;
466 dom_trees.get(uri).cloned()
467 }
468
469 pub async fn get_config(&self) -> Config {
471 self.config.read().await.clone()
472 }
473
474 pub async fn set_config(&self, config: Config) {
476 let mut stored = self.config.write().await;
477 *stored = config;
478 }
479
480 pub async fn rebuild_color_index(&self) {
482 let snapshot = {
483 let vars = self.variables.read().await;
484 vars.clone()
485 };
486
487 let mut color_variables: HashMap<NormalizedColorKey, HashSet<String>> = HashMap::new();
488 for name in snapshot.keys() {
489 if let Some(key) = resolve_variable_color_key_from_map(name, &snapshot) {
490 color_variables.entry(key).or_default().insert(name.clone());
491 }
492 }
493
494 let mut stored = self.color_variables.write().await;
495 *stored = color_variables;
496 }
497}
498
499fn extract_var_reference(value: &str) -> Option<String> {
500 let trimmed = value.trim();
501 let start = trimmed.find("var(")?;
502 let mut idx = start + 4;
503 let bytes = trimmed.as_bytes();
504 let mut depth = 1i32;
505 while idx < bytes.len() {
506 match bytes[idx] {
507 b'(' => depth += 1,
508 b')' => {
509 depth -= 1;
510 if depth == 0 {
511 break;
512 }
513 }
514 _ => {}
515 }
516 idx += 1;
517 }
518 if depth != 0 {
519 return None;
520 }
521
522 let inner = trimmed[start + 4..idx].trim_start();
523 let inner = inner.strip_prefix("--")?;
524 let mut name_len = 0usize;
525 for ch in inner.chars() {
526 if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
527 name_len += ch.len_utf8();
528 } else {
529 break;
530 }
531 }
532 if name_len == 0 {
533 return None;
534 }
535 Some(format!("--{}", &inner[..name_len]))
536}
537
538fn resolve_variable_color_key_from_map(
539 name: &str,
540 variables: &HashMap<String, Vec<CssVariable>>,
541) -> Option<NormalizedColorKey> {
542 let mut seen = HashSet::new();
543 let mut current = name.to_string();
544
545 loop {
546 if seen.contains(¤t) {
547 return None;
548 }
549 seen.insert(current.clone());
550
551 let mut definitions = variables.get(¤t)?.clone();
552 if definitions.is_empty() {
553 return None;
554 }
555
556 sort_by_cascade(&mut definitions);
557 let variable = &definitions[0];
558
559 if let Some(next_name) = extract_var_reference(&variable.value) {
560 current = next_name;
561 continue;
562 }
563
564 return parse_color(&variable.value).map(normalize_color);
565 }
566}
567
568#[cfg(test)]
569mod tests {
570 use super::*;
571 use ls_types::{Position, Range, Uri};
572 use std::str::FromStr;
573
574 fn create_test_variable(name: &str, value: &str, selector: &str, uri: &str) -> CssVariable {
575 CssVariable {
576 name: name.to_string(),
577 value: value.to_string(),
578 selector: selector.to_string(),
579 range: Range::new(Position::new(0, 0), Position::new(0, 10)),
580 name_range: None,
581 value_range: None,
582 uri: Uri::from_str(uri).unwrap(),
583 important: false,
584 inline: false,
585 source_position: 0,
586 }
587 }
588
589 fn create_test_usage(name: &str, context: &str, uri: &str) -> CssVariableUsage {
590 CssVariableUsage {
591 name: name.to_string(),
592 range: Range::new(Position::new(0, 0), Position::new(0, 10)),
593 name_range: None,
594 uri: Uri::from_str(uri).unwrap(),
595 usage_context: context.to_string(),
596 dom_node: None,
597 }
598 }
599
600 fn create_literal_color(
601 text: &str,
602 uri: &str,
603 color: &str,
604 context: &str,
605 ) -> LiteralColorOccurrence {
606 LiteralColorOccurrence {
607 text: text.to_string(),
608 uri: Uri::from_str(uri).unwrap(),
609 range: Range::new(Position::new(0, 0), Position::new(0, text.len() as u32)),
610 usage_context: context.to_string(),
611 normalized_color: crate::color::normalized_color_key(color).unwrap(),
612 }
613 }
614
615 #[test]
616 fn extract_var_reference_allows_fallbacks_and_trailing_tokens() {
617 assert_eq!(
618 extract_var_reference("var(--primary, #fff)"),
619 Some("--primary".to_string())
620 );
621 assert_eq!(
622 extract_var_reference("var(--primary) !important"),
623 Some("--primary".to_string())
624 );
625 assert_eq!(
626 extract_var_reference("calc(1px + var(--spacing))"),
627 Some("--spacing".to_string())
628 );
629 }
630
631 #[tokio::test]
632 async fn test_manager_add_and_get_variables() {
633 let manager = CssVariableManager::new(Config::default());
634 let var = create_test_variable("--primary", "#3b82f6", ":root", "file:///test.css");
635
636 manager
637 .add_variable(var.clone())
638 .await
639 .expect("add_variable failed");
640
641 let variables = manager.get_variables("--primary").await;
642 assert_eq!(variables.len(), 1);
643 assert_eq!(variables[0].name, "--primary");
644 assert_eq!(variables[0].value, "#3b82f6");
645 }
646
647 #[tokio::test]
648 async fn test_manager_multiple_definitions() {
649 let manager = CssVariableManager::new(Config::default());
650
651 let var1 = create_test_variable("--color", "red", ":root", "file:///test.css");
652 let var2 = create_test_variable("--color", "blue", ".class", "file:///test.css");
653
654 manager
655 .add_variable(var1)
656 .await
657 .expect("add_variable failed");
658 manager
659 .add_variable(var2)
660 .await
661 .expect("add_variable failed");
662
663 let variables = manager.get_variables("--color").await;
664 assert_eq!(variables.len(), 2);
665 }
666
667 #[tokio::test]
668 async fn replace_document_variables_is_atomic_and_respects_limits() {
669 let manager = CssVariableManager::new(Config {
670 max_documents: 1,
671 ..Config::default()
672 });
673 let first_uri = Uri::from_str("file:///first.css").unwrap();
674 let second_uri = Uri::from_str("file:///second.css").unwrap();
675
676 manager
677 .replace_document_variables(
678 &first_uri,
679 vec![create_test_variable(
680 "--old",
681 "red",
682 ":root",
683 first_uri.as_str(),
684 )],
685 )
686 .await
687 .unwrap();
688
689 let error = manager
690 .replace_document_variables(
691 &second_uri,
692 vec![create_test_variable(
693 "--blocked",
694 "blue",
695 ":root",
696 second_uri.as_str(),
697 )],
698 )
699 .await
700 .unwrap_err();
701 assert!(error.contains("Maximum document limit"));
702 assert_eq!(manager.get_variables("--old").await.len(), 1);
703 assert!(manager.get_variables("--blocked").await.is_empty());
704
705 manager
706 .replace_document_variables(
707 &first_uri,
708 vec![
709 create_test_variable("--new-a", "red", ":root", first_uri.as_str()),
710 create_test_variable("--new-b", "blue", ":root", first_uri.as_str()),
711 ],
712 )
713 .await
714 .unwrap();
715 assert!(manager.get_variables("--old").await.is_empty());
716 assert_eq!(manager.get_variables("--new-a").await.len(), 1);
717 assert_eq!(manager.get_variables("--new-b").await.len(), 1);
718 }
719
720 #[tokio::test]
721 async fn replace_document_analysis_replaces_definitions_and_usages_together() {
722 let manager = CssVariableManager::new(Config::default());
723 let uri = Uri::from_str("file:///vite.config.ts").unwrap();
724
725 manager
726 .replace_document_analysis(
727 &uri,
728 vec![create_test_variable("--old", "red", ":root", uri.as_str())],
729 vec![create_test_usage("--old-dependency", ":root", uri.as_str())],
730 )
731 .await
732 .unwrap();
733
734 manager
735 .replace_document_analysis(
736 &uri,
737 vec![create_test_variable("--new", "blue", ":root", uri.as_str())],
738 vec![create_test_usage("--new-dependency", ":root", uri.as_str())],
739 )
740 .await
741 .unwrap();
742
743 assert!(manager.get_variables("--old").await.is_empty());
744 assert!(manager.get_usages("--old-dependency").await.is_empty());
745 assert_eq!(manager.get_variables("--new").await.len(), 1);
746 assert_eq!(manager.get_usages("--new-dependency").await.len(), 1);
747 }
748
749 #[tokio::test]
750 async fn versioned_analysis_rejects_obsolete_replacements() {
751 let manager = CssVariableManager::new(Config::default());
752 let uri = Uri::from_str("file:///vite.config.ts").unwrap();
753
754 assert!(manager.accept_document_version(&uri, 1).await);
755 assert_eq!(manager.get_document_version(&uri).await, Some(1));
756 assert!(manager
757 .replace_document_analysis_at_version(
758 &uri,
759 1,
760 vec![create_test_variable("--old", "red", ":root", uri.as_str())],
761 vec![create_test_usage("--old-use", ":root", uri.as_str())],
762 )
763 .await
764 .unwrap());
765
766 assert!(manager.accept_document_version(&uri, 2).await);
767 assert!(!manager.accept_document_version(&uri, 2).await);
768 assert!(!manager.accept_document_version(&uri, 1).await);
769
770 assert!(!manager
771 .replace_document_analysis_at_version(
772 &uri,
773 1,
774 vec![create_test_variable(
775 "--stale",
776 "blue",
777 ":root",
778 uri.as_str()
779 )],
780 vec![create_test_usage("--stale-use", ":root", uri.as_str())],
781 )
782 .await
783 .unwrap());
784 assert_eq!(manager.get_variables("--old").await.len(), 1);
785 assert!(manager.get_variables("--stale").await.is_empty());
786 assert_eq!(manager.get_usages("--old-use").await.len(), 1);
787
788 assert!(manager
789 .replace_document_analysis_at_version(
790 &uri,
791 2,
792 vec![create_test_variable("--new", "blue", ":root", uri.as_str())],
793 vec![create_test_usage("--new-use", ":root", uri.as_str())],
794 )
795 .await
796 .unwrap());
797 assert!(manager.get_variables("--old").await.is_empty());
798 assert_eq!(manager.get_variables("--new").await.len(), 1);
799 assert!(manager.get_usages("--old-use").await.is_empty());
800 assert_eq!(manager.get_usages("--new-use").await.len(), 1);
801
802 manager.clear_document_version(&uri).await;
803 assert_eq!(manager.get_document_version(&uri).await, None);
804 }
805
806 #[tokio::test]
807 async fn usage_only_analysis_is_tracked_and_respects_document_limits() {
808 let manager = CssVariableManager::new(Config {
809 max_documents: 1,
810 ..Config::default()
811 });
812 let usage_uri = Uri::from_str("file:///vite.config.ts").unwrap();
813 let blocked_uri = Uri::from_str("file:///blocked.css").unwrap();
814
815 manager
816 .replace_document_analysis(
817 &usage_uri,
818 Vec::new(),
819 vec![create_test_usage(
820 "--dependency",
821 ":root",
822 usage_uri.as_str(),
823 )],
824 )
825 .await
826 .unwrap();
827
828 assert!(manager.get_document_uris().await.contains(&usage_uri));
829 let error = manager
830 .replace_document_analysis(
831 &blocked_uri,
832 vec![create_test_variable(
833 "--blocked",
834 "red",
835 ":root",
836 blocked_uri.as_str(),
837 )],
838 Vec::new(),
839 )
840 .await
841 .unwrap_err();
842 assert!(error.contains("Maximum document limit"));
843 assert_eq!(manager.get_usages("--dependency").await.len(), 1);
844 assert!(manager.get_variables("--blocked").await.is_empty());
845 }
846
847 #[tokio::test]
848 async fn test_manager_add_and_get_usages() {
849 let manager = CssVariableManager::new(Config::default());
850 let usage = create_test_usage("--primary", ".button", "file:///test.css");
851
852 manager.add_usage(usage.clone()).await;
853
854 let usages = manager.get_usages("--primary").await;
855 assert_eq!(usages.len(), 1);
856 assert_eq!(usages[0].name, "--primary");
857 assert_eq!(usages[0].usage_context, ".button");
858 }
859
860 #[tokio::test]
861 async fn incremental_usage_insertion_respects_document_limits() {
862 let manager = CssVariableManager::new(Config {
863 max_documents: 1,
864 ..Config::default()
865 });
866 let first_uri = Uri::from_str("file:///first.css").unwrap();
867 let blocked_uri = Uri::from_str("file:///blocked.css").unwrap();
868
869 manager
870 .add_usage(create_test_usage(
871 "--first-dependency",
872 ".first",
873 first_uri.as_str(),
874 ))
875 .await;
876 manager
877 .add_usage(create_test_usage(
878 "--blocked-dependency",
879 ".blocked",
880 blocked_uri.as_str(),
881 ))
882 .await;
883
884 assert!(manager.get_document_uris().await.contains(&first_uri));
885 assert_eq!(manager.get_usages("--first-dependency").await.len(), 1);
886 assert!(manager.get_usages("--blocked-dependency").await.is_empty());
887 }
888
889 #[tokio::test]
890 async fn test_manager_get_references() {
891 let manager = CssVariableManager::new(Config::default());
892
893 let var = create_test_variable("--spacing", "1rem", ":root", "file:///test.css");
894 let usage = create_test_usage("--spacing", ".card", "file:///test.css");
895
896 manager
897 .add_variable(var)
898 .await
899 .expect("add_variable failed");
900 manager.add_usage(usage).await;
901
902 let (defs, usages) = manager.get_references("--spacing").await;
903 assert_eq!(defs.len(), 1);
904 assert_eq!(usages.len(), 1);
905 }
906
907 #[tokio::test]
908 async fn test_manager_remove_document() {
909 let manager = CssVariableManager::new(Config::default());
910 let uri = Uri::from_str("file:///test.css").unwrap();
911
912 let var = create_test_variable("--primary", "blue", ":root", "file:///test.css");
913 let usage = create_test_usage("--primary", ".button", "file:///test.css");
914 let literal = create_literal_color("blue", "file:///test.css", "blue", ".button");
915
916 manager
917 .add_variable(var)
918 .await
919 .expect("add_variable failed");
920 manager.add_usage(usage).await;
921 manager.add_literal_color(literal).await;
922
923 assert_eq!(manager.get_variables("--primary").await.len(), 1);
925 assert_eq!(manager.get_usages("--primary").await.len(), 1);
926 assert_eq!(manager.get_document_literal_colors(&uri).await.len(), 1);
927
928 manager.remove_document(&uri).await;
930
931 assert_eq!(manager.get_variables("--primary").await.len(), 0);
933 assert_eq!(manager.get_usages("--primary").await.len(), 0);
934 assert_eq!(manager.get_document_literal_colors(&uri).await.len(), 0);
935 }
936
937 #[tokio::test]
938 async fn test_manager_get_all_variables() {
939 let manager = CssVariableManager::new(Config::default());
940
941 manager
942 .add_variable(create_test_variable(
943 "--primary",
944 "blue",
945 ":root",
946 "file:///test.css",
947 ))
948 .await
949 .expect("add_variable failed");
950 manager
951 .add_variable(create_test_variable(
952 "--secondary",
953 "red",
954 ":root",
955 "file:///test.css",
956 ))
957 .await
958 .expect("add_variable failed");
959 manager
960 .add_variable(create_test_variable(
961 "--spacing",
962 "1rem",
963 ":root",
964 "file:///test.css",
965 ))
966 .await
967 .expect("add_variable failed");
968
969 let all_vars = manager.get_all_variables().await;
970 assert_eq!(all_vars.len(), 3);
971 }
972
973 #[tokio::test]
974 async fn test_manager_resolve_variable_color() {
975 let manager = CssVariableManager::new(Config::default());
976
977 let var = create_test_variable("--primary-color", "#3b82f6", ":root", "file:///test.css");
978 manager
979 .add_variable(var)
980 .await
981 .expect("add_variable failed");
982
983 let color = manager.resolve_variable_color("--primary-color").await;
984 assert!(color.is_some());
985 }
986
987 #[tokio::test]
988 async fn test_manager_resolve_variable_color_key_chain() {
989 let manager = CssVariableManager::new(Config::default());
990
991 manager
992 .add_variable(create_test_variable(
993 "--base-color",
994 "#fff",
995 ":root",
996 "file:///test.css",
997 ))
998 .await
999 .expect("add_variable failed");
1000 manager
1001 .add_variable(create_test_variable(
1002 "--alias-color",
1003 "var(--base-color)",
1004 ":root",
1005 "file:///test.css",
1006 ))
1007 .await
1008 .expect("add_variable failed");
1009
1010 let key = manager.resolve_variable_color_key("--alias-color").await;
1011 assert_eq!(key, crate::color::normalized_color_key("white"));
1012 }
1013
1014 #[tokio::test]
1015 async fn test_manager_get_variables_by_color_key_excludes_non_colors() {
1016 let manager = CssVariableManager::new(Config::default());
1017
1018 manager
1019 .add_variable(create_test_variable(
1020 "--spacing",
1021 "1rem",
1022 ":root",
1023 "file:///test.css",
1024 ))
1025 .await
1026 .expect("add_variable failed");
1027 manager
1028 .add_variable(create_test_variable(
1029 "--text-color",
1030 "#fff",
1031 ":root",
1032 "file:///test.css",
1033 ))
1034 .await
1035 .expect("add_variable failed");
1036
1037 manager.rebuild_color_index().await;
1038
1039 let matches = manager
1040 .get_variables_by_color_key(&crate::color::normalized_color_key("white").unwrap())
1041 .await;
1042 assert_eq!(matches.len(), 1);
1043 assert_eq!(matches[0].name, "--text-color");
1044 }
1045
1046 #[tokio::test]
1047 async fn test_manager_get_variables_by_color_key_multiple_names() {
1048 let manager = CssVariableManager::new(Config::default());
1049
1050 manager
1051 .add_variable(create_test_variable(
1052 "--text-color",
1053 "#fff",
1054 ":root",
1055 "file:///test.css",
1056 ))
1057 .await
1058 .expect("add_variable failed");
1059 manager
1060 .add_variable(create_test_variable(
1061 "--surface",
1062 "rgb(255 255 255)",
1063 ":root",
1064 "file:///test.css",
1065 ))
1066 .await
1067 .expect("add_variable failed");
1068
1069 manager.rebuild_color_index().await;
1070
1071 let matches = manager
1072 .get_variables_by_color_key(&crate::color::normalized_color_key("white").unwrap())
1073 .await;
1074 assert_eq!(matches.len(), 2);
1075 assert!(matches.iter().any(|var| var.name == "--surface"));
1076 assert!(matches.iter().any(|var| var.name == "--text-color"));
1077 }
1078
1079 #[tokio::test]
1080 async fn test_manager_cross_file_references() {
1081 let manager = CssVariableManager::new(Config::default());
1082
1083 let var = create_test_variable("--theme", "dark", ":root", "file:///variables.css");
1085 manager
1086 .add_variable(var)
1087 .await
1088 .expect("add_variable failed");
1089
1090 let usage = create_test_usage("--theme", ".app", "file:///app.css");
1092 manager.add_usage(usage).await;
1093
1094 let (defs, usages) = manager.get_references("--theme").await;
1095 assert_eq!(defs.len(), 1);
1096 assert_eq!(usages.len(), 1);
1097 assert_ne!(defs[0].uri, usages[0].uri);
1098 }
1099
1100 #[tokio::test]
1101 async fn test_manager_document_isolation() {
1102 let manager = CssVariableManager::new(Config::default());
1103 let uri1 = Uri::from_str("file:///file1.css").unwrap();
1104 let _uri2 = Uri::from_str("file:///file2.css").unwrap();
1105
1106 manager
1107 .add_variable(create_test_variable(
1108 "--color",
1109 "red",
1110 ":root",
1111 "file:///file1.css",
1112 ))
1113 .await
1114 .expect("add_variable failed");
1115 manager
1116 .add_variable(create_test_variable(
1117 "--color",
1118 "blue",
1119 ":root",
1120 "file:///file2.css",
1121 ))
1122 .await
1123 .expect("add_variable failed");
1124
1125 assert_eq!(manager.get_variables("--color").await.len(), 2);
1127
1128 manager.remove_document(&uri1).await;
1130
1131 let vars = manager.get_variables("--color").await;
1133 assert_eq!(vars.len(), 1);
1134 assert_eq!(vars[0].value, "blue");
1135 }
1136
1137 #[tokio::test]
1138 async fn test_manager_color_index_stale_after_remove() {
1139 let manager = CssVariableManager::new(Config::default());
1142 let uri = Uri::from_str("file:///test.css").unwrap();
1143 let white_key = crate::color::normalized_color_key("white").unwrap();
1144
1145 let var = create_test_variable("--bg", "#ffffff", ":root", "file:///test.css");
1147 manager
1148 .add_variable(var)
1149 .await
1150 .expect("add_variable failed");
1151 manager.rebuild_color_index().await;
1152
1153 assert_eq!(
1155 manager.get_variables_by_color_key(&white_key).await.len(),
1156 1,
1157 "Variable should be indexed by color"
1158 );
1159
1160 manager.remove_document(&uri).await;
1162
1163 assert_eq!(
1165 manager.get_variables("--bg").await.len(),
1166 0,
1167 "Variable should be removed from variables map"
1168 );
1169
1170 let color_matches = manager.get_variables_by_color_key(&white_key).await;
1174 assert_eq!(
1175 color_matches.len(),
1176 0,
1177 "BUG: color index is stale, returns 0 instead of correctly handling removal"
1178 );
1179
1180 manager.rebuild_color_index().await;
1182 assert_eq!(
1183 manager.get_variables_by_color_key(&white_key).await.len(),
1184 0,
1185 "After rebuild, color index is correct"
1186 );
1187 }
1188
1189 #[tokio::test]
1192 async fn test_manager_important_flag() {
1193 let manager = CssVariableManager::new(Config::default());
1194
1195 let mut var = create_test_variable("--color", "red", ":root", "file:///test.css");
1196 var.important = true;
1197
1198 manager
1199 .add_variable(var)
1200 .await
1201 .expect("add_variable failed");
1202
1203 let vars = manager.get_variables("--color").await;
1204 assert_eq!(vars.len(), 1);
1205 assert!(vars[0].important);
1206 }
1207
1208 #[tokio::test]
1209 async fn test_manager_inline_flag() {
1210 let manager = CssVariableManager::new(Config::default());
1211
1212 let mut var = create_test_variable(
1213 "--inline-color",
1214 "green",
1215 "inline-style",
1216 "file:///test.html",
1217 );
1218 var.inline = true;
1219
1220 manager
1221 .add_variable(var)
1222 .await
1223 .expect("add_variable failed");
1224
1225 let vars = manager.get_variables("--inline-color").await;
1226 assert_eq!(vars.len(), 1);
1227 assert!(vars[0].inline);
1228 }
1229
1230 #[tokio::test]
1231 async fn test_manager_empty_queries() {
1232 let manager = CssVariableManager::new(Config::default());
1233
1234 let vars = manager.get_variables("--does-not-exist").await;
1236 assert_eq!(vars.len(), 0);
1237
1238 let usages = manager.get_usages("--does-not-exist").await;
1239 assert_eq!(usages.len(), 0);
1240
1241 let (defs, usages) = manager.get_references("--does-not-exist").await;
1242 assert_eq!(defs.len(), 0);
1243 assert_eq!(usages.len(), 0);
1244 }
1245
1246 #[tokio::test]
1253 async fn test_manager_has_memory_limits() {
1254 use ls_types::{Position, Range};
1255 use std::str::FromStr;
1256
1257 let config = Config {
1258 max_documents: 100,
1259 ..Default::default()
1260 };
1261
1262 let manager = CssVariableManager::new(config);
1263
1264 let mut success_count = 0;
1266 let mut failure_count = 0;
1267
1268 for i in 0..200 {
1269 let var = CssVariable {
1270 name: format!("--var-{}", i),
1271 value: "red".to_string(),
1272 selector: ":root".to_string(),
1273 range: Range::new(Position::new(0, 0), Position::new(0, 10)),
1274 name_range: None,
1275 value_range: None,
1276 uri: Uri::from_str(&format!("file:///test/doc_{}.css", i)).unwrap(),
1277 important: false,
1278 inline: false,
1279 source_position: 0,
1280 };
1281
1282 match manager.add_variable(var).await {
1283 Ok(()) => success_count += 1,
1284 Err(_) => failure_count += 1,
1285 }
1286 }
1287
1288 assert_eq!(
1290 success_count, 100,
1291 "Should successfully add exactly 100 documents (the limit)"
1292 );
1293 assert_eq!(
1294 failure_count, 100,
1295 "Should fail to add the remaining 100 documents beyond the limit"
1296 );
1297
1298 let vars = manager.variables.read().await;
1300 assert!(
1301 vars.len() <= 100,
1302 "Manager should not have more than 100 documents, but has {}",
1303 vars.len()
1304 );
1305 }
1306
1307 #[tokio::test]
1308 async fn test_add_and_remove_use_consistent_lock_order() {
1309 use tokio::time::{timeout, Duration};
1310
1311 let manager = CssVariableManager::new(Config::default());
1312 let existing_uri = Uri::from_str("file:///existing.css").unwrap();
1313 manager
1314 .add_variable(create_test_variable(
1315 "--existing",
1316 "red",
1317 ":root",
1318 existing_uri.as_str(),
1319 ))
1320 .await
1321 .unwrap();
1322
1323 let variables_guard = manager.variables.write().await;
1326 let remove_manager = manager.clone();
1327 let remove_task = tokio::spawn(async move {
1328 remove_manager.remove_document(&existing_uri).await;
1329 });
1330 tokio::task::yield_now().await;
1331
1332 let add_manager = manager.clone();
1333 let add_task = tokio::spawn(async move {
1334 add_manager
1335 .add_variable(create_test_variable(
1336 "--added",
1337 "blue",
1338 ":root",
1339 "file:///added.css",
1340 ))
1341 .await
1342 });
1343 tokio::task::yield_now().await;
1344 drop(variables_guard);
1345
1346 timeout(Duration::from_secs(1), async {
1347 remove_task.await.unwrap();
1348 add_task.await.unwrap().unwrap();
1349 })
1350 .await
1351 .expect("concurrent add/remove should not deadlock");
1352 }
1353
1354 #[tokio::test]
1363 async fn test_manager_color_index_concurrent_consistency() {
1364 use ls_types::{Position, Range};
1365 use std::str::FromStr;
1366 use std::sync::atomic::{AtomicUsize, Ordering};
1367 use std::sync::Arc;
1368
1369 let config = Config::default();
1370 let manager = CssVariableManager::new(config);
1371
1372 let inconsistencies = Arc::new(AtomicUsize::new(0));
1374
1375 let var = CssVariable {
1377 name: "--color-primary".to_string(),
1378 value: "#ff0000".to_string(),
1379 selector: ":root".to_string(),
1380 range: Range::new(Position::new(0, 0), Position::new(0, 10)),
1381 name_range: None,
1382 value_range: None,
1383 uri: Uri::from_str("file:///test/colors.css").unwrap(),
1384 important: false,
1385 inline: false,
1386 source_position: 0,
1387 };
1388 manager
1389 .add_variable(var)
1390 .await
1391 .expect("add_variable failed");
1392 manager.rebuild_color_index().await;
1393
1394 let mut handles = vec![];
1396
1397 for i in 0..100 {
1399 let manager_clone = manager.clone();
1400 handles.push(tokio::spawn(async move {
1401 let var = CssVariable {
1402 name: format!("--color-{}", i),
1403 value: format!("hsl({}, 100%, 50%)", i * 3),
1404 selector: ":root".to_string(),
1405 range: Range::new(Position::new(0, 0), Position::new(0, 10)),
1406 name_range: None,
1407 value_range: None,
1408 uri: Uri::from_str(&format!("file:///test/color_{}.css", i)).unwrap(),
1409 important: false,
1410 inline: false,
1411 source_position: 0,
1412 };
1413 manager_clone
1414 .add_variable(var)
1415 .await
1416 .expect("add_variable failed");
1417 manager_clone.rebuild_color_index().await;
1419 }));
1420 }
1421
1422 let inconsistencies_clone = inconsistencies.clone();
1424 let manager_reader = manager.clone();
1425 handles.push(tokio::spawn(async move {
1426 for _ in 0..50 {
1427 let all_vars = manager_reader.get_all_variables().await;
1429
1430 let color_count = all_vars
1432 .iter()
1433 .filter(|v| crate::color::parse_color(&v.value).is_some())
1434 .count();
1435
1436 manager_reader.rebuild_color_index().await;
1438
1439 let white_key = crate::color::normalized_color_key("white").unwrap();
1441 let white_matches = manager_reader.get_variables_by_color_key(&white_key).await;
1442
1443 if white_matches.len() > color_count + 10 {
1446 inconsistencies_clone.fetch_add(1, Ordering::SeqCst);
1447 }
1448
1449 tokio::time::sleep(tokio::time::Duration::from_micros(1)).await;
1450 }
1451 }));
1452
1453 for handle in handles {
1455 let _ = handle.await;
1456 }
1457
1458 assert_eq!(
1462 inconsistencies.load(Ordering::SeqCst),
1463 0,
1464 "Color index had {} inconsistencies during concurrent access",
1465 inconsistencies.load(Ordering::SeqCst)
1466 );
1467 }
1468}