Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:47:33.277Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

color_extractor.rs

137 lines · 4.4 KB · rust
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}
137
Served at tenant.openagents/omega Member data and write actions are omitted.