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