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