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#[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_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 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 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 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 pub async fn get_config(&self) -> Config {
169 self.config.read().await.clone()
170 }
171
172 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 assert_eq!(manager.get_variables("--primary").await.len(), 1);
333 assert_eq!(manager.get_usages("--primary").await.len(), 1);
334
335 manager.remove_document(&uri).await;
337
338 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 let var = create_test_variable("--theme", "dark", ":root", "file:///variables.css");
393 manager.add_variable(var).await;
394
395 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 assert_eq!(manager.get_variables("--color").await.len(), 2);
430
431 manager.remove_document(&uri1).await;
433
434 let vars = manager.get_variables("--color").await;
436 assert_eq!(vars.len(), 1);
437 assert_eq!(vars[0].value, "blue");
438 }
439
440 #[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 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}