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