1use std::sync::LazyLock;
2
3use gpui::{Hsla, Rgba};
4use lsp::{CompletionItem, Documentation};
5use regex::{Regex, RegexBuilder};
6
7const HEX: &str = r#"(#(?:[\da-fA-F]{3}){1,2})"#;
8const RGB_OR_HSL: &str = r#"(rgba?|hsla?)\(\s*(\d{1,3}%?)\s*,\s*(\d{1,3}%?)\s*,\s*(\d{1,3}%?)\s*(?:,\s*(1|0?\.\d+))?\s*\)"#;
9
10static RELAXED_HEX_REGEX: LazyLock<Regex> = LazyLock::new(|| {
11 RegexBuilder::new(HEX)
12 .case_insensitive(false)
13 .build()
14 .expect("Failed to create RELAXED_HEX_REGEX")
15});
16
17static STRICT_HEX_REGEX: LazyLock<Regex> = LazyLock::new(|| {
18 RegexBuilder::new(&format!("^{HEX}$"))
19 .case_insensitive(true)
20 .build()
21 .expect("Failed to create STRICT_HEX_REGEX")
22});
23
24static RELAXED_RGB_OR_HSL_REGEX: LazyLock<Regex> = LazyLock::new(|| {
25 RegexBuilder::new(RGB_OR_HSL)
26 .case_insensitive(false)
27 .build()
28 .expect("Failed to create RELAXED_RGB_OR_HSL_REGEX")
29});
30
31static STRICT_RGB_OR_HSL_REGEX: LazyLock<Regex> = LazyLock::new(|| {
32 RegexBuilder::new(&format!("^{RGB_OR_HSL}$"))
33 .case_insensitive(true)
34 .build()
35 .expect("Failed to create STRICT_RGB_OR_HSL_REGEX")
36});
37
38/// Extracts a color from an LSP [`CompletionItem`].
39///
40/// Adapted from https://github.com/microsoft/vscode/blob/a6870fcb6d79093738c17e8319b760cf1c41764a/src/vs/editor/contrib/suggest/browser/suggestWidgetRenderer.ts#L34-L61
41pub fn extract_color(item: &CompletionItem) -> Option<Hsla> {
42 // Try to extract from entire `label` field.
43 parse(&item.label, ParseMode::Strict)
44 // Try to extract from entire `detail` field.
45 .or_else(|| {
46 item.detail
47 .as_ref()
48 .and_then(|detail| parse(detail, ParseMode::Strict))
49 })
50 // Try to extract from beginning or end of `documentation` field.
51 .or_else(|| match item.documentation {
52 Some(Documentation::String(ref str)) => parse(str, ParseMode::Relaxed),
53 Some(Documentation::MarkupContent(ref markup)) => {
54 parse(&markup.value, ParseMode::Relaxed)
55 }
56 None => None,
57 })
58}
59
60enum ParseMode {
61 Strict,
62 Relaxed,
63}
64
65fn parse(str: &str, mode: ParseMode) -> Option<Hsla> {
66 let (hex, rgb) = match mode {
67 ParseMode::Strict => (&STRICT_HEX_REGEX, &STRICT_RGB_OR_HSL_REGEX),
68 ParseMode::Relaxed => (&RELAXED_HEX_REGEX, &RELAXED_RGB_OR_HSL_REGEX),
69 };
70
71 if let Some(captures) = hex.captures(str) {
72 let rmatch = captures.get(0)?;
73
74 // Color must be anchored to start or end of string.
75 if rmatch.start() > 0 && rmatch.end() != str.len() {
76 return None;
77 }
78
79 let hex = captures.get(1)?.as_str();
80
81 return from_hex(hex);
82 }
83
84 if let Some(captures) = rgb.captures(str) {
85 let rmatch = captures.get(0)?;
86
87 // Color must be anchored to start or end of string.
88 if rmatch.start() > 0 && rmatch.end() != str.len() {
89 return None;
90 }
91
92 let typ = captures.get(1)?.as_str();
93 let r_or_h = captures.get(2)?.as_str();
94 let g_or_s = captures.get(3)?.as_str();
95 let b_or_l = captures.get(4)?.as_str();
96 let a = captures.get(5).map(|a| a.as_str());
97
98 return match (typ, a) {
99 ("rgb", None) | ("rgba", Some(_)) => from_rgb(r_or_h, g_or_s, b_or_l, a),
100 ("hsl", None) | ("hsla", Some(_)) => from_hsl(r_or_h, g_or_s, b_or_l, a),
101 _ => None,
102 };
103 }
104
105 None
106}
107
108fn parse_component(value: &str, max: f32) -> Option<f32> {
109 if let Some(field) = value.strip_suffix("%") {
110 field.parse::<f32>().map(|value| value / 100.).ok()
111 } else {
112 value.parse::<f32>().map(|value| value / max).ok()
113 }
114}
115
116fn from_hex(hex: &str) -> Option<Hsla> {
117 Rgba::try_from(hex).map(Hsla::from).ok()
118}
119
120fn from_rgb(r: &str, g: &str, b: &str, a: Option<&str>) -> Option<Hsla> {
121 let r = parse_component(r, 255.)?;
122 let g = parse_component(g, 255.)?;
123 let b = parse_component(b, 255.)?;
124 let a = a.and_then(|a| parse_component(a, 1.0)).unwrap_or(1.0);
125
126 Some(Rgba { r, g, b, a }.into())
127}
128
129fn from_hsl(h: &str, s: &str, l: &str, a: Option<&str>) -> Option<Hsla> {
130 let h = parse_component(h, 360.)?;
131 let s = parse_component(s, 100.)?;
132 let l = parse_component(l, 100.)?;
133 let a = a.and_then(|a| parse_component(a, 1.0)).unwrap_or(1.0);
134
135 Some(Hsla { h, s, l, a })
136}