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 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 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 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 assert_eq!(manager.get_variables("--primary").await.len(), 1);
267 assert_eq!(manager.get_usages("--primary").await.len(), 1);
268
269 manager.remove_document(&uri).await;
271
272 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 let var = create_test_variable("--theme", "dark", ":root", "file:///variables.css");
327 manager.add_variable(var).await;
328
329 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 assert_eq!(manager.get_variables("--color").await.len(), 2);
364
365 manager.remove_document(&uri1).await;
367
368 let vars = manager.get_variables("--color").await;
370 assert_eq!(vars.len(), 1);
371 assert_eq!(vars[0].value, "blue");
372 }
373
374 #[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 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}