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