Skip to main content

css_variable_lsp/
types.rs

1use ls_types::{Position, Range, Uri};
2use serde::{Deserialize, Serialize};
3
4use crate::color::NormalizedColorKey;
5use crate::runtime_config::RuntimeConfig;
6
7/// Represents a CSS variable definition
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct CssVariable {
10    /// Variable name (e.g., "--primary-color")
11    pub name: String,
12
13    /// Variable value (e.g., "#3b82f6")
14    pub value: String,
15
16    /// Document URI where the variable is defined
17    pub uri: Uri,
18
19    /// Range of the entire declaration (e.g., "--foo: red")
20    pub range: Range,
21
22    /// Range of just the variable name (e.g., "--foo")
23    pub name_range: Option<Range>,
24
25    /// Range of just the value part (e.g., "red")
26    pub value_range: Option<Range>,
27
28    /// CSS selector where this variable is defined (e.g., ":root", "div", ".class")
29    pub selector: String,
30
31    /// Whether this definition uses !important
32    pub important: bool,
33
34    /// Whether this definition is from an inline style attribute
35    pub inline: bool,
36
37    /// Character position in file (for source order in cascade)
38    pub source_position: usize,
39}
40
41/// Represents a CSS variable usage (var() call)
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct CssVariableUsage {
44    /// Variable name being used
45    pub name: String,
46
47    /// Document URI where the variable is used
48    pub uri: Uri,
49
50    /// Range of the var() call
51    pub range: Range,
52
53    /// Range of just the variable name in var()
54    pub name_range: Option<Range>,
55
56    /// CSS selector context where variable is used
57    pub usage_context: String,
58
59    /// DOM node info if usage is in HTML (for inline styles)
60    pub dom_node: Option<DOMNodeInfo>,
61}
62
63/// Represents a literal color token found in a CSS value.
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct LiteralColorOccurrence {
66    /// Original token text (for example `#fff` or `rgb(255, 255, 255)`)
67    pub text: String,
68
69    /// Document URI where the token appears
70    pub uri: Uri,
71
72    /// Range of just the literal color token
73    pub range: Range,
74
75    /// Selector or context where the token appears
76    pub usage_context: String,
77
78    /// Normalized RGBA key used for exact matching
79    pub normalized_color: NormalizedColorKey,
80}
81
82/// Information about a DOM node
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct DOMNodeInfo {
85    /// Tag name (e.g., "div", "span")
86    pub tag: String,
87
88    /// ID attribute if present
89    pub id: Option<String>,
90
91    /// Classes
92    pub classes: Vec<String>,
93
94    /// Position in document
95    pub position: usize,
96
97    /// Internal node index for selector matching
98    pub node_index: Option<usize>,
99}
100
101/// Configuration settings
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct Config {
104    /// File patterns to scan for CSS variables
105    pub lookup_files: Vec<String>,
106
107    /// Glob patterns to ignore
108    pub ignore_globs: Vec<String>,
109
110    /// Enable color provider
111    pub enable_color_provider: bool,
112
113    /// Only show colors on variables (not inline values)
114    pub color_only_on_variables: bool,
115
116    /// Maximum number of documents to track (0 = unlimited)
117    pub max_documents: usize,
118}
119
120impl Default for Config {
121    fn default() -> Self {
122        Self {
123            lookup_files: vec![
124                "**/*.css".to_string(),
125                "**/*.scss".to_string(),
126                "**/*.sass".to_string(),
127                "**/*.less".to_string(),
128                "**/*.html".to_string(),
129                "**/*.vue".to_string(),
130                "**/*.svelte".to_string(),
131                "**/*.astro".to_string(),
132                "**/*.jsx".to_string(),
133                "**/*.tsx".to_string(),
134                "**/*.ripple".to_string(),
135            ],
136            ignore_globs: vec![
137                "**/node_modules/**".to_string(),
138                "**/dist/**".to_string(),
139                "**/out/**".to_string(),
140                "**/.git/**".to_string(),
141            ],
142            enable_color_provider: true,
143            color_only_on_variables: false,
144            max_documents: 10_000, // Default limit of 10,000 documents
145        }
146    }
147}
148
149impl Config {
150    pub fn from_runtime(runtime: &RuntimeConfig) -> Self {
151        let mut config = Config::default();
152        if let Some(lookup) = &runtime.lookup_files {
153            if !lookup.is_empty() {
154                config.lookup_files = lookup.clone();
155            }
156        }
157        if let Some(ignore) = &runtime.ignore_globs {
158            if !ignore.is_empty() {
159                config.ignore_globs = ignore.clone();
160            }
161        }
162        config.enable_color_provider = runtime.enable_color_provider;
163        config.color_only_on_variables = runtime.color_only_on_variables;
164        config
165    }
166}
167
168/// Helper to convert byte offset to LSP Position
169pub fn offset_to_position(text: &str, offset: usize) -> Position {
170    let mut line = 0;
171    let mut character = 0;
172
173    for (idx, ch) in text.char_indices() {
174        if idx >= offset {
175            break;
176        }
177        if ch == '\n' {
178            line += 1;
179            character = 0;
180        } else {
181            character += ch.len_utf16() as u32;
182        }
183    }
184
185    Position::new(line, character)
186}
187
188/// Helper to convert LSP Position to byte offset
189pub fn position_to_offset(text: &str, position: Position) -> Option<usize> {
190    let mut line = 0;
191    let mut character = 0;
192
193    for (idx, ch) in text.char_indices() {
194        if line == position.line && character == position.character {
195            return Some(idx);
196        }
197        if ch == '\n' {
198            line += 1;
199            character = 0;
200        } else {
201            character += ch.len_utf16() as u32;
202        }
203    }
204
205    if line == position.line && character == position.character {
206        Some(text.len())
207    } else {
208        None
209    }
210}