Skip to main content

css_variable_lsp/
manager.rs

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/// Manages CSS variables across the workspace
14#[derive(Clone)]
15pub struct CssVariableManager {
16    /// Map of variable name -> list of definitions
17    variables: Arc<RwLock<HashMap<String, Vec<CssVariable>>>>,
18
19    /// Map of variable name -> list of usages
20    usages: Arc<RwLock<HashMap<String, Vec<CssVariableUsage>>>>,
21
22    /// Literal color occurrences grouped by document and line
23    /// Outer map: URI -> Inner map (line number -> colors on that line)
24    literal_colors: Arc<RwLock<LiteralColorMap>>,
25
26    /// Map of normalized colors to matching variable names
27    color_variables: Arc<RwLock<HashMap<NormalizedColorKey, HashSet<String>>>>,
28
29    /// Configuration
30    config: Arc<RwLock<Config>>,
31
32    /// DOM trees for HTML documents
33    dom_trees: Arc<RwLock<HashMap<Uri, DomTree>>>,
34
35    /// Set of tracked document URIs (for counting unique documents)
36    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    /// Add a variable definition
53    pub async fn add_variable(&self, variable: CssVariable) -> Result<(), String> {
54        let config = self.config.read().await;
55
56        // Check document limit
57        if config.max_documents > 0 {
58            let tracked = self.tracked_documents.read().await;
59            if tracked.contains(&variable.uri) {
60                // Document already tracked, no limit check needed
61            } 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            // Track this document
70            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    /// Add a variable usage
83    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    /// Add a literal color occurrence
92    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    /// Get all definitions of a variable
104    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    /// Get all usages of a variable
110    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    /// Resolve a variable name to a color using cascade ordering and var() chains.
116    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    /// Resolve a variable name to a normalized color key using cascade ordering and var() chains.
123    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    /// Get all variables (for completion)
129    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    /// Get all references (definitions + usages) for a variable
135    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    /// Get literal color occurrences in a specific document.
142    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    /// Get literal color occurrences at a specific position (O(1) line lookup + O(k) scan).
151    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    /// Get all variables whose resolved color exactly matches the normalized color key.
164    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    /// Get the set of resolved variable colors currently defined in a specific document.
185    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    /// Remove all data for a document
195    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        // Remove from tracked documents
203        tracked.remove(uri);
204
205        // Remove variables from this document
206        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        // Remove usages from this document
212        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        // FIX: Rebuild color index to remove stale entries
221        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    /// Get all variables defined in a specific document
230    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    /// Get the set of variable names defined in a specific document
240    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    /// Get all variable usages in a specific document
246    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    /// Set DOM tree for a document
257    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    /// Get DOM tree for a document
263    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    /// Get current configuration
269    pub async fn get_config(&self) -> Config {
270        self.config.read().await.clone()
271    }
272
273    /// Replace the current configuration.
274    pub async fn set_config(&self, config: Config) {
275        let mut stored = self.config.write().await;
276        *stored = config;
277    }
278
279    /// Rebuild the normalized-color -> variable-name lookup from current workspace state.
280    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(&current) {
346            return None;
347        }
348        seen.insert(current.clone());
349
350        let mut definitions = variables.get(&current)?.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        // Verify they exist
514        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        // Remove document
519        manager.remove_document(&uri).await;
520
521        // Verify they're gone
522        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        // Variable defined in one file
674        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        // Used in another file
681        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        // Should have both definitions
716        assert_eq!(manager.get_variables("--color").await.len(), 2);
717
718        // Remove one document
719        manager.remove_document(&uri1).await;
720
721        // Should only have one definition now
722        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        // Regression test: color_variables becomes stale after remove_document()
730        // when rebuild_color_index() is not called
731        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        // Add a color variable and build the index
736        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        // Verify it's indexed
744        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        // Remove document WITHOUT rebuilding index (simulates the bug)
751        manager.remove_document(&uri).await;
752
753        // After removal, the variable should be gone from both collections
754        assert_eq!(
755            manager.get_variables("--bg").await.len(),
756            0,
757            "Variable should be removed from variables map"
758        );
759
760        // The bug: color_variables still contains the stale entry,
761        // but get_variables_by_color_key() silently skips names not found in variables.
762        // This test verifies the current (buggy) behavior - returns 0 matches.
763        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        // Workaround: manually rebuild to get correct behavior
771        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    // Note: extract_var_name is not a public function, so we skip testing it directly
780
781    #[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        // Query for non-existent variable
825        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    /// Memory limit enforcement in CssVariableManager
837    ///
838    /// ISSUE: The manager uses unbounded HashMaps that can grow indefinitely.
839    /// Large workspaces could accumulate many documents without cleanup.
840    ///
841    /// After fix: Should have a document limit enforced.
842    #[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        // Try to add 200 documents (beyond the limit of 100)
855        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        // Verify the limit was enforced
879        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        // Check how many documents are actually stored
889        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    /// Bug demonstration: Color index can be stale during concurrent access
897    ///
898    /// ISSUE: rebuild_color_index() reads from variables and writes to color_variables.
899    /// While individual operations are atomic, there's a brief window where the
900    /// color index could be stale during concurrent updates.
901    ///
902    /// EXPECTED TO FAIL: This test proves the race condition exists.
903    /// After fix: Color index should be properly synchronized.
904    #[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        // Track inconsistencies between color index and variables
915        let inconsistencies = Arc::new(AtomicUsize::new(0));
916
917        // Add initial color variables
918        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        // Spawn concurrent readers and writers
937        let mut handles = vec![];
938
939        // Writer: Continuously add color variables
940        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                // Rebuild index after each add to simulate real usage
960                manager_clone.rebuild_color_index().await;
961            }));
962        }
963
964        // Reader: Continuously check color index consistency
965        let inconsistencies_clone = inconsistencies.clone();
966        let manager_reader = manager.clone();
967        handles.push(tokio::spawn(async move {
968            for _ in 0..50 {
969                // Get all variables
970                let all_vars = manager_reader.get_all_variables().await;
971
972                // Count color variables by checking if they have color values
973                let color_count = all_vars
974                    .iter()
975                    .filter(|v| crate::color::parse_color(&v.value).is_some())
976                    .count();
977
978                // Rebuild and check the color index
979                manager_reader.rebuild_color_index().await;
980
981                // Get variables by a sample color key
982                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 the counts are wildly different, there's an inconsistency
986                // (This is a simplified check - real race conditions are harder to detect)
987                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        // Wait for all operations
996        for handle in handles {
997            let _ = handle.await;
998        }
999
1000        // BUG: Currently this assertion will FAIL because race condition exists
1001        // The color index may be temporarily stale during concurrent updates
1002        // After fix: inconsistencies should be 0
1003        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}