css_variable_lsp/
color.rs

1use csscolorparser::Color as CssColor;
2use tower_lsp::lsp_types::{Color, ColorPresentation, Range, TextEdit};
3
4/// Parse a CSS color value and return an LSP Color
5pub fn parse_color(value: &str) -> Option<Color> {
6    parse_csscolorparser(value.trim())
7}
8
9fn parse_csscolorparser(value: &str) -> Option<Color> {
10    let parsed: CssColor = value.parse().ok()?;
11    Some(Color {
12        red: parsed.r as f32,
13        green: parsed.g as f32,
14        blue: parsed.b as f32,
15        alpha: parsed.a as f32,
16    })
17}
18
19/// Generate color presentations for color picker
20pub fn generate_color_presentations(color: Color, range: Range) -> Vec<ColorPresentation> {
21    let mut presentations = Vec::new();
22
23    let hex_str = format_color_as_hex(color);
24    presentations.push(ColorPresentation {
25        label: hex_str.clone(),
26        text_edit: Some(TextEdit {
27            range,
28            new_text: hex_str,
29        }),
30        additional_text_edits: None,
31    });
32
33    let rgb_str = format_color_as_rgb(color);
34    presentations.push(ColorPresentation {
35        label: rgb_str.clone(),
36        text_edit: Some(TextEdit {
37            range,
38            new_text: rgb_str,
39        }),
40        additional_text_edits: None,
41    });
42
43    let hsl_str = format_color_as_hsl(color);
44    presentations.push(ColorPresentation {
45        label: hsl_str.clone(),
46        text_edit: Some(TextEdit {
47            range,
48            new_text: hsl_str,
49        }),
50        additional_text_edits: None,
51    });
52
53    presentations
54}
55
56pub fn format_color_as_hex(color: Color) -> String {
57    let r = (color.red.clamp(0.0, 1.0) * 255.0).round() as u8;
58    let g = (color.green.clamp(0.0, 1.0) * 255.0).round() as u8;
59    let b = (color.blue.clamp(0.0, 1.0) * 255.0).round() as u8;
60    let a = (color.alpha.clamp(0.0, 1.0) * 255.0).round() as u8;
61
62    if a == 255 {
63        format!("#{:02x}{:02x}{:02x}", r, g, b)
64    } else {
65        format!("#{:02x}{:02x}{:02x}{:02x}", r, g, b, a)
66    }
67}
68
69pub fn format_color_as_rgb(color: Color) -> String {
70    let r = (color.red.clamp(0.0, 1.0) * 255.0).round() as u8;
71    let g = (color.green.clamp(0.0, 1.0) * 255.0).round() as u8;
72    let b = (color.blue.clamp(0.0, 1.0) * 255.0).round() as u8;
73    let a = color.alpha.clamp(0.0, 1.0);
74
75    if a >= 1.0 {
76        format!("rgb({}, {}, {})", r, g, b)
77    } else {
78        format!("rgba({}, {}, {}, {:.2})", r, g, b, a)
79    }
80}
81
82pub fn format_color_as_hsl(color: Color) -> String {
83    let (h, s, l) = rgb_to_hsl(color.red, color.green, color.blue);
84    let a = color.alpha.clamp(0.0, 1.0);
85
86    let h_deg = (h * 360.0).round();
87    let s_pct = (s * 100.0).round();
88    let l_pct = (l * 100.0).round();
89
90    if a >= 1.0 {
91        format!("hsl({}, {}%, {}%)", h_deg, s_pct, l_pct)
92    } else {
93        format!("hsla({}, {}%, {}%, {:.2})", h_deg, s_pct, l_pct, a)
94    }
95}
96
97fn rgb_to_hsl(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
98    let max = r.max(g).max(b);
99    let min = r.min(g).min(b);
100    let l = (max + min) / 2.0;
101
102    if max == min {
103        return (0.0, 0.0, l);
104    }
105
106    let d = max - min;
107    let s = if l > 0.5 {
108        d / (2.0 - max - min)
109    } else {
110        d / (max + min)
111    };
112
113    let h = if max == r {
114        ((g - b) / d + if g < b { 6.0 } else { 0.0 }) / 6.0
115    } else if max == g {
116        ((b - r) / d + 2.0) / 6.0
117    } else {
118        ((r - g) / d + 4.0) / 6.0
119    };
120
121    (h, s, l)
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use tower_lsp::lsp_types::Position;
128
129    fn approx_eq(a: f32, b: f32) -> bool {
130        (a - b).abs() < 0.01
131    }
132
133    #[test]
134    fn parse_color_hex_and_named() {
135        let color = parse_color("#abc").expect("hex");
136        assert!(approx_eq(color.red, 0xAA as f32 / 255.0));
137        assert!(approx_eq(color.green, 0xBB as f32 / 255.0));
138        assert!(approx_eq(color.blue, 0xCC as f32 / 255.0));
139        assert!(approx_eq(color.alpha, 1.0));
140
141        let color = parse_color("#abcd").expect("hex with alpha");
142        assert!(approx_eq(color.red, 0xAA as f32 / 255.0));
143        assert!(approx_eq(color.alpha, 0xDD as f32 / 255.0));
144
145        let color = parse_color("blue").expect("named");
146        assert!(approx_eq(color.blue, 1.0));
147        assert!(approx_eq(color.red, 0.0));
148    }
149
150    #[test]
151    fn parse_color_rgb_variants() {
152        let color = parse_color("rgb(255, 0, 128)").expect("rgb");
153        assert!(approx_eq(color.red, 1.0));
154        assert!(approx_eq(color.green, 0.0));
155        assert!(approx_eq(color.blue, 128.0 / 255.0));
156
157        let color = parse_color("rgba(255, 0, 0, 0.5)").expect("rgba");
158        assert!(approx_eq(color.red, 1.0));
159        assert!(approx_eq(color.alpha, 0.5));
160
161        let color = parse_color("rgb(100%, 0%, 50%)").expect("rgb percent");
162        assert!(approx_eq(color.red, 1.0));
163        assert!(approx_eq(color.blue, 0.5));
164
165        let color = parse_color("rgba(255, 0, 0, 50%)").expect("rgba percent");
166        assert!(approx_eq(color.alpha, 0.5));
167    }
168
169    #[test]
170    fn generate_color_presentations_formats_output() {
171        let range = Range::new(Position::new(0, 0), Position::new(0, 4));
172        let color = Color {
173            red: 1.0,
174            green: 0.0,
175            blue: 0.5,
176            alpha: 1.0,
177        };
178        let presentations = generate_color_presentations(color, range);
179        assert_eq!(presentations.len(), 3);
180        assert!(presentations[0].label.starts_with('#'));
181        assert!(presentations[1].label.starts_with("rgb("));
182        assert!(presentations[2].label.starts_with("hsl("));
183
184        let color = Color {
185            red: 1.0,
186            green: 0.0,
187            blue: 0.0,
188            alpha: 0.5,
189        };
190        let presentations = generate_color_presentations(color, range);
191        assert_eq!(presentations.len(), 3);
192        assert!(presentations[0].label.starts_with('#'));
193        assert!(presentations[1].label.starts_with("rgba("));
194        assert!(presentations[2].label.starts_with("hsla("));
195    }
196
197    #[test]
198    fn format_color_hex_opaque_and_transparent() {
199        // Opaque color (alpha = 255)
200        let color = Color {
201            red: 0.0,
202            green: 0.5,
203            blue: 1.0,
204            alpha: 1.0,
205        };
206        let hex = format_color_as_hex(color);
207        assert_eq!(hex, "#0080ff");
208
209        // Transparent color (alpha < 255)
210        let color = Color {
211            red: 1.0,
212            green: 0.0,
213            blue: 0.0,
214            alpha: 0.5,
215        };
216        let hex = format_color_as_hex(color);
217        assert_eq!(hex, "#ff000080");
218    }
219
220    #[test]
221    fn format_color_rgb_with_alpha() {
222        let color = Color {
223            red: 0.5,
224            green: 0.5,
225            blue: 0.5,
226            alpha: 1.0,
227        };
228        let rgb = format_color_as_rgb(color);
229        assert_eq!(rgb, "rgb(128, 128, 128)");
230
231        let color = Color {
232            red: 1.0,
233            green: 0.0,
234            blue: 0.0,
235            alpha: 0.75,
236        };
237        let rgba = format_color_as_rgb(color);
238        assert_eq!(rgba, "rgba(255, 0, 0, 0.75)");
239    }
240
241    #[test]
242    fn format_color_hsl_with_alpha() {
243        let color = Color {
244            red: 1.0,
245            green: 0.0,
246            blue: 0.0,
247            alpha: 1.0,
248        };
249        let hsl = format_color_as_hsl(color);
250        assert!(hsl.starts_with("hsl("));
251        assert!(hsl.contains("0,") || hsl.contains("360,")); // Red hue
252
253        let color = Color {
254            red: 0.0,
255            green: 0.5,
256            blue: 1.0,
257            alpha: 0.5,
258        };
259        let hsla = format_color_as_hsl(color);
260        assert!(hsla.starts_with("hsla("));
261        assert!(hsla.contains("0.50"));
262    }
263
264    #[test]
265    fn rgb_to_hsl_conversion() {
266        // Pure red
267        let (h, s, l) = rgb_to_hsl(1.0, 0.0, 0.0);
268        assert!(approx_eq(h, 0.0));
269        assert!(approx_eq(s, 1.0));
270        assert!(approx_eq(l, 0.5));
271
272        // Pure green
273        let (h, s, l) = rgb_to_hsl(0.0, 1.0, 0.0);
274        assert!(approx_eq(h, 1.0 / 3.0));
275        assert!(approx_eq(s, 1.0));
276        assert!(approx_eq(l, 0.5));
277
278        // Gray (no saturation)
279        let (_h, s, l) = rgb_to_hsl(0.5, 0.5, 0.5);
280        assert!(approx_eq(s, 0.0));
281        assert!(approx_eq(l, 0.5));
282    }
283
284    #[test]
285    fn parse_color_edge_cases() {
286        // Invalid colors should return None
287        assert!(parse_color("not-a-color").is_none());
288        assert!(parse_color("").is_none());
289        assert!(parse_color("rgb(999, 999, 999)").is_some()); // Clamped by parser
290
291        // Named colors
292        assert!(parse_color("rebeccapurple").is_some());
293        assert!(parse_color("aliceblue").is_some());
294
295        // Transparent keyword
296        let color = parse_color("transparent").expect("transparent");
297        assert!(approx_eq(color.alpha, 0.0));
298    }
299
300    #[test]
301    fn color_clamping() {
302        // Test that colors are properly clamped to [0, 1]
303        let color = Color {
304            red: 1.5,
305            green: -0.5,
306            blue: 0.5,
307            alpha: 2.0,
308        };
309
310        let hex = format_color_as_hex(color);
311        assert!(hex.starts_with('#'));
312
313        let rgb = format_color_as_rgb(color);
314        assert!(rgb.contains("255")); // Red clamped to max
315        assert!(rgb.contains("0")); // Green clamped to min
316    }
317}