Skip to main content

gpui_rhai/
native_collection.rs

1use std::cmp::Ordering;
2use std::collections::{BTreeMap, BTreeSet};
3use std::fmt;
4use std::sync::{Arc, Mutex};
5
6use rhai::{
7    CustomType, Dynamic, EvalAltResult, FuncRegistration, ImmutableString, Map, Position,
8    TypeBuilder,
9};
10use thiserror::Error;
11
12use crate::{ComponentInstancePath, UiValue};
13
14const MAX_TABLE_ORDER_CACHE_ENTRIES: usize = 64;
15const MAX_FUZZY_ORDER_CACHE_ENTRIES: usize = 64;
16
17#[derive(Clone)]
18pub struct NativeCollection {
19    source: Arc<NativeCollectionSource>,
20    order: Arc<Vec<NativeCollectionEntry>>,
21    sticky_headers: Arc<BTreeSet<usize>>,
22    projection: Option<Arc<CollectionProjection>>,
23}
24
25#[derive(Clone, Debug, Eq, PartialEq)]
26enum NativeCollectionEntry {
27    Row(usize),
28    Group(GroupEntry),
29}
30
31#[derive(Clone, Debug, PartialEq)]
32enum CollectionProjection {
33    Table(TableProjection),
34    Fuzzy(FuzzyProjection),
35}
36
37#[derive(Clone, Debug, Eq, PartialEq)]
38struct GroupEntry {
39    key: String,
40    value: String,
41    count: usize,
42    collapsed: bool,
43}
44
45#[derive(Clone)]
46struct TableOrder {
47    entries: Arc<Vec<NativeCollectionEntry>>,
48    sticky_headers: Arc<BTreeSet<usize>>,
49}
50
51struct NativeCollectionSource {
52    key_field: String,
53    rows: Vec<UiValue>,
54    keys: Vec<String>,
55    sorted_orders: Mutex<BTreeMap<SortSpec, Arc<Vec<usize>>>>,
56    table_orders: Mutex<BTreeMap<TableOrderSpec, TableOrder>>,
57    fuzzy_orders: Mutex<BTreeMap<FuzzyOrderSpec, Arc<Vec<NativeCollectionEntry>>>>,
58}
59
60impl fmt::Debug for NativeCollectionSource {
61    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
62        formatter
63            .debug_struct("NativeCollectionSource")
64            .field("key_field", &self.key_field)
65            .field("rows", &self.rows.len())
66            .finish_non_exhaustive()
67    }
68}
69
70impl fmt::Debug for NativeCollection {
71    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
72        formatter
73            .debug_struct("NativeCollection")
74            .field("key_field", &self.source.key_field)
75            .field("len", &self.order.len())
76            .field("projected", &self.projection.is_some())
77            .finish_non_exhaustive()
78    }
79}
80
81impl PartialEq for NativeCollection {
82    fn eq(&self, other: &Self) -> bool {
83        Arc::ptr_eq(&self.source, &other.source)
84            && (Arc::ptr_eq(&self.order, &other.order) || self.order == other.order)
85            && (Arc::ptr_eq(&self.sticky_headers, &other.sticky_headers)
86                || self.sticky_headers == other.sticky_headers)
87            && self.projection == other.projection
88    }
89}
90
91impl NativeCollection {
92    /// Build an immutable Rust-owned keyed collection.
93    ///
94    /// # Errors
95    ///
96    /// Returns an error unless every row contains one unique scalar key.
97    pub fn new(
98        key_field: impl Into<String>,
99        rows: impl IntoIterator<Item = BTreeMap<String, UiValue>>,
100    ) -> Result<Self, NativeCollectionError> {
101        Self::from_values(key_field, rows.into_iter().map(UiValue::Map).collect())
102    }
103
104    /// Build a collection from already-normalized [`UiValue`] maps.
105    ///
106    /// # Errors
107    ///
108    /// Returns an error for an unsafe key field, non-map rows, missing keys,
109    /// unsupported key values, or duplicate keys.
110    pub fn from_values(
111        key_field: impl Into<String>,
112        rows: Vec<UiValue>,
113    ) -> Result<Self, NativeCollectionError> {
114        let key_field = key_field.into();
115        validate_name(&key_field, "key field")?;
116        let mut seen = BTreeSet::new();
117        let keys = rows
118            .iter()
119            .enumerate()
120            .map(|(index, row)| {
121                let UiValue::Map(row) = row else {
122                    return Err(NativeCollectionError::RowNotMap(index));
123                };
124                let key = row
125                    .get(&key_field)
126                    .ok_or_else(|| NativeCollectionError::MissingKey {
127                        index,
128                        field: key_field.clone(),
129                    })?;
130                let key = scalar_text(key).ok_or_else(|| NativeCollectionError::InvalidKey {
131                    index,
132                    field: key_field.clone(),
133                })?;
134                if key.is_empty() {
135                    return Err(NativeCollectionError::InvalidKey {
136                        index,
137                        field: key_field.clone(),
138                    });
139                }
140                if !seen.insert(key.clone()) {
141                    return Err(NativeCollectionError::DuplicateKey(key));
142                }
143                Ok(key)
144            })
145            .collect::<Result<Vec<_>, _>>()?;
146        let order = Arc::new((0..rows.len()).map(NativeCollectionEntry::Row).collect());
147        Ok(Self {
148            source: Arc::new(NativeCollectionSource {
149                key_field,
150                rows,
151                keys,
152                sorted_orders: Mutex::new(BTreeMap::new()),
153                table_orders: Mutex::new(BTreeMap::new()),
154                fuzzy_orders: Mutex::new(BTreeMap::new()),
155            }),
156            order,
157            sticky_headers: Arc::new(BTreeSet::new()),
158            projection: None,
159        })
160    }
161
162    #[must_use]
163    pub fn len(&self) -> usize {
164        self.order.len()
165    }
166
167    #[must_use]
168    pub fn is_empty(&self) -> bool {
169        self.order.is_empty()
170    }
171
172    #[must_use]
173    pub fn key_field(&self) -> &str {
174        &self.source.key_field
175    }
176
177    pub(crate) fn key(&self, index: usize) -> Option<&str> {
178        match self.order.get(index)? {
179            NativeCollectionEntry::Row(source) => self.source.keys.get(*source).map(String::as_str),
180            NativeCollectionEntry::Group(group) => Some(group.key.as_str()),
181        }
182    }
183
184    pub(crate) fn item(&self, index: usize) -> Result<Option<UiValue>, NativeCollectionError> {
185        let Some(entry) = self.order.get(index) else {
186            return Ok(None);
187        };
188        match entry {
189            NativeCollectionEntry::Row(source_index) => {
190                let row = self
191                    .source
192                    .rows
193                    .get(*source_index)
194                    .ok_or(NativeCollectionError::CorruptOrder(*source_index))?;
195                self.projection.as_ref().map_or_else(
196                    || Ok(Some(row.clone())),
197                    |projection| {
198                        projection
199                            .project_row(row, index, self.key(index))
200                            .map(Some)
201                    },
202                )
203            }
204            NativeCollectionEntry::Group(group) => self
205                .projection
206                .as_ref()
207                .ok_or(NativeCollectionError::UnexpectedGroupEntry)
208                .map(|projection| Some(projection.project_group(group))),
209        }
210    }
211
212    pub(crate) fn sticky_headers(&self) -> Arc<BTreeSet<usize>> {
213        Arc::clone(&self.sticky_headers)
214    }
215
216    pub(crate) fn table_view(&self, config: Map) -> Result<Self, NativeCollectionError> {
217        let config = UiValue::from_dynamic(Dynamic::from_map(config))
218            .map_err(|error| NativeCollectionError::InvalidTableConfig(error.to_string()))?;
219        let projection = TableProjection::decode(config, self.key_field())?;
220        let order = self.table_order(&projection)?;
221        Ok(Self {
222            source: Arc::clone(&self.source),
223            order: order.entries,
224            sticky_headers: order.sticky_headers,
225            projection: Some(Arc::new(CollectionProjection::Table(projection))),
226        })
227    }
228
229    pub(crate) fn fuzzy_view(&self, config: Map) -> Result<Self, NativeCollectionError> {
230        let projection = FuzzyProjection::decode(config)?;
231        let order = self.fuzzy_order(&projection)?;
232        Ok(Self {
233            source: Arc::clone(&self.source),
234            order,
235            sticky_headers: Arc::new(BTreeSet::new()),
236            projection: Some(Arc::new(CollectionProjection::Fuzzy(projection))),
237        })
238    }
239
240    fn fuzzy_order(
241        &self,
242        projection: &FuzzyProjection,
243    ) -> Result<Arc<Vec<NativeCollectionEntry>>, NativeCollectionError> {
244        let spec = FuzzyOrderSpec::from(projection);
245        if let Some(order) = self
246            .source
247            .fuzzy_orders
248            .lock()
249            .map_err(|_| NativeCollectionError::Poisoned)?
250            .get(&spec)
251            .cloned()
252        {
253            return Ok(order);
254        }
255        let order = Arc::new(projection.grouped_order(&self.source)?);
256        let mut cache = self
257            .source
258            .fuzzy_orders
259            .lock()
260            .map_err(|_| NativeCollectionError::Poisoned)?;
261        if cache.len() >= MAX_FUZZY_ORDER_CACHE_ENTRIES
262            && let Some(victim) = cache.keys().next().cloned()
263        {
264            cache.remove(&victim);
265        }
266        cache.insert(spec, Arc::clone(&order));
267        Ok(order)
268    }
269
270    pub(crate) fn fuzzy_edge(&self, last: bool) -> Result<String, NativeCollectionError> {
271        let projection = self.fuzzy_projection()?;
272        let rows = if last {
273            Box::new(self.order.iter().rev()) as Box<dyn Iterator<Item = &NativeCollectionEntry>>
274        } else {
275            Box::new(self.order.iter()) as Box<dyn Iterator<Item = &NativeCollectionEntry>>
276        };
277        for entry in rows {
278            if let NativeCollectionEntry::Row(index) = entry
279                && !projection.row_disabled(&self.source, *index)?
280            {
281                return self
282                    .source
283                    .keys
284                    .get(*index)
285                    .cloned()
286                    .ok_or(NativeCollectionError::CorruptOrder(*index));
287            }
288        }
289        Ok(String::new())
290    }
291
292    pub(crate) fn fuzzy_adjacent(
293        &self,
294        active: &str,
295        step: i64,
296    ) -> Result<String, NativeCollectionError> {
297        let projection = self.fuzzy_projection()?;
298        let mut enabled = Vec::new();
299        for entry in self.order.iter() {
300            if let NativeCollectionEntry::Row(index) = entry
301                && !projection.row_disabled(&self.source, *index)?
302            {
303                enabled.push(*index);
304            }
305        }
306        if enabled.is_empty() {
307            return Ok(String::new());
308        }
309        let current = enabled.iter().position(|index| {
310            self.source
311                .keys
312                .get(*index)
313                .is_some_and(|key| key == active)
314        });
315        let next = match step.cmp(&0) {
316            Ordering::Equal => current.unwrap_or(0),
317            Ordering::Less => (current.unwrap_or(0) + enabled.len() - 1) % enabled.len(),
318            Ordering::Greater => (current.unwrap_or(0) + 1) % enabled.len(),
319        };
320        self.source
321            .keys
322            .get(enabled[next])
323            .cloned()
324            .ok_or(NativeCollectionError::CorruptOrder(enabled[next]))
325    }
326
327    fn fuzzy_projection(&self) -> Result<&FuzzyProjection, NativeCollectionError> {
328        match self.projection.as_deref() {
329            Some(CollectionProjection::Fuzzy(projection)) => Ok(projection),
330            _ => Err(NativeCollectionError::NotFuzzyProjection),
331        }
332    }
333
334    fn table_order(
335        &self,
336        projection: &TableProjection,
337    ) -> Result<TableOrder, NativeCollectionError> {
338        if self.projection.is_none()
339            && projection.sort.is_none()
340            && projection.group_by.is_none()
341            && projection.query.is_empty()
342            && projection.page_size.is_none()
343        {
344            return Ok(TableOrder {
345                entries: Arc::clone(&self.order),
346                sticky_headers: Arc::clone(&self.sticky_headers),
347            });
348        }
349        let spec = TableOrderSpec::from(projection);
350        if let Some(order) = self
351            .source
352            .table_orders
353            .lock()
354            .map_err(|_| NativeCollectionError::Poisoned)?
355            .get(&spec)
356            .cloned()
357        {
358            return Ok(order);
359        }
360        let rows = projection.sort.as_ref().map_or_else(
361            || Ok(Arc::new((0..self.source.rows.len()).collect())),
362            |sort| self.sorted_order(sort),
363        )?;
364        let rows = projection.visible_rows(&self.source, rows.as_ref());
365        let entries = Arc::new(projection.grouped_order(&self.source, &rows)?);
366        let sticky_headers = Arc::new(
367            entries
368                .iter()
369                .enumerate()
370                .filter_map(|(index, entry)| {
371                    matches!(entry, NativeCollectionEntry::Group(_)).then_some(index)
372                })
373                .collect(),
374        );
375        let order = TableOrder {
376            entries,
377            sticky_headers,
378        };
379        let mut cache = self
380            .source
381            .table_orders
382            .lock()
383            .map_err(|_| NativeCollectionError::Poisoned)?;
384        if cache.len() >= MAX_TABLE_ORDER_CACHE_ENTRIES
385            && let Some(victim) = cache.keys().next().cloned()
386        {
387            cache.remove(&victim);
388        }
389        cache.insert(spec, order.clone());
390        Ok(order)
391    }
392
393    fn sorted_order(&self, sort: &SortSpec) -> Result<Arc<Vec<usize>>, NativeCollectionError> {
394        if let Some(cached) = self
395            .source
396            .sorted_orders
397            .lock()
398            .map_err(|_| NativeCollectionError::Poisoned)?
399            .get(sort)
400            .cloned()
401        {
402            return Ok(cached);
403        }
404        let mut order = (0..self.source.rows.len()).collect::<Vec<_>>();
405        let mut failure = None;
406        order.sort_by(|left, right| {
407            let result = compare_rows(
408                &self.source.rows[*left],
409                &self.source.rows[*right],
410                &sort.key,
411            );
412            match result {
413                Ok(ordering) => {
414                    let ordering = if sort.descending {
415                        ordering.reverse()
416                    } else {
417                        ordering
418                    };
419                    ordering.then_with(|| left.cmp(right))
420                }
421                Err(error) => {
422                    failure.get_or_insert(error);
423                    left.cmp(right)
424                }
425            }
426        });
427        if let Some(error) = failure {
428            return Err(error);
429        }
430        let order = Arc::new(order);
431        self.source
432            .sorted_orders
433            .lock()
434            .map_err(|_| NativeCollectionError::Poisoned)?
435            .insert(sort.clone(), Arc::clone(&order));
436        Ok(order)
437    }
438}
439
440impl CustomType for NativeCollection {
441    fn build(mut builder: TypeBuilder<Self>) {
442        builder
443            .with_name("NativeCollection")
444            .with_get("len", |collection: &mut Self| {
445                i64::try_from(collection.len()).unwrap_or(i64::MAX)
446            })
447            .with_get("key_field", |collection: &mut Self| {
448                ImmutableString::from(collection.key_field().to_owned())
449            })
450            .with_fn("to_string", |collection: &mut Self| {
451                format!("NativeCollection(len={})", collection.len())
452            });
453    }
454}
455
456pub(crate) fn register_native_collection_api(engine: &mut rhai::Engine) {
457    engine.build_type::<NativeCollection>();
458    FuncRegistration::new("is_native_collection")
459        .in_global_namespace()
460        .register_into_engine(engine, |value: Dynamic| value.is::<NativeCollection>());
461    FuncRegistration::new("native_table_view")
462        .in_global_namespace()
463        .register_into_engine(
464            engine,
465            |collection: NativeCollection,
466             config: Map|
467             -> Result<NativeCollection, Box<EvalAltResult>> {
468                collection.table_view(config).map_err(|error| {
469                    Box::new(EvalAltResult::ErrorRuntime(
470                        error.to_string().into(),
471                        Position::NONE,
472                    ))
473                })
474            },
475        );
476    FuncRegistration::new("native_fuzzy_view")
477        .in_global_namespace()
478        .register_into_engine(
479            engine,
480            |collection: NativeCollection,
481             config: Map|
482             -> Result<NativeCollection, Box<EvalAltResult>> {
483                collection
484                    .fuzzy_view(config)
485                    .map_err(|error| Box::new(native_collection_runtime_error(&error)))
486            },
487        );
488    FuncRegistration::new("native_fuzzy_edge")
489        .in_global_namespace()
490        .register_into_engine(
491            engine,
492            |collection: NativeCollection,
493             last: bool|
494             -> Result<ImmutableString, Box<EvalAltResult>> {
495                collection
496                    .fuzzy_edge(last)
497                    .map(ImmutableString::from)
498                    .map_err(|error| Box::new(native_collection_runtime_error(&error)))
499            },
500        );
501    FuncRegistration::new("native_fuzzy_adjacent")
502        .in_global_namespace()
503        .register_into_engine(
504            engine,
505            |collection: NativeCollection,
506             active: ImmutableString,
507             step: i64|
508             -> Result<ImmutableString, Box<EvalAltResult>> {
509                collection
510                    .fuzzy_adjacent(active.as_str(), step)
511                    .map(ImmutableString::from)
512                    .map_err(|error| Box::new(native_collection_runtime_error(&error)))
513            },
514        );
515}
516
517fn native_collection_runtime_error(error: &NativeCollectionError) -> EvalAltResult {
518    EvalAltResult::ErrorRuntime(error.to_string().into(), Position::NONE)
519}
520
521impl CollectionProjection {
522    fn project_row(
523        &self,
524        row: &UiValue,
525        index: usize,
526        key: Option<&str>,
527    ) -> Result<UiValue, NativeCollectionError> {
528        match self {
529            Self::Table(projection) => projection.project_row(row, index),
530            Self::Fuzzy(projection) => projection.project_row(row, index, key),
531        }
532    }
533
534    fn project_group(&self, group: &GroupEntry) -> UiValue {
535        match self {
536            Self::Table(projection) => projection.project_group(group),
537            Self::Fuzzy(projection) => projection.project_group(group),
538        }
539    }
540}
541
542#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
543struct FuzzyOrderSpec {
544    query: String,
545    label_field: String,
546    keywords_field: String,
547    group_field: String,
548}
549
550impl From<&FuzzyProjection> for FuzzyOrderSpec {
551    fn from(projection: &FuzzyProjection) -> Self {
552        Self {
553            query: projection.query.clone(),
554            label_field: projection.label_field.clone(),
555            keywords_field: projection.keywords_field.clone(),
556            group_field: projection.group_field.clone(),
557        }
558    }
559}
560
561#[derive(Clone, Debug, PartialEq)]
562struct FuzzyProjection {
563    query: String,
564    label_field: String,
565    keywords_field: String,
566    group_field: String,
567    shortcut_field: String,
568    disabled_field: String,
569    active: String,
570    row_height: f64,
571}
572
573impl FuzzyProjection {
574    fn decode(config: Map) -> Result<Self, NativeCollectionError> {
575        let UiValue::Map(mut config) = UiValue::from_dynamic(Dynamic::from_map(config))
576            .map_err(|error| NativeCollectionError::InvalidFuzzyConfig(error.to_string()))?
577        else {
578            return Err(NativeCollectionError::InvalidFuzzyConfig(
579                "configuration must be a map".to_owned(),
580            ));
581        };
582        let query = fuzzy_take_string(&mut config, "query")?.to_lowercase();
583        let label_field = fuzzy_take_string(&mut config, "label_field")?;
584        let keywords_field = fuzzy_take_string(&mut config, "keywords_field")?;
585        let group_field = fuzzy_take_string(&mut config, "group_field")?;
586        let shortcut_field = fuzzy_take_string(&mut config, "shortcut_field")?;
587        let disabled_field = fuzzy_take_string(&mut config, "disabled_field")?;
588        let active = fuzzy_take_string(&mut config, "active")?;
589        let row_height = fuzzy_take_number(&mut config, "row_height")?;
590        if !row_height.is_finite() || row_height <= 0.0 {
591            return Err(NativeCollectionError::InvalidFuzzyConfig(
592                "row_height must be finite and positive".to_owned(),
593            ));
594        }
595        for (field, label) in [
596            (&label_field, "label field"),
597            (&keywords_field, "keywords field"),
598            (&group_field, "group field"),
599            (&shortcut_field, "shortcut field"),
600            (&disabled_field, "disabled field"),
601        ] {
602            validate_name(field, label)?;
603        }
604        if !config.is_empty() {
605            return Err(NativeCollectionError::InvalidFuzzyConfig(format!(
606                "unknown configuration fields: {}",
607                config.keys().cloned().collect::<Vec<_>>().join(", ")
608            )));
609        }
610        Ok(Self {
611            query,
612            label_field,
613            keywords_field,
614            group_field,
615            shortcut_field,
616            disabled_field,
617            active,
618            row_height,
619        })
620    }
621
622    fn grouped_order(
623        &self,
624        source: &NativeCollectionSource,
625    ) -> Result<Vec<NativeCollectionEntry>, NativeCollectionError> {
626        let mut grouped = Vec::new();
627        for (index, row) in source.rows.iter().enumerate() {
628            let label = Self::row_string(row, index, &self.label_field, true)?;
629            let keywords = self.row_keywords(row, index)?;
630            let score = fuzzy_score(&label, &keywords, &self.query);
631            if score < 0 {
632                continue;
633            }
634            let group = Self::row_string(row, index, &self.group_field, false)?;
635            grouped.push((group, (index, score)));
636        }
637        let source_keys = source
638            .keys
639            .iter()
640            .map(String::as_str)
641            .collect::<BTreeSet<_>>();
642        let mut header_keys = BTreeSet::new();
643        let mut entries = Vec::new();
644        for (group, mut rows) in crate::collection_projection::stable_groups(grouped) {
645            rows.sort_by(|(left_index, left_score), (right_index, right_score)| {
646                right_score
647                    .cmp(left_score)
648                    .then_with(|| left_index.cmp(right_index))
649            });
650            if !group.is_empty() {
651                let key =
652                    unique_group_key(&format!("command:{group}"), &source_keys, &mut header_keys);
653                entries.push(NativeCollectionEntry::Group(GroupEntry {
654                    key,
655                    value: group,
656                    count: rows.len(),
657                    collapsed: false,
658                }));
659            }
660            entries.extend(
661                rows.into_iter()
662                    .map(|(index, _)| NativeCollectionEntry::Row(index)),
663            );
664        }
665        Ok(entries)
666    }
667
668    fn project_row(
669        &self,
670        row: &UiValue,
671        index: usize,
672        key: Option<&str>,
673    ) -> Result<UiValue, NativeCollectionError> {
674        let key = key.ok_or(NativeCollectionError::CorruptOrder(index))?;
675        let label = Self::row_string(row, index, &self.label_field, true)?;
676        let shortcut = Self::row_string(row, index, &self.shortcut_field, false)?;
677        let disabled = self.row_disabled_value(row, index)?;
678        Ok(UiValue::Map(BTreeMap::from([
679            ("kind".to_owned(), UiValue::String("item".to_owned())),
680            ("key".to_owned(), UiValue::String(format!("item:{key}"))),
681            ("value".to_owned(), UiValue::String(key.to_owned())),
682            ("label".to_owned(), UiValue::String(label)),
683            ("shortcut".to_owned(), UiValue::String(shortcut)),
684            ("disabled".to_owned(), UiValue::Bool(disabled)),
685            ("active".to_owned(), UiValue::Bool(self.active == key)),
686            ("height".to_owned(), UiValue::Float(self.row_height)),
687        ])))
688    }
689
690    fn project_group(&self, group: &GroupEntry) -> UiValue {
691        UiValue::Map(BTreeMap::from([
692            ("kind".to_owned(), UiValue::String("group".to_owned())),
693            ("key".to_owned(), UiValue::String(group.key.clone())),
694            ("label".to_owned(), UiValue::String(group.value.clone())),
695            ("height".to_owned(), UiValue::Float(self.row_height)),
696        ]))
697    }
698
699    fn row_disabled(
700        &self,
701        source: &NativeCollectionSource,
702        index: usize,
703    ) -> Result<bool, NativeCollectionError> {
704        let row = source
705            .rows
706            .get(index)
707            .ok_or(NativeCollectionError::CorruptOrder(index))?;
708        self.row_disabled_value(row, index)
709    }
710
711    fn row_disabled_value(
712        &self,
713        row: &UiValue,
714        index: usize,
715    ) -> Result<bool, NativeCollectionError> {
716        match row_field(row, &self.disabled_field) {
717            None | Some(UiValue::Null) => Ok(false),
718            Some(UiValue::Bool(value)) => Ok(*value),
719            Some(_) => Err(NativeCollectionError::InvalidFuzzyConfig(format!(
720                "row {index} field `{}` must be bool when present",
721                self.disabled_field
722            ))),
723        }
724    }
725
726    fn row_string(
727        row: &UiValue,
728        index: usize,
729        field: &str,
730        required: bool,
731    ) -> Result<String, NativeCollectionError> {
732        match row_field(row, field) {
733            Some(UiValue::String(value)) if !required || !value.is_empty() => Ok(value.clone()),
734            None | Some(UiValue::Null) if !required => Ok(String::new()),
735            _ => Err(NativeCollectionError::InvalidFuzzyConfig(format!(
736                "row {index} field `{field}` must be {}string",
737                if required { "a non-empty " } else { "a " }
738            ))),
739        }
740    }
741
742    fn row_keywords(
743        &self,
744        row: &UiValue,
745        index: usize,
746    ) -> Result<Vec<String>, NativeCollectionError> {
747        match row_field(row, &self.keywords_field) {
748            None | Some(UiValue::Null) => Ok(Vec::new()),
749            Some(UiValue::Array(values)) => values
750                .iter()
751                .enumerate()
752                .map(|(keyword, value)| match value {
753                    UiValue::String(value) => Ok(value.clone()),
754                    _ => Err(NativeCollectionError::InvalidFuzzyConfig(format!(
755                        "row {index} field `{}` item {keyword} must be a string",
756                        self.keywords_field
757                    ))),
758                })
759                .collect(),
760            Some(_) => Err(NativeCollectionError::InvalidFuzzyConfig(format!(
761                "row {index} field `{}` must be an array of strings",
762                self.keywords_field
763            ))),
764        }
765    }
766}
767
768fn fuzzy_score(label: &str, keywords: &[String], query: &str) -> i64 {
769    std::iter::once((label, 0))
770        .chain(keywords.iter().map(|keyword| (keyword.as_str(), -100)))
771        .map(|(candidate, penalty)| fuzzy_candidate_score(candidate, query) + penalty)
772        .max()
773        .unwrap_or(-1)
774}
775
776fn fuzzy_candidate_score(candidate: &str, query: &str) -> i64 {
777    if query.is_empty() {
778        return 0;
779    }
780    let candidate = candidate.to_lowercase();
781    if candidate == query {
782        return 10_000;
783    }
784    let length = i64::try_from(candidate.chars().count()).unwrap_or(i64::MAX);
785    if candidate.starts_with(query) {
786        return 8_000 - length;
787    }
788    if let Some(position) = candidate.find(query) {
789        let position = i64::try_from(candidate[..position].chars().count()).unwrap_or(i64::MAX);
790        return 6_000 - position.saturating_mul(8) - length;
791    }
792    let chars = candidate.chars().collect::<Vec<_>>();
793    let mut position = 0usize;
794    let mut gaps = 0i64;
795    for needle in query.chars() {
796        let Some(found) = chars[position..]
797            .iter()
798            .position(|candidate| *candidate == needle)
799        else {
800            return -1;
801        };
802        gaps = gaps.saturating_add(i64::try_from(found).unwrap_or(i64::MAX));
803        position = position.saturating_add(found).saturating_add(1);
804    }
805    4_000 - gaps.saturating_mul(12) - length
806}
807
808fn fuzzy_take_string(
809    values: &mut BTreeMap<String, UiValue>,
810    name: &str,
811) -> Result<String, NativeCollectionError> {
812    match values.remove(name) {
813        Some(UiValue::String(value)) => Ok(value),
814        _ => Err(NativeCollectionError::InvalidFuzzyConfig(format!(
815            "{name} must be a string"
816        ))),
817    }
818}
819
820fn fuzzy_take_number(
821    values: &mut BTreeMap<String, UiValue>,
822    name: &str,
823) -> Result<f64, NativeCollectionError> {
824    match values.remove(name) {
825        Some(UiValue::Float(value)) => Ok(value),
826        Some(UiValue::Integer(value)) => Ok(integer_float(value)),
827        _ => Err(NativeCollectionError::InvalidFuzzyConfig(format!(
828            "{name} must be a number"
829        ))),
830    }
831}
832
833#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
834struct SortSpec {
835    key: String,
836    descending: bool,
837}
838
839#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
840struct TableOrderSpec {
841    sort: Option<SortSpec>,
842    group_by: Option<String>,
843    collapsed_groups: BTreeSet<String>,
844    query: String,
845    search_fields: Vec<String>,
846    page: usize,
847    page_size: Option<usize>,
848}
849
850impl From<&TableProjection> for TableOrderSpec {
851    fn from(projection: &TableProjection) -> Self {
852        Self {
853            sort: projection.sort.clone(),
854            group_by: projection.group_by.clone(),
855            collapsed_groups: projection.collapsed_groups.clone(),
856            query: projection.query.clone(),
857            search_fields: projection.search_fields.clone(),
858            page: projection.page,
859            page_size: projection.page_size,
860        }
861    }
862}
863
864#[derive(Clone, Debug, PartialEq)]
865struct TableProjection {
866    label: String,
867    row_key: String,
868    columns: Vec<TableColumn>,
869    selected_keys: Vec<String>,
870    selected: BTreeSet<String>,
871    selection_mode: SelectionMode,
872    striped: bool,
873    row_height: f64,
874    sort: Option<SortSpec>,
875    group_by: Option<String>,
876    collapsed_groups: BTreeSet<String>,
877    group_toggle: bool,
878    query: String,
879    search_fields: Vec<String>,
880    page: usize,
881    page_size: Option<usize>,
882}
883
884#[derive(Clone, Debug, PartialEq)]
885struct TableColumn {
886    key: String,
887    width: UiValue,
888    align: String,
889    resize_signal_key: Option<String>,
890    adornments: Vec<TableAdornment>,
891}
892
893#[derive(Clone, Debug, Eq, PartialEq)]
894struct TableAdornment {
895    text_key: String,
896    variant: String,
897    variant_key: Option<String>,
898    dot: bool,
899}
900
901#[derive(Clone, Copy, Debug, Eq, PartialEq)]
902enum SelectionMode {
903    None,
904    Single,
905    Multiple,
906}
907
908impl TableProjection {
909    fn decode(config: UiValue, source_key: &str) -> Result<Self, NativeCollectionError> {
910        let UiValue::Map(mut config) = config else {
911            return Err(NativeCollectionError::InvalidTableConfig(
912                "configuration must be a map".to_owned(),
913            ));
914        };
915        let row_key = take_string(&mut config, "row_key")?;
916        if row_key != source_key {
917            return Err(NativeCollectionError::KeyFieldMismatch {
918                source_key: source_key.to_owned(),
919                requested: row_key,
920            });
921        }
922        let label = take_string(&mut config, "label")?;
923        let columns = take_array(&mut config, "columns")?
924            .into_iter()
925            .map(TableColumn::decode)
926            .collect::<Result<Vec<_>, _>>()?;
927        let selected_keys = take_array(&mut config, "selected_keys")?
928            .into_iter()
929            .enumerate()
930            .map(|(index, value)| match value {
931                UiValue::String(value) => Ok(value),
932                _ => Err(NativeCollectionError::InvalidTableConfig(format!(
933                    "selected_keys[{index}] must be a string"
934                ))),
935            })
936            .collect::<Result<Vec<_>, _>>()?;
937        let selected = selected_keys.iter().cloned().collect();
938        let selection_mode = match take_string(&mut config, "selection_mode")?.as_str() {
939            "none" => SelectionMode::None,
940            "single" => SelectionMode::Single,
941            "multiple" => SelectionMode::Multiple,
942            value => {
943                return Err(NativeCollectionError::InvalidTableConfig(format!(
944                    "unknown selection mode `{value}`"
945                )));
946            }
947        };
948        let striped = take_bool(&mut config, "striped")?;
949        let row_height = take_number(&mut config, "row_height")?;
950        if !row_height.is_finite() || row_height <= 0.0 {
951            return Err(NativeCollectionError::InvalidTableConfig(
952                "row_height must be finite and positive".to_owned(),
953            ));
954        }
955        let sort = decode_sort(config.remove("sort"))?;
956        let (group_by, collapsed_groups, group_toggle) = decode_grouping(&mut config, &columns)?;
957        let (query, search_fields, page, page_size) =
958            decode_table_window(&mut config, &row_key, &columns)?;
959        if !config.is_empty() {
960            return Err(NativeCollectionError::InvalidTableConfig(format!(
961                "unknown configuration fields: {}",
962                config.keys().cloned().collect::<Vec<_>>().join(", ")
963            )));
964        }
965        Ok(Self {
966            label,
967            row_key,
968            columns,
969            selected_keys,
970            selected,
971            selection_mode,
972            striped,
973            row_height,
974            sort,
975            group_by,
976            collapsed_groups,
977            group_toggle,
978            query,
979            search_fields,
980            page,
981            page_size,
982        })
983    }
984
985    fn visible_rows(&self, source: &NativeCollectionSource, rows: &[usize]) -> Vec<usize> {
986        let mut visible = if self.query.is_empty() {
987            rows.to_vec()
988        } else {
989            rows.iter()
990                .copied()
991                .filter(|index| {
992                    source.rows.get(*index).is_some_and(|row| {
993                        self.search_fields.iter().any(|field| {
994                            row_field(row, field)
995                                .and_then(scalar_text)
996                                .is_some_and(|value| value.to_lowercase().contains(&self.query))
997                        })
998                    })
999                })
1000                .collect()
1001        };
1002        if let Some(page_size) = self.page_size {
1003            let start = self.page.saturating_sub(1).saturating_mul(page_size);
1004            if start >= visible.len() {
1005                visible.clear();
1006            } else {
1007                let end = start.saturating_add(page_size).min(visible.len());
1008                visible = visible[start..end].to_vec();
1009            }
1010        }
1011        visible
1012    }
1013
1014    fn grouped_order(
1015        &self,
1016        source: &NativeCollectionSource,
1017        rows: &[usize],
1018    ) -> Result<Vec<NativeCollectionEntry>, NativeCollectionError> {
1019        let Some(group_by) = &self.group_by else {
1020            return Ok(rows
1021                .iter()
1022                .copied()
1023                .map(NativeCollectionEntry::Row)
1024                .collect());
1025        };
1026        let mut grouped = Vec::with_capacity(rows.len());
1027        for source_index in rows {
1028            let row = source
1029                .rows
1030                .get(*source_index)
1031                .ok_or(NativeCollectionError::CorruptOrder(*source_index))?;
1032            let value = match row_field(row, group_by) {
1033                Some(UiValue::String(value)) if !value.is_empty() => value.clone(),
1034                _ => {
1035                    return Err(NativeCollectionError::InvalidGroupField {
1036                        index: *source_index,
1037                        field: group_by.clone(),
1038                    });
1039                }
1040            };
1041            grouped.push((value, *source_index));
1042        }
1043        let source_keys = source
1044            .keys
1045            .iter()
1046            .map(String::as_str)
1047            .collect::<BTreeSet<_>>();
1048        let mut header_keys = BTreeSet::new();
1049        let groups = crate::collection_projection::stable_groups(grouped);
1050        let mut entries = Vec::with_capacity(rows.len().saturating_add(groups.len()));
1051        for (value, rows) in groups {
1052            let key = unique_group_key(&value, &source_keys, &mut header_keys);
1053            let collapsed = self.collapsed_groups.contains(&value);
1054            entries.push(NativeCollectionEntry::Group(GroupEntry {
1055                key,
1056                value,
1057                count: rows.len(),
1058                collapsed,
1059            }));
1060            if !collapsed {
1061                entries.extend(rows.into_iter().map(NativeCollectionEntry::Row));
1062            }
1063        }
1064        Ok(entries)
1065    }
1066
1067    fn project_row(&self, row: &UiValue, index: usize) -> Result<UiValue, NativeCollectionError> {
1068        let UiValue::Map(row) = row else {
1069            return Err(NativeCollectionError::RowNotMap(index));
1070        };
1071        let key = row.get(&self.row_key).and_then(scalar_text);
1072        let key = key.ok_or_else(|| {
1073            NativeCollectionError::InvalidTableConfig("projected row key is unavailable".to_owned())
1074        })?;
1075        let selected = self.selected.contains(&key);
1076        let cells = self
1077            .columns
1078            .iter()
1079            .map(|column| {
1080                let text = row
1081                    .get(&column.key)
1082                    .map_or_else(String::new, display_scalar);
1083                let adornments = column
1084                    .adornments
1085                    .iter()
1086                    .map(|adornment| adornment.project(row))
1087                    .collect::<Result<Vec<_>, _>>()?
1088                    .into_iter()
1089                    .flatten()
1090                    .collect();
1091                let mut cell = BTreeMap::from([
1092                    ("key".to_owned(), UiValue::String(column.key.clone())),
1093                    ("text".to_owned(), UiValue::String(text)),
1094                    ("width".to_owned(), column.width.clone()),
1095                    ("align".to_owned(), UiValue::String(column.align.clone())),
1096                    ("adornments".to_owned(), UiValue::Array(adornments)),
1097                ]);
1098                if let Some(signal_key) = &column.resize_signal_key {
1099                    cell.insert(
1100                        "resize_signal_key".to_owned(),
1101                        UiValue::String(signal_key.clone()),
1102                    );
1103                }
1104                Ok(UiValue::Map(cell))
1105            })
1106            .collect::<Result<Vec<_>, NativeCollectionError>>()?;
1107        let selection = match self.selection_mode {
1108            SelectionMode::None => UiValue::Null,
1109            SelectionMode::Single if selected => UiValue::Array(Vec::new()),
1110            SelectionMode::Single => UiValue::Array(vec![UiValue::String(key.clone())]),
1111            SelectionMode::Multiple => {
1112                let mut next = self
1113                    .selected_keys
1114                    .iter()
1115                    .filter(|candidate| *candidate != &key)
1116                    .cloned()
1117                    .map(UiValue::String)
1118                    .collect::<Vec<_>>();
1119                if !selected {
1120                    next.push(UiValue::String(key.clone()));
1121                }
1122                UiValue::Array(next)
1123            }
1124        };
1125        Ok(UiValue::Map(BTreeMap::from([
1126            ("kind".to_owned(), UiValue::String("row".to_owned())),
1127            ("key".to_owned(), UiValue::String(key.clone())),
1128            (
1129                "label".to_owned(),
1130                UiValue::String(format!("{} row {}", self.label, index + 1)),
1131            ),
1132            ("cells".to_owned(), UiValue::Array(cells)),
1133            ("selected".to_owned(), UiValue::Bool(selected)),
1134            ("selection".to_owned(), selection),
1135            (
1136                "striped".to_owned(),
1137                UiValue::Bool(self.striped && index % 2 == 1),
1138            ),
1139            ("height".to_owned(), UiValue::Float(self.row_height)),
1140        ])))
1141    }
1142
1143    fn project_group(&self, group: &GroupEntry) -> UiValue {
1144        UiValue::Map(BTreeMap::from([
1145            ("kind".to_owned(), UiValue::String("group".to_owned())),
1146            ("key".to_owned(), UiValue::String(group.key.clone())),
1147            ("group".to_owned(), UiValue::String(group.value.clone())),
1148            (
1149                "count".to_owned(),
1150                UiValue::Integer(i64::try_from(group.count).unwrap_or(i64::MAX)),
1151            ),
1152            ("collapsed".to_owned(), UiValue::Bool(group.collapsed)),
1153            ("toggle".to_owned(), UiValue::Bool(self.group_toggle)),
1154            ("height".to_owned(), UiValue::Float(self.row_height)),
1155        ]))
1156    }
1157}
1158
1159fn decode_table_window(
1160    config: &mut BTreeMap<String, UiValue>,
1161    row_key: &str,
1162    columns: &[TableColumn],
1163) -> Result<(String, Vec<String>, usize, Option<usize>), NativeCollectionError> {
1164    let query = match config.remove("query") {
1165        None => String::new(),
1166        Some(UiValue::String(value)) => value.to_lowercase(),
1167        _ => {
1168            return Err(NativeCollectionError::InvalidTableConfig(
1169                "query must be a string".to_owned(),
1170            ));
1171        }
1172    };
1173    let search_fields = match config.remove("search_fields") {
1174        None => Vec::new(),
1175        Some(UiValue::Array(values)) => values,
1176        _ => {
1177            return Err(NativeCollectionError::InvalidTableConfig(
1178                "search_fields must be an array".to_owned(),
1179            ));
1180        }
1181    }
1182    .into_iter()
1183    .enumerate()
1184    .map(|(index, value)| match value {
1185        UiValue::String(value) if !value.is_empty() => Ok(value),
1186        _ => Err(NativeCollectionError::InvalidTableConfig(format!(
1187            "search_fields[{index}] must be a non-empty string"
1188        ))),
1189    })
1190    .collect::<Result<Vec<_>, _>>()?;
1191    if !query.is_empty() && search_fields.is_empty() {
1192        return Err(NativeCollectionError::InvalidTableConfig(
1193            "non-empty query requires search_fields".to_owned(),
1194        ));
1195    }
1196    if let Some(field) = search_fields.iter().find(|field| {
1197        field.as_str() != row_key && !columns.iter().any(|column| column.key == field.as_str())
1198    }) {
1199        return Err(NativeCollectionError::InvalidTableConfig(format!(
1200            "search field `{field}` is not the row key or a declared column"
1201        )));
1202    }
1203    let page = if config.contains_key("page") {
1204        take_positive_usize(config, "page")?
1205    } else {
1206        1
1207    };
1208    let page_size = take_optional_positive_usize(config, "page_size")?;
1209    Ok((query, search_fields, page, page_size))
1210}
1211
1212fn decode_sort(value: Option<UiValue>) -> Result<Option<SortSpec>, NativeCollectionError> {
1213    let Some(value) = value else {
1214        return Ok(None);
1215    };
1216    let UiValue::Map(mut sort) = value else {
1217        if value == UiValue::Null {
1218            return Ok(None);
1219        }
1220        return Err(NativeCollectionError::InvalidTableConfig(
1221            "sort must be null or a map".to_owned(),
1222        ));
1223    };
1224    let key = take_string(&mut sort, "key")?;
1225    let direction = take_string(&mut sort, "direction")?;
1226    if !sort.is_empty() {
1227        return Err(NativeCollectionError::InvalidTableConfig(
1228            "sort contains unknown fields".to_owned(),
1229        ));
1230    }
1231    let descending = match direction.as_str() {
1232        "ascending" => false,
1233        "descending" => true,
1234        _ => {
1235            return Err(NativeCollectionError::InvalidTableConfig(format!(
1236                "unknown sort direction `{direction}`"
1237            )));
1238        }
1239    };
1240    Ok(Some(SortSpec { key, descending }))
1241}
1242
1243fn decode_grouping(
1244    config: &mut BTreeMap<String, UiValue>,
1245    columns: &[TableColumn],
1246) -> Result<(Option<String>, BTreeSet<String>, bool), NativeCollectionError> {
1247    let group_by = take_optional_string(config, "group_by")?;
1248    if let Some(group_by) = &group_by
1249        && !columns.iter().any(|column| &column.key == group_by)
1250    {
1251        return Err(NativeCollectionError::InvalidTableConfig(format!(
1252            "group_by `{group_by}` is not a declared column"
1253        )));
1254    }
1255    let collapsed = if config.contains_key("collapsed_groups") {
1256        take_string_set(config, "collapsed_groups")?
1257    } else {
1258        BTreeSet::new()
1259    };
1260    if group_by.is_none() && !collapsed.is_empty() {
1261        return Err(NativeCollectionError::InvalidTableConfig(
1262            "collapsed_groups requires group_by".to_owned(),
1263        ));
1264    }
1265    let toggle = if config.contains_key("group_toggle") {
1266        take_bool(config, "group_toggle")?
1267    } else {
1268        false
1269    };
1270    Ok((group_by, collapsed, toggle))
1271}
1272
1273fn unique_group_key(
1274    value: &str,
1275    source_keys: &BTreeSet<&str>,
1276    header_keys: &mut BTreeSet<String>,
1277) -> String {
1278    let mut key = format!("__gpui_rhai_group__:{value}");
1279    while source_keys.contains(key.as_str()) || !header_keys.insert(key.clone()) {
1280        key.insert(0, '_');
1281    }
1282    key
1283}
1284
1285impl TableColumn {
1286    fn decode(value: UiValue) -> Result<Self, NativeCollectionError> {
1287        let UiValue::Map(mut column) = value else {
1288            return Err(NativeCollectionError::InvalidTableConfig(
1289                "each column must be a map".to_owned(),
1290            ));
1291        };
1292        let key = take_string(&mut column, "key")?;
1293        let width = column.remove("width").ok_or_else(|| {
1294            NativeCollectionError::InvalidTableConfig("column.width is required".to_owned())
1295        })?;
1296        let align = match column.remove("align") {
1297            None => "start".to_owned(),
1298            Some(UiValue::String(value)) => value,
1299            Some(_) => {
1300                return Err(NativeCollectionError::InvalidTableConfig(
1301                    "column.align must be a string".to_owned(),
1302                ));
1303            }
1304        };
1305        let resize_signal_key = match column.remove("resize_signal_key") {
1306            Some(UiValue::String(value)) => Some(value),
1307            Some(_) => {
1308                return Err(NativeCollectionError::InvalidTableConfig(
1309                    "resize_signal_key must be a string".to_owned(),
1310                ));
1311            }
1312            None => None,
1313        };
1314        let adornments = match column.remove("adornments") {
1315            Some(UiValue::Array(values)) => values
1316                .into_iter()
1317                .map(TableAdornment::decode)
1318                .collect::<Result<Vec<_>, _>>()?,
1319            Some(_) => {
1320                return Err(NativeCollectionError::InvalidTableConfig(
1321                    "column.adornments must be an array".to_owned(),
1322                ));
1323            }
1324            None => Vec::new(),
1325        };
1326        for normalized_only in [
1327            "title",
1328            "sortable",
1329            "resize_enabled",
1330            "resize_ref_key",
1331            "min_width",
1332            "max_width",
1333        ] {
1334            column.remove(normalized_only);
1335        }
1336        if !column.is_empty() {
1337            return Err(NativeCollectionError::InvalidTableConfig(format!(
1338                "unknown column fields: {}",
1339                column.keys().cloned().collect::<Vec<_>>().join(", ")
1340            )));
1341        }
1342        Ok(Self {
1343            key,
1344            width,
1345            align,
1346            resize_signal_key,
1347            adornments,
1348        })
1349    }
1350}
1351
1352impl TableAdornment {
1353    fn decode(value: UiValue) -> Result<Self, NativeCollectionError> {
1354        let UiValue::Map(mut value) = value else {
1355            return Err(NativeCollectionError::InvalidTableConfig(
1356                "each column adornment must be a map".to_owned(),
1357            ));
1358        };
1359        let text_key = take_string(&mut value, "text_key")?;
1360        let variant = match value.remove("variant") {
1361            Some(UiValue::String(value)) => value,
1362            Some(_) => {
1363                return Err(NativeCollectionError::InvalidTableConfig(
1364                    "adornment.variant must be a string".to_owned(),
1365                ));
1366            }
1367            None => "neutral".to_owned(),
1368        };
1369        validate_badge_variant(&variant)?;
1370        let variant_key = match value.remove("variant_key") {
1371            Some(UiValue::String(value)) => Some(value),
1372            Some(UiValue::Null) | None => None,
1373            Some(_) => {
1374                return Err(NativeCollectionError::InvalidTableConfig(
1375                    "adornment.variant_key must be a string".to_owned(),
1376                ));
1377            }
1378        };
1379        let dot = match value.remove("dot") {
1380            Some(UiValue::Bool(value)) => value,
1381            Some(_) => {
1382                return Err(NativeCollectionError::InvalidTableConfig(
1383                    "adornment.dot must be a bool".to_owned(),
1384                ));
1385            }
1386            None => false,
1387        };
1388        if !value.is_empty() {
1389            return Err(NativeCollectionError::InvalidTableConfig(format!(
1390                "unknown adornment fields: {}",
1391                value.keys().cloned().collect::<Vec<_>>().join(", ")
1392            )));
1393        }
1394        Ok(Self {
1395            text_key,
1396            variant,
1397            variant_key,
1398            dot,
1399        })
1400    }
1401
1402    fn project(
1403        &self,
1404        row: &BTreeMap<String, UiValue>,
1405    ) -> Result<Option<UiValue>, NativeCollectionError> {
1406        let text = row
1407            .get(&self.text_key)
1408            .map_or_else(String::new, display_scalar);
1409        if text.is_empty() {
1410            return Ok(None);
1411        }
1412        let variant = self.variant_key.as_ref().map_or_else(
1413            || Ok(self.variant.clone()),
1414            |key| match row.get(key) {
1415                Some(UiValue::String(value)) => Ok(value.clone()),
1416                _ => Err(NativeCollectionError::InvalidTableConfig(format!(
1417                    "adornment variant field `{key}` must be a string"
1418                ))),
1419            },
1420        )?;
1421        validate_badge_variant(&variant)?;
1422        Ok(Some(UiValue::Map(BTreeMap::from([
1423            ("text".to_owned(), UiValue::String(text)),
1424            ("variant".to_owned(), UiValue::String(variant)),
1425            ("dot".to_owned(), UiValue::Bool(self.dot)),
1426        ]))))
1427    }
1428}
1429
1430fn validate_badge_variant(value: &str) -> Result<(), NativeCollectionError> {
1431    if matches!(
1432        value,
1433        "neutral" | "accent" | "success" | "warning" | "danger"
1434    ) {
1435        Ok(())
1436    } else {
1437        Err(NativeCollectionError::InvalidTableConfig(format!(
1438            "unknown adornment variant `{value}`"
1439        )))
1440    }
1441}
1442
1443fn compare_rows(
1444    left: &UiValue,
1445    right: &UiValue,
1446    key: &str,
1447) -> Result<Ordering, NativeCollectionError> {
1448    let left = row_field(left, key)
1449        .ok_or_else(|| NativeCollectionError::MissingSortField(key.to_owned()))?;
1450    let right = row_field(right, key)
1451        .ok_or_else(|| NativeCollectionError::MissingSortField(key.to_owned()))?;
1452    compare_values(left, right)
1453        .ok_or_else(|| NativeCollectionError::UnsortableField(key.to_owned()))
1454}
1455
1456fn row_field<'a>(row: &'a UiValue, key: &str) -> Option<&'a UiValue> {
1457    match row {
1458        UiValue::Map(row) => row.get(key),
1459        _ => None,
1460    }
1461}
1462
1463fn compare_values(left: &UiValue, right: &UiValue) -> Option<Ordering> {
1464    match (left, right) {
1465        (UiValue::Null, UiValue::Null) => Some(Ordering::Equal),
1466        (UiValue::Bool(left), UiValue::Bool(right)) => Some(left.cmp(right)),
1467        (UiValue::Integer(left), UiValue::Integer(right)) => Some(left.cmp(right)),
1468        (UiValue::Float(left), UiValue::Float(right)) => left.partial_cmp(right),
1469        (UiValue::Integer(left), UiValue::Float(right)) => integer_float(*left).partial_cmp(right),
1470        (UiValue::Float(left), UiValue::Integer(right)) => left.partial_cmp(&integer_float(*right)),
1471        (UiValue::String(left), UiValue::String(right)) => Some(left.cmp(right)),
1472        _ => None,
1473    }
1474}
1475
1476fn scalar_text(value: &UiValue) -> Option<String> {
1477    match value {
1478        UiValue::Bool(value) => Some(value.to_string()),
1479        UiValue::Integer(value) => Some(value.to_string()),
1480        UiValue::Float(value) if value.is_finite() => Some(value.to_string()),
1481        UiValue::String(value) => Some(value.clone()),
1482        UiValue::Null
1483        | UiValue::Float(_)
1484        | UiValue::Array(_)
1485        | UiValue::Map(_)
1486        | UiValue::Handle(_) => None,
1487    }
1488}
1489
1490fn display_scalar(value: &UiValue) -> String {
1491    scalar_text(value).unwrap_or_default()
1492}
1493
1494fn take_string(
1495    values: &mut BTreeMap<String, UiValue>,
1496    name: &str,
1497) -> Result<String, NativeCollectionError> {
1498    match values.remove(name) {
1499        Some(UiValue::String(value)) => Ok(value),
1500        _ => Err(NativeCollectionError::InvalidTableConfig(format!(
1501            "{name} must be a string"
1502        ))),
1503    }
1504}
1505
1506fn take_optional_string(
1507    values: &mut BTreeMap<String, UiValue>,
1508    name: &str,
1509) -> Result<Option<String>, NativeCollectionError> {
1510    match values.remove(name) {
1511        None | Some(UiValue::Null) => Ok(None),
1512        Some(UiValue::String(value)) if !value.is_empty() => Ok(Some(value)),
1513        _ => Err(NativeCollectionError::InvalidTableConfig(format!(
1514            "{name} must be null or a non-empty string"
1515        ))),
1516    }
1517}
1518
1519fn take_string_set(
1520    values: &mut BTreeMap<String, UiValue>,
1521    name: &str,
1522) -> Result<BTreeSet<String>, NativeCollectionError> {
1523    take_array(values, name)?
1524        .into_iter()
1525        .enumerate()
1526        .map(|(index, value)| match value {
1527            UiValue::String(value) if !value.is_empty() => Ok(value),
1528            _ => Err(NativeCollectionError::InvalidTableConfig(format!(
1529                "{name}[{index}] must be a non-empty string"
1530            ))),
1531        })
1532        .collect()
1533}
1534
1535fn take_array(
1536    values: &mut BTreeMap<String, UiValue>,
1537    name: &str,
1538) -> Result<Vec<UiValue>, NativeCollectionError> {
1539    match values.remove(name) {
1540        Some(UiValue::Array(value)) => Ok(value),
1541        _ => Err(NativeCollectionError::InvalidTableConfig(format!(
1542            "{name} must be an array"
1543        ))),
1544    }
1545}
1546
1547fn take_bool(
1548    values: &mut BTreeMap<String, UiValue>,
1549    name: &str,
1550) -> Result<bool, NativeCollectionError> {
1551    match values.remove(name) {
1552        Some(UiValue::Bool(value)) => Ok(value),
1553        _ => Err(NativeCollectionError::InvalidTableConfig(format!(
1554            "{name} must be a bool"
1555        ))),
1556    }
1557}
1558
1559fn take_positive_usize(
1560    values: &mut BTreeMap<String, UiValue>,
1561    name: &str,
1562) -> Result<usize, NativeCollectionError> {
1563    match values.remove(name) {
1564        Some(UiValue::Integer(value)) if value > 0 => usize::try_from(value).map_err(|_| {
1565            NativeCollectionError::InvalidTableConfig(format!(
1566                "{name} exceeds the supported platform size"
1567            ))
1568        }),
1569        _ => Err(NativeCollectionError::InvalidTableConfig(format!(
1570            "{name} must be a positive integer"
1571        ))),
1572    }
1573}
1574
1575fn take_optional_positive_usize(
1576    values: &mut BTreeMap<String, UiValue>,
1577    name: &str,
1578) -> Result<Option<usize>, NativeCollectionError> {
1579    match values.remove(name) {
1580        None | Some(UiValue::Null) => Ok(None),
1581        Some(UiValue::Integer(value)) if value > 0 => {
1582            usize::try_from(value).map(Some).map_err(|_| {
1583                NativeCollectionError::InvalidTableConfig(format!(
1584                    "{name} exceeds the supported platform size"
1585                ))
1586            })
1587        }
1588        _ => Err(NativeCollectionError::InvalidTableConfig(format!(
1589            "{name} must be null or a positive integer"
1590        ))),
1591    }
1592}
1593
1594fn take_number(
1595    values: &mut BTreeMap<String, UiValue>,
1596    name: &str,
1597) -> Result<f64, NativeCollectionError> {
1598    match values.remove(name) {
1599        Some(UiValue::Float(value)) => Ok(value),
1600        Some(UiValue::Integer(value)) => Ok(integer_float(value)),
1601        _ => Err(NativeCollectionError::InvalidTableConfig(format!(
1602            "{name} must be a number"
1603        ))),
1604    }
1605}
1606
1607fn integer_float(value: i64) -> f64 {
1608    value.to_string().parse().unwrap_or_else(|_| {
1609        if value.is_negative() {
1610            f64::MIN
1611        } else {
1612            f64::MAX
1613        }
1614    })
1615}
1616
1617fn validate_name(name: &str, label: &'static str) -> Result<(), NativeCollectionError> {
1618    if name.is_empty() || name.len() > 256 || name.chars().any(char::is_control) {
1619        Err(NativeCollectionError::InvalidName(label, name.to_owned()))
1620    } else {
1621        Ok(())
1622    }
1623}
1624
1625const MAX_MISSING_NAMES_PER_READER: usize = 64;
1626const MAX_MISSING_READER_PAIRS: usize = 4_096;
1627const MAX_MISSING_NAME_BYTES: usize = 64 * 1024;
1628
1629#[derive(Clone, Debug, Default)]
1630pub struct NativeCollectionRegistry {
1631    collections: BTreeMap<String, NativeCollection>,
1632    readers: BTreeMap<String, BTreeSet<crate::read_dependency::ReadDependency>>,
1633    missing_readers: BTreeMap<String, BTreeSet<crate::read_dependency::ReadDependency>>,
1634}
1635
1636impl NativeCollectionRegistry {
1637    #[must_use]
1638    pub fn new() -> Self {
1639        Self::default()
1640    }
1641
1642    /// Register one application-owned collection before initial render.
1643    ///
1644    /// # Errors
1645    ///
1646    /// Returns an error for an unsafe or duplicate name.
1647    pub fn register(
1648        &mut self,
1649        name: impl Into<String>,
1650        collection: NativeCollection,
1651    ) -> Result<(), NativeCollectionError> {
1652        self.register_with_invalidated(name.into(), collection)?;
1653        Ok(())
1654    }
1655
1656    pub(crate) fn register_with_invalidated(
1657        &mut self,
1658        name: String,
1659        collection: NativeCollection,
1660    ) -> Result<BTreeSet<ComponentInstancePath>, NativeCollectionError> {
1661        validate_name(&name, "collection name")?;
1662        if self.collections.contains_key(&name) {
1663            return Err(NativeCollectionError::DuplicateCollection(name));
1664        }
1665        self.collections.insert(name.clone(), collection);
1666        Ok(crate::read_dependency::owners(
1667            self.missing_readers.remove(&name).unwrap_or_default(),
1668        ))
1669    }
1670
1671    /// Replace an existing collection and return its exact subscribed readers.
1672    ///
1673    /// # Errors
1674    ///
1675    /// Returns an error when the collection is unknown.
1676    pub fn replace(
1677        &mut self,
1678        name: &str,
1679        collection: NativeCollection,
1680    ) -> Result<BTreeSet<ComponentInstancePath>, NativeCollectionError> {
1681        let current = self
1682            .collections
1683            .get_mut(name)
1684            .ok_or_else(|| NativeCollectionError::UnknownCollection(name.to_owned()))?;
1685        if current == &collection {
1686            return Ok(BTreeSet::new());
1687        }
1688        *current = collection;
1689        Ok(crate::read_dependency::owners(
1690            self.readers.get(name).cloned().unwrap_or_default(),
1691        ))
1692    }
1693
1694    #[cfg(test)]
1695    pub(crate) fn read_tracked(
1696        &mut self,
1697        reader: &ComponentInstancePath,
1698        name: &str,
1699    ) -> Result<NativeCollection, NativeCollectionError> {
1700        self.read_tracked_with_missing(reader, name, true)
1701    }
1702
1703    #[cfg(test)]
1704    pub(crate) fn read_tracked_with_missing(
1705        &mut self,
1706        reader: &ComponentInstancePath,
1707        name: &str,
1708        track_missing: bool,
1709    ) -> Result<NativeCollection, NativeCollectionError> {
1710        self.read_dependency(
1711            &crate::read_dependency::ReadDependency::component(reader),
1712            name,
1713            track_missing,
1714        )
1715    }
1716
1717    pub(crate) fn read_dependency(
1718        &mut self,
1719        reader: &crate::read_dependency::ReadDependency,
1720        name: &str,
1721        track_missing: bool,
1722    ) -> Result<NativeCollection, NativeCollectionError> {
1723        validate_name(name, "collection name")?;
1724        let Some(collection) = self.collections.get(name).cloned() else {
1725            if track_missing {
1726                self.track_missing_reader(reader, name)?;
1727            }
1728            return Err(NativeCollectionError::UnknownCollection(name.to_owned()));
1729        };
1730        if let Some(readers) = self.missing_readers.get_mut(name) {
1731            readers.remove(reader);
1732            if readers.is_empty() {
1733                self.missing_readers.remove(name);
1734            }
1735        }
1736        self.readers
1737            .entry(name.to_owned())
1738            .or_default()
1739            .insert(reader.clone());
1740        Ok(collection)
1741    }
1742
1743    fn track_missing_reader(
1744        &mut self,
1745        reader: &crate::read_dependency::ReadDependency,
1746        name: &str,
1747    ) -> Result<(), NativeCollectionError> {
1748        if self
1749            .missing_readers
1750            .get(name)
1751            .is_some_and(|readers| readers.contains(reader))
1752        {
1753            return Ok(());
1754        }
1755        let reader_names = self
1756            .missing_readers
1757            .values()
1758            .filter(|readers| readers.contains(reader))
1759            .count();
1760        if reader_names >= MAX_MISSING_NAMES_PER_READER {
1761            return Err(NativeCollectionError::MissingDependencyBudget {
1762                kind: "names per component",
1763                limit: MAX_MISSING_NAMES_PER_READER,
1764            });
1765        }
1766        let pairs = self
1767            .missing_readers
1768            .values()
1769            .map(BTreeSet::len)
1770            .sum::<usize>();
1771        if pairs >= MAX_MISSING_READER_PAIRS {
1772            return Err(NativeCollectionError::MissingDependencyBudget {
1773                kind: "component/name pairs",
1774                limit: MAX_MISSING_READER_PAIRS,
1775            });
1776        }
1777        if !self.missing_readers.contains_key(name) {
1778            let bytes = self.missing_readers.keys().map(String::len).sum::<usize>();
1779            if bytes.saturating_add(name.len()) > MAX_MISSING_NAME_BYTES {
1780                return Err(NativeCollectionError::MissingDependencyBudget {
1781                    kind: "name bytes",
1782                    limit: MAX_MISSING_NAME_BYTES,
1783                });
1784            }
1785        }
1786        self.missing_readers
1787            .entry(name.to_owned())
1788            .or_default()
1789            .insert(reader.clone());
1790        Ok(())
1791    }
1792
1793    pub(crate) fn reset_reader(&mut self, reader: &ComponentInstancePath) {
1794        self.reset_contribution(&crate::read_dependency::ReadDependency::component(reader));
1795    }
1796
1797    pub(crate) fn reset_contribution(&mut self, reader: &crate::read_dependency::ReadDependency) {
1798        for readers in self.readers.values_mut() {
1799            readers.remove(reader);
1800        }
1801        self.readers.retain(|_, readers| !readers.is_empty());
1802        for readers in self.missing_readers.values_mut() {
1803            readers.remove(reader);
1804        }
1805        self.missing_readers
1806            .retain(|_, readers| !readers.is_empty());
1807    }
1808
1809    pub(crate) fn retain_reader_scope(
1810        &mut self,
1811        root: &ComponentInstancePath,
1812        active: &BTreeSet<ComponentInstancePath>,
1813    ) {
1814        for readers in self.readers.values_mut() {
1815            readers.retain(|reader| reader.retained_in_owner_scope(root, active));
1816        }
1817        self.readers.retain(|_, readers| !readers.is_empty());
1818        for readers in self.missing_readers.values_mut() {
1819            readers.retain(|reader| reader.retained_in_owner_scope(root, active));
1820        }
1821        self.missing_readers
1822            .retain(|_, readers| !readers.is_empty());
1823    }
1824
1825    pub(crate) fn remove_reader_scope(&mut self, root: &ComponentInstancePath) {
1826        for readers in self.readers.values_mut() {
1827            readers.retain(|reader| !reader.owner.is_within(root));
1828        }
1829        self.readers.retain(|_, readers| !readers.is_empty());
1830        for readers in self.missing_readers.values_mut() {
1831            readers.retain(|reader| !reader.owner.is_within(root));
1832        }
1833        self.missing_readers
1834            .retain(|_, readers| !readers.is_empty());
1835    }
1836
1837    pub(crate) fn retain_contributions(
1838        &mut self,
1839        scope: &ComponentInstancePath,
1840        active: &BTreeSet<crate::read_dependency::ReadContribution>,
1841    ) {
1842        crate::read_dependency::retain_readers(&mut self.readers, |reader| {
1843            reader.retained_in_contribution_scope(scope, active)
1844        });
1845        crate::read_dependency::retain_readers(&mut self.missing_readers, |reader| {
1846            reader.retained_in_contribution_scope(scope, active)
1847        });
1848    }
1849}
1850
1851#[derive(Clone, Debug, PartialEq)]
1852pub enum VirtualCollectionData {
1853    Values(Vec<UiValue>),
1854    Native(NativeCollection),
1855}
1856
1857impl From<Vec<UiValue>> for VirtualCollectionData {
1858    fn from(values: Vec<UiValue>) -> Self {
1859        Self::Values(values)
1860    }
1861}
1862
1863impl From<NativeCollection> for VirtualCollectionData {
1864    fn from(collection: NativeCollection) -> Self {
1865        Self::Native(collection)
1866    }
1867}
1868
1869impl FromIterator<UiValue> for VirtualCollectionData {
1870    fn from_iter<T: IntoIterator<Item = UiValue>>(iter: T) -> Self {
1871        Self::Values(iter.into_iter().collect())
1872    }
1873}
1874
1875impl VirtualCollectionData {
1876    #[must_use]
1877    pub fn len(&self) -> usize {
1878        match self {
1879            Self::Values(values) => values.len(),
1880            Self::Native(collection) => collection.len(),
1881        }
1882    }
1883
1884    #[must_use]
1885    pub fn is_empty(&self) -> bool {
1886        self.len() == 0
1887    }
1888
1889    pub(crate) fn sticky_headers(&self) -> Arc<BTreeSet<usize>> {
1890        match self {
1891            Self::Values(_) => Arc::new(BTreeSet::new()),
1892            Self::Native(collection) => collection.sticky_headers(),
1893        }
1894    }
1895
1896    pub(crate) fn item(&self, index: usize) -> Result<Option<UiValue>, NativeCollectionError> {
1897        match self {
1898            Self::Values(values) => Ok(values.get(index).cloned()),
1899            Self::Native(collection) => collection.item(index),
1900        }
1901    }
1902
1903    pub(crate) fn key(&self, index: usize) -> Option<&str> {
1904        match self {
1905            Self::Values(values) => values.get(index).and_then(|item| match item {
1906                UiValue::Map(item) => match item.get("key") {
1907                    Some(UiValue::String(key)) => Some(key.as_str()),
1908                    _ => None,
1909                },
1910                _ => None,
1911            }),
1912            Self::Native(collection) => collection.key(index),
1913        }
1914    }
1915}
1916
1917#[derive(Clone, Debug, Error, PartialEq)]
1918pub enum NativeCollectionError {
1919    #[error("invalid {0} `{1}`")]
1920    InvalidName(&'static str, String),
1921    #[error("native collection row {0} must be a map")]
1922    RowNotMap(usize),
1923    #[error("native collection row {index} is missing key field `{field}`")]
1924    MissingKey { index: usize, field: String },
1925    #[error("native collection row {index} key field `{field}` must be a non-empty scalar")]
1926    InvalidKey { index: usize, field: String },
1927    #[error("native collection key `{0}` is duplicated")]
1928    DuplicateKey(String),
1929    #[error("native collection `{0}` is already registered")]
1930    DuplicateCollection(String),
1931    #[error("native collection `{0}` is not registered")]
1932    UnknownCollection(String),
1933    #[error("native collection missing-dependency {kind} exceeds limit {limit}")]
1934    MissingDependencyBudget { kind: &'static str, limit: usize },
1935    #[error(
1936        "native collection key field `{source_key}` does not match requested row key `{requested}`"
1937    )]
1938    KeyFieldMismatch {
1939        source_key: String,
1940        requested: String,
1941    },
1942    #[error("native collection sort field `{0}` is missing")]
1943    MissingSortField(String),
1944    #[error("native collection sort field `{0}` is not a consistently comparable scalar")]
1945    UnsortableField(String),
1946    #[error("native collection order references missing source row {0}")]
1947    CorruptOrder(usize),
1948    #[error("native collection contains a group header outside a table projection")]
1949    UnexpectedGroupEntry,
1950    #[error("native collection is not a fuzzy projection")]
1951    NotFuzzyProjection,
1952    #[error("native collection row {index} group field `{field}` must be a non-empty string")]
1953    InvalidGroupField { index: usize, field: String },
1954    #[error("invalid native Table configuration: {0}")]
1955    InvalidTableConfig(String),
1956    #[error("invalid native fuzzy-view configuration: {0}")]
1957    InvalidFuzzyConfig(String),
1958    #[error("native collection cache is poisoned")]
1959    Poisoned,
1960}
1961
1962#[cfg(test)]
1963mod tests {
1964    use super::*;
1965
1966    fn rows() -> NativeCollection {
1967        NativeCollection::new(
1968            "id",
1969            [
1970                BTreeMap::from([
1971                    ("id".to_owned(), UiValue::String("b".to_owned())),
1972                    ("score".to_owned(), UiValue::Integer(2)),
1973                ]),
1974                BTreeMap::from([
1975                    ("id".to_owned(), UiValue::String("a".to_owned())),
1976                    ("score".to_owned(), UiValue::Integer(1)),
1977                ]),
1978            ],
1979        )
1980        .unwrap()
1981    }
1982
1983    fn grouped_rows() -> NativeCollection {
1984        NativeCollection::new(
1985            "id",
1986            [
1987                BTreeMap::from([
1988                    ("id".to_owned(), UiValue::String("b".to_owned())),
1989                    ("track".to_owned(), UiValue::String("track-b".to_owned())),
1990                    ("score".to_owned(), UiValue::Integer(2)),
1991                ]),
1992                BTreeMap::from([
1993                    ("id".to_owned(), UiValue::String("a".to_owned())),
1994                    ("track".to_owned(), UiValue::String("track-a".to_owned())),
1995                    ("score".to_owned(), UiValue::Integer(3)),
1996                ]),
1997                BTreeMap::from([
1998                    ("id".to_owned(), UiValue::String("c".to_owned())),
1999                    ("track".to_owned(), UiValue::String("track-b".to_owned())),
2000                    ("score".to_owned(), UiValue::Integer(1)),
2001                ]),
2002            ],
2003        )
2004        .unwrap()
2005    }
2006
2007    fn table_config(collapsed: &[&str]) -> Map {
2008        let columns = ["id", "track", "score"]
2009            .into_iter()
2010            .map(|key| {
2011                UiValue::Map(BTreeMap::from([
2012                    ("key".to_owned(), UiValue::String(key.to_owned())),
2013                    ("width".to_owned(), UiValue::Integer(80)),
2014                ]))
2015            })
2016            .collect();
2017        UiValue::Map(BTreeMap::from([
2018            ("row_key".to_owned(), UiValue::String("id".to_owned())),
2019            ("label".to_owned(), UiValue::String("Clusters".to_owned())),
2020            ("columns".to_owned(), UiValue::Array(columns)),
2021            ("selected_keys".to_owned(), UiValue::Array(Vec::new())),
2022            (
2023                "selection_mode".to_owned(),
2024                UiValue::String("multiple".to_owned()),
2025            ),
2026            ("striped".to_owned(), UiValue::Bool(true)),
2027            ("row_height".to_owned(), UiValue::Float(30.0)),
2028            (
2029                "sort".to_owned(),
2030                UiValue::Map(BTreeMap::from([
2031                    ("key".to_owned(), UiValue::String("score".to_owned())),
2032                    (
2033                        "direction".to_owned(),
2034                        UiValue::String("ascending".to_owned()),
2035                    ),
2036                ])),
2037            ),
2038            ("group_by".to_owned(), UiValue::String("track".to_owned())),
2039            (
2040                "collapsed_groups".to_owned(),
2041                UiValue::Array(
2042                    collapsed
2043                        .iter()
2044                        .map(|value| UiValue::String((*value).to_owned()))
2045                        .collect(),
2046                ),
2047            ),
2048            ("group_toggle".to_owned(), UiValue::Bool(true)),
2049        ]))
2050        .into_dynamic()
2051        .cast::<Map>()
2052    }
2053
2054    fn fuzzy_rows() -> NativeCollection {
2055        NativeCollection::new(
2056            "id",
2057            [
2058                BTreeMap::from([
2059                    ("id".to_owned(), UiValue::String("new".to_owned())),
2060                    ("label".to_owned(), UiValue::String("New file".to_owned())),
2061                    ("group".to_owned(), UiValue::String("File".to_owned())),
2062                    (
2063                        "keywords".to_owned(),
2064                        UiValue::Array(vec![UiValue::String("create document".to_owned())]),
2065                    ),
2066                    ("shortcut".to_owned(), UiValue::String("⌘N".to_owned())),
2067                    ("disabled".to_owned(), UiValue::Bool(false)),
2068                ]),
2069                BTreeMap::from([
2070                    ("id".to_owned(), UiValue::String("open".to_owned())),
2071                    ("label".to_owned(), UiValue::String("Open file".to_owned())),
2072                    ("group".to_owned(), UiValue::String("File".to_owned())),
2073                    (
2074                        "keywords".to_owned(),
2075                        UiValue::Array(vec![UiValue::String("load document".to_owned())]),
2076                    ),
2077                    ("shortcut".to_owned(), UiValue::String("⌘O".to_owned())),
2078                    ("disabled".to_owned(), UiValue::Bool(false)),
2079                ]),
2080                BTreeMap::from([
2081                    ("id".to_owned(), UiValue::String("close".to_owned())),
2082                    (
2083                        "label".to_owned(),
2084                        UiValue::String("Close window".to_owned()),
2085                    ),
2086                    ("group".to_owned(), UiValue::String("Window".to_owned())),
2087                    ("keywords".to_owned(), UiValue::Array(Vec::new())),
2088                    ("shortcut".to_owned(), UiValue::String("⌘W".to_owned())),
2089                    ("disabled".to_owned(), UiValue::Bool(true)),
2090                ]),
2091            ],
2092        )
2093        .unwrap()
2094    }
2095
2096    fn fuzzy_config(query: &str, active: &str) -> Map {
2097        UiValue::Map(BTreeMap::from([
2098            ("query".to_owned(), UiValue::String(query.to_owned())),
2099            (
2100                "label_field".to_owned(),
2101                UiValue::String("label".to_owned()),
2102            ),
2103            (
2104                "keywords_field".to_owned(),
2105                UiValue::String("keywords".to_owned()),
2106            ),
2107            (
2108                "group_field".to_owned(),
2109                UiValue::String("group".to_owned()),
2110            ),
2111            (
2112                "shortcut_field".to_owned(),
2113                UiValue::String("shortcut".to_owned()),
2114            ),
2115            (
2116                "disabled_field".to_owned(),
2117                UiValue::String("disabled".to_owned()),
2118            ),
2119            ("active".to_owned(), UiValue::String(active.to_owned())),
2120            ("row_height".to_owned(), UiValue::Float(32.0)),
2121        ]))
2122        .into_dynamic()
2123        .cast::<Map>()
2124    }
2125
2126    #[test]
2127    fn fuzzy_projection_filters_groups_projects_and_reuses_structural_order() {
2128        let source = fuzzy_rows();
2129        let view = source.fuzzy_view(fuzzy_config("opn", "")).unwrap();
2130        assert_eq!(view.len(), 2);
2131        assert_eq!(view.fuzzy_edge(false).unwrap(), "open");
2132        assert_eq!(view.fuzzy_adjacent("", 0).unwrap(), "open");
2133        let UiValue::Map(group) = view.item(0).unwrap().unwrap() else {
2134            panic!("first fuzzy entry must be a group");
2135        };
2136        assert_eq!(group["kind"], UiValue::String("group".to_owned()));
2137        let UiValue::Map(item) = view.item(1).unwrap().unwrap() else {
2138            panic!("second fuzzy entry must be a command");
2139        };
2140        assert_eq!(item["value"], UiValue::String("open".to_owned()));
2141        assert_eq!(item["active"], UiValue::Bool(false));
2142
2143        let active = source.fuzzy_view(fuzzy_config("opn", "open")).unwrap();
2144        assert!(Arc::ptr_eq(&view.order, &active.order));
2145        let UiValue::Map(item) = active.item(1).unwrap().unwrap() else {
2146            unreachable!()
2147        };
2148        assert_eq!(item["active"], UiValue::Bool(true));
2149
2150        let all = source.fuzzy_view(fuzzy_config("", "new")).unwrap();
2151        assert_eq!(all.fuzzy_edge(true).unwrap(), "open");
2152        assert_eq!(all.fuzzy_adjacent("new", 1).unwrap(), "open");
2153        assert_eq!(all.fuzzy_adjacent("open", 1).unwrap(), "new");
2154    }
2155
2156    #[test]
2157    fn validates_keys_and_caches_rust_sort_order() {
2158        fn assert_send_sync<T: Send + Sync>() {}
2159        assert_send_sync::<NativeCollection>();
2160        let collection = rows();
2161        let sort = SortSpec {
2162            key: "score".to_owned(),
2163            descending: false,
2164        };
2165        let first = collection.sorted_order(&sort).unwrap();
2166        let second = collection.sorted_order(&sort).unwrap();
2167        assert!(Arc::ptr_eq(&first, &second));
2168        assert_eq!(collection.source.keys[first[0]], "a");
2169    }
2170
2171    #[test]
2172    fn tracked_replacement_invalidates_only_collection_readers() {
2173        let mut registry = NativeCollectionRegistry::new();
2174        registry.register("accounts", rows()).unwrap();
2175        let reader = ComponentInstancePath::root("View", "main");
2176        let _ = registry.read_tracked(&reader, "accounts").unwrap();
2177        let changed = NativeCollection::new(
2178            "id",
2179            [BTreeMap::from([(
2180                "id".to_owned(),
2181                UiValue::String("next".to_owned()),
2182            )])],
2183        )
2184        .unwrap();
2185        assert_eq!(
2186            registry.replace("accounts", changed).unwrap(),
2187            [reader].into()
2188        );
2189    }
2190
2191    #[test]
2192    fn registering_a_missing_name_invalidates_its_exact_readers() {
2193        let mut registry = NativeCollectionRegistry::new();
2194        let reader = ComponentInstancePath::root("View", "late-child");
2195        assert!(registry.read_tracked(&reader, "late").is_err());
2196        assert_eq!(
2197            registry
2198                .register_with_invalidated("late".to_owned(), rows())
2199                .unwrap(),
2200            BTreeSet::from([reader])
2201        );
2202    }
2203
2204    #[test]
2205    fn missing_dependencies_are_render_only_validated_and_bounded() {
2206        let mut registry = NativeCollectionRegistry::new();
2207        let reader = ComponentInstancePath::root("View", "missing-budget");
2208        assert!(matches!(
2209            registry.read_tracked_with_missing(&reader, &"x".repeat(257), true),
2210            Err(NativeCollectionError::InvalidName(_, _))
2211        ));
2212        assert!(registry.missing_readers.is_empty());
2213
2214        for index in 0..128 {
2215            assert!(
2216                registry
2217                    .read_tracked_with_missing(&reader, &format!("event-{index}"), false)
2218                    .is_err()
2219            );
2220        }
2221        assert!(registry.missing_readers.is_empty());
2222
2223        for index in 0..MAX_MISSING_NAMES_PER_READER {
2224            assert!(
2225                registry
2226                    .read_tracked_with_missing(&reader, &format!("render-{index}"), true)
2227                    .is_err()
2228            );
2229        }
2230        assert_eq!(registry.missing_readers.len(), MAX_MISSING_NAMES_PER_READER);
2231        assert!(matches!(
2232            registry.read_tracked_with_missing(&reader, "one-too-many", true),
2233            Err(NativeCollectionError::MissingDependencyBudget { .. })
2234        ));
2235        assert_eq!(registry.missing_readers.len(), MAX_MISSING_NAMES_PER_READER);
2236    }
2237
2238    #[test]
2239    fn grouped_table_projection_flattens_headers_and_sorted_rows() {
2240        let source = grouped_rows();
2241        let grouped = source.table_view(table_config(&[])).unwrap();
2242        let mut selected_config = table_config(&[]);
2243        selected_config.insert(
2244            "selected_keys".into(),
2245            Dynamic::from_array(vec![Dynamic::from("a")]),
2246        );
2247        let reused = source.table_view(selected_config).unwrap();
2248        assert!(Arc::ptr_eq(&grouped.order, &reused.order));
2249        assert_eq!(grouped.len(), 5);
2250        assert_eq!(grouped.sticky_headers().as_ref(), &BTreeSet::from([0, 3]));
2251        let values = (0..grouped.len())
2252            .map(|index| grouped.item(index).unwrap().unwrap())
2253            .collect::<Vec<_>>();
2254        let field = |value: &UiValue, name: &str| match value {
2255            UiValue::Map(value) => value[name].clone(),
2256            _ => panic!("projected table item must be a map"),
2257        };
2258        assert_eq!(field(&values[0], "kind"), UiValue::String("group".into()));
2259        assert_eq!(
2260            field(&values[0], "group"),
2261            UiValue::String("track-b".into())
2262        );
2263        assert_eq!(field(&values[0], "count"), UiValue::Integer(2));
2264        assert_eq!(field(&values[1], "key"), UiValue::String("c".into()));
2265        assert_eq!(field(&values[2], "key"), UiValue::String("b".into()));
2266        assert_eq!(
2267            field(&values[3], "group"),
2268            UiValue::String("track-a".into())
2269        );
2270        assert_eq!(field(&values[4], "key"), UiValue::String("a".into()));
2271    }
2272
2273    #[test]
2274    fn grouped_table_projection_collapses_rows_but_retains_header_counts() {
2275        let grouped = grouped_rows()
2276            .table_view(table_config(&["track-b"]))
2277            .unwrap();
2278        assert_eq!(grouped.len(), 3);
2279        assert_eq!(grouped.sticky_headers().as_ref(), &BTreeSet::from([0, 1]));
2280        let UiValue::Map(first) = grouped.item(0).unwrap().unwrap() else {
2281            panic!("group header must be a map");
2282        };
2283        assert_eq!(first["group"], UiValue::String("track-b".into()));
2284        assert_eq!(first["count"], UiValue::Integer(2));
2285        assert_eq!(first["collapsed"], UiValue::Bool(true));
2286        assert_eq!(grouped.key(2), Some("a"));
2287    }
2288
2289    #[test]
2290    fn table_projection_filters_then_pages_the_sorted_native_rows() {
2291        let source = grouped_rows();
2292        let page = |number: i64| {
2293            let mut config = table_config(&[]);
2294            config.insert("group_by".into(), Dynamic::UNIT);
2295            config.insert("query".into(), Dynamic::from("track-b"));
2296            config.insert(
2297                "search_fields".into(),
2298                Dynamic::from_array(vec![Dynamic::from("track")]),
2299            );
2300            config.insert("page".into(), Dynamic::from_int(number));
2301            config.insert("page_size".into(), Dynamic::from_int(1));
2302            source.table_view(config).unwrap()
2303        };
2304        let first = page(1);
2305        let second = page(2);
2306        assert_eq!(first.len(), 1);
2307        assert_eq!(second.len(), 1);
2308        assert_eq!(first.key(0), Some("c"));
2309        assert_eq!(second.key(0), Some("b"));
2310        assert!(!Arc::ptr_eq(&first.order, &second.order));
2311    }
2312}