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#[derive(Clone)]
13pub struct CssVariableManager {
14 variables: Arc<RwLock<HashMap<String, Vec<CssVariable>>>>,
16
17 usages: Arc<RwLock<HashMap<String, Vec<CssVariableUsage>>>>,
19
20 config: Arc<RwLock<Config>>,
22
23 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 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 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 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 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 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(¤t) {
73 return None;
74 }
75 seen.insert(current.clone());
76
77 let mut variables = self.get_variables(¤t).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 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 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 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 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 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 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 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 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 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 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 assert_eq!(manager.get_variables("--primary").await.len(), 1);
321 assert_eq!(manager.get_usages("--primary").await.len(), 1);
322
323 manager.remove_document(&uri).await;
325
326 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 let var = create_test_variable("--theme", "dark", ":root", "file:///variables.css");
381 manager.add_variable(var).await;
382
383 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 assert_eq!(manager.get_variables("--color").await.len(), 2);
418
419 manager.remove_document(&uri1).await;
421
422 let vars = manager.get_variables("--color").await;
424 assert_eq!(vars.len(), 1);
425 assert_eq!(vars[0].value, "blue");
426 }
427
428 #[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 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}