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