Skip to main content

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    /// 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 tower_lsp::lsp_types::{Position, Range, Url};
222
223    fn create_test_variable(name: &str, value: &str, selector: &str, uri: &str) -> CssVariable {
224        CssVariable {
225            name: name.to_string(),
226            value: value.to_string(),
227            selector: selector.to_string(),
228            range: Range::new(Position::new(0, 0), Position::new(0, 10)),
229            name_range: None,
230            value_range: None,
231            uri: Url::parse(uri).unwrap(),
232            important: false,
233            inline: false,
234            source_position: 0,
235        }
236    }
237
238    fn create_test_usage(name: &str, context: &str, uri: &str) -> CssVariableUsage {
239        CssVariableUsage {
240            name: name.to_string(),
241            range: Range::new(Position::new(0, 0), Position::new(0, 10)),
242            name_range: None,
243            uri: Url::parse(uri).unwrap(),
244            usage_context: context.to_string(),
245            dom_node: None,
246        }
247    }
248
249    #[test]
250    fn extract_var_reference_allows_fallbacks_and_trailing_tokens() {
251        assert_eq!(
252            extract_var_reference("var(--primary, #fff)"),
253            Some("--primary".to_string())
254        );
255        assert_eq!(
256            extract_var_reference("var(--primary) !important"),
257            Some("--primary".to_string())
258        );
259        assert_eq!(
260            extract_var_reference("calc(1px + var(--spacing))"),
261            Some("--spacing".to_string())
262        );
263    }
264
265    #[tokio::test]
266    async fn test_manager_add_and_get_variables() {
267        let manager = CssVariableManager::new(Config::default());
268        let var = create_test_variable("--primary", "#3b82f6", ":root", "file:///test.css");
269
270        manager.add_variable(var.clone()).await;
271
272        let variables = manager.get_variables("--primary").await;
273        assert_eq!(variables.len(), 1);
274        assert_eq!(variables[0].name, "--primary");
275        assert_eq!(variables[0].value, "#3b82f6");
276    }
277
278    #[tokio::test]
279    async fn test_manager_multiple_definitions() {
280        let manager = CssVariableManager::new(Config::default());
281
282        let var1 = create_test_variable("--color", "red", ":root", "file:///test.css");
283        let var2 = create_test_variable("--color", "blue", ".class", "file:///test.css");
284
285        manager.add_variable(var1).await;
286        manager.add_variable(var2).await;
287
288        let variables = manager.get_variables("--color").await;
289        assert_eq!(variables.len(), 2);
290    }
291
292    #[tokio::test]
293    async fn test_manager_add_and_get_usages() {
294        let manager = CssVariableManager::new(Config::default());
295        let usage = create_test_usage("--primary", ".button", "file:///test.css");
296
297        manager.add_usage(usage.clone()).await;
298
299        let usages = manager.get_usages("--primary").await;
300        assert_eq!(usages.len(), 1);
301        assert_eq!(usages[0].name, "--primary");
302        assert_eq!(usages[0].usage_context, ".button");
303    }
304
305    #[tokio::test]
306    async fn test_manager_get_references() {
307        let manager = CssVariableManager::new(Config::default());
308
309        let var = create_test_variable("--spacing", "1rem", ":root", "file:///test.css");
310        let usage = create_test_usage("--spacing", ".card", "file:///test.css");
311
312        manager.add_variable(var).await;
313        manager.add_usage(usage).await;
314
315        let (defs, usages) = manager.get_references("--spacing").await;
316        assert_eq!(defs.len(), 1);
317        assert_eq!(usages.len(), 1);
318    }
319
320    #[tokio::test]
321    async fn test_manager_remove_document() {
322        let manager = CssVariableManager::new(Config::default());
323        let uri = Url::parse("file:///test.css").unwrap();
324
325        let var = create_test_variable("--primary", "blue", ":root", "file:///test.css");
326        let usage = create_test_usage("--primary", ".button", "file:///test.css");
327
328        manager.add_variable(var).await;
329        manager.add_usage(usage).await;
330
331        // Verify they exist
332        assert_eq!(manager.get_variables("--primary").await.len(), 1);
333        assert_eq!(manager.get_usages("--primary").await.len(), 1);
334
335        // Remove document
336        manager.remove_document(&uri).await;
337
338        // Verify they're gone
339        assert_eq!(manager.get_variables("--primary").await.len(), 0);
340        assert_eq!(manager.get_usages("--primary").await.len(), 0);
341    }
342
343    #[tokio::test]
344    async fn test_manager_get_all_variables() {
345        let manager = CssVariableManager::new(Config::default());
346
347        manager
348            .add_variable(create_test_variable(
349                "--primary",
350                "blue",
351                ":root",
352                "file:///test.css",
353            ))
354            .await;
355        manager
356            .add_variable(create_test_variable(
357                "--secondary",
358                "red",
359                ":root",
360                "file:///test.css",
361            ))
362            .await;
363        manager
364            .add_variable(create_test_variable(
365                "--spacing",
366                "1rem",
367                ":root",
368                "file:///test.css",
369            ))
370            .await;
371
372        let all_vars = manager.get_all_variables().await;
373        assert_eq!(all_vars.len(), 3);
374    }
375
376    #[tokio::test]
377    async fn test_manager_resolve_variable_color() {
378        let manager = CssVariableManager::new(Config::default());
379
380        let var = create_test_variable("--primary-color", "#3b82f6", ":root", "file:///test.css");
381        manager.add_variable(var).await;
382
383        let color = manager.resolve_variable_color("--primary-color").await;
384        assert!(color.is_some());
385    }
386
387    #[tokio::test]
388    async fn test_manager_cross_file_references() {
389        let manager = CssVariableManager::new(Config::default());
390
391        // Variable defined in one file
392        let var = create_test_variable("--theme", "dark", ":root", "file:///variables.css");
393        manager.add_variable(var).await;
394
395        // Used in another file
396        let usage = create_test_usage("--theme", ".app", "file:///app.css");
397        manager.add_usage(usage).await;
398
399        let (defs, usages) = manager.get_references("--theme").await;
400        assert_eq!(defs.len(), 1);
401        assert_eq!(usages.len(), 1);
402        assert_ne!(defs[0].uri, usages[0].uri);
403    }
404
405    #[tokio::test]
406    async fn test_manager_document_isolation() {
407        let manager = CssVariableManager::new(Config::default());
408        let uri1 = Url::parse("file:///file1.css").unwrap();
409        let _uri2 = Url::parse("file:///file2.css").unwrap();
410
411        manager
412            .add_variable(create_test_variable(
413                "--color",
414                "red",
415                ":root",
416                "file:///file1.css",
417            ))
418            .await;
419        manager
420            .add_variable(create_test_variable(
421                "--color",
422                "blue",
423                ":root",
424                "file:///file2.css",
425            ))
426            .await;
427
428        // Should have both definitions
429        assert_eq!(manager.get_variables("--color").await.len(), 2);
430
431        // Remove one document
432        manager.remove_document(&uri1).await;
433
434        // Should only have one definition now
435        let vars = manager.get_variables("--color").await;
436        assert_eq!(vars.len(), 1);
437        assert_eq!(vars[0].value, "blue");
438    }
439
440    // Note: extract_var_name is not a public function, so we skip testing it directly
441
442    #[tokio::test]
443    async fn test_manager_important_flag() {
444        let manager = CssVariableManager::new(Config::default());
445
446        let mut var = create_test_variable("--color", "red", ":root", "file:///test.css");
447        var.important = true;
448
449        manager.add_variable(var).await;
450
451        let vars = manager.get_variables("--color").await;
452        assert_eq!(vars.len(), 1);
453        assert!(vars[0].important);
454    }
455
456    #[tokio::test]
457    async fn test_manager_inline_flag() {
458        let manager = CssVariableManager::new(Config::default());
459
460        let mut var = create_test_variable(
461            "--inline-color",
462            "green",
463            "inline-style",
464            "file:///test.html",
465        );
466        var.inline = true;
467
468        manager.add_variable(var).await;
469
470        let vars = manager.get_variables("--inline-color").await;
471        assert_eq!(vars.len(), 1);
472        assert!(vars[0].inline);
473    }
474
475    #[tokio::test]
476    async fn test_manager_empty_queries() {
477        let manager = CssVariableManager::new(Config::default());
478
479        // Query for non-existent variable
480        let vars = manager.get_variables("--does-not-exist").await;
481        assert_eq!(vars.len(), 0);
482
483        let usages = manager.get_usages("--does-not-exist").await;
484        assert_eq!(usages.len(), 0);
485
486        let (defs, usages) = manager.get_references("--does-not-exist").await;
487        assert_eq!(defs.len(), 0);
488        assert_eq!(usages.len(), 0);
489    }
490}