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}