Skip to main content

gpui_rhai/
native_collection.rs

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