Skip to main content

omena_bridge/
style_intelligence.rs

1use omena_abstract_value::FactPrecision;
2use serde::Serialize;
3
4use omena_parser::ParserRangeV0;
5
6use crate::{
7    SourceClassValueUniverseEntryV0, SourceDomainClassReferenceFactV0, SourceSyntaxIndexV0,
8};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
11#[serde(rename_all = "camelCase")]
12pub struct StyleIntelligenceProviderMetadataV0 {
13    pub provider_id: &'static str,
14    pub version: &'static str,
15    pub stability: &'static str,
16    pub domains: &'static [&'static str],
17    pub owns_surfaces: &'static [&'static str],
18    pub import_targets: &'static [&'static str],
19    pub utility_targets: &'static [&'static str],
20    pub precision: FactPrecision,
21    pub precision_backed: bool,
22}
23
24#[derive(Debug, Clone, Copy)]
25pub struct StyleIntelligenceSnapshotV0<'snapshot> {
26    source_syntax_index: &'snapshot SourceSyntaxIndexV0,
27    graph_bindings: &'snapshot [StyleIntelligenceGraphBindingV0],
28}
29
30impl<'snapshot> StyleIntelligenceSnapshotV0<'snapshot> {
31    pub const fn new(source_syntax_index: &'snapshot SourceSyntaxIndexV0) -> Self {
32        Self {
33            source_syntax_index,
34            graph_bindings: &[],
35        }
36    }
37
38    pub const fn with_graph_bindings(
39        source_syntax_index: &'snapshot SourceSyntaxIndexV0,
40        graph_bindings: &'snapshot [StyleIntelligenceGraphBindingV0],
41    ) -> Self {
42        Self {
43            source_syntax_index,
44            graph_bindings,
45        }
46    }
47
48    pub const fn source_syntax_index(&self) -> &'snapshot SourceSyntaxIndexV0 {
49        self.source_syntax_index
50    }
51
52    pub const fn graph_bindings(&self) -> &'snapshot [StyleIntelligenceGraphBindingV0] {
53        self.graph_bindings
54    }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
58#[serde(rename_all = "camelCase")]
59pub struct StyleIntelligenceGraphBindingV0 {
60    pub class_name: String,
61    pub uri: String,
62    pub range: ParserRangeV0,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub struct StyleIntelligenceSourceContextV0 {
67    pub byte_offset: usize,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
71#[serde(rename_all = "camelCase")]
72pub struct StyleIntelligenceClassUniverseV0 {
73    pub provider_id: &'static str,
74    pub entries: Vec<SourceClassValueUniverseEntryV0>,
75    pub precision: FactPrecision,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
79#[serde(rename_all = "camelCase")]
80pub struct StyleIntelligenceCompletionV0 {
81    pub provider_id: &'static str,
82    pub label: String,
83    pub precision: FactPrecision,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
87#[serde(rename_all = "camelCase")]
88pub struct StyleIntelligenceHoverV0 {
89    pub provider_id: &'static str,
90    pub owner_name: String,
91    pub domain: &'static str,
92    pub axis_name: String,
93    pub current_option: Option<String>,
94    pub known_options: Vec<String>,
95    pub known_patterns: Vec<String>,
96    pub unresolved_reasons: Vec<String>,
97    pub graph_bindings: Vec<StyleIntelligenceGraphBindingV0>,
98    pub precision: FactPrecision,
99}
100
101pub trait StyleIntelligenceProvider: Sync {
102    fn metadata(&self) -> &'static StyleIntelligenceProviderMetadataV0;
103
104    fn class_universe(
105        &self,
106        snapshot: &StyleIntelligenceSnapshotV0<'_>,
107    ) -> StyleIntelligenceClassUniverseV0;
108
109    fn completions(
110        &self,
111        snapshot: &StyleIntelligenceSnapshotV0<'_>,
112        context: StyleIntelligenceSourceContextV0,
113    ) -> Vec<StyleIntelligenceCompletionV0>;
114
115    fn hover(
116        &self,
117        snapshot: &StyleIntelligenceSnapshotV0<'_>,
118        context: StyleIntelligenceSourceContextV0,
119    ) -> Option<StyleIntelligenceHoverV0>;
120}
121
122#[derive(Debug, Clone, Copy)]
123pub struct BuiltInStyleIntelligenceProviderV0 {
124    metadata: &'static StyleIntelligenceProviderMetadataV0,
125    pub(crate) binder_summary_visible: bool,
126    pub(crate) recipe: Option<BuiltInRecipeProviderConfigV0>,
127}
128
129impl StyleIntelligenceProvider for BuiltInStyleIntelligenceProviderV0 {
130    fn metadata(&self) -> &'static StyleIntelligenceProviderMetadataV0 {
131        self.metadata
132    }
133
134    fn class_universe(
135        &self,
136        snapshot: &StyleIntelligenceSnapshotV0<'_>,
137    ) -> StyleIntelligenceClassUniverseV0 {
138        StyleIntelligenceClassUniverseV0 {
139            provider_id: self.metadata.provider_id,
140            entries: snapshot
141                .source_syntax_index
142                .class_value_universes
143                .iter()
144                .filter(|entry| entry.plugin_id == self.metadata.provider_id)
145                .cloned()
146                .collect(),
147            precision: self.metadata.precision,
148        }
149    }
150
151    fn completions(
152        &self,
153        snapshot: &StyleIntelligenceSnapshotV0<'_>,
154        context: StyleIntelligenceSourceContextV0,
155    ) -> Vec<StyleIntelligenceCompletionV0> {
156        let Some(reference) = reference_at_offset(
157            snapshot.source_syntax_index,
158            context.byte_offset,
159            self.metadata.provider_id,
160        ) else {
161            return Vec::new();
162        };
163        let universe = self.class_universe(snapshot);
164        let mut labels = universe
165            .entries
166            .iter()
167            .filter(|entry| entry_matches_reference(entry, reference))
168            .flat_map(|entry| {
169                let mut values = entry
170                    .axes
171                    .iter()
172                    .filter(|axis| axis.axis_name == reference.axis_name)
173                    .flat_map(|axis| axis.values.iter().cloned())
174                    .collect::<Vec<_>>();
175                if reference.plugin_id == "tailwind-uno-utility-domain" {
176                    values.extend(entry.class_names.iter().cloned());
177                    values.extend(
178                        entry
179                            .patterns
180                            .iter()
181                            .map(|pattern| pattern.completion_hint.clone()),
182                    );
183                }
184                values
185            })
186            .collect::<Vec<_>>();
187        labels.sort();
188        labels.dedup();
189        labels
190            .into_iter()
191            .map(|label| StyleIntelligenceCompletionV0 {
192                provider_id: self.metadata.provider_id,
193                label,
194                precision: universe.precision,
195            })
196            .collect()
197    }
198
199    fn hover(
200        &self,
201        snapshot: &StyleIntelligenceSnapshotV0<'_>,
202        context: StyleIntelligenceSourceContextV0,
203    ) -> Option<StyleIntelligenceHoverV0> {
204        let reference = reference_at_offset(
205            snapshot.source_syntax_index,
206            context.byte_offset,
207            self.metadata.provider_id,
208        )?;
209        let known_options = self
210            .completions(snapshot, context)
211            .into_iter()
212            .map(|item| item.label)
213            .collect();
214        let universe = self.class_universe(snapshot);
215        let mut known_patterns = universe
216            .entries
217            .iter()
218            .filter(|entry| entry_matches_reference(entry, reference))
219            .flat_map(|entry| entry.patterns.iter().map(|pattern| pattern.source.clone()))
220            .collect::<Vec<_>>();
221        known_patterns.sort();
222        known_patterns.dedup();
223        let mut unresolved_reasons = universe
224            .entries
225            .iter()
226            .filter(|entry| entry_matches_reference(entry, reference))
227            .flat_map(|entry| entry.unresolved.iter().map(|item| item.reason.clone()))
228            .collect::<Vec<_>>();
229        unresolved_reasons.sort();
230        unresolved_reasons.dedup();
231        let current_option = reference
232            .option_name
233            .clone()
234            .or_else(|| reference.prefix.clone());
235        let graph_bindings = snapshot
236            .graph_bindings
237            .iter()
238            .filter(|binding| current_option.as_ref() == Some(&binding.class_name))
239            .cloned()
240            .collect();
241        Some(StyleIntelligenceHoverV0 {
242            provider_id: self.metadata.provider_id,
243            owner_name: reference.owner_name.clone(),
244            domain: reference.domain,
245            axis_name: reference.axis_name.clone(),
246            current_option,
247            known_options,
248            known_patterns,
249            unresolved_reasons,
250            graph_bindings,
251            precision: self.metadata.precision,
252        })
253    }
254}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq)]
257pub(crate) enum BuiltInRecipeCallShapeV0 {
258    BaseThenConfig,
259    ObjectConfig,
260}
261
262#[derive(Debug, Clone, Copy)]
263pub(crate) struct BuiltInRecipeProviderConfigV0 {
264    pub(crate) plugin_id: &'static str,
265    pub(crate) domain: &'static str,
266    pub(crate) import_sources: &'static [&'static str],
267    pub(crate) import_names: &'static [&'static str],
268    pub(crate) call_shape: BuiltInRecipeCallShapeV0,
269}
270
271const CSS_MODULES_METADATA: StyleIntelligenceProviderMetadataV0 =
272    StyleIntelligenceProviderMetadataV0 {
273        provider_id: "css-modules-classnames-bind",
274        version: "0",
275        stability: "builtIn",
276        domains: &["css-modules"],
277        owns_surfaces: &[
278            "styleImportRecognition",
279            "classUtilityRecognition",
280            "classReferenceExtraction",
281            "sourceExpressionProjection",
282        ],
283        import_targets: &["*.module.css", "*.module.scss", "*.module.less"],
284        utility_targets: &["classnames/bind", "classnames", "clsx", "clsx/lite"],
285        precision: FactPrecision::Exact,
286        precision_backed: true,
287    };
288
289const UTILITY_DOMAIN_METADATA: StyleIntelligenceProviderMetadataV0 =
290    StyleIntelligenceProviderMetadataV0 {
291        provider_id: "tailwind-uno-utility-domain",
292        version: "0",
293        stability: "builtIn",
294        domains: &["tailwind-utilities", "unocss-utilities", "utility-classes"],
295        owns_surfaces: &[
296            "domainClassReferenceExtraction",
297            "classUniverseProjection",
298            "completionProjection",
299            "graphBoundHoverProjection",
300        ],
301        import_targets: &[],
302        utility_targets: &["class", "className", "classnames", "clsx", "clsx/lite"],
303        precision: FactPrecision::Unknown,
304        precision_backed: false,
305    };
306
307const VANILLA_EXTRACT_METADATA: StyleIntelligenceProviderMetadataV0 =
308    StyleIntelligenceProviderMetadataV0 {
309        provider_id: "vanilla-extract-recipe-domain",
310        version: "0",
311        stability: "builtIn",
312        domains: &["vanilla-extract-recipes"],
313        owns_surfaces: &["domainClassReferenceExtraction"],
314        import_targets: &["@vanilla-extract/recipes"],
315        utility_targets: &["recipe"],
316        precision: FactPrecision::Exact,
317        precision_backed: true,
318    };
319
320const VUE_STYLE_MODULE_METADATA: StyleIntelligenceProviderMetadataV0 =
321    StyleIntelligenceProviderMetadataV0 {
322        provider_id: "vue-style-module-domain",
323        version: "0",
324        stability: "builtIn",
325        domains: &["vue-style-modules"],
326        owns_surfaces: &["domainClassReferenceExtraction"],
327        import_targets: &["*.vue"],
328        utility_targets: &["useCssModule"],
329        precision: FactPrecision::Exact,
330        precision_backed: true,
331    };
332
333const CVA_RECIPE_METADATA: StyleIntelligenceProviderMetadataV0 =
334    StyleIntelligenceProviderMetadataV0 {
335        provider_id: "cva-recipe-domain",
336        version: "0",
337        stability: "builtIn",
338        domains: &["cva-recipe"],
339        owns_surfaces: &["domainClassReferenceExtraction"],
340        import_targets: &["class-variance-authority", "cva"],
341        utility_targets: &["cva"],
342        precision: FactPrecision::Exact,
343        precision_backed: true,
344    };
345
346const VANILLA_EXTRACT_RECIPE_CONFIG: BuiltInRecipeProviderConfigV0 =
347    BuiltInRecipeProviderConfigV0 {
348        plugin_id: "vanilla-extract-recipe-domain",
349        domain: "vanilla-extract-recipe",
350        import_sources: &["@vanilla-extract/recipes"],
351        import_names: &["recipe"],
352        call_shape: BuiltInRecipeCallShapeV0::ObjectConfig,
353    };
354
355const CVA_RECIPE_CONFIG: BuiltInRecipeProviderConfigV0 = BuiltInRecipeProviderConfigV0 {
356    plugin_id: "cva-recipe-domain",
357    domain: "cva-recipe",
358    import_sources: &["class-variance-authority", "cva"],
359    import_names: &["cva"],
360    call_shape: BuiltInRecipeCallShapeV0::BaseThenConfig,
361};
362
363const BUILT_IN_STYLE_INTELLIGENCE_PROVIDERS: [BuiltInStyleIntelligenceProviderV0; 5] = [
364    BuiltInStyleIntelligenceProviderV0 {
365        metadata: &CSS_MODULES_METADATA,
366        binder_summary_visible: true,
367        recipe: None,
368    },
369    BuiltInStyleIntelligenceProviderV0 {
370        metadata: &UTILITY_DOMAIN_METADATA,
371        binder_summary_visible: true,
372        recipe: None,
373    },
374    BuiltInStyleIntelligenceProviderV0 {
375        metadata: &VANILLA_EXTRACT_METADATA,
376        binder_summary_visible: true,
377        recipe: Some(VANILLA_EXTRACT_RECIPE_CONFIG),
378    },
379    BuiltInStyleIntelligenceProviderV0 {
380        metadata: &VUE_STYLE_MODULE_METADATA,
381        binder_summary_visible: true,
382        recipe: None,
383    },
384    BuiltInStyleIntelligenceProviderV0 {
385        metadata: &CVA_RECIPE_METADATA,
386        binder_summary_visible: false,
387        recipe: Some(CVA_RECIPE_CONFIG),
388    },
389];
390
391pub fn built_in_style_intelligence_providers() -> &'static [BuiltInStyleIntelligenceProviderV0] {
392    &BUILT_IN_STYLE_INTELLIGENCE_PROVIDERS
393}
394
395pub fn built_in_style_intelligence_provider(
396    provider_id: &str,
397) -> Option<&'static BuiltInStyleIntelligenceProviderV0> {
398    BUILT_IN_STYLE_INTELLIGENCE_PROVIDERS
399        .iter()
400        .find(|provider| provider.metadata.provider_id == provider_id)
401}
402
403pub(crate) fn built_in_recipe_provider_configs() -> Vec<BuiltInRecipeProviderConfigV0> {
404    let mut configs = BUILT_IN_STYLE_INTELLIGENCE_PROVIDERS
405        .iter()
406        .filter_map(|provider| provider.recipe)
407        .collect::<Vec<_>>();
408    configs.sort_by_key(|config| match config.plugin_id {
409        "cva-recipe-domain" => 0,
410        _ => 1,
411    });
412    configs
413}
414
415pub fn style_intelligence_completions_at_offset(
416    snapshot: &StyleIntelligenceSnapshotV0<'_>,
417    byte_offset: usize,
418) -> Vec<StyleIntelligenceCompletionV0> {
419    let Some(reference) = snapshot
420        .source_syntax_index
421        .domain_class_references
422        .iter()
423        .find(|reference| {
424            byte_offset >= reference.byte_span.start && byte_offset <= reference.byte_span.end
425        })
426    else {
427        return Vec::new();
428    };
429    built_in_style_intelligence_provider(reference.plugin_id).map_or_else(Vec::new, |provider| {
430        provider.completions(snapshot, StyleIntelligenceSourceContextV0 { byte_offset })
431    })
432}
433
434pub fn style_intelligence_hover_at_offset(
435    snapshot: &StyleIntelligenceSnapshotV0<'_>,
436    byte_offset: usize,
437) -> Option<StyleIntelligenceHoverV0> {
438    let reference = snapshot
439        .source_syntax_index
440        .domain_class_references
441        .iter()
442        .find(|reference| {
443            byte_offset >= reference.byte_span.start && byte_offset <= reference.byte_span.end
444        })?;
445    built_in_style_intelligence_provider(reference.plugin_id)?
446        .hover(snapshot, StyleIntelligenceSourceContextV0 { byte_offset })
447}
448
449fn reference_at_offset<'snapshot>(
450    index: &'snapshot SourceSyntaxIndexV0,
451    byte_offset: usize,
452    provider_id: &str,
453) -> Option<&'snapshot SourceDomainClassReferenceFactV0> {
454    index.domain_class_references.iter().find(|reference| {
455        reference.plugin_id == provider_id
456            && byte_offset >= reference.byte_span.start
457            && byte_offset <= reference.byte_span.end
458    })
459}
460
461fn entry_matches_reference(
462    entry: &SourceClassValueUniverseEntryV0,
463    reference: &SourceDomainClassReferenceFactV0,
464) -> bool {
465    entry.owner_name == reference.owner_name
466        && (entry.domain == reference.domain
467            || reference.plugin_id == "tailwind-uno-utility-domain")
468}
469
470#[cfg(test)]
471mod tests {
472    use omena_abstract_value::FactPrecision;
473    use std::path::Path;
474
475    use super::*;
476    use crate::{
477        append_omena_bridge_utility_class_intelligence, summarize_omena_bridge_source_syntax_index,
478        summarize_omena_bridge_utility_class_intelligence_for_config,
479    };
480
481    #[test]
482    fn provider_registry_supersedes_binder_and_recipe_catalogs() -> Result<(), &'static str> {
483        let providers = built_in_style_intelligence_providers();
484        assert_eq!(providers.len(), 5);
485        assert_eq!(
486            providers
487                .iter()
488                .filter(|provider| provider.binder_summary_visible)
489                .count(),
490            4
491        );
492        let utility = built_in_style_intelligence_provider("tailwind-uno-utility-domain")
493            .ok_or("utility provider should be registered")?;
494        assert_eq!(utility.metadata.precision, FactPrecision::Unknown);
495        assert!(!utility.metadata.precision_backed);
496        assert!(
497            providers
498                .iter()
499                .filter(|provider| {
500                    provider.metadata.provider_id != "tailwind-uno-utility-domain"
501                })
502                .all(|provider| provider.metadata.precision_backed)
503        );
504        assert!(built_in_style_intelligence_provider("cva-recipe-domain").is_some());
505        Ok(())
506    }
507
508    #[test]
509    fn provider_projects_recipe_completions_and_hover_from_source_facts() -> Result<(), &'static str>
510    {
511        let source = r#"import { cva } from "class-variance-authority";
512const button = cva("btn", { variants: { intent: { primary: "a", secondary: "b" } } });
513const value = button({ intent: "pri" });"#;
514        let index = summarize_omena_bridge_source_syntax_index(source, Vec::new(), Vec::new());
515        let reference = index
516            .domain_class_references
517            .first()
518            .ok_or("CVA reference should be indexed")?;
519        let snapshot = StyleIntelligenceSnapshotV0::new(&index);
520        let completions =
521            style_intelligence_completions_at_offset(&snapshot, reference.byte_span.start);
522        assert_eq!(
523            completions
524                .iter()
525                .map(|item| item.label.as_str())
526                .collect::<Vec<_>>(),
527            vec!["primary", "secondary"]
528        );
529        assert!(
530            completions
531                .iter()
532                .all(|item| item.precision == FactPrecision::Exact)
533        );
534
535        let hover = style_intelligence_hover_at_offset(&snapshot, reference.byte_span.start)
536            .ok_or("CVA hover should be projected")?;
537        assert_eq!(hover.provider_id, "cva-recipe-domain");
538        assert_eq!(hover.known_options, vec!["primary", "secondary"]);
539        assert_eq!(hover.precision, FactPrecision::Exact);
540        Ok(())
541    }
542
543    #[test]
544    fn utility_provider_projects_patterns_and_graph_bindings_from_shared_facts()
545    -> Result<(), &'static str> {
546        let source = r#"export const Card = () => <div className="bg-brand" />;"#;
547        let mut index = summarize_omena_bridge_source_syntax_index(source, Vec::new(), Vec::new());
548        let report = summarize_omena_bridge_utility_class_intelligence_for_config(
549            Path::new("tailwind.config.ts"),
550            r##"export default { theme: { extend: { colors: { brand: "#123" } } } }"##,
551        );
552        append_omena_bridge_utility_class_intelligence(&mut index, source, &report);
553        let reference = index
554            .domain_class_references
555            .iter()
556            .find(|reference| reference.plugin_id == "tailwind-uno-utility-domain")
557            .ok_or("missing utility reference")?;
558        let graph_bindings = vec![StyleIntelligenceGraphBindingV0 {
559            class_name: "bg-brand".to_string(),
560            uri: "file:///workspace/theme.css".to_string(),
561            range: ParserRangeV0 {
562                start: omena_parser::ParserPositionV0 {
563                    line: 2,
564                    character: 0,
565                },
566                end: omena_parser::ParserPositionV0 {
567                    line: 2,
568                    character: 9,
569                },
570            },
571        }];
572        let snapshot = StyleIntelligenceSnapshotV0::with_graph_bindings(&index, &graph_bindings);
573        let completions =
574            style_intelligence_completions_at_offset(&snapshot, reference.byte_span.start);
575        assert!(completions.iter().any(|item| item.label == "bg-brand"));
576        assert!(completions.iter().any(|item| item.label == "bg-[...]"));
577        let hover = style_intelligence_hover_at_offset(&snapshot, reference.byte_span.start)
578            .ok_or("missing utility hover")?;
579        assert_eq!(hover.current_option.as_deref(), Some("bg-brand"));
580        assert_eq!(hover.graph_bindings, graph_bindings);
581        assert!(
582            hover
583                .known_patterns
584                .iter()
585                .any(|pattern| pattern == "bg-[<value>]")
586        );
587        Ok(())
588    }
589}