Skip to main content

css_variable_lsp/
manager.rs

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