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