css_variable_lsp/
manager.rs

1use std::collections::HashMap;
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 all variable usages in a specific document
139    pub async fn get_document_usages(&self, uri: &Url) -> Vec<CssVariableUsage> {
140        let usages = self.usages.read().await;
141        usages
142            .values()
143            .flatten()
144            .filter(|u| &u.uri == uri)
145            .cloned()
146            .collect()
147    }
148
149    /// Set DOM tree for a document
150    pub async fn set_dom_tree(&self, uri: Url, dom_tree: DomTree) {
151        let mut dom_trees = self.dom_trees.write().await;
152        dom_trees.insert(uri, dom_tree);
153    }
154
155    /// Get DOM tree for a document
156    pub async fn get_dom_tree(&self, uri: &Url) -> Option<DomTree> {
157        let dom_trees = self.dom_trees.read().await;
158        dom_trees.get(uri).cloned()
159    }
160
161    /// Get current configuration
162    pub async fn get_config(&self) -> Config {
163        self.config.read().await.clone()
164    }
165}
166
167fn extract_var_reference(value: &str) -> Option<String> {
168    let trimmed = value.trim();
169    let start = trimmed.find("var(")?;
170    let mut idx = start + 4;
171    let bytes = trimmed.as_bytes();
172    let mut depth = 1i32;
173    while idx < bytes.len() {
174        match bytes[idx] {
175            b'(' => depth += 1,
176            b')' => {
177                depth -= 1;
178                if depth == 0 {
179                    break;
180                }
181            }
182            _ => {}
183        }
184        idx += 1;
185    }
186    if depth != 0 {
187        return None;
188    }
189
190    let inner = trimmed[start + 4..idx].trim_start();
191    let inner = inner.strip_prefix("--")?;
192    let mut name_len = 0usize;
193    for ch in inner.chars() {
194        if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
195            name_len += ch.len_utf8();
196        } else {
197            break;
198        }
199    }
200    if name_len == 0 {
201        return None;
202    }
203    Some(format!("--{}", &inner[..name_len]))
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use tower_lsp::lsp_types::{Position, Range, Url};
210
211    fn create_test_variable(name: &str, value: &str, selector: &str, uri: &str) -> CssVariable {
212        CssVariable {
213            name: name.to_string(),
214            value: value.to_string(),
215            selector: selector.to_string(),
216            range: Range::new(Position::new(0, 0), Position::new(0, 10)),
217            name_range: None,
218            value_range: None,
219            uri: Url::parse(uri).unwrap(),
220            important: false,
221            inline: false,
222            source_position: 0,
223        }
224    }
225
226    fn create_test_usage(name: &str, context: &str, uri: &str) -> CssVariableUsage {
227        CssVariableUsage {
228            name: name.to_string(),
229            range: Range::new(Position::new(0, 0), Position::new(0, 10)),
230            name_range: None,
231            uri: Url::parse(uri).unwrap(),
232            usage_context: context.to_string(),
233            dom_node: None,
234        }
235    }
236
237    #[test]
238    fn extract_var_reference_allows_fallbacks_and_trailing_tokens() {
239        assert_eq!(
240            extract_var_reference("var(--primary, #fff)"),
241            Some("--primary".to_string())
242        );
243        assert_eq!(
244            extract_var_reference("var(--primary) !important"),
245            Some("--primary".to_string())
246        );
247        assert_eq!(
248            extract_var_reference("calc(1px + var(--spacing))"),
249            Some("--spacing".to_string())
250        );
251    }
252
253    #[tokio::test]
254    async fn test_manager_add_and_get_variables() {
255        let manager = CssVariableManager::new(Config::default());
256        let var = create_test_variable("--primary", "#3b82f6", ":root", "file:///test.css");
257
258        manager.add_variable(var.clone()).await;
259
260        let variables = manager.get_variables("--primary").await;
261        assert_eq!(variables.len(), 1);
262        assert_eq!(variables[0].name, "--primary");
263        assert_eq!(variables[0].value, "#3b82f6");
264    }
265
266    #[tokio::test]
267    async fn test_manager_multiple_definitions() {
268        let manager = CssVariableManager::new(Config::default());
269
270        let var1 = create_test_variable("--color", "red", ":root", "file:///test.css");
271        let var2 = create_test_variable("--color", "blue", ".class", "file:///test.css");
272
273        manager.add_variable(var1).await;
274        manager.add_variable(var2).await;
275
276        let variables = manager.get_variables("--color").await;
277        assert_eq!(variables.len(), 2);
278    }
279
280    #[tokio::test]
281    async fn test_manager_add_and_get_usages() {
282        let manager = CssVariableManager::new(Config::default());
283        let usage = create_test_usage("--primary", ".button", "file:///test.css");
284
285        manager.add_usage(usage.clone()).await;
286
287        let usages = manager.get_usages("--primary").await;
288        assert_eq!(usages.len(), 1);
289        assert_eq!(usages[0].name, "--primary");
290        assert_eq!(usages[0].usage_context, ".button");
291    }
292
293    #[tokio::test]
294    async fn test_manager_get_references() {
295        let manager = CssVariableManager::new(Config::default());
296
297        let var = create_test_variable("--spacing", "1rem", ":root", "file:///test.css");
298        let usage = create_test_usage("--spacing", ".card", "file:///test.css");
299
300        manager.add_variable(var).await;
301        manager.add_usage(usage).await;
302
303        let (defs, usages) = manager.get_references("--spacing").await;
304        assert_eq!(defs.len(), 1);
305        assert_eq!(usages.len(), 1);
306    }
307
308    #[tokio::test]
309    async fn test_manager_remove_document() {
310        let manager = CssVariableManager::new(Config::default());
311        let uri = Url::parse("file:///test.css").unwrap();
312
313        let var = create_test_variable("--primary", "blue", ":root", "file:///test.css");
314        let usage = create_test_usage("--primary", ".button", "file:///test.css");
315
316        manager.add_variable(var).await;
317        manager.add_usage(usage).await;
318
319        // Verify they exist
320        assert_eq!(manager.get_variables("--primary").await.len(), 1);
321        assert_eq!(manager.get_usages("--primary").await.len(), 1);
322
323        // Remove document
324        manager.remove_document(&uri).await;
325
326        // Verify they're gone
327        assert_eq!(manager.get_variables("--primary").await.len(), 0);
328        assert_eq!(manager.get_usages("--primary").await.len(), 0);
329    }
330
331    #[tokio::test]
332    async fn test_manager_get_all_variables() {
333        let manager = CssVariableManager::new(Config::default());
334
335        manager
336            .add_variable(create_test_variable(
337                "--primary",
338                "blue",
339                ":root",
340                "file:///test.css",
341            ))
342            .await;
343        manager
344            .add_variable(create_test_variable(
345                "--secondary",
346                "red",
347                ":root",
348                "file:///test.css",
349            ))
350            .await;
351        manager
352            .add_variable(create_test_variable(
353                "--spacing",
354                "1rem",
355                ":root",
356                "file:///test.css",
357            ))
358            .await;
359
360        let all_vars = manager.get_all_variables().await;
361        assert_eq!(all_vars.len(), 3);
362    }
363
364    #[tokio::test]
365    async fn test_manager_resolve_variable_color() {
366        let manager = CssVariableManager::new(Config::default());
367
368        let var = create_test_variable("--primary-color", "#3b82f6", ":root", "file:///test.css");
369        manager.add_variable(var).await;
370
371        let color = manager.resolve_variable_color("--primary-color").await;
372        assert!(color.is_some());
373    }
374
375    #[tokio::test]
376    async fn test_manager_cross_file_references() {
377        let manager = CssVariableManager::new(Config::default());
378
379        // Variable defined in one file
380        let var = create_test_variable("--theme", "dark", ":root", "file:///variables.css");
381        manager.add_variable(var).await;
382
383        // Used in another file
384        let usage = create_test_usage("--theme", ".app", "file:///app.css");
385        manager.add_usage(usage).await;
386
387        let (defs, usages) = manager.get_references("--theme").await;
388        assert_eq!(defs.len(), 1);
389        assert_eq!(usages.len(), 1);
390        assert_ne!(defs[0].uri, usages[0].uri);
391    }
392
393    #[tokio::test]
394    async fn test_manager_document_isolation() {
395        let manager = CssVariableManager::new(Config::default());
396        let uri1 = Url::parse("file:///file1.css").unwrap();
397        let _uri2 = Url::parse("file:///file2.css").unwrap();
398
399        manager
400            .add_variable(create_test_variable(
401                "--color",
402                "red",
403                ":root",
404                "file:///file1.css",
405            ))
406            .await;
407        manager
408            .add_variable(create_test_variable(
409                "--color",
410                "blue",
411                ":root",
412                "file:///file2.css",
413            ))
414            .await;
415
416        // Should have both definitions
417        assert_eq!(manager.get_variables("--color").await.len(), 2);
418
419        // Remove one document
420        manager.remove_document(&uri1).await;
421
422        // Should only have one definition now
423        let vars = manager.get_variables("--color").await;
424        assert_eq!(vars.len(), 1);
425        assert_eq!(vars[0].value, "blue");
426    }
427
428    // Note: extract_var_name is not a public function, so we skip testing it directly
429
430    #[tokio::test]
431    async fn test_manager_important_flag() {
432        let manager = CssVariableManager::new(Config::default());
433
434        let mut var = create_test_variable("--color", "red", ":root", "file:///test.css");
435        var.important = true;
436
437        manager.add_variable(var).await;
438
439        let vars = manager.get_variables("--color").await;
440        assert_eq!(vars.len(), 1);
441        assert!(vars[0].important);
442    }
443
444    #[tokio::test]
445    async fn test_manager_inline_flag() {
446        let manager = CssVariableManager::new(Config::default());
447
448        let mut var = create_test_variable(
449            "--inline-color",
450            "green",
451            "inline-style",
452            "file:///test.html",
453        );
454        var.inline = true;
455
456        manager.add_variable(var).await;
457
458        let vars = manager.get_variables("--inline-color").await;
459        assert_eq!(vars.len(), 1);
460        assert!(vars[0].inline);
461    }
462
463    #[tokio::test]
464    async fn test_manager_empty_queries() {
465        let manager = CssVariableManager::new(Config::default());
466
467        // Query for non-existent variable
468        let vars = manager.get_variables("--does-not-exist").await;
469        assert_eq!(vars.len(), 0);
470
471        let usages = manager.get_usages("--does-not-exist").await;
472        assert_eq!(usages.len(), 0);
473
474        let (defs, usages) = manager.get_references("--does-not-exist").await;
475        assert_eq!(defs.len(), 0);
476        assert_eq!(usages.len(), 0);
477    }
478}