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 group_order = Vec::new();
627        let mut groups = BTreeMap::<String, Vec<(usize, i64)>>::new();
628        for (index, row) in source.rows.iter().enumerate() {
629            let label = Self::row_string(row, index, &self.label_field, true)?;
630            let keywords = self.row_keywords(row, index)?;
631            let score = fuzzy_score(&label, &keywords, &self.query);
632            if score < 0 {
633                continue;
634            }
635            let group = Self::row_string(row, index, &self.group_field, false)?;
636            if !groups.contains_key(&group) {
637                group_order.push(group.clone());
638            }
639            groups.entry(group).or_default().push((index, score));
640        }
641        let source_keys = source
642            .keys
643            .iter()
644            .map(String::as_str)
645            .collect::<BTreeSet<_>>();
646        let mut header_keys = BTreeSet::new();
647        let mut entries = Vec::new();
648        for group in group_order {
649            let mut rows = groups.remove(&group).unwrap_or_default();
650            rows.sort_by(|(left_index, left_score), (right_index, right_score)| {
651                right_score
652                    .cmp(left_score)
653                    .then_with(|| left_index.cmp(right_index))
654            });
655            if !group.is_empty() {
656                let key =
657                    unique_group_key(&format!("command:{group}"), &source_keys, &mut header_keys);
658                entries.push(NativeCollectionEntry::Group(GroupEntry {
659                    key,
660                    value: group,
661                    count: rows.len(),
662                    collapsed: false,
663                }));
664            }
665            entries.extend(
666                rows.into_iter()
667                    .map(|(index, _)| NativeCollectionEntry::Row(index)),
668            );
669        }
670        Ok(entries)
671    }
672
673    fn project_row(
674        &self,
675        row: &UiValue,
676        index: usize,
677        key: Option<&str>,
678    ) -> Result<UiValue, NativeCollectionError> {
679        let key = key.ok_or(NativeCollectionError::CorruptOrder(index))?;
680        let label = Self::row_string(row, index, &self.label_field, true)?;
681        let shortcut = Self::row_string(row, index, &self.shortcut_field, false)?;
682        let disabled = self.row_disabled_value(row, index)?;
683        Ok(UiValue::Map(BTreeMap::from([
684            ("kind".to_owned(), UiValue::String("item".to_owned())),
685            ("key".to_owned(), UiValue::String(format!("item:{key}"))),
686            ("value".to_owned(), UiValue::String(key.to_owned())),
687            ("label".to_owned(), UiValue::String(label)),
688            ("shortcut".to_owned(), UiValue::String(shortcut)),
689            ("disabled".to_owned(), UiValue::Bool(disabled)),
690            ("active".to_owned(), UiValue::Bool(self.active == key)),
691            ("height".to_owned(), UiValue::Float(self.row_height)),
692        ])))
693    }
694
695    fn project_group(&self, group: &GroupEntry) -> UiValue {
696        UiValue::Map(BTreeMap::from([
697            ("kind".to_owned(), UiValue::String("group".to_owned())),
698            ("key".to_owned(), UiValue::String(group.key.clone())),
699            ("label".to_owned(), UiValue::String(group.value.clone())),
700            ("height".to_owned(), UiValue::Float(self.row_height)),
701        ]))
702    }
703
704    fn row_disabled(
705        &self,
706        source: &NativeCollectionSource,
707        index: usize,
708    ) -> Result<bool, NativeCollectionError> {
709        let row = source
710            .rows
711            .get(index)
712            .ok_or(NativeCollectionError::CorruptOrder(index))?;
713        self.row_disabled_value(row, index)
714    }
715
716    fn row_disabled_value(
717        &self,
718        row: &UiValue,
719        index: usize,
720    ) -> Result<bool, NativeCollectionError> {
721        match row_field(row, &self.disabled_field) {
722            None | Some(UiValue::Null) => Ok(false),
723            Some(UiValue::Bool(value)) => Ok(*value),
724            Some(_) => Err(NativeCollectionError::InvalidFuzzyConfig(format!(
725                "row {index} field `{}` must be bool when present",
726                self.disabled_field
727            ))),
728        }
729    }
730
731    fn row_string(
732        row: &UiValue,
733        index: usize,
734        field: &str,
735        required: bool,
736    ) -> Result<String, NativeCollectionError> {
737        match row_field(row, field) {
738            Some(UiValue::String(value)) if !required || !value.is_empty() => Ok(value.clone()),
739            None | Some(UiValue::Null) if !required => Ok(String::new()),
740            _ => Err(NativeCollectionError::InvalidFuzzyConfig(format!(
741                "row {index} field `{field}` must be {}string",
742                if required { "a non-empty " } else { "a " }
743            ))),
744        }
745    }
746
747    fn row_keywords(
748        &self,
749        row: &UiValue,
750        index: usize,
751    ) -> Result<Vec<String>, NativeCollectionError> {
752        match row_field(row, &self.keywords_field) {
753            None | Some(UiValue::Null) => Ok(Vec::new()),
754            Some(UiValue::Array(values)) => values
755                .iter()
756                .enumerate()
757                .map(|(keyword, value)| match value {
758                    UiValue::String(value) => Ok(value.clone()),
759                    _ => Err(NativeCollectionError::InvalidFuzzyConfig(format!(
760                        "row {index} field `{}` item {keyword} must be a string",
761                        self.keywords_field
762                    ))),
763                })
764                .collect(),
765            Some(_) => Err(NativeCollectionError::InvalidFuzzyConfig(format!(
766                "row {index} field `{}` must be an array of strings",
767                self.keywords_field
768            ))),
769        }
770    }
771}
772
773fn fuzzy_score(label: &str, keywords: &[String], query: &str) -> i64 {
774    std::iter::once((label, 0))
775        .chain(keywords.iter().map(|keyword| (keyword.as_str(), -100)))
776        .map(|(candidate, penalty)| fuzzy_candidate_score(candidate, query) + penalty)
777        .max()
778        .unwrap_or(-1)
779}
780
781fn fuzzy_candidate_score(candidate: &str, query: &str) -> i64 {
782    if query.is_empty() {
783        return 0;
784    }
785    let candidate = candidate.to_lowercase();
786    if candidate == query {
787        return 10_000;
788    }
789    let length = i64::try_from(candidate.chars().count()).unwrap_or(i64::MAX);
790    if candidate.starts_with(query) {
791        return 8_000 - length;
792    }
793    if let Some(position) = candidate.find(query) {
794        let position = i64::try_from(candidate[..position].chars().count()).unwrap_or(i64::MAX);
795        return 6_000 - position.saturating_mul(8) - length;
796    }
797    let chars = candidate.chars().collect::<Vec<_>>();
798    let mut position = 0usize;
799    let mut gaps = 0i64;
800    for needle in query.chars() {
801        let Some(found) = chars[position..]
802            .iter()
803            .position(|candidate| *candidate == needle)
804        else {
805            return -1;
806        };
807        gaps = gaps.saturating_add(i64::try_from(found).unwrap_or(i64::MAX));
808        position = position.saturating_add(found).saturating_add(1);
809    }
810    4_000 - gaps.saturating_mul(12) - length
811}
812
813fn fuzzy_take_string(
814    values: &mut BTreeMap<String, UiValue>,
815    name: &str,
816) -> Result<String, NativeCollectionError> {
817    match values.remove(name) {
818        Some(UiValue::String(value)) => Ok(value),
819        _ => Err(NativeCollectionError::InvalidFuzzyConfig(format!(
820            "{name} must be a string"
821        ))),
822    }
823}
824
825fn fuzzy_take_number(
826    values: &mut BTreeMap<String, UiValue>,
827    name: &str,
828) -> Result<f64, NativeCollectionError> {
829    match values.remove(name) {
830        Some(UiValue::Float(value)) => Ok(value),
831        Some(UiValue::Integer(value)) => Ok(integer_float(value)),
832        _ => Err(NativeCollectionError::InvalidFuzzyConfig(format!(
833            "{name} must be a number"
834        ))),
835    }
836}
837
838#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
839struct SortSpec {
840    key: String,
841    descending: bool,
842}
843
844#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
845struct TableOrderSpec {
846    sort: Option<SortSpec>,
847    group_by: Option<String>,
848    collapsed_groups: BTreeSet<String>,
849    query: String,
850    search_fields: Vec<String>,
851    page: usize,
852    page_size: Option<usize>,
853}
854
855impl From<&TableProjection> for TableOrderSpec {
856    fn from(projection: &TableProjection) -> Self {
857        Self {
858            sort: projection.sort.clone(),
859            group_by: projection.group_by.clone(),
860            collapsed_groups: projection.collapsed_groups.clone(),
861            query: projection.query.clone(),
862            search_fields: projection.search_fields.clone(),
863            page: projection.page,
864            page_size: projection.page_size,
865        }
866    }
867}
868
869#[derive(Clone, Debug, PartialEq)]
870struct TableProjection {
871    label: String,
872    row_key: String,
873    columns: Vec<TableColumn>,
874    selected_keys: Vec<String>,
875    selected: BTreeSet<String>,
876    selection_mode: SelectionMode,
877    striped: bool,
878    row_height: f64,
879    sort: Option<SortSpec>,
880    group_by: Option<String>,
881    collapsed_groups: BTreeSet<String>,
882    group_toggle: bool,
883    query: String,
884    search_fields: Vec<String>,
885    page: usize,
886    page_size: Option<usize>,
887}
888
889#[derive(Clone, Debug, PartialEq)]
890struct TableColumn {
891    key: String,
892    width: UiValue,
893    align: String,
894    resize_signal_key: Option<String>,
895    adornments: Vec<TableAdornment>,
896}
897
898#[derive(Clone, Debug, Eq, PartialEq)]
899struct TableAdornment {
900    text_key: String,
901    variant: String,
902    variant_key: Option<String>,
903    dot: bool,
904}
905
906#[derive(Clone, Copy, Debug, Eq, PartialEq)]
907enum SelectionMode {
908    None,
909    Single,
910    Multiple,
911}
912
913impl TableProjection {
914    fn decode(config: UiValue, source_key: &str) -> Result<Self, NativeCollectionError> {
915        let UiValue::Map(mut config) = config else {
916            return Err(NativeCollectionError::InvalidTableConfig(
917                "configuration must be a map".to_owned(),
918            ));
919        };
920        let row_key = take_string(&mut config, "row_key")?;
921        if row_key != source_key {
922            return Err(NativeCollectionError::KeyFieldMismatch {
923                source_key: source_key.to_owned(),
924                requested: row_key,
925            });
926        }
927        let label = take_string(&mut config, "label")?;
928        let columns = take_array(&mut config, "columns")?
929            .into_iter()
930            .map(TableColumn::decode)
931            .collect::<Result<Vec<_>, _>>()?;
932        let selected_keys = take_array(&mut config, "selected_keys")?
933            .into_iter()
934            .enumerate()
935            .map(|(index, value)| match value {
936                UiValue::String(value) => Ok(value),
937                _ => Err(NativeCollectionError::InvalidTableConfig(format!(
938                    "selected_keys[{index}] must be a string"
939                ))),
940            })
941            .collect::<Result<Vec<_>, _>>()?;
942        let selected = selected_keys.iter().cloned().collect();
943        let selection_mode = match take_string(&mut config, "selection_mode")?.as_str() {
944            "none" => SelectionMode::None,
945            "single" => SelectionMode::Single,
946            "multiple" => SelectionMode::Multiple,
947            value => {
948                return Err(NativeCollectionError::InvalidTableConfig(format!(
949                    "unknown selection mode `{value}`"
950                )));
951            }
952        };
953        let striped = take_bool(&mut config, "striped")?;
954        let row_height = take_number(&mut config, "row_height")?;
955        if !row_height.is_finite() || row_height <= 0.0 {
956            return Err(NativeCollectionError::InvalidTableConfig(
957                "row_height must be finite and positive".to_owned(),
958            ));
959        }
960        let sort = decode_sort(config.remove("sort"))?;
961        let (group_by, collapsed_groups, group_toggle) = decode_grouping(&mut config, &columns)?;
962        let (query, search_fields, page, page_size) =
963            decode_table_window(&mut config, &row_key, &columns)?;
964        if !config.is_empty() {
965            return Err(NativeCollectionError::InvalidTableConfig(format!(
966                "unknown configuration fields: {}",
967                config.keys().cloned().collect::<Vec<_>>().join(", ")
968            )));
969        }
970        Ok(Self {
971            label,
972            row_key,
973            columns,
974            selected_keys,
975            selected,
976            selection_mode,
977            striped,
978            row_height,
979            sort,
980            group_by,
981            collapsed_groups,
982            group_toggle,
983            query,
984            search_fields,
985            page,
986            page_size,
987        })
988    }
989
990    fn visible_rows(&self, source: &NativeCollectionSource, rows: &[usize]) -> Vec<usize> {
991        let mut visible = if self.query.is_empty() {
992            rows.to_vec()
993        } else {
994            rows.iter()
995                .copied()
996                .filter(|index| {
997                    source.rows.get(*index).is_some_and(|row| {
998                        self.search_fields.iter().any(|field| {
999                            row_field(row, field)
1000                                .and_then(scalar_text)
1001                                .is_some_and(|value| value.to_lowercase().contains(&self.query))
1002                        })
1003                    })
1004                })
1005                .collect()
1006        };
1007        if let Some(page_size) = self.page_size {
1008            let start = self.page.saturating_sub(1).saturating_mul(page_size);
1009            if start >= visible.len() {
1010                visible.clear();
1011            } else {
1012                let end = start.saturating_add(page_size).min(visible.len());
1013                visible = visible[start..end].to_vec();
1014            }
1015        }
1016        visible
1017    }
1018
1019    fn grouped_order(
1020        &self,
1021        source: &NativeCollectionSource,
1022        rows: &[usize],
1023    ) -> Result<Vec<NativeCollectionEntry>, NativeCollectionError> {
1024        let Some(group_by) = &self.group_by else {
1025            return Ok(rows
1026                .iter()
1027                .copied()
1028                .map(NativeCollectionEntry::Row)
1029                .collect());
1030        };
1031        let mut order = Vec::new();
1032        let mut groups = BTreeMap::<String, Vec<usize>>::new();
1033        for source_index in rows {
1034            let row = source
1035                .rows
1036                .get(*source_index)
1037                .ok_or(NativeCollectionError::CorruptOrder(*source_index))?;
1038            let value = match row_field(row, group_by) {
1039                Some(UiValue::String(value)) if !value.is_empty() => value.clone(),
1040                _ => {
1041                    return Err(NativeCollectionError::InvalidGroupField {
1042                        index: *source_index,
1043                        field: group_by.clone(),
1044                    });
1045                }
1046            };
1047            if !groups.contains_key(&value) {
1048                order.push(value.clone());
1049            }
1050            groups.entry(value).or_default().push(*source_index);
1051        }
1052        let source_keys = source
1053            .keys
1054            .iter()
1055            .map(String::as_str)
1056            .collect::<BTreeSet<_>>();
1057        let mut header_keys = BTreeSet::new();
1058        let mut entries = Vec::with_capacity(rows.len().saturating_add(order.len()));
1059        for value in order {
1060            let rows = groups.remove(&value).unwrap_or_default();
1061            let key = unique_group_key(&value, &source_keys, &mut header_keys);
1062            let collapsed = self.collapsed_groups.contains(&value);
1063            entries.push(NativeCollectionEntry::Group(GroupEntry {
1064                key,
1065                value,
1066                count: rows.len(),
1067                collapsed,
1068            }));
1069            if !collapsed {
1070                entries.extend(rows.into_iter().map(NativeCollectionEntry::Row));
1071            }
1072        }
1073        Ok(entries)
1074    }
1075
1076    fn project_row(&self, row: &UiValue, index: usize) -> Result<UiValue, NativeCollectionError> {
1077        let UiValue::Map(row) = row else {
1078            return Err(NativeCollectionError::RowNotMap(index));
1079        };
1080        let key = row.get(&self.row_key).and_then(scalar_text);
1081        let key = key.ok_or_else(|| {
1082            NativeCollectionError::InvalidTableConfig("projected row key is unavailable".to_owned())
1083        })?;
1084        let selected = self.selected.contains(&key);
1085        let cells = self
1086            .columns
1087            .iter()
1088            .map(|column| {
1089                let text = row
1090                    .get(&column.key)
1091                    .map_or_else(String::new, display_scalar);
1092                let adornments = column
1093                    .adornments
1094                    .iter()
1095                    .map(|adornment| adornment.project(row))
1096                    .collect::<Result<Vec<_>, _>>()?
1097                    .into_iter()
1098                    .flatten()
1099                    .collect();
1100                let mut cell = BTreeMap::from([
1101                    ("key".to_owned(), UiValue::String(column.key.clone())),
1102                    ("text".to_owned(), UiValue::String(text)),
1103                    ("width".to_owned(), column.width.clone()),
1104                    ("align".to_owned(), UiValue::String(column.align.clone())),
1105                    ("adornments".to_owned(), UiValue::Array(adornments)),
1106                ]);
1107                if let Some(signal_key) = &column.resize_signal_key {
1108                    cell.insert(
1109                        "resize_signal_key".to_owned(),
1110                        UiValue::String(signal_key.clone()),
1111                    );
1112                }
1113                Ok(UiValue::Map(cell))
1114            })
1115            .collect::<Result<Vec<_>, NativeCollectionError>>()?;
1116        let selection = match self.selection_mode {
1117            SelectionMode::None => UiValue::Null,
1118            SelectionMode::Single if selected => UiValue::Array(Vec::new()),
1119            SelectionMode::Single => UiValue::Array(vec![UiValue::String(key.clone())]),
1120            SelectionMode::Multiple => {
1121                let mut next = self
1122                    .selected_keys
1123                    .iter()
1124                    .filter(|candidate| *candidate != &key)
1125                    .cloned()
1126                    .map(UiValue::String)
1127                    .collect::<Vec<_>>();
1128                if !selected {
1129                    next.push(UiValue::String(key.clone()));
1130                }
1131                UiValue::Array(next)
1132            }
1133        };
1134        Ok(UiValue::Map(BTreeMap::from([
1135            ("kind".to_owned(), UiValue::String("row".to_owned())),
1136            ("key".to_owned(), UiValue::String(key.clone())),
1137            (
1138                "label".to_owned(),
1139                UiValue::String(format!("{} row {}", self.label, index + 1)),
1140            ),
1141            ("cells".to_owned(), UiValue::Array(cells)),
1142            ("selected".to_owned(), UiValue::Bool(selected)),
1143            ("selection".to_owned(), selection),
1144            (
1145                "striped".to_owned(),
1146                UiValue::Bool(self.striped && index % 2 == 1),
1147            ),
1148            ("height".to_owned(), UiValue::Float(self.row_height)),
1149        ])))
1150    }
1151
1152    fn project_group(&self, group: &GroupEntry) -> UiValue {
1153        UiValue::Map(BTreeMap::from([
1154            ("kind".to_owned(), UiValue::String("group".to_owned())),
1155            ("key".to_owned(), UiValue::String(group.key.clone())),
1156            ("group".to_owned(), UiValue::String(group.value.clone())),
1157            (
1158                "count".to_owned(),
1159                UiValue::Integer(i64::try_from(group.count).unwrap_or(i64::MAX)),
1160            ),
1161            ("collapsed".to_owned(), UiValue::Bool(group.collapsed)),
1162            ("toggle".to_owned(), UiValue::Bool(self.group_toggle)),
1163            ("height".to_owned(), UiValue::Float(self.row_height)),
1164        ]))
1165    }
1166}
1167
1168fn decode_table_window(
1169    config: &mut BTreeMap<String, UiValue>,
1170    row_key: &str,
1171    columns: &[TableColumn],
1172) -> Result<(String, Vec<String>, usize, Option<usize>), NativeCollectionError> {
1173    let query = match config.remove("query") {
1174        None => String::new(),
1175        Some(UiValue::String(value)) => value.to_lowercase(),
1176        _ => {
1177            return Err(NativeCollectionError::InvalidTableConfig(
1178                "query must be a string".to_owned(),
1179            ));
1180        }
1181    };
1182    let search_fields = match config.remove("search_fields") {
1183        None => Vec::new(),
1184        Some(UiValue::Array(values)) => values,
1185        _ => {
1186            return Err(NativeCollectionError::InvalidTableConfig(
1187                "search_fields must be an array".to_owned(),
1188            ));
1189        }
1190    }
1191    .into_iter()
1192    .enumerate()
1193    .map(|(index, value)| match value {
1194        UiValue::String(value) if !value.is_empty() => Ok(value),
1195        _ => Err(NativeCollectionError::InvalidTableConfig(format!(
1196            "search_fields[{index}] must be a non-empty string"
1197        ))),
1198    })
1199    .collect::<Result<Vec<_>, _>>()?;
1200    if !query.is_empty() && search_fields.is_empty() {
1201        return Err(NativeCollectionError::InvalidTableConfig(
1202            "non-empty query requires search_fields".to_owned(),
1203        ));
1204    }
1205    if let Some(field) = search_fields.iter().find(|field| {
1206        field.as_str() != row_key && !columns.iter().any(|column| column.key == field.as_str())
1207    }) {
1208        return Err(NativeCollectionError::InvalidTableConfig(format!(
1209            "search field `{field}` is not the row key or a declared column"
1210        )));
1211    }
1212    let page = if config.contains_key("page") {
1213        take_positive_usize(config, "page")?
1214    } else {
1215        1
1216    };
1217    let page_size = take_optional_positive_usize(config, "page_size")?;
1218    Ok((query, search_fields, page, page_size))
1219}
1220
1221fn decode_sort(value: Option<UiValue>) -> Result<Option<SortSpec>, NativeCollectionError> {
1222    let Some(value) = value else {
1223        return Ok(None);
1224    };
1225    let UiValue::Map(mut sort) = value else {
1226        if value == UiValue::Null {
1227            return Ok(None);
1228        }
1229        return Err(NativeCollectionError::InvalidTableConfig(
1230            "sort must be null or a map".to_owned(),
1231        ));
1232    };
1233    let key = take_string(&mut sort, "key")?;
1234    let direction = take_string(&mut sort, "direction")?;
1235    if !sort.is_empty() {
1236        return Err(NativeCollectionError::InvalidTableConfig(
1237            "sort contains unknown fields".to_owned(),
1238        ));
1239    }
1240    let descending = match direction.as_str() {
1241        "ascending" => false,
1242        "descending" => true,
1243        _ => {
1244            return Err(NativeCollectionError::InvalidTableConfig(format!(
1245                "unknown sort direction `{direction}`"
1246            )));
1247        }
1248    };
1249    Ok(Some(SortSpec { key, descending }))
1250}
1251
1252fn decode_grouping(
1253    config: &mut BTreeMap<String, UiValue>,
1254    columns: &[TableColumn],
1255) -> Result<(Option<String>, BTreeSet<String>, bool), NativeCollectionError> {
1256    let group_by = take_optional_string(config, "group_by")?;
1257    if let Some(group_by) = &group_by
1258        && !columns.iter().any(|column| &column.key == group_by)
1259    {
1260        return Err(NativeCollectionError::InvalidTableConfig(format!(
1261            "group_by `{group_by}` is not a declared column"
1262        )));
1263    }
1264    let collapsed = if config.contains_key("collapsed_groups") {
1265        take_string_set(config, "collapsed_groups")?
1266    } else {
1267        BTreeSet::new()
1268    };
1269    if group_by.is_none() && !collapsed.is_empty() {
1270        return Err(NativeCollectionError::InvalidTableConfig(
1271            "collapsed_groups requires group_by".to_owned(),
1272        ));
1273    }
1274    let toggle = if config.contains_key("group_toggle") {
1275        take_bool(config, "group_toggle")?
1276    } else {
1277        false
1278    };
1279    Ok((group_by, collapsed, toggle))
1280}
1281
1282fn unique_group_key(
1283    value: &str,
1284    source_keys: &BTreeSet<&str>,
1285    header_keys: &mut BTreeSet<String>,
1286) -> String {
1287    let mut key = format!("__gpui_rhai_group__:{value}");
1288    while source_keys.contains(key.as_str()) || !header_keys.insert(key.clone()) {
1289        key.insert(0, '_');
1290    }
1291    key
1292}
1293
1294impl TableColumn {
1295    fn decode(value: UiValue) -> Result<Self, NativeCollectionError> {
1296        let UiValue::Map(mut column) = value else {
1297            return Err(NativeCollectionError::InvalidTableConfig(
1298                "each column must be a map".to_owned(),
1299            ));
1300        };
1301        let key = take_string(&mut column, "key")?;
1302        let width = column.remove("width").ok_or_else(|| {
1303            NativeCollectionError::InvalidTableConfig("column.width is required".to_owned())
1304        })?;
1305        let align = match column.remove("align") {
1306            None => "start".to_owned(),
1307            Some(UiValue::String(value)) => value,
1308            Some(_) => {
1309                return Err(NativeCollectionError::InvalidTableConfig(
1310                    "column.align must be a string".to_owned(),
1311                ));
1312            }
1313        };
1314        let resize_signal_key = match column.remove("resize_signal_key") {
1315            Some(UiValue::String(value)) => Some(value),
1316            Some(_) => {
1317                return Err(NativeCollectionError::InvalidTableConfig(
1318                    "resize_signal_key must be a string".to_owned(),
1319                ));
1320            }
1321            None => None,
1322        };
1323        let adornments = match column.remove("adornments") {
1324            Some(UiValue::Array(values)) => values
1325                .into_iter()
1326                .map(TableAdornment::decode)
1327                .collect::<Result<Vec<_>, _>>()?,
1328            Some(_) => {
1329                return Err(NativeCollectionError::InvalidTableConfig(
1330                    "column.adornments must be an array".to_owned(),
1331                ));
1332            }
1333            None => Vec::new(),
1334        };
1335        for normalized_only in [
1336            "title",
1337            "sortable",
1338            "resize_enabled",
1339            "resize_ref_key",
1340            "min_width",
1341            "max_width",
1342        ] {
1343            column.remove(normalized_only);
1344        }
1345        if !column.is_empty() {
1346            return Err(NativeCollectionError::InvalidTableConfig(format!(
1347                "unknown column fields: {}",
1348                column.keys().cloned().collect::<Vec<_>>().join(", ")
1349            )));
1350        }
1351        Ok(Self {
1352            key,
1353            width,
1354            align,
1355            resize_signal_key,
1356            adornments,
1357        })
1358    }
1359}
1360
1361impl TableAdornment {
1362    fn decode(value: UiValue) -> Result<Self, NativeCollectionError> {
1363        let UiValue::Map(mut value) = value else {
1364            return Err(NativeCollectionError::InvalidTableConfig(
1365                "each column adornment must be a map".to_owned(),
1366            ));
1367        };
1368        let text_key = take_string(&mut value, "text_key")?;
1369        let variant = match value.remove("variant") {
1370            Some(UiValue::String(value)) => value,
1371            Some(_) => {
1372                return Err(NativeCollectionError::InvalidTableConfig(
1373                    "adornment.variant must be a string".to_owned(),
1374                ));
1375            }
1376            None => "neutral".to_owned(),
1377        };
1378        validate_badge_variant(&variant)?;
1379        let variant_key = match value.remove("variant_key") {
1380            Some(UiValue::String(value)) => Some(value),
1381            Some(UiValue::Null) | None => None,
1382            Some(_) => {
1383                return Err(NativeCollectionError::InvalidTableConfig(
1384                    "adornment.variant_key must be a string".to_owned(),
1385                ));
1386            }
1387        };
1388        let dot = match value.remove("dot") {
1389            Some(UiValue::Bool(value)) => value,
1390            Some(_) => {
1391                return Err(NativeCollectionError::InvalidTableConfig(
1392                    "adornment.dot must be a bool".to_owned(),
1393                ));
1394            }
1395            None => false,
1396        };
1397        if !value.is_empty() {
1398            return Err(NativeCollectionError::InvalidTableConfig(format!(
1399                "unknown adornment fields: {}",
1400                value.keys().cloned().collect::<Vec<_>>().join(", ")
1401            )));
1402        }
1403        Ok(Self {
1404            text_key,
1405            variant,
1406            variant_key,
1407            dot,
1408        })
1409    }
1410
1411    fn project(
1412        &self,
1413        row: &BTreeMap<String, UiValue>,
1414    ) -> Result<Option<UiValue>, NativeCollectionError> {
1415        let text = row
1416            .get(&self.text_key)
1417            .map_or_else(String::new, display_scalar);
1418        if text.is_empty() {
1419            return Ok(None);
1420        }
1421        let variant = self.variant_key.as_ref().map_or_else(
1422            || Ok(self.variant.clone()),
1423            |key| match row.get(key) {
1424                Some(UiValue::String(value)) => Ok(value.clone()),
1425                _ => Err(NativeCollectionError::InvalidTableConfig(format!(
1426                    "adornment variant field `{key}` must be a string"
1427                ))),
1428            },
1429        )?;
1430        validate_badge_variant(&variant)?;
1431        Ok(Some(UiValue::Map(BTreeMap::from([
1432            ("text".to_owned(), UiValue::String(text)),
1433            ("variant".to_owned(), UiValue::String(variant)),
1434            ("dot".to_owned(), UiValue::Bool(self.dot)),
1435        ]))))
1436    }
1437}
1438
1439fn validate_badge_variant(value: &str) -> Result<(), NativeCollectionError> {
1440    if matches!(
1441        value,
1442        "neutral" | "accent" | "success" | "warning" | "danger"
1443    ) {
1444        Ok(())
1445    } else {
1446        Err(NativeCollectionError::InvalidTableConfig(format!(
1447            "unknown adornment variant `{value}`"
1448        )))
1449    }
1450}
1451
1452fn compare_rows(
1453    left: &UiValue,
1454    right: &UiValue,
1455    key: &str,
1456) -> Result<Ordering, NativeCollectionError> {
1457    let left = row_field(left, key)
1458        .ok_or_else(|| NativeCollectionError::MissingSortField(key.to_owned()))?;
1459    let right = row_field(right, key)
1460        .ok_or_else(|| NativeCollectionError::MissingSortField(key.to_owned()))?;
1461    compare_values(left, right)
1462        .ok_or_else(|| NativeCollectionError::UnsortableField(key.to_owned()))
1463}
1464
1465fn row_field<'a>(row: &'a UiValue, key: &str) -> Option<&'a UiValue> {
1466    match row {
1467        UiValue::Map(row) => row.get(key),
1468        _ => None,
1469    }
1470}
1471
1472fn compare_values(left: &UiValue, right: &UiValue) -> Option<Ordering> {
1473    match (left, right) {
1474        (UiValue::Null, UiValue::Null) => Some(Ordering::Equal),
1475        (UiValue::Bool(left), UiValue::Bool(right)) => Some(left.cmp(right)),
1476        (UiValue::Integer(left), UiValue::Integer(right)) => Some(left.cmp(right)),
1477        (UiValue::Float(left), UiValue::Float(right)) => left.partial_cmp(right),
1478        (UiValue::Integer(left), UiValue::Float(right)) => integer_float(*left).partial_cmp(right),
1479        (UiValue::Float(left), UiValue::Integer(right)) => left.partial_cmp(&integer_float(*right)),
1480        (UiValue::String(left), UiValue::String(right)) => Some(left.cmp(right)),
1481        _ => None,
1482    }
1483}
1484
1485fn scalar_text(value: &UiValue) -> Option<String> {
1486    match value {
1487        UiValue::Bool(value) => Some(value.to_string()),
1488        UiValue::Integer(value) => Some(value.to_string()),
1489        UiValue::Float(value) if value.is_finite() => Some(value.to_string()),
1490        UiValue::String(value) => Some(value.clone()),
1491        UiValue::Null
1492        | UiValue::Float(_)
1493        | UiValue::Array(_)
1494        | UiValue::Map(_)
1495        | UiValue::Handle(_) => None,
1496    }
1497}
1498
1499fn display_scalar(value: &UiValue) -> String {
1500    scalar_text(value).unwrap_or_default()
1501}
1502
1503fn take_string(
1504    values: &mut BTreeMap<String, UiValue>,
1505    name: &str,
1506) -> Result<String, NativeCollectionError> {
1507    match values.remove(name) {
1508        Some(UiValue::String(value)) => Ok(value),
1509        _ => Err(NativeCollectionError::InvalidTableConfig(format!(
1510            "{name} must be a string"
1511        ))),
1512    }
1513}
1514
1515fn take_optional_string(
1516    values: &mut BTreeMap<String, UiValue>,
1517    name: &str,
1518) -> Result<Option<String>, NativeCollectionError> {
1519    match values.remove(name) {
1520        None | Some(UiValue::Null) => Ok(None),
1521        Some(UiValue::String(value)) if !value.is_empty() => Ok(Some(value)),
1522        _ => Err(NativeCollectionError::InvalidTableConfig(format!(
1523            "{name} must be null or a non-empty string"
1524        ))),
1525    }
1526}
1527
1528fn take_string_set(
1529    values: &mut BTreeMap<String, UiValue>,
1530    name: &str,
1531) -> Result<BTreeSet<String>, NativeCollectionError> {
1532    take_array(values, name)?
1533        .into_iter()
1534        .enumerate()
1535        .map(|(index, value)| match value {
1536            UiValue::String(value) if !value.is_empty() => Ok(value),
1537            _ => Err(NativeCollectionError::InvalidTableConfig(format!(
1538                "{name}[{index}] must be a non-empty string"
1539            ))),
1540        })
1541        .collect()
1542}
1543
1544fn take_array(
1545    values: &mut BTreeMap<String, UiValue>,
1546    name: &str,
1547) -> Result<Vec<UiValue>, NativeCollectionError> {
1548    match values.remove(name) {
1549        Some(UiValue::Array(value)) => Ok(value),
1550        _ => Err(NativeCollectionError::InvalidTableConfig(format!(
1551            "{name} must be an array"
1552        ))),
1553    }
1554}
1555
1556fn take_bool(
1557    values: &mut BTreeMap<String, UiValue>,
1558    name: &str,
1559) -> Result<bool, NativeCollectionError> {
1560    match values.remove(name) {
1561        Some(UiValue::Bool(value)) => Ok(value),
1562        _ => Err(NativeCollectionError::InvalidTableConfig(format!(
1563            "{name} must be a bool"
1564        ))),
1565    }
1566}
1567
1568fn take_positive_usize(
1569    values: &mut BTreeMap<String, UiValue>,
1570    name: &str,
1571) -> Result<usize, NativeCollectionError> {
1572    match values.remove(name) {
1573        Some(UiValue::Integer(value)) if value > 0 => usize::try_from(value).map_err(|_| {
1574            NativeCollectionError::InvalidTableConfig(format!(
1575                "{name} exceeds the supported platform size"
1576            ))
1577        }),
1578        _ => Err(NativeCollectionError::InvalidTableConfig(format!(
1579            "{name} must be a positive integer"
1580        ))),
1581    }
1582}
1583
1584fn take_optional_positive_usize(
1585    values: &mut BTreeMap<String, UiValue>,
1586    name: &str,
1587) -> Result<Option<usize>, NativeCollectionError> {
1588    match values.remove(name) {
1589        None | Some(UiValue::Null) => Ok(None),
1590        Some(UiValue::Integer(value)) if value > 0 => {
1591            usize::try_from(value).map(Some).map_err(|_| {
1592                NativeCollectionError::InvalidTableConfig(format!(
1593                    "{name} exceeds the supported platform size"
1594                ))
1595            })
1596        }
1597        _ => Err(NativeCollectionError::InvalidTableConfig(format!(
1598            "{name} must be null or a positive integer"
1599        ))),
1600    }
1601}
1602
1603fn take_number(
1604    values: &mut BTreeMap<String, UiValue>,
1605    name: &str,
1606) -> Result<f64, NativeCollectionError> {
1607    match values.remove(name) {
1608        Some(UiValue::Float(value)) => Ok(value),
1609        Some(UiValue::Integer(value)) => Ok(integer_float(value)),
1610        _ => Err(NativeCollectionError::InvalidTableConfig(format!(
1611            "{name} must be a number"
1612        ))),
1613    }
1614}
1615
1616fn integer_float(value: i64) -> f64 {
1617    value.to_string().parse().unwrap_or_else(|_| {
1618        if value.is_negative() {
1619            f64::MIN
1620        } else {
1621            f64::MAX
1622        }
1623    })
1624}
1625
1626fn validate_name(name: &str, label: &'static str) -> Result<(), NativeCollectionError> {
1627    if name.is_empty() || name.len() > 256 || name.chars().any(char::is_control) {
1628        Err(NativeCollectionError::InvalidName(label, name.to_owned()))
1629    } else {
1630        Ok(())
1631    }
1632}
1633
1634#[derive(Clone, Debug, Default)]
1635pub struct NativeCollectionRegistry {
1636    collections: BTreeMap<String, NativeCollection>,
1637    readers: BTreeMap<String, BTreeSet<ComponentInstancePath>>,
1638}
1639
1640impl NativeCollectionRegistry {
1641    #[must_use]
1642    pub fn new() -> Self {
1643        Self::default()
1644    }
1645
1646    /// Register one application-owned collection before initial render.
1647    ///
1648    /// # Errors
1649    ///
1650    /// Returns an error for an unsafe or duplicate name.
1651    pub fn register(
1652        &mut self,
1653        name: impl Into<String>,
1654        collection: NativeCollection,
1655    ) -> Result<(), NativeCollectionError> {
1656        let name = name.into();
1657        validate_name(&name, "collection name")?;
1658        if self.collections.contains_key(&name) {
1659            return Err(NativeCollectionError::DuplicateCollection(name));
1660        }
1661        self.collections.insert(name, collection);
1662        Ok(())
1663    }
1664
1665    /// Replace an existing collection and return its exact subscribed readers.
1666    ///
1667    /// # Errors
1668    ///
1669    /// Returns an error when the collection is unknown.
1670    pub fn replace(
1671        &mut self,
1672        name: &str,
1673        collection: NativeCollection,
1674    ) -> Result<BTreeSet<ComponentInstancePath>, NativeCollectionError> {
1675        let current = self
1676            .collections
1677            .get_mut(name)
1678            .ok_or_else(|| NativeCollectionError::UnknownCollection(name.to_owned()))?;
1679        if current == &collection {
1680            return Ok(BTreeSet::new());
1681        }
1682        *current = collection;
1683        Ok(self.readers.get(name).cloned().unwrap_or_default())
1684    }
1685
1686    pub(crate) fn read_tracked(
1687        &mut self,
1688        reader: &ComponentInstancePath,
1689        name: &str,
1690    ) -> Result<NativeCollection, NativeCollectionError> {
1691        let collection = self
1692            .collections
1693            .get(name)
1694            .cloned()
1695            .ok_or_else(|| NativeCollectionError::UnknownCollection(name.to_owned()))?;
1696        self.readers
1697            .entry(name.to_owned())
1698            .or_default()
1699            .insert(reader.clone());
1700        Ok(collection)
1701    }
1702
1703    pub(crate) fn reset_reader(&mut self, reader: &ComponentInstancePath) {
1704        for readers in self.readers.values_mut() {
1705            readers.remove(reader);
1706        }
1707        self.readers.retain(|_, readers| !readers.is_empty());
1708    }
1709
1710    pub(crate) fn retain_reader_scope(
1711        &mut self,
1712        root: &ComponentInstancePath,
1713        active: &BTreeSet<ComponentInstancePath>,
1714    ) {
1715        for readers in self.readers.values_mut() {
1716            readers.retain(|reader| {
1717                !reader.is_within(root) || reader == root || active.contains(reader)
1718            });
1719        }
1720        self.readers.retain(|_, readers| !readers.is_empty());
1721    }
1722
1723    pub(crate) fn remove_reader_scope(&mut self, root: &ComponentInstancePath) {
1724        for readers in self.readers.values_mut() {
1725            readers.retain(|reader| !reader.is_within(root));
1726        }
1727        self.readers.retain(|_, readers| !readers.is_empty());
1728    }
1729}
1730
1731#[derive(Clone, Debug, PartialEq)]
1732pub enum VirtualCollectionData {
1733    Values(Vec<UiValue>),
1734    Native(NativeCollection),
1735}
1736
1737impl From<Vec<UiValue>> for VirtualCollectionData {
1738    fn from(values: Vec<UiValue>) -> Self {
1739        Self::Values(values)
1740    }
1741}
1742
1743impl From<NativeCollection> for VirtualCollectionData {
1744    fn from(collection: NativeCollection) -> Self {
1745        Self::Native(collection)
1746    }
1747}
1748
1749impl FromIterator<UiValue> for VirtualCollectionData {
1750    fn from_iter<T: IntoIterator<Item = UiValue>>(iter: T) -> Self {
1751        Self::Values(iter.into_iter().collect())
1752    }
1753}
1754
1755impl VirtualCollectionData {
1756    #[must_use]
1757    pub fn len(&self) -> usize {
1758        match self {
1759            Self::Values(values) => values.len(),
1760            Self::Native(collection) => collection.len(),
1761        }
1762    }
1763
1764    #[must_use]
1765    pub fn is_empty(&self) -> bool {
1766        self.len() == 0
1767    }
1768
1769    pub(crate) fn sticky_headers(&self) -> Arc<BTreeSet<usize>> {
1770        match self {
1771            Self::Values(_) => Arc::new(BTreeSet::new()),
1772            Self::Native(collection) => collection.sticky_headers(),
1773        }
1774    }
1775
1776    pub(crate) fn item(&self, index: usize) -> Result<Option<UiValue>, NativeCollectionError> {
1777        match self {
1778            Self::Values(values) => Ok(values.get(index).cloned()),
1779            Self::Native(collection) => collection.item(index),
1780        }
1781    }
1782
1783    pub(crate) fn key(&self, index: usize) -> Option<&str> {
1784        match self {
1785            Self::Values(values) => values.get(index).and_then(|item| match item {
1786                UiValue::Map(item) => match item.get("key") {
1787                    Some(UiValue::String(key)) => Some(key.as_str()),
1788                    _ => None,
1789                },
1790                _ => None,
1791            }),
1792            Self::Native(collection) => collection.key(index),
1793        }
1794    }
1795}
1796
1797#[derive(Clone, Debug, Error, PartialEq)]
1798pub enum NativeCollectionError {
1799    #[error("invalid {0} `{1}`")]
1800    InvalidName(&'static str, String),
1801    #[error("native collection row {0} must be a map")]
1802    RowNotMap(usize),
1803    #[error("native collection row {index} is missing key field `{field}`")]
1804    MissingKey { index: usize, field: String },
1805    #[error("native collection row {index} key field `{field}` must be a non-empty scalar")]
1806    InvalidKey { index: usize, field: String },
1807    #[error("native collection key `{0}` is duplicated")]
1808    DuplicateKey(String),
1809    #[error("native collection `{0}` is already registered")]
1810    DuplicateCollection(String),
1811    #[error("native collection `{0}` is not registered")]
1812    UnknownCollection(String),
1813    #[error(
1814        "native collection key field `{source_key}` does not match requested row key `{requested}`"
1815    )]
1816    KeyFieldMismatch {
1817        source_key: String,
1818        requested: String,
1819    },
1820    #[error("native collection sort field `{0}` is missing")]
1821    MissingSortField(String),
1822    #[error("native collection sort field `{0}` is not a consistently comparable scalar")]
1823    UnsortableField(String),
1824    #[error("native collection order references missing source row {0}")]
1825    CorruptOrder(usize),
1826    #[error("native collection contains a group header outside a table projection")]
1827    UnexpectedGroupEntry,
1828    #[error("native collection is not a fuzzy projection")]
1829    NotFuzzyProjection,
1830    #[error("native collection row {index} group field `{field}` must be a non-empty string")]
1831    InvalidGroupField { index: usize, field: String },
1832    #[error("invalid native Table configuration: {0}")]
1833    InvalidTableConfig(String),
1834    #[error("invalid native fuzzy-view configuration: {0}")]
1835    InvalidFuzzyConfig(String),
1836    #[error("native collection cache is poisoned")]
1837    Poisoned,
1838}
1839
1840#[cfg(test)]
1841mod tests {
1842    use super::*;
1843
1844    fn rows() -> NativeCollection {
1845        NativeCollection::new(
1846            "id",
1847            [
1848                BTreeMap::from([
1849                    ("id".to_owned(), UiValue::String("b".to_owned())),
1850                    ("score".to_owned(), UiValue::Integer(2)),
1851                ]),
1852                BTreeMap::from([
1853                    ("id".to_owned(), UiValue::String("a".to_owned())),
1854                    ("score".to_owned(), UiValue::Integer(1)),
1855                ]),
1856            ],
1857        )
1858        .unwrap()
1859    }
1860
1861    fn grouped_rows() -> NativeCollection {
1862        NativeCollection::new(
1863            "id",
1864            [
1865                BTreeMap::from([
1866                    ("id".to_owned(), UiValue::String("b".to_owned())),
1867                    ("track".to_owned(), UiValue::String("track-b".to_owned())),
1868                    ("score".to_owned(), UiValue::Integer(2)),
1869                ]),
1870                BTreeMap::from([
1871                    ("id".to_owned(), UiValue::String("a".to_owned())),
1872                    ("track".to_owned(), UiValue::String("track-a".to_owned())),
1873                    ("score".to_owned(), UiValue::Integer(3)),
1874                ]),
1875                BTreeMap::from([
1876                    ("id".to_owned(), UiValue::String("c".to_owned())),
1877                    ("track".to_owned(), UiValue::String("track-b".to_owned())),
1878                    ("score".to_owned(), UiValue::Integer(1)),
1879                ]),
1880            ],
1881        )
1882        .unwrap()
1883    }
1884
1885    fn table_config(collapsed: &[&str]) -> Map {
1886        let columns = ["id", "track", "score"]
1887            .into_iter()
1888            .map(|key| {
1889                UiValue::Map(BTreeMap::from([
1890                    ("key".to_owned(), UiValue::String(key.to_owned())),
1891                    ("width".to_owned(), UiValue::Integer(80)),
1892                ]))
1893            })
1894            .collect();
1895        UiValue::Map(BTreeMap::from([
1896            ("row_key".to_owned(), UiValue::String("id".to_owned())),
1897            ("label".to_owned(), UiValue::String("Clusters".to_owned())),
1898            ("columns".to_owned(), UiValue::Array(columns)),
1899            ("selected_keys".to_owned(), UiValue::Array(Vec::new())),
1900            (
1901                "selection_mode".to_owned(),
1902                UiValue::String("multiple".to_owned()),
1903            ),
1904            ("striped".to_owned(), UiValue::Bool(true)),
1905            ("row_height".to_owned(), UiValue::Float(30.0)),
1906            (
1907                "sort".to_owned(),
1908                UiValue::Map(BTreeMap::from([
1909                    ("key".to_owned(), UiValue::String("score".to_owned())),
1910                    (
1911                        "direction".to_owned(),
1912                        UiValue::String("ascending".to_owned()),
1913                    ),
1914                ])),
1915            ),
1916            ("group_by".to_owned(), UiValue::String("track".to_owned())),
1917            (
1918                "collapsed_groups".to_owned(),
1919                UiValue::Array(
1920                    collapsed
1921                        .iter()
1922                        .map(|value| UiValue::String((*value).to_owned()))
1923                        .collect(),
1924                ),
1925            ),
1926            ("group_toggle".to_owned(), UiValue::Bool(true)),
1927        ]))
1928        .into_dynamic()
1929        .cast::<Map>()
1930    }
1931
1932    fn fuzzy_rows() -> NativeCollection {
1933        NativeCollection::new(
1934            "id",
1935            [
1936                BTreeMap::from([
1937                    ("id".to_owned(), UiValue::String("new".to_owned())),
1938                    ("label".to_owned(), UiValue::String("New file".to_owned())),
1939                    ("group".to_owned(), UiValue::String("File".to_owned())),
1940                    (
1941                        "keywords".to_owned(),
1942                        UiValue::Array(vec![UiValue::String("create document".to_owned())]),
1943                    ),
1944                    ("shortcut".to_owned(), UiValue::String("⌘N".to_owned())),
1945                    ("disabled".to_owned(), UiValue::Bool(false)),
1946                ]),
1947                BTreeMap::from([
1948                    ("id".to_owned(), UiValue::String("open".to_owned())),
1949                    ("label".to_owned(), UiValue::String("Open file".to_owned())),
1950                    ("group".to_owned(), UiValue::String("File".to_owned())),
1951                    (
1952                        "keywords".to_owned(),
1953                        UiValue::Array(vec![UiValue::String("load document".to_owned())]),
1954                    ),
1955                    ("shortcut".to_owned(), UiValue::String("⌘O".to_owned())),
1956                    ("disabled".to_owned(), UiValue::Bool(false)),
1957                ]),
1958                BTreeMap::from([
1959                    ("id".to_owned(), UiValue::String("close".to_owned())),
1960                    (
1961                        "label".to_owned(),
1962                        UiValue::String("Close window".to_owned()),
1963                    ),
1964                    ("group".to_owned(), UiValue::String("Window".to_owned())),
1965                    ("keywords".to_owned(), UiValue::Array(Vec::new())),
1966                    ("shortcut".to_owned(), UiValue::String("⌘W".to_owned())),
1967                    ("disabled".to_owned(), UiValue::Bool(true)),
1968                ]),
1969            ],
1970        )
1971        .unwrap()
1972    }
1973
1974    fn fuzzy_config(query: &str, active: &str) -> Map {
1975        UiValue::Map(BTreeMap::from([
1976            ("query".to_owned(), UiValue::String(query.to_owned())),
1977            (
1978                "label_field".to_owned(),
1979                UiValue::String("label".to_owned()),
1980            ),
1981            (
1982                "keywords_field".to_owned(),
1983                UiValue::String("keywords".to_owned()),
1984            ),
1985            (
1986                "group_field".to_owned(),
1987                UiValue::String("group".to_owned()),
1988            ),
1989            (
1990                "shortcut_field".to_owned(),
1991                UiValue::String("shortcut".to_owned()),
1992            ),
1993            (
1994                "disabled_field".to_owned(),
1995                UiValue::String("disabled".to_owned()),
1996            ),
1997            ("active".to_owned(), UiValue::String(active.to_owned())),
1998            ("row_height".to_owned(), UiValue::Float(32.0)),
1999        ]))
2000        .into_dynamic()
2001        .cast::<Map>()
2002    }
2003
2004    #[test]
2005    fn fuzzy_projection_filters_groups_projects_and_reuses_structural_order() {
2006        let source = fuzzy_rows();
2007        let view = source.fuzzy_view(fuzzy_config("opn", "")).unwrap();
2008        assert_eq!(view.len(), 2);
2009        assert_eq!(view.fuzzy_edge(false).unwrap(), "open");
2010        assert_eq!(view.fuzzy_adjacent("", 0).unwrap(), "open");
2011        let UiValue::Map(group) = view.item(0).unwrap().unwrap() else {
2012            panic!("first fuzzy entry must be a group");
2013        };
2014        assert_eq!(group["kind"], UiValue::String("group".to_owned()));
2015        let UiValue::Map(item) = view.item(1).unwrap().unwrap() else {
2016            panic!("second fuzzy entry must be a command");
2017        };
2018        assert_eq!(item["value"], UiValue::String("open".to_owned()));
2019        assert_eq!(item["active"], UiValue::Bool(false));
2020
2021        let active = source.fuzzy_view(fuzzy_config("opn", "open")).unwrap();
2022        assert!(Arc::ptr_eq(&view.order, &active.order));
2023        let UiValue::Map(item) = active.item(1).unwrap().unwrap() else {
2024            unreachable!()
2025        };
2026        assert_eq!(item["active"], UiValue::Bool(true));
2027
2028        let all = source.fuzzy_view(fuzzy_config("", "new")).unwrap();
2029        assert_eq!(all.fuzzy_edge(true).unwrap(), "open");
2030        assert_eq!(all.fuzzy_adjacent("new", 1).unwrap(), "open");
2031        assert_eq!(all.fuzzy_adjacent("open", 1).unwrap(), "new");
2032    }
2033
2034    #[test]
2035    fn validates_keys_and_caches_rust_sort_order() {
2036        fn assert_send_sync<T: Send + Sync>() {}
2037        assert_send_sync::<NativeCollection>();
2038        let collection = rows();
2039        let sort = SortSpec {
2040            key: "score".to_owned(),
2041            descending: false,
2042        };
2043        let first = collection.sorted_order(&sort).unwrap();
2044        let second = collection.sorted_order(&sort).unwrap();
2045        assert!(Arc::ptr_eq(&first, &second));
2046        assert_eq!(collection.source.keys[first[0]], "a");
2047    }
2048
2049    #[test]
2050    fn tracked_replacement_invalidates_only_collection_readers() {
2051        let mut registry = NativeCollectionRegistry::new();
2052        registry.register("accounts", rows()).unwrap();
2053        let reader = ComponentInstancePath::root("View", "main");
2054        let _ = registry.read_tracked(&reader, "accounts").unwrap();
2055        let changed = NativeCollection::new(
2056            "id",
2057            [BTreeMap::from([(
2058                "id".to_owned(),
2059                UiValue::String("next".to_owned()),
2060            )])],
2061        )
2062        .unwrap();
2063        assert_eq!(
2064            registry.replace("accounts", changed).unwrap(),
2065            [reader].into()
2066        );
2067    }
2068
2069    #[test]
2070    fn grouped_table_projection_flattens_headers_and_sorted_rows() {
2071        let source = grouped_rows();
2072        let grouped = source.table_view(table_config(&[])).unwrap();
2073        let mut selected_config = table_config(&[]);
2074        selected_config.insert(
2075            "selected_keys".into(),
2076            Dynamic::from_array(vec![Dynamic::from("a")]),
2077        );
2078        let reused = source.table_view(selected_config).unwrap();
2079        assert!(Arc::ptr_eq(&grouped.order, &reused.order));
2080        assert_eq!(grouped.len(), 5);
2081        assert_eq!(grouped.sticky_headers().as_ref(), &BTreeSet::from([0, 3]));
2082        let values = (0..grouped.len())
2083            .map(|index| grouped.item(index).unwrap().unwrap())
2084            .collect::<Vec<_>>();
2085        let field = |value: &UiValue, name: &str| match value {
2086            UiValue::Map(value) => value[name].clone(),
2087            _ => panic!("projected table item must be a map"),
2088        };
2089        assert_eq!(field(&values[0], "kind"), UiValue::String("group".into()));
2090        assert_eq!(
2091            field(&values[0], "group"),
2092            UiValue::String("track-b".into())
2093        );
2094        assert_eq!(field(&values[0], "count"), UiValue::Integer(2));
2095        assert_eq!(field(&values[1], "key"), UiValue::String("c".into()));
2096        assert_eq!(field(&values[2], "key"), UiValue::String("b".into()));
2097        assert_eq!(
2098            field(&values[3], "group"),
2099            UiValue::String("track-a".into())
2100        );
2101        assert_eq!(field(&values[4], "key"), UiValue::String("a".into()));
2102    }
2103
2104    #[test]
2105    fn grouped_table_projection_collapses_rows_but_retains_header_counts() {
2106        let grouped = grouped_rows()
2107            .table_view(table_config(&["track-b"]))
2108            .unwrap();
2109        assert_eq!(grouped.len(), 3);
2110        assert_eq!(grouped.sticky_headers().as_ref(), &BTreeSet::from([0, 1]));
2111        let UiValue::Map(first) = grouped.item(0).unwrap().unwrap() else {
2112            panic!("group header must be a map");
2113        };
2114        assert_eq!(first["group"], UiValue::String("track-b".into()));
2115        assert_eq!(first["count"], UiValue::Integer(2));
2116        assert_eq!(first["collapsed"], UiValue::Bool(true));
2117        assert_eq!(grouped.key(2), Some("a"));
2118    }
2119
2120    #[test]
2121    fn table_projection_filters_then_pages_the_sorted_native_rows() {
2122        let source = grouped_rows();
2123        let page = |number: i64| {
2124            let mut config = table_config(&[]);
2125            config.insert("group_by".into(), Dynamic::UNIT);
2126            config.insert("query".into(), Dynamic::from("track-b"));
2127            config.insert(
2128                "search_fields".into(),
2129                Dynamic::from_array(vec![Dynamic::from("track")]),
2130            );
2131            config.insert("page".into(), Dynamic::from_int(number));
2132            config.insert("page_size".into(), Dynamic::from_int(1));
2133            source.table_view(config).unwrap()
2134        };
2135        let first = page(1);
2136        let second = page(2);
2137        assert_eq!(first.len(), 1);
2138        assert_eq!(second.len(), 1);
2139        assert_eq!(first.key(0), Some("c"));
2140        assert_eq!(second.key(0), Some("b"));
2141        assert!(!Arc::ptr_eq(&first.order, &second.order));
2142    }
2143}