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