use super::*;
use super::color_names::lookup_named;
use crate::project::collect_string_literals;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Rgba {
r: u8,
g: u8,
b: u8,
a: u8,
}
impl Rgba {
fn to_lsp(self) -> Color {
let f = |c: u8| c as f32 / 255.0;
Color {
red: f(self.r),
green: f(self.g),
blue: f(self.b),
alpha: f(self.a),
}
}
fn from_lsp(color: &Color) -> Rgba {
let q = |x: f32| (x.clamp(0.0, 1.0) * 255.0).round() as u8;
Rgba {
r: q(color.red),
g: q(color.green),
b: q(color.blue),
a: q(color.alpha),
}
}
fn to_hex(self) -> String {
if self.a == 255 {
format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
} else {
format!("#{:02x}{:02x}{:02x}{:02x}", self.r, self.g, self.b, self.a)
}
}
}
fn parse_hex(s: &str) -> Option<Rgba> {
let h = s.strip_prefix('#')?;
if !h.bytes().all(|b| b.is_ascii_hexdigit()) {
return None;
}
let byte = |i: usize| u8::from_str_radix(&h[i..i + 2], 16).ok();
match h.len() {
6 => Some(Rgba {
r: byte(0)?,
g: byte(2)?,
b: byte(4)?,
a: 255,
}),
8 => Some(Rgba {
r: byte(0)?,
g: byte(2)?,
b: byte(4)?,
a: byte(6)?,
}),
_ => None,
}
}
fn color_of_content(content: &str) -> Option<Rgba> {
if let Some(rgba) = parse_hex(content) {
return Some(rgba);
}
let (r, g, b) = lookup_named(content)?;
Some(Rgba { r, g, b, a: 255 })
}
pub fn compute_document_colors(text: &str) -> Vec<ColorInformation> {
let root = parse(text).cst;
let line_index = LineIndex::new(text);
collect_string_literals(&root)
.into_iter()
.filter_map(|literal| {
let rgba = color_of_content(&literal.spelling)?;
let range = text_range_to_lsp_range(&line_index, literal.literal_range);
(range.start.line == range.end.line).then(|| ColorInformation {
range,
color: rgba.to_lsp(),
})
})
.collect()
}
pub fn compute_color_presentations(
text: &str,
color: &Color,
range: Range,
) -> Vec<ColorPresentation> {
let line_index = LineIndex::new(text);
let start = line_index.position_to_byte(range.start);
let quote = if text.as_bytes().get(start) == Some(&b'\'') {
'\''
} else {
'"'
};
let hex = Rgba::from_lsp(color).to_hex();
let new_text = format!("{quote}{hex}{quote}");
vec![ColorPresentation {
label: hex,
text_edit: Some(TextEdit { range, new_text }),
additional_text_edits: None,
}]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_six_digit_hex_opaque() {
assert_eq!(
parse_hex("#ff0000"),
Some(Rgba {
r: 255,
g: 0,
b: 0,
a: 255
})
);
}
#[test]
fn parses_eight_digit_hex_with_alpha() {
assert_eq!(
parse_hex("#ff000080"),
Some(Rgba {
r: 255,
g: 0,
b: 0,
a: 128
})
);
}
#[test]
fn hex_parse_is_case_insensitive() {
assert_eq!(parse_hex("#FF0000"), parse_hex("#ff0000"));
}
#[test]
fn rejects_short_and_malformed_hex() {
assert_eq!(parse_hex("#fff"), None); assert_eq!(parse_hex("#ff00"), None); assert_eq!(parse_hex("#gggggg"), None); assert_eq!(parse_hex("ff0000"), None); assert_eq!(parse_hex("#ff0000 "), None); }
#[test]
fn content_recognizes_hex_and_named_colors() {
assert_eq!(
color_of_content("#ff0000").map(|c| (c.r, c.g, c.b, c.a)),
Some((255, 0, 0, 255))
);
assert_eq!(
color_of_content("red").map(|c| (c.r, c.g, c.b)),
Some((255, 0, 0))
);
assert_eq!(
color_of_content("Red").map(|c| (c.r, c.g, c.b)),
Some((255, 0, 0))
);
assert_eq!(color_of_content("hello"), None);
}
#[test]
fn rgba_round_trips_through_lsp() {
let c = Rgba {
r: 1,
g: 128,
b: 255,
a: 64,
};
assert_eq!(Rgba::from_lsp(&c.to_lsp()), c);
}
#[test]
fn hex_output_drops_alpha_when_opaque() {
assert_eq!(
Rgba {
r: 255,
g: 0,
b: 0,
a: 255
}
.to_hex(),
"#ff0000"
);
assert_eq!(
Rgba {
r: 255,
g: 0,
b: 0,
a: 128
}
.to_hex(),
"#ff000080"
);
}
}