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