css_variable_lsp/
manager.rs

1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3use tokio::sync::RwLock;
4use tower_lsp::lsp_types::Url;
5
6use crate::color::parse_color;
7use crate::dom_tree::DomTree;
8use crate::specificity::sort_by_cascade;
9use crate::types::{Config, CssVariable, CssVariableUsage};
10
11/// Manages CSS variables across the workspace
12#[derive(Clone)]
13pub struct CssVariableManager {
14    /// Map of variable name -> list of definitions
15    variables: Arc<RwLock<HashMap<String, Vec<CssVariable>>>>,
16
17    /// Map of variable name -> list of usages
18    usages: Arc<RwLock<HashMap<String, Vec<CssVariableUsage>>>>,
19
20    /// Configuration
21    config: Arc<RwLock<Config>>,
22
23    /// DOM trees for HTML documents
24    dom_trees: Arc<RwLock<HashMap<Url, DomTree>>>,
25}
26
27impl CssVariableManager {
28    pub fn new(config: Config) -> Self {
29        Self {
30            variables: Arc::new(RwLock::new(HashMap::new())),
31            usages: Arc::new(RwLock::new(HashMap::new())),
32            config: Arc::new(RwLock::new(config)),
33            dom_trees: Arc::new(RwLock::new(HashMap::new())),
34        }
35    }
36
37    /// Add a variable definition
38    pub async fn add_variable(&self, variable: CssVariable) {
39        let mut vars = self.variables.write().await;
40        vars.entry(variable.name.clone())
41            .or_insert_with(Vec::new)
42            .push(variable);
43    }
44
45    /// Add a variable usage
46    pub async fn add_usage(&self, usage: CssVariableUsage) {
47        let mut usages = self.usages.write().await;
48        usages
49            .entry(usage.name.clone())
50            .or_insert_with(Vec::new)
51            .push(usage);
52    }
53
54    /// Get all definitions of a variable
55    pub async fn get_variables(&self, name: &str) -> Vec<CssVariable> {
56        let vars = self.variables.read().await;
57        vars.get(name).cloned().unwrap_or_default()
58    }
59
60    /// Get all usages of a variable
61    pub async fn get_usages(&self, name: &str) -> Vec<CssVariableUsage> {
62        let usages = self.usages.read().await;
63        usages.get(name).cloned().unwrap_or_default()
64    }
65
66    /// Resolve a variable name to a color using cascade ordering and var() chains.
67    pub async fn resolve_variable_color(&self, name: &str) -> Option<tower_lsp::lsp_types::Color> {
68        let mut seen = std::collections::HashSet::new();
69        let mut current = name.to_string();
70
71        loop {
72            if seen.contains(&current) {
73                return None;
74            }
75            seen.insert(current.clone());
76
77            let mut variables = self.get_variables(&current).await;
78            if variables.is_empty() {
79                return None;
80            }
81
82            sort_by_cascade(&mut variables);
83            let variable = &variables[0];
84
85            if let Some(next_name) = extract_var_reference(&variable.value) {
86                current = next_name;
87                continue;
88            }
89
90            return parse_color(&variable.value);
91        }
92    }
93
94    /// Get all variables (for completion)
95    pub async fn get_all_variables(&self) -> Vec<CssVariable> {
96        let vars = self.variables.read().await;
97        vars.values().flatten().cloned().collect()
98    }
99
100    /// Get all references (definitions + usages) for a variable
101    pub async fn get_references(&self, name: &str) -> (Vec<CssVariable>, Vec<CssVariableUsage>) {
102        let definitions = self.get_variables(name).await;
103        let usages = self.get_usages(name).await;
104        (definitions, usages)
105    }
106
107    /// Remove all data for a document
108    pub async fn remove_document(&self, uri: &Url) {
109        let mut vars = self.variables.write().await;
110        let mut usages = self.usages.write().await;
111        let mut dom_trees = self.dom_trees.write().await;
112
113        // Remove variables from this document
114        for (_, var_list) in vars.iter_mut() {
115            var_list.retain(|v| &v.uri != uri);
116        }
117        vars.retain(|_, var_list| !var_list.is_empty());
118
119        // Remove usages from this document
120        for (_, usage_list) in usages.iter_mut() {
121            usage_list.retain(|u| &u.uri != uri);
122        }
123        usages.retain(|_, usage_list| !usage_list.is_empty());
124
125        dom_trees.remove(uri);
126    }
127
128    /// Get all variables defined in a specific document
129    pub async fn get_document_variables(&self, uri: &Url) -> Vec<CssVariable> {
130        let vars = self.variables.read().await;
131        vars.values()
132            .flatten()
133            .filter(|v| &v.uri == uri)
134            .cloned()
135            .collect()
136    }
137
138    /// Get the set of variable names defined in a specific document
139    pub async fn get_document_variable_names(&self, uri: &Url) -> HashSet<String> {
140        let vars = self.get_document_variables(uri).await;
141        vars.into_iter().map(|v| v.name).collect()
142    }
143
144    /// Get all variable usages in a specific document
145    pub async fn get_document_usages(&self, uri: &Url) -> Vec<CssVariableUsage> {
146        let usages = self.usages.read().await;
147        usages
148            .values()
149            .flatten()
150            .filter(|u| &u.uri == uri)
151            .cloned()
152            .collect()
153    }
154
155    /// Set DOM tree for a document
156    pub async fn set_dom_tree(&self, uri: Url, dom_tree: DomTree) {
157        let mut dom_trees = self.dom_trees.write().await;
158        dom_trees.insert(uri, dom_tree);
159    }
160
161    /// Get DOM tree for a document
162    pub async fn get_dom_tree(&self, uri: &Url) -> Option<DomTree> {
163        let dom_trees = self.dom_trees.read().await;
164        dom_trees.get(uri).cloned()
165    }
166
167    /// Get current configuration
168    pub async fn get_config(&self) -> Config {
169        self.config.read().await.clone()
170    }
171}
172
173fn extract_var_reference(value: &str) -> Option<String> {
174    let trimmed = value.trim();
175    let start = trimmed.find("var(")?;
176    let mut idx = start + 4;
177    let bytes = trimmed.as_bytes();
178    let mut depth = 1i32;
179    while idx < bytes.len() {
180        match bytes[idx] {
181            b'(' => depth += 1,
182            b')' => {
183                depth -= 1;
184                if depth == 0 {
185                    break;
186                }
187            }
188            _ => {}
189        }
190        idx += 1;
191    }
192    if depth != 0 {
193        return None;
194    }
195
196    let inner = trimmed[start + 4..idx].trim_start();
197    let inner = inner.strip_prefix("--")?;
198    let mut name_len = 0usize;
199    for ch in inner.chars() {
200        if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
201            name_len += ch.len_utf8();
202        } else {
203            break;
204        }
205    }
206    if name_len == 0 {
207        return None;
208    }
209    Some(format!("--{}", &inner[..name_len]))
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215    use tower_lsp::lsp_types::{Position, Range, Url};
216
217    fn create_test_variable(name: &str, value: &str, selector: &str, uri: &str) -> CssVariable {
218        CssVariable {
219            name: name.to_string(),
220            value: value.to_string(),
221            selector: selector.to_string(),
222            range: Range::new(Position::new(0, 0), Position::new(0, 10)),
223            name_range: None,
224            value_range: None,
225            uri: Url::parse(uri).unwrap(),
226            important: false,
227            inline: false,
228            source_position: 0,
229        }
230    }
231
232    fn create_test_usage(name: &str, context: &str, uri: &str) -> CssVariableUsage {
233        CssVariableUsage {
234            name: name.to_string(),
235            range: Range::new(Position::new(0, 0), Position::new(0, 10)),
236            name_range: None,
237            uri: Url::parse(uri).unwrap(),
238            usage_context: context.to_string(),
239            dom_node: None,
240        }
241    }
242
243    #[test]
244    fn extract_var_reference_allows_fallbacks_and_trailing_tokens() {
245        assert_eq!(
246            extract_var_reference("var(--primary, #fff)"),
247            Some("--primary".to_string())
248        );
249        assert_eq!(
250            extract_var_reference("var(--primary) !important"),
251            Some("--primary".to_string())
252        );
253        assert_eq!(
254            extract_var_reference("calc(1px + var(--spacing))"),
255            Some("--spacing".to_string())
256        );
257    }
258
259    #[tokio::test]
260    async fn test_manager_add_and_get_variables() {
261        let manager = CssVariableManager::new(Config::default());
262        let var = create_test_variable("--primary", "#3b82f6", ":root", "file:///test.css");
263
264        manager.add_variable(var.clone()).await;
265
266        let variables = manager.get_variables("--primary").await;
267        assert_eq!(variables.len(), 1);
268        assert_eq!(variables[0].name, "--primary");
269        assert_eq!(variables[0].value, "#3b82f6");
270    }
271
272    #[tokio::test]
273    async fn test_manager_multiple_definitions() {
274        let manager = CssVariableManager::new(Config::default());
275
276        let var1 = create_test_variable("--color", "red", ":root", "file:///test.css");
277        let var2 = create_test_variable("--color", "blue", ".class", "file:///test.css");
278
279        manager.add_variable(var1).await;
280        manager.add_variable(var2).await;
281
282        let variables = manager.get_variables("--color").await;
283        assert_eq!(variables.len(), 2);
284    }
285
286    #[tokio::test]
287    async fn test_manager_add_and_get_usages() {
288        let manager = CssVariableManager::new(Config::default());
289        let usage = create_test_usage("--primary", ".button", "file:///test.css");
290
291        manager.add_usage(usage.clone()).await;
292
293        let usages = manager.get_usages("--primary").await;
294        assert_eq!(usages.len(), 1);
295        assert_eq!(usages[0].name, "--primary");
296        assert_eq!(usages[0].usage_context, ".button");
297    }
298
299    #[tokio::test]
300    async fn test_manager_get_references() {
301        let manager = CssVariableManager::new(Config::default());
302
303        let var = create_test_variable("--spacing", "1rem", ":root", "file:///test.css");
304        let usage = create_test_usage("--spacing", ".card", "file:///test.css");
305
306        manager.add_variable(var).await;
307        manager.add_usage(usage).await;
308
309        let (defs, usages) = manager.get_references("--spacing").await;
310        assert_eq!(defs.len(), 1);
311        assert_eq!(usages.len(), 1);
312    }
313
314    #[tokio::test]
315    async fn test_manager_remove_document() {
316        let manager = CssVariableManager::new(Config::default());
317        let uri = Url::parse("file:///test.css").unwrap();
318
319        let var = create_test_variable("--primary", "blue", ":root", "file:///test.css");
320        let usage = create_test_usage("--primary", ".button", "file:///test.css");
321
322        manager.add_variable(var).await;
323        manager.add_usage(usage).await;
324
325        // Verify they exist
326        assert_eq!(manager.get_variables("--primary").await.len(), 1);
327        assert_eq!(manager.get_usages("--primary").await.len(), 1);
328
329        // Remove document
330        manager.remove_document(&uri).await;
331
332        // Verify they're gone
333        assert_eq!(manager.get_variables("--primary").await.len(), 0);
334        assert_eq!(manager.get_usages("--primary").await.len(), 0);
335    }
336
337    #[tokio::test]
338    async fn test_manager_get_all_variables() {
339        let manager = CssVariableManager::new(Config::default());
340
341        manager
342            .add_variable(create_test_variable(
343                "--primary",
344                "blue",
345                ":root",
346                "file:///test.css",
347            ))
348            .await;
349        manager
350            .add_variable(create_test_variable(
351                "--secondary",
352                "red",
353                ":root",
354                "file:///test.css",
355            ))
356            .await;
357        manager
358            .add_variable(create_test_variable(
359                "--spacing",
360                "1rem",
361                ":root",
362                "file:///test.css",
363            ))
364            .await;
365
366        let all_vars = manager.get_all_variables().await;
367        assert_eq!(all_vars.len(), 3);
368    }
369
370    #[tokio::test]
371    async fn test_manager_resolve_variable_color() {
372        let manager = CssVariableManager::new(Config::default());
373
374        let var = create_test_variable("--primary-color", "#3b82f6", ":root", "file:///test.css");
375        manager.add_variable(var).await;
376
377        let color = manager.resolve_variable_color("--primary-color").await;
378        assert!(color.is_some());
379    }
380
381    #[tokio::test]
382    async fn test_manager_cross_file_references() {
383        let manager = CssVariableManager::new(Config::default());
384
385        // Variable defined in one file
386        let var = create_test_variable("--theme", "dark", ":root", "file:///variables.css");
387        manager.add_variable(var).await;
388
389        // Used in another file
390        let usage = create_test_usage("--theme", ".app", "file:///app.css");
391        manager.add_usage(usage).await;
392
393        let (defs, usages) = manager.get_references("--theme").await;
394        assert_eq!(defs.len(), 1);
395        assert_eq!(usages.len(), 1);
396        assert_ne!(defs[0].uri, usages[0].uri);
397    }
398
399    #[tokio::test]
400    async fn test_manager_document_isolation() {
401        let manager = CssVariableManager::new(Config::default());
402        let uri1 = Url::parse("file:///file1.css").unwrap();
403        let _uri2 = Url::parse("file:///file2.css").unwrap();
404
405        manager
406            .add_variable(create_test_variable(
407                "--color",
408                "red",
409                ":root",
410                "file:///file1.css",
411            ))
412            .await;
413        manager
414            .add_variable(create_test_variable(
415                "--color",
416                "blue",
417                ":root",
418                "file:///file2.css",
419            ))
420            .await;
421
422        // Should have both definitions
423        assert_eq!(manager.get_variables("--color").await.len(), 2);
424
425        // Remove one document
426        manager.remove_document(&uri1).await;
427
428        // Should only have one definition now
429        let vars = manager.get_variables("--color").await;
430        assert_eq!(vars.len(), 1);
431        assert_eq!(vars[0].value, "blue");
432    }
433
434    // Note: extract_var_name is not a public function, so we skip testing it directly
435
436    #[tokio::test]
437    async fn test_manager_important_flag() {
438        let manager = CssVariableManager::new(Config::default());
439
440        let mut var = create_test_variable("--color", "red", ":root", "file:///test.css");
441        var.important = true;
442
443        manager.add_variable(var).await;
444
445        let vars = manager.get_variables("--color").await;
446        assert_eq!(vars.len(), 1);
447        assert!(vars[0].important);
448    }
449
450    #[tokio::test]
451    async fn test_manager_inline_flag() {
452        let manager = CssVariableManager::new(Config::default());
453
454        let mut var = create_test_variable(
455            "--inline-color",
456            "green",
457            "inline-style",
458            "file:///test.html",
459        );
460        var.inline = true;
461
462        manager.add_variable(var).await;
463
464        let vars = manager.get_variables("--inline-color").await;
465        assert_eq!(vars.len(), 1);
466        assert!(vars[0].inline);
467    }
468
469    #[tokio::test]
470    async fn test_manager_empty_queries() {
471        let manager = CssVariableManager::new(Config::default());
472
473        // Query for non-existent variable
474        let vars = manager.get_variables("--does-not-exist").await;
475        assert_eq!(vars.len(), 0);
476
477        let usages = manager.get_usages("--does-not-exist").await;
478        assert_eq!(usages.len(), 0);
479
480        let (defs, usages) = manager.get_references("--does-not-exist").await;
481        assert_eq!(defs.len(), 0);
482        assert_eq!(usages.len(), 0);
483    }
484}