1use crate::canvas::{
12 DataProvider, SuggestionItem, SuggestionQuery, SuggestionTrigger, TextInputDataProvider,
13 TextInputEventOutcome, TextInputState,
14};
15use super::query::{PickerCommandArgument, PickerCommandQuery, PickerCommandSpec};
16use nucleo::{
17 Config as NucleoConfig, Matcher, Utf32String,
18 pattern::{CaseMatching, Normalization, Pattern},
19};
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct PickerField {
23 pub key: String,
24 pub value: String,
25}
26
27impl PickerField {
28 pub fn new<K: Into<String>, V: Into<String>>(key: K, value: V) -> Self {
29 Self {
30 key: key.into(),
31 value: value.into(),
32 }
33 }
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct PickerScope {
38 pub token: String,
39 pub aliases: Vec<String>,
40 pub label: String,
41 pub field_keys: Vec<String>,
42 pub completion_key: Option<String>,
43 pub value_token_limit: Option<usize>,
44 pub command_argument: PickerCommandArgument,
45 pub suppress_value_completion: bool,
49 pub replace_value_with_completion: bool,
50}
51
52impl PickerScope {
53 pub fn new<T: Into<String>, L: Into<String>>(
54 token: T,
55 label: L,
56 field_keys: Vec<String>,
57 ) -> Self {
58 Self {
59 token: token.into(),
60 aliases: Vec::new(),
61 label: label.into(),
62 field_keys,
63 completion_key: None,
64 value_token_limit: None,
65 command_argument: PickerCommandArgument::None,
66 suppress_value_completion: false,
67 replace_value_with_completion: false,
68 }
69 }
70
71 pub fn with_aliases<I, S>(mut self, aliases: I) -> Self
72 where
73 I: IntoIterator<Item = S>,
74 S: Into<String>,
75 {
76 self.aliases = aliases.into_iter().map(Into::into).collect();
77 self
78 }
79
80 pub fn with_completion_key<S: Into<String>>(mut self, key: S) -> Self {
81 self.completion_key = Some(key.into());
82 self
83 }
84
85 pub fn with_value_token_limit(mut self, limit: usize) -> Self {
86 self.value_token_limit = Some(limit);
87 if limit == 1 {
88 self.command_argument = PickerCommandArgument::Required;
89 }
90 self
91 }
92
93 pub fn with_command_argument(mut self) -> Self {
94 self.command_argument = PickerCommandArgument::Required;
95 self.value_token_limit = Some(1);
96 self
97 }
98
99 pub fn without_command_argument(mut self) -> Self {
100 self.command_argument = PickerCommandArgument::None;
101 self.value_token_limit = None;
102 self
103 }
104
105 pub fn with_suppressed_value_completion(mut self) -> Self {
107 self.suppress_value_completion = true;
108 self
109 }
110
111 pub fn with_value_replacement_completion(mut self) -> Self {
112 self.replace_value_with_completion = true;
113 self
114 }
115
116 fn matches_token(&self, token: &str) -> bool {
117 self.token == token || self.aliases.iter().any(|alias| alias == token)
118 }
119
120 fn completion_value<'a>(&'a self, entry: &'a PickerEntry) -> Option<&'a str> {
121 if let Some(key) = &self.completion_key {
122 if key == "label" {
123 return Some(entry.label.as_str());
124 }
125
126 if let Some(field) = entry.fields.iter().find(|field| &field.key == key) {
127 return Some(field.value.as_str());
128 }
129 }
130
131 for key in &self.field_keys {
132 if key == "label" {
133 return Some(entry.label.as_str());
134 }
135
136 if let Some(field) = entry.fields.iter().find(|field| &field.key == key) {
137 return Some(field.value.as_str());
138 }
139 }
140
141 Some(default_completion_value(entry))
142 }
143}
144
145#[derive(Debug, Clone, Default, PartialEq, Eq)]
146pub struct PickerEntry {
147 pub label: String,
148 pub preview_lines: Vec<String>,
149 pub fields: Vec<PickerField>,
150}
151
152impl PickerEntry {
153 pub fn new<L: Into<String>>(label: L, preview_lines: Vec<String>) -> Self {
154 let label = label.into();
155 Self {
156 fields: vec![PickerField::new("label", label.clone())],
157 label,
158 preview_lines,
159 }
160 }
161
162 pub fn with_fields(mut self, fields: Vec<PickerField>) -> Self {
163 self.fields.extend(fields);
164 self
165 }
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
169pub enum PickerLayout {
170 #[default]
171 Responsive,
172 SinglePane,
173}
174
175#[derive(Debug, Clone, PartialEq, Eq)]
176pub struct PickerHead {
177 pub columns: Vec<PickerHeadColumn>,
178}
179
180impl Default for PickerHead {
181 fn default() -> Self {
182 Self::new()
183 }
184}
185
186impl PickerHead {
187 pub fn new() -> Self {
188 Self {
189 columns: Vec::new(),
190 }
191 }
192
193 pub fn with_columns(columns: Vec<PickerHeadColumn>) -> Self {
194 Self { columns }
195 }
196
197 pub fn field<L: Into<String>, K: Into<String>>(
198 self,
199 label: L,
200 field_key: K,
201 ) -> PickerHeadColumnBuilder {
202 PickerHeadColumnBuilder {
203 head: self,
204 label: label.into(),
205 field_key: field_key.into(),
206 }
207 }
208}
209
210#[derive(Debug, Clone, PartialEq, Eq)]
211pub struct PickerHeadColumnBuilder {
212 head: PickerHead,
213 label: String,
214 field_key: String,
215}
216
217impl PickerHeadColumnBuilder {
218 pub fn width(mut self, width: usize) -> PickerHead {
219 self.head
220 .columns
221 .push(PickerHeadColumn::new(self.label, self.field_key, width));
222 self.head
223 }
224
225 pub fn flex(self) -> PickerHead {
226 self.width(0)
227 }
228}
229
230#[derive(Debug, Clone, PartialEq, Eq)]
231pub struct PickerHeadColumn {
232 pub label: String,
233 pub field_key: String,
234 pub width: usize,
235}
236
237impl PickerHeadColumn {
238 pub fn new<L: Into<String>, K: Into<String>>(label: L, field_key: K, width: usize) -> Self {
239 Self {
240 label: label.into(),
241 field_key: field_key.into(),
242 width,
243 }
244 }
245}
246
247#[derive(Debug, Clone, PartialEq, Eq)]
254pub struct PickerFieldWeights {
255 default: u32,
256 overrides: Vec<(String, u32)>,
257}
258
259impl Default for PickerFieldWeights {
260 fn default() -> Self {
261 Self {
262 default: 10,
263 overrides: vec![("label".to_string(), 16)],
264 }
265 }
266}
267
268impl PickerFieldWeights {
269 pub fn uniform(weight: u32) -> Self {
271 Self {
272 default: weight,
273 overrides: Vec::new(),
274 }
275 }
276
277 pub fn with_default(mut self, weight: u32) -> Self {
278 self.default = weight;
279 self
280 }
281
282 pub fn with_override<K: Into<String>>(mut self, key: K, weight: u32) -> Self {
283 let key = key.into();
284 self.overrides.retain(|(existing, _)| existing != &key);
285 self.overrides.push((key, weight));
286 self
287 }
288
289 pub fn weight(&self, key: &str) -> u32 {
290 self.overrides
291 .iter()
292 .find(|(existing, _)| existing == key)
293 .map(|(_, weight)| *weight)
294 .unwrap_or(self.default)
295 }
296}
297
298pub struct PickerData<M = ()> {
304 pub title: Option<String>,
305 pub input: TextInputState<PickerInputProvider>,
306 pub query_placeholder: String,
307 pub source_entries: Vec<PickerEntry>,
308 pub filtered_indices: Vec<usize>,
309 pub selected_filtered_index: Option<usize>,
310 pub empty_preview: String,
311 pub scopes: Vec<PickerScope>,
312 pub layout: PickerLayout,
313 pub head: Option<PickerHead>,
314 mode: Option<M>,
315 field_weights: PickerFieldWeights,
316 implicit_scopes: Vec<PickerImplicitScope>,
317 search_cache: Vec<PickerEntrySearchCache>,
318 matcher: Matcher,
319}
320
321#[derive(Debug, Clone, Default, PartialEq, Eq)]
322pub struct PickerInputProvider {
323 value: String,
324 scopes: Vec<PickerScope>,
325 suggestion_scopes: Option<Vec<PickerScope>>,
326}
327
328impl PickerInputProvider {
329 fn set_scopes(&mut self, scopes: Vec<PickerScope>) {
330 self.scopes = scopes;
331 self.suggestion_scopes = None;
332 }
333
334 fn set_suggestion_scopes(&mut self, scopes: Option<Vec<PickerScope>>) {
335 self.suggestion_scopes = scopes;
336 }
337
338 fn active_suggestion_scopes(&self) -> &[PickerScope] {
339 self.suggestion_scopes
340 .as_deref()
341 .unwrap_or(self.scopes.as_slice())
342 }
343
344 fn scope_query(&self, cursor_char: usize) -> Option<SuggestionQuery> {
345 let chars = self.value.chars().collect::<Vec<_>>();
346 let cursor = cursor_char.min(chars.len());
347 let mut start = cursor;
348 while start > 0 && !chars[start - 1].is_whitespace() {
349 start -= 1;
350 }
351
352 let mut end = cursor;
353 while end < chars.len() && !chars[end].is_whitespace() {
354 end += 1;
355 }
356
357 let typed = chars[start..cursor].iter().collect::<String>();
358 typed
359 .starts_with('%')
360 .then(|| SuggestionQuery::with_replace_range(typed, (start, end)))
361 }
362}
363
364impl DataProvider for PickerInputProvider {
365 fn field_count(&self) -> usize {
366 1
367 }
368
369 fn field_name(&self, _index: usize) -> &str {
370 "Search"
371 }
372
373 fn field_value(&self, index: usize) -> &str {
374 if index == 0 { &self.value } else { "" }
375 }
376
377 fn set_field_value(&mut self, index: usize, value: String) {
378 if index == 0 {
379 self.value = sanitize_picker_input(value);
380 }
381 }
382
383 fn supports_suggestions(&self, field_index: usize) -> bool {
384 field_index == 0 && !self.active_suggestion_scopes().is_empty()
385 }
386
387 fn suggestion_trigger(&self, field_index: usize) -> SuggestionTrigger {
388 if self.supports_suggestions(field_index) {
389 SuggestionTrigger::SpecialChar('%')
390 } else {
391 SuggestionTrigger::None
392 }
393 }
394
395 fn suggestion_query(&self, _field_index: usize, cursor_char: usize) -> Option<SuggestionQuery> {
396 self.scope_query(cursor_char)
397 }
398
399 fn fetch_suggestions_sync(&self, _field_index: usize, query: &str) -> Vec<SuggestionItem> {
400 let query = query.to_lowercase();
401 self.active_suggestion_scopes()
402 .iter()
403 .filter_map(|scope| {
404 let token = format!("%{}", scope.token);
405 let token_matches = token.starts_with(&query);
406 let alias_matches = scope
407 .aliases
408 .iter()
409 .map(|alias| format!("%{}", alias))
410 .any(|alias| alias.starts_with(&query));
411 (token_matches || alias_matches).then(|| {
412 SuggestionItem::new(
413 format!("{} {}", token, scope.label),
414 format!("{} ", token),
415 )
416 })
417 })
418 .collect()
419 }
420
421 fn accept_suggestion(
422 &mut self,
423 field_index: usize,
424 _cursor_char: usize,
425 suggestion: &SuggestionItem,
426 query: &SuggestionQuery,
427 ) -> usize {
428 if field_index != 0 {
429 return 0;
430 }
431
432 let replacement = suggestion.value_to_store.as_str();
433 let (start, end) = query
434 .replace_range
435 .unwrap_or((0, self.value.chars().count()));
436 let chars = self.value.chars().collect::<Vec<_>>();
437 let start = start.min(chars.len());
438 let end = end.min(chars.len()).max(start);
439
440 let mut updated = String::new();
441 updated.extend(chars[..start].iter());
442 updated.push_str(replacement);
443 updated.extend(chars[end..].iter());
444 self.value = sanitize_picker_input(updated);
445 start + replacement.chars().count()
446 }
447}
448
449impl TextInputDataProvider for PickerInputProvider {
450 fn from_text(text: String) -> Self {
451 Self {
452 value: sanitize_picker_input(text),
453 scopes: Vec::new(),
454 suggestion_scopes: None,
455 }
456 }
457
458 fn to_text(&self) -> String {
459 self.value.clone()
460 }
461
462 fn set_text(&mut self, text: String) {
463 self.value = sanitize_picker_input(text);
464 }
465}
466
467fn sanitize_picker_input(text: String) -> String {
468 text.chars()
469 .take_while(|&ch| ch != '\n' && ch != '\r')
470 .collect()
471}
472
473fn normalize_picker_input_whitespace_experimental(text: &str, cursor: usize) -> (String, usize) {
476 let normalized = normalize_picker_input_text_whitespace_experimental(text);
477 let prefix = text.chars().take(cursor).collect::<String>();
478 let normalized_cursor = normalize_picker_input_text_whitespace_experimental(&prefix)
479 .chars()
480 .count()
481 .min(normalized.chars().count());
482
483 (normalized, normalized_cursor)
484}
485
486fn normalize_picker_input_text_whitespace_experimental(text: &str) -> String {
487 let mut normalized = String::new();
488 let mut pending_separator = false;
489
490 for ch in text.chars() {
491 if ch.is_whitespace() {
492 if !normalized.is_empty() {
493 pending_separator = true;
494 }
495 continue;
496 }
497
498 if pending_separator {
499 normalized.push(' ');
500 pending_separator = false;
501 }
502 normalized.push(ch);
503 }
504
505 if pending_separator && !normalized.is_empty() {
506 normalized.push(' ');
507 }
508
509 normalized
510}
511
512impl<M> PickerData<M> {
513 pub fn new() -> Self {
514 Self::default()
515 }
516
517 pub fn with_entries(entries: Vec<PickerEntry>) -> Self {
518 let mut picker = Self {
519 source_entries: entries,
520 ..Self::default()
521 };
522 picker.rebuild_search_cache();
523 picker.refresh_filter();
524 picker
525 }
526
527 pub fn with_layout(mut self, layout: PickerLayout) -> Self {
528 self.layout = layout;
529 self
530 }
531
532 pub fn single_pane(mut self) -> Self {
533 self.layout = PickerLayout::SinglePane;
534 self
535 }
536
537 pub fn set_layout(&mut self, layout: PickerLayout) {
538 self.layout = layout;
539 }
540
541 pub fn with_head(mut self, head: PickerHead) -> Self {
542 self.head = Some(head);
543 self
544 }
545
546 pub fn set_head(&mut self, head: PickerHead) {
547 self.head = Some(head);
548 }
549
550 pub fn clear_head(&mut self) {
551 self.head = None;
552 }
553
554 pub fn with_field_weights(mut self, weights: PickerFieldWeights) -> Self {
555 self.field_weights = weights;
556 self.refresh_filter();
557 self
558 }
559
560 pub fn set_field_weights(&mut self, weights: PickerFieldWeights) {
561 self.field_weights = weights;
562 self.refresh_filter();
563 }
564
565 pub fn field_weights(&self) -> &PickerFieldWeights {
566 &self.field_weights
567 }
568
569 pub fn set_mode(&mut self, mode: M) {
573 self.mode = Some(mode);
574 }
575
576 pub fn with_mode(mut self, mode: M) -> Self {
577 self.mode = Some(mode);
578 self
579 }
580
581 pub fn clear_mode(&mut self) {
582 self.mode = None;
583 }
584
585 pub fn take_mode(&mut self) -> Option<M> {
586 self.mode.take()
587 }
588
589 pub fn mode_ref(&self) -> Option<&M> {
590 self.mode.as_ref()
591 }
592
593 pub fn mode_mut(&mut self) -> Option<&mut M> {
594 self.mode.as_mut()
595 }
596
597 pub fn set_entries(&mut self, entries: Vec<PickerEntry>) {
598 if self.source_entries == entries {
599 return;
600 }
601
602 self.source_entries = entries;
603 self.rebuild_search_cache();
604 self.refresh_filter();
605 }
606
607 pub fn set_scopes(&mut self, scopes: Vec<PickerScope>) {
608 if self.scopes == scopes {
609 return;
610 }
611
612 self.scopes = scopes;
613 self.input
614 .data_provider_mut()
615 .set_scopes(self.scopes.clone());
616 self.refresh_filter();
617 }
618
619 pub fn set_suggestion_scopes(&mut self, scopes: Option<Vec<PickerScope>>) {
620 self.input.data_provider_mut().set_suggestion_scopes(scopes);
621 self.input.check_suggestion_trigger();
622 }
623
624 pub fn clear_suggestion_scopes(&mut self) {
625 self.set_suggestion_scopes(None);
626 }
627
628 pub fn set_implicit_scope(&mut self, token: &str, value: &str) {
629 self.implicit_scopes.retain(|scope| scope.token != token);
630 if !value.trim().is_empty() {
631 self.implicit_scopes
632 .push(PickerImplicitScope::new(token, value));
633 }
634 self.refresh_filter();
635 }
636
637 pub fn set_selected_source_index(&mut self, source_index: Option<usize>) {
638 self.selected_filtered_index = source_index.and_then(|source_index| {
639 self.filtered_indices
640 .iter()
641 .position(|candidate| *candidate == source_index)
642 });
643
644 self.apply_selection_change();
645 }
646
647 pub fn set_selected_filtered_index(&mut self, filtered_index: Option<usize>) {
648 self.selected_filtered_index = filtered_index;
649 self.apply_selection_change();
650 }
651
652 pub fn push_char(&mut self, ch: char) {
653 self.input.enter_edit_mode();
654 let _ = self.input.insert_char(ch);
655 self.refresh_filter();
656 }
657
658 pub fn pop_char(&mut self) {
659 let _ = self.input.delete_backward();
660 self.refresh_filter();
661 }
662
663 pub fn clear_input(&mut self) {
664 self.input.set_text("");
665 self.input.enter_edit_mode();
666 self.refresh_filter();
667 }
668
669 pub fn input_ref(&self) -> &TextInputState<PickerInputProvider> {
670 &self.input
671 }
672
673 pub fn input_mut(&mut self) -> &mut TextInputState<PickerInputProvider> {
674 &mut self.input
675 }
676
677 pub fn refresh_filter(&mut self) {
678 self.normalize_input_whitespace_experimental();
679 self.apply_query_change();
680 }
681
682 pub fn move_up(&mut self) {
683 if self.input.is_suggestions_active() {
684 self.input.suggestions_prev();
685 return;
686 }
687
688 if let Some(index) = self.selected_filtered_index {
689 if index > 0 {
690 self.selected_filtered_index = Some(index - 1);
691 }
692 } else if !self.filtered_indices.is_empty() {
693 self.selected_filtered_index = Some(0);
694 }
695
696 self.apply_selection_change();
697 }
698
699 pub fn move_down(&mut self) {
700 if self.input.is_suggestions_active() {
701 self.input.suggestions_next();
702 return;
703 }
704
705 if let Some(index) = self.selected_filtered_index {
706 if index + 1 < self.filtered_indices.len() {
707 self.selected_filtered_index = Some(index + 1);
708 }
709 } else if !self.filtered_indices.is_empty() {
710 self.selected_filtered_index = Some(0);
711 }
712
713 self.apply_selection_change();
714 }
715
716 pub fn filtered_len(&self) -> usize {
717 self.filtered_indices.len()
718 }
719
720 pub fn visible_entries(&self) -> impl Iterator<Item = (usize, &PickerEntry)> {
721 self.filtered_indices
722 .iter()
723 .enumerate()
724 .filter_map(|(filtered_index, source_index)| {
725 self.source_entries
726 .get(*source_index)
727 .map(|entry| (filtered_index, entry))
728 })
729 }
730
731 pub fn selected_entry(&self) -> Option<&PickerEntry> {
732 self.selected_filtered_index
733 .and_then(|filtered_index| self.filtered_indices.get(filtered_index))
734 .and_then(|source_index| self.source_entries.get(*source_index))
735 }
736
737 pub fn count_label(&self) -> String {
738 match self.selected_filtered_index {
739 Some(index) if !self.filtered_indices.is_empty() => {
740 format!("{}/{}", index + 1, self.filtered_indices.len())
741 }
742 _ => format!("0/{}", self.filtered_indices.len()),
743 }
744 }
745
746 pub fn scope_hint(&self) -> String {
747 if self.scopes.is_empty() {
748 return String::new();
749 }
750
751 self.scopes
752 .iter()
753 .map(|scope| format!("%{}", scope.token))
754 .collect::<Vec<_>>()
755 .join(" ")
756 }
757
758 pub fn active_scope_label(&self) -> Option<String> {
759 ParsedQuery::parse(&self.input.text(), &self.scopes)
760 .active_scope_label(self.input.cursor_position())
761 .map(str::to_string)
762 }
763
764 pub fn paste(&mut self, text: &str) -> TextInputEventOutcome {
765 let outcome = self.input.paste(text);
766 if matches!(outcome, TextInputEventOutcome::Handled) {
767 self.refresh_filter();
768 }
769 outcome
770 }
771
772 pub fn autocomplete_selected(&mut self) -> TextInputEventOutcome {
773 if self.input.is_suggestions_active() {
774 if self.input.apply_suggestion().is_some() {
775 self.refresh_filter();
776 return TextInputEventOutcome::Handled;
777 }
778 }
779
780 if let Some((text, cursor)) = self.scoped_value_completion_replacement() {
781 self.input.set_text(text);
782 self.input.set_cursor_position(cursor);
783 self.refresh_filter();
784 return TextInputEventOutcome::Handled;
785 }
786
787 let outcome = self.input.accept_suggestion_suffix();
788 if matches!(outcome, TextInputEventOutcome::Handled) {
789 self.refresh_filter();
790 }
791 outcome
792 }
793
794 fn apply_query_change(&mut self) {
795 self.rebuild_filtered_indices();
796 self.normalize_selection();
797 self.recompute_inline_suggestion();
798 self.input.check_suggestion_trigger();
799 }
800
801 fn apply_selection_change(&mut self) {
802 self.normalize_selection();
803 self.recompute_inline_suggestion();
804 }
805
806 fn normalize_input_whitespace_experimental(&mut self) {
807 let text = self.input.text();
808 let cursor = self.input.cursor_position();
809 let (normalized, normalized_cursor) =
810 normalize_picker_input_whitespace_experimental(&text, cursor);
811 if normalized != text {
812 self.input.set_text(normalized);
813 self.input.set_cursor_position(normalized_cursor);
814 }
815 }
816
817 fn rebuild_filtered_indices(&mut self) {
818 let query = self.input.text();
819 let parsed_query = ParsedQuery::parse(&query, &self.scopes)
820 .with_implicit_scopes(&self.implicit_scopes, &self.scopes);
821 let weights = &self.field_weights;
822 let mut matches = self
823 .source_entries
824 .iter()
825 .enumerate()
826 .filter_map(|(index, _)| {
827 let cache = self.search_cache.get(index)?;
828 let score = parsed_query.score_entry(cache, &mut self.matcher, weights)?;
829 Some(PickerRankedEntry {
830 source_index: index,
831 score,
832 })
833 })
834 .collect::<Vec<_>>();
835 matches.sort_by(|left, right| {
836 right
837 .score
838 .cmp(&left.score)
839 .then_with(|| left.source_index.cmp(&right.source_index))
840 });
841 self.filtered_indices = matches
842 .into_iter()
843 .map(|entry| entry.source_index)
844 .collect();
845 }
846
847 fn normalize_selection(&mut self) {
848 self.selected_filtered_index = match self.selected_filtered_index {
849 Some(index) if index < self.filtered_indices.len() => Some(index),
850 _ if self.filtered_indices.is_empty() => None,
851 _ => Some(0),
852 };
853 }
854
855 fn recompute_inline_suggestion(&mut self) {
856 let query = self.input.text();
857 let cursor = self.input.cursor_position();
858 let parsed = ParsedQuery::parse(&query, &self.scopes);
859 let suffix = self
860 .selected_entry()
861 .and_then(|entry| parsed.trailing_completion_suffix(&query, cursor, entry));
862
863 if let Some(suffix) = suffix {
864 self.input.set_suggestion_suffix(suffix);
865 } else {
866 self.input.clear_suggestion_suffix();
867 }
868 }
869
870 fn scoped_value_completion_replacement(&self) -> Option<(String, usize)> {
871 let query = self.input.text();
872 let cursor = self.input.cursor_position();
873 let entry = self.selected_entry()?;
874 replacement_for_active_scoped_value(&query, cursor, &self.scopes, entry)
875 }
876
877 fn rebuild_search_cache(&mut self) {
878 self.search_cache = self
879 .source_entries
880 .iter()
881 .map(PickerEntrySearchCache::from_entry)
882 .collect();
883 }
884}
885
886impl<M> Default for PickerData<M> {
887 fn default() -> Self {
888 let mut input = TextInputState::<PickerInputProvider>::default();
889 input.enter_edit_mode();
890 Self {
891 title: None,
892 input,
893 query_placeholder: String::new(),
894 source_entries: Vec::new(),
895 filtered_indices: Vec::new(),
896 selected_filtered_index: None,
897 empty_preview: String::new(),
898 scopes: Vec::new(),
899 layout: PickerLayout::Responsive,
900 head: None,
901 mode: None,
902 field_weights: PickerFieldWeights::default(),
903 implicit_scopes: Vec::new(),
904 search_cache: Vec::new(),
905 matcher: build_picker_matcher(),
906 }
907 }
908}
909
910impl<M: Clone> Clone for PickerData<M> {
911 fn clone(&self) -> Self {
912 let mut input = TextInputState::<PickerInputProvider>::default();
913 input.set_text(self.input.text());
914 input.data_provider_mut().set_scopes(self.scopes.clone());
915 input.enter_edit_mode();
916 let mut picker = Self {
917 title: self.title.clone(),
918 input,
919 query_placeholder: self.query_placeholder.clone(),
920 source_entries: self.source_entries.clone(),
921 filtered_indices: self.filtered_indices.clone(),
922 selected_filtered_index: self.selected_filtered_index,
923 empty_preview: self.empty_preview.clone(),
924 scopes: self.scopes.clone(),
925 layout: self.layout,
926 head: self.head.clone(),
927 mode: self.mode.clone(),
928 field_weights: self.field_weights.clone(),
929 implicit_scopes: self.implicit_scopes.clone(),
930 search_cache: Vec::new(),
931 matcher: build_picker_matcher(),
932 };
933 picker.rebuild_search_cache();
934 picker.recompute_inline_suggestion();
935 picker
936 }
937}
938
939impl<M: PartialEq> PartialEq for PickerData<M> {
940 fn eq(&self, other: &Self) -> bool {
941 self.title == other.title
942 && self.input.text() == other.input.text()
943 && self.query_placeholder == other.query_placeholder
944 && self.source_entries == other.source_entries
945 && self.filtered_indices == other.filtered_indices
946 && self.selected_filtered_index == other.selected_filtered_index
947 && self.empty_preview == other.empty_preview
948 && self.scopes == other.scopes
949 && self.layout == other.layout
950 && self.head == other.head
951 && self.mode == other.mode
952 && self.field_weights == other.field_weights
953 && self.implicit_scopes == other.implicit_scopes
954 }
955}
956
957impl<M: Eq> Eq for PickerData<M> {}
958
959impl<M: std::fmt::Debug> std::fmt::Debug for PickerData<M> {
960 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
961 f.debug_struct("PickerData")
962 .field("title", &self.title)
963 .field("input_text", &self.input.text())
964 .field("query_placeholder", &self.query_placeholder)
965 .field("source_entries", &self.source_entries)
966 .field("filtered_indices", &self.filtered_indices)
967 .field("selected_filtered_index", &self.selected_filtered_index)
968 .field("empty_preview", &self.empty_preview)
969 .field("scopes", &self.scopes)
970 .field("layout", &self.layout)
971 .field("head", &self.head)
972 .field("mode", &self.mode)
973 .field("field_weights", &self.field_weights)
974 .field("implicit_scopes", &self.implicit_scopes)
975 .finish()
976 }
977}
978
979#[derive(Debug, Clone)]
980struct QueryTerm {
981 text: String,
982 pattern: Pattern,
983}
984
985#[derive(Debug, Clone)]
986enum QuerySegmentKind<'a> {
987 Default,
988 Scoped {
989 scope: &'a PickerScope,
990 scope_token_char_end: usize,
991 },
992}
993
994#[derive(Debug, Clone)]
995struct QuerySegment<'a> {
996 kind: QuerySegmentKind<'a>,
997 text: String,
998 terms: Vec<QueryTerm>,
999}
1000
1001#[derive(Debug, Clone, PartialEq, Eq)]
1002struct QueryTokenSpan {
1003 text: String,
1004 start_char: usize,
1005 end_char: usize,
1006}
1007
1008#[derive(Debug, Clone)]
1009struct PickerCachedField {
1010 key: String,
1011 value: String,
1012 haystack: Utf32String,
1013}
1014
1015#[derive(Debug, Clone, Default)]
1016struct PickerEntrySearchCache {
1017 fields: Vec<PickerCachedField>,
1018}
1019
1020#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1021struct PickerRankedEntry {
1022 source_index: usize,
1023 score: u32,
1024}
1025
1026#[derive(Debug, Clone, PartialEq, Eq)]
1027struct PickerImplicitScope {
1028 token: String,
1029 value: String,
1030}
1031
1032impl PickerImplicitScope {
1033 fn new(token: &str, value: &str) -> Self {
1034 Self {
1035 token: token.to_string(),
1036 value: value.trim().to_string(),
1037 }
1038 }
1039}
1040
1041#[derive(Debug, Clone)]
1042struct ParsedQuery<'a> {
1043 query_char_len: usize,
1044 ends_with_whitespace: bool,
1045 segments: Vec<QuerySegment<'a>>,
1046}
1047
1048impl<'a> ParsedQuery<'a> {
1049 fn parse(query: &'a str, scopes: &'a [PickerScope]) -> Self {
1050 let mut segments = Vec::new();
1051 let query_char_len = query.chars().count();
1052 let command_specs = command_specs_for_scopes(scopes);
1053 let parsed = PickerCommandQuery::parse_with_specs(query, &command_specs);
1054
1055 for clause in parsed.clauses() {
1056 let Some(command) = clause.command() else {
1057 push_default_segment(&mut segments, clause.selection_query_text());
1058 continue;
1059 };
1060
1061 let Some(scope) = scopes.iter().find(|scope| scope.matches_token(command)) else {
1062 let mut text = format!("%{}", command);
1063 let terms = clause.terms();
1064 if !terms.is_empty() {
1065 text.push(' ');
1066 text.push_str(&terms.join(" "));
1067 }
1068 push_default_segment(&mut segments, text);
1069 continue;
1070 };
1071
1072 let scoped_text = match scope.command_argument {
1073 PickerCommandArgument::Required => clause.argument_or_empty().to_string(),
1074 PickerCommandArgument::None => clause.selection_query_text(),
1075 };
1076 segments.push(QuerySegment::from_scoped_text(scope, scoped_text));
1077
1078 if scope.command_argument == PickerCommandArgument::Required {
1079 push_default_segment(&mut segments, clause.selection_query_text());
1080 }
1081 }
1082
1083 Self {
1084 query_char_len,
1085 ends_with_whitespace: query.chars().last().is_some_and(char::is_whitespace),
1086 segments,
1087 }
1088 }
1089
1090 fn with_implicit_scopes(
1091 mut self,
1092 implicit_scopes: &[PickerImplicitScope],
1093 scopes: &'a [PickerScope],
1094 ) -> Self {
1095 for implicit_scope in implicit_scopes {
1096 if self.has_scope(&implicit_scope.token) {
1097 continue;
1098 }
1099
1100 let Some(scope) = scopes
1101 .iter()
1102 .find(|scope| scope.matches_token(&implicit_scope.token))
1103 else {
1104 continue;
1105 };
1106
1107 self.segments.push(QuerySegment::from_scoped_value(
1108 scope,
1109 implicit_scope.value.clone(),
1110 ));
1111 }
1112
1113 self
1114 }
1115
1116 fn has_scope(&self, token: &str) -> bool {
1117 self.segments.iter().any(|segment| match &segment.kind {
1118 QuerySegmentKind::Scoped { scope, .. } => scope.matches_token(token),
1119 QuerySegmentKind::Default => false,
1120 })
1121 }
1122
1123 fn active_scope_label(&self, cursor_chars: usize) -> Option<&'a str> {
1124 let QuerySegmentKind::Scoped { scope, .. } =
1125 self.trailing_active_segment(cursor_chars)?.kind
1126 else {
1127 return None;
1128 };
1129
1130 Some(scope.label.as_str())
1131 }
1132
1133 fn trailing_completion_suffix(
1134 &self,
1135 _query: &str,
1136 cursor_chars: usize,
1137 entry: &PickerEntry,
1138 ) -> Option<String> {
1139 if cursor_chars == self.query_char_len {
1143 if let Some(QuerySegment {
1144 kind: QuerySegmentKind::Scoped { scope, .. },
1145 ..
1146 }) = self.segments.last()
1147 {
1148 let completed_required_argument = scope.command_argument
1149 == PickerCommandArgument::Required
1150 && self.ends_with_whitespace;
1151 if scope.suppress_value_completion && !completed_required_argument {
1152 return None;
1153 }
1154 }
1155 }
1156
1157 let Some(segment) = self.trailing_active_segment(cursor_chars) else {
1158 if cursor_chars == self.query_char_len
1159 && self.query_char_len == 0
1160 && !default_completion_value(entry).is_empty()
1161 {
1162 return Some(default_completion_value(entry).to_string());
1163 }
1164
1165 if cursor_chars == self.query_char_len
1166 && self.ends_with_whitespace
1167 && self
1168 .segments
1169 .iter()
1170 .any(|segment| matches!(segment.kind, QuerySegmentKind::Scoped { .. }))
1171 && !default_completion_value(entry).is_empty()
1172 {
1173 return Some(default_completion_value(entry).to_string());
1174 }
1175
1176 return None;
1177 };
1178 let candidate = match &segment.kind {
1179 QuerySegmentKind::Default => default_completion_value(entry),
1180 QuerySegmentKind::Scoped { scope, .. } => scope.completion_value(entry)?,
1181 };
1182
1183 let typed = segment.text.as_str();
1184 if typed.is_empty() {
1185 return match &segment.kind {
1186 QuerySegmentKind::Scoped {
1187 scope_token_char_end,
1188 ..
1189 } if *scope_token_char_end == self.query_char_len => {
1190 Some(format!(" {}", candidate))
1191 }
1192 _ if candidate.is_empty() => None,
1193 _ => Some(candidate.to_string()),
1194 };
1195 }
1196
1197 let Some(suffix_start) = case_insensitive_prefix_boundary(candidate, typed) else {
1198 return None;
1199 };
1200 let suffix = candidate[suffix_start..].to_string();
1201 if suffix.is_empty() {
1202 return None;
1203 }
1204
1205 Some(suffix)
1206 }
1207
1208 fn trailing_active_segment(&self, cursor_chars: usize) -> Option<&QuerySegment<'a>> {
1209 if cursor_chars != self.query_char_len {
1210 return None;
1211 }
1212
1213 let segment = self.segments.last()?;
1214 if !self.ends_with_whitespace {
1215 return Some(segment);
1216 }
1217
1218 match &segment.kind {
1219 QuerySegmentKind::Scoped { .. } if segment.text.is_empty() => Some(segment),
1220 QuerySegmentKind::Default | QuerySegmentKind::Scoped { .. } => None,
1221 }
1222 }
1223
1224 fn score_entry(
1225 &self,
1226 entry_cache: &PickerEntrySearchCache,
1227 matcher: &mut Matcher,
1228 weights: &PickerFieldWeights,
1229 ) -> Option<u32> {
1230 if self.segments.is_empty() {
1231 return Some(0);
1232 }
1233
1234 let mut total_score = 0;
1235 for segment in &self.segments {
1236 total_score += segment.score(entry_cache, matcher, weights)?;
1237 }
1238
1239 Some(total_score)
1240 }
1241}
1242
1243impl<'a> QuerySegment<'a> {
1244 fn from_text(kind: QuerySegmentKind<'a>, text: String) -> Self {
1245 Self {
1246 kind,
1247 terms: split_terms(&text),
1248 text,
1249 }
1250 }
1251
1252 fn from_scoped_text(scope: &'a PickerScope, text: String) -> Self {
1253 Self::from_text(
1254 QuerySegmentKind::Scoped {
1255 scope,
1256 scope_token_char_end: 0,
1257 },
1258 text,
1259 )
1260 }
1261
1262 fn from_scoped_value(scope: &'a PickerScope, text: String) -> Self {
1263 Self::from_scoped_text(scope, text)
1264 }
1265}
1266
1267fn push_default_segment<'a>(segments: &mut Vec<QuerySegment<'a>>, text: String) {
1268 if !text.trim().is_empty() {
1269 segments.push(QuerySegment::from_text(
1270 QuerySegmentKind::Default,
1271 text.trim().to_string(),
1272 ));
1273 }
1274}
1275
1276fn default_completion_value(entry: &PickerEntry) -> &str {
1277 entry
1278 .fields
1279 .iter()
1280 .find(|field| field.key == "completion")
1281 .map(|field| field.value.as_str())
1282 .unwrap_or(entry.label.as_str())
1283}
1284
1285fn replacement_for_active_scoped_value(
1286 query: &str,
1287 cursor_chars: usize,
1288 scopes: &[PickerScope],
1289 entry: &PickerEntry,
1290) -> Option<(String, usize)> {
1291 let query_char_len = query.chars().count();
1292 if cursor_chars != query_char_len {
1293 return None;
1294 }
1295
1296 let tokens = query_token_spans(query);
1297 let mut index = 0usize;
1298 let mut active_replacement = None;
1299
1300 while index < tokens.len() {
1301 let token = &tokens[index];
1302 let Some(command) = token
1303 .text
1304 .strip_prefix('%')
1305 .filter(|command| !command.is_empty())
1306 else {
1307 index += 1;
1308 continue;
1309 };
1310 let Some(scope) = scopes.iter().find(|scope| scope.matches_token(command)) else {
1311 index += 1;
1312 continue;
1313 };
1314
1315 index += 1;
1316 let mut value_start = token.end_char;
1317 let mut value_end = cursor_chars;
1318 if scope.command_argument == PickerCommandArgument::Required
1319 && index < tokens.len()
1320 && !tokens[index].text.starts_with('%')
1321 {
1322 value_start = tokens[index].start_char;
1323 index += 1;
1324 } else if scope.command_argument == PickerCommandArgument::None
1325 && index < tokens.len()
1326 && !tokens[index].text.starts_with('%')
1327 {
1328 value_start = tokens[index].start_char;
1329 }
1330
1331 while index < tokens.len() && !tokens[index].text.starts_with('%') {
1332 value_end = tokens[index].end_char;
1333 index += 1;
1334 }
1335
1336 if index == tokens.len() {
1337 active_replacement = Some((scope, value_start, value_end.max(cursor_chars)));
1338 }
1339 }
1340
1341 let (scope, start, end) = active_replacement?;
1342 if !scope.replace_value_with_completion {
1343 return None;
1344 }
1345
1346 let candidate = scope.completion_value(entry)?.trim();
1347 if candidate.is_empty() {
1348 return None;
1349 }
1350
1351 let replacement = if start == end || query_char_slice(query, start, end).trim().is_empty() {
1352 format!(" {}", candidate)
1353 } else {
1354 candidate.to_string()
1355 };
1356 let updated = replace_query_char_range(query, start, end, &replacement);
1357 let cursor = start + replacement.chars().count();
1358
1359 Some((updated, cursor))
1360}
1361
1362fn query_token_spans(query: &str) -> Vec<QueryTokenSpan> {
1363 let chars = query.chars().collect::<Vec<_>>();
1364 let mut tokens = Vec::new();
1365 let mut index = 0usize;
1366
1367 while index < chars.len() {
1368 while index < chars.len() && chars[index].is_whitespace() {
1369 index += 1;
1370 }
1371 if index >= chars.len() {
1372 break;
1373 }
1374
1375 let start = index;
1376 while index < chars.len() && !chars[index].is_whitespace() {
1377 index += 1;
1378 }
1379 let end = index;
1380 tokens.push(QueryTokenSpan {
1381 text: chars[start..end].iter().collect(),
1382 start_char: start,
1383 end_char: end,
1384 });
1385 }
1386
1387 tokens
1388}
1389
1390fn query_char_slice(query: &str, start: usize, end: usize) -> String {
1391 query.chars().skip(start).take(end - start).collect()
1392}
1393
1394fn replace_query_char_range(query: &str, start: usize, end: usize, replacement: &str) -> String {
1395 let chars = query.chars().collect::<Vec<_>>();
1396 let start = start.min(chars.len());
1397 let end = end.min(chars.len()).max(start);
1398 let mut updated = String::new();
1399 updated.extend(chars[..start].iter());
1400 updated.push_str(replacement);
1401 updated.extend(chars[end..].iter());
1402 updated
1403}
1404
1405fn command_specs_for_scopes(scopes: &[PickerScope]) -> Vec<PickerCommandSpec> {
1406 scopes
1407 .iter()
1408 .map(|scope| {
1409 PickerCommandSpec::new(scope.token.clone(), scope.command_argument)
1410 .with_aliases(scope.aliases.clone())
1411 })
1412 .collect()
1413}
1414
1415impl QuerySegment<'_> {
1416 fn score(
1417 &self,
1418 entry_cache: &PickerEntrySearchCache,
1419 matcher: &mut Matcher,
1420 weights: &PickerFieldWeights,
1421 ) -> Option<u32> {
1422 if self.terms.is_empty() {
1423 return match &self.kind {
1424 QuerySegmentKind::Default => Some(0),
1425 QuerySegmentKind::Scoped { scope, .. } => entry_cache
1426 .fields
1427 .iter()
1428 .any(|field| {
1429 !field.value.trim().is_empty()
1430 && scope.field_keys.iter().any(|key| key == &field.key)
1431 })
1432 .then_some(0),
1433 };
1434 }
1435
1436 let mut segment_score = 0;
1437 for term in &self.terms {
1438 let best = match &self.kind {
1439 QuerySegmentKind::Default => {
1440 term.best_score(entry_cache.fields.iter(), matcher, weights)
1441 }
1442 QuerySegmentKind::Scoped { scope, .. } => {
1443 let scoped_fields = entry_cache
1444 .fields
1445 .iter()
1446 .filter(|field| scope.field_keys.iter().any(|key| key == &field.key))
1447 .collect::<Vec<_>>();
1448
1449 if scoped_fields.is_empty() {
1450 term.best_score(
1451 entry_cache
1452 .fields
1453 .iter()
1454 .filter(|field| field.key == "label"),
1455 matcher,
1456 weights,
1457 )
1458 } else {
1459 term.best_score(scoped_fields.into_iter(), matcher, weights)
1460 }
1461 }
1462 }?;
1463
1464 segment_score += best;
1465 }
1466
1467 Some(segment_score)
1468 }
1469}
1470
1471impl QueryTerm {
1472 fn new(text: &str) -> Self {
1473 Self {
1474 text: text.to_string(),
1475 pattern: Pattern::parse(text, CaseMatching::Smart, Normalization::Smart),
1476 }
1477 }
1478
1479 fn best_score<'a, I>(
1480 &self,
1481 candidates: I,
1482 matcher: &mut Matcher,
1483 weights: &PickerFieldWeights,
1484 ) -> Option<u32>
1485 where
1486 I: IntoIterator<Item = &'a PickerCachedField>,
1487 {
1488 let mut best = None;
1489
1490 for candidate in candidates {
1491 let Some(base_score) = self.pattern.score(candidate.haystack.slice(..), matcher) else {
1492 continue;
1493 };
1494
1495 let score = base_score.saturating_mul(weights.weight(&candidate.key))
1496 + field_match_bonus(&self.text, &candidate.value);
1497 best = Some(best.map_or(score, |current: u32| current.max(score)));
1498 }
1499
1500 best
1501 }
1502}
1503
1504impl PickerEntrySearchCache {
1505 fn from_entry(entry: &PickerEntry) -> Self {
1506 Self {
1507 fields: entry
1508 .fields
1509 .iter()
1510 .map(|field| PickerCachedField {
1511 key: field.key.clone(),
1512 value: field.value.clone(),
1513 haystack: Utf32String::from(field.value.as_str()),
1514 })
1515 .collect(),
1516 }
1517 }
1518}
1519
1520fn split_terms(text: &str) -> Vec<QueryTerm> {
1521 text.split_whitespace().map(QueryTerm::new).collect()
1522}
1523
1524fn build_picker_matcher() -> Matcher {
1525 let mut config = NucleoConfig::DEFAULT;
1526 config.prefer_prefix = true;
1527 Matcher::new(config)
1528}
1529
1530fn field_match_bonus(query: &str, value: &str) -> u32 {
1531 let needle = query.trim();
1532 if needle.is_empty() {
1533 return 0;
1534 }
1535
1536 let needle_lower = needle.to_lowercase();
1537 let value_lower = value.to_lowercase();
1538
1539 if value_lower == needle_lower {
1540 2_000
1541 } else if value_lower.starts_with(&needle_lower) {
1542 900
1543 } else if value_lower
1544 .split(|ch: char| !ch.is_alphanumeric())
1545 .any(|part| !part.is_empty() && part.starts_with(&needle_lower))
1546 {
1547 450
1548 } else if value_lower.contains(&needle_lower) {
1549 150
1550 } else {
1551 0
1552 }
1553}
1554
1555fn case_insensitive_prefix_boundary(candidate: &str, typed: &str) -> Option<usize> {
1556 let typed_lower = typed.to_lowercase();
1557
1558 let mut candidate_prefix_lower = String::new();
1559 for (byte_index, ch) in candidate.char_indices() {
1560 candidate_prefix_lower.extend(ch.to_lowercase());
1561 let next_boundary = byte_index + ch.len_utf8();
1562
1563 if candidate_prefix_lower.len() >= typed_lower.len() {
1564 return candidate_prefix_lower
1565 .starts_with(&typed_lower)
1566 .then_some(next_boundary);
1567 }
1568
1569 if !typed_lower.starts_with(&candidate_prefix_lower) {
1570 return None;
1571 }
1572 }
1573
1574 None
1575}
1576
1577#[cfg(test)]
1578mod tests {
1579 use super::*;
1580
1581 fn build_picker() -> PickerData {
1582 let entries = vec![
1583 PickerEntry::new("invoice", vec![]).with_fields(vec![
1584 PickerField::new("profile", "finance"),
1585 PickerField::new("dependency", "customer department"),
1586 ]),
1587 PickerEntry::new("customer", vec![]).with_fields(vec![
1588 PickerField::new("profile", "finance"),
1589 PickerField::new("dependency", ""),
1590 ]),
1591 PickerEntry::new("shipment", vec![]).with_fields(vec![
1592 PickerField::new("profile", "ops"),
1593 PickerField::new("dependency", "warehouse"),
1594 ]),
1595 ];
1596
1597 let mut picker = PickerData::with_entries(entries);
1598 picker.set_scopes(vec![
1599 PickerScope::new("table", "Table", vec!["label".to_string()])
1600 .with_completion_key("label"),
1601 PickerScope::new("profile", "Profile", vec!["profile".to_string()])
1602 .with_completion_key("profile"),
1603 PickerScope::new("dependency", "Dependency", vec!["dependency".to_string()])
1604 .with_aliases(["dep"])
1605 .with_completion_key("dependency"),
1606 ]);
1607 picker
1608 }
1609
1610 #[test]
1611 fn plain_query_filters_entries() {
1612 let mut picker = build_picker();
1613 picker.input.set_text("invoice");
1614 picker.refresh_filter();
1615
1616 assert_eq!(picker.filtered_len(), 1);
1617 assert_eq!(
1618 picker.selected_entry().map(|entry| entry.label.as_str()),
1619 Some("invoice")
1620 );
1621 }
1622
1623 #[test]
1624 fn scoped_query_filters_matching_field() {
1625 let mut picker = build_picker();
1626 picker.input.set_text("%dependency department");
1627 picker.refresh_filter();
1628
1629 assert_eq!(picker.filtered_len(), 1);
1630 assert_eq!(
1631 picker.selected_entry().map(|entry| entry.label.as_str()),
1632 Some("invoice")
1633 );
1634 }
1635
1636 #[test]
1637 fn repeated_scoped_clauses_filter_with_and_logic() {
1638 let mut picker = build_picker();
1639 picker
1640 .input
1641 .set_text("invoice %profile finance %dependency customer");
1642 picker.refresh_filter();
1643
1644 assert_eq!(picker.filtered_len(), 1);
1645 assert_eq!(
1646 picker.selected_entry().map(|entry| entry.label.as_str()),
1647 Some("invoice")
1648 );
1649 }
1650
1651 #[test]
1652 fn scope_aliases_are_supported() {
1653 let mut picker = build_picker();
1654 picker.input.set_text("%dep customer");
1655 picker.refresh_filter();
1656
1657 assert_eq!(picker.filtered_len(), 1);
1658 assert_eq!(
1659 picker.selected_entry().map(|entry| entry.label.as_str()),
1660 Some("invoice")
1661 );
1662 }
1663
1664 #[test]
1665 fn selects_matching_source_index_after_filtering() {
1666 let mut picker = build_picker();
1667 picker.set_selected_source_index(Some(1));
1668
1669 assert_eq!(
1670 picker.selected_entry().map(|entry| entry.label.as_str()),
1671 Some("customer")
1672 );
1673 }
1674
1675 #[test]
1676 fn selected_entry_exposes_inline_suffix_for_prefix_query() {
1677 let mut picker = build_picker();
1678 picker.input.set_text("inv");
1679 picker.refresh_filter();
1680
1681 assert_eq!(picker.input.suggestion_suffix(), Some("oice"));
1682 }
1683
1684 #[test]
1685 fn default_inline_suffix_prefers_completion_field() {
1686 let entries = vec![PickerEntry::new("very long database row label", vec![])
1687 .with_fields(vec![PickerField::new("completion", "very long database...")])];
1688 let picker = PickerData::<()>::with_entries(entries);
1689
1690 assert_eq!(picker.input.suggestion_suffix(), Some("very long database..."));
1691 }
1692
1693 #[test]
1694 fn empty_query_exposes_selected_entry_as_initial_suffix() {
1695 let picker = build_picker();
1696
1697 assert_eq!(
1698 picker.selected_entry().map(|entry| entry.label.as_str()),
1699 Some("invoice")
1700 );
1701 assert_eq!(picker.input.suggestion_suffix(), Some("invoice"));
1702 }
1703
1704 #[test]
1705 fn inline_suffix_uses_unicode_boundary_after_case_folded_prefix() {
1706 let entries = vec![PickerEntry::new("İstanbul", vec![])];
1707 let mut picker = PickerData::<()>::with_entries(entries);
1708 picker.input.set_text("i");
1709 picker.refresh_filter();
1710
1711 assert_eq!(picker.input.suggestion_suffix(), Some("stanbul"));
1712 }
1713
1714 #[test]
1715 fn autocomplete_selected_accepts_highlighted_suffix() {
1716 let mut picker = build_picker();
1717 picker.input.set_text("inv");
1718 picker.refresh_filter();
1719
1720 let outcome = picker.autocomplete_selected();
1721
1722 assert_eq!(outcome, TextInputEventOutcome::Handled);
1723 assert_eq!(picker.input.text(), "invoice");
1724 assert_eq!(picker.input.suggestion_suffix(), None);
1725 }
1726
1727 #[test]
1728 fn moving_selection_updates_inline_suffix() {
1729 let entries = vec![PickerEntry::new("apple", vec![]), PickerEntry::new("ananas", vec![])];
1730 let mut picker = PickerData::<()>::with_entries(entries);
1731 picker.input.set_text("a");
1732 picker.refresh_filter();
1733
1734 assert_eq!(picker.input.suggestion_suffix(), Some("pple"));
1735
1736 picker.move_down();
1737
1738 assert_eq!(
1739 picker.selected_entry().map(|entry| entry.label.as_str()),
1740 Some("ananas")
1741 );
1742 assert_eq!(picker.input.suggestion_suffix(), Some("nanas"));
1743 }
1744
1745 #[test]
1746 fn moving_selection_updates_initial_inline_suffix() {
1747 let entries = vec![PickerEntry::new("apple", vec![]), PickerEntry::new("ananas", vec![])];
1748 let mut picker = PickerData::<()>::with_entries(entries);
1749
1750 assert_eq!(picker.input.suggestion_suffix(), Some("apple"));
1751
1752 picker.move_down();
1753
1754 assert_eq!(
1755 picker.selected_entry().map(|entry| entry.label.as_str()),
1756 Some("ananas")
1757 );
1758 assert_eq!(picker.input.suggestion_suffix(), Some("ananas"));
1759 }
1760
1761 #[test]
1762 fn setting_filtered_selection_updates_inline_suffix() {
1763 let entries = vec![PickerEntry::new("apple", vec![]), PickerEntry::new("ananas", vec![])];
1764 let mut picker = PickerData::<()>::with_entries(entries);
1765 picker.input.set_text("a");
1766 picker.refresh_filter();
1767
1768 picker.set_selected_filtered_index(Some(1));
1769
1770 assert_eq!(
1771 picker.selected_entry().map(|entry| entry.label.as_str()),
1772 Some("ananas")
1773 );
1774 assert_eq!(picker.input.suggestion_suffix(), Some("nanas"));
1775 }
1776
1777 #[test]
1778 fn deleting_accepted_completion_recomputes_inline_suffix() {
1779 let entries = vec![PickerEntry::new("apple", vec![]), PickerEntry::new("ananas", vec![])];
1780 let mut picker = PickerData::<()>::with_entries(entries);
1781 picker.input.set_text("app");
1782 picker.refresh_filter();
1783
1784 assert_eq!(picker.autocomplete_selected(), TextInputEventOutcome::Handled);
1785 assert_eq!(picker.input.text(), "apple");
1786
1787 for _ in 0..4 {
1788 picker.pop_char();
1789 }
1790
1791 assert_eq!(picker.input.text(), "a");
1792 assert_eq!(picker.input.suggestion_suffix(), Some("pple"));
1793
1794 picker.move_down();
1795
1796 assert_eq!(picker.input.suggestion_suffix(), Some("nanas"));
1797 }
1798
1799 #[test]
1800 fn percent_opens_scope_suggestions() {
1801 let mut picker = build_picker();
1802 picker.input.set_text("%");
1803 picker.refresh_filter();
1804
1805 assert!(picker.input.is_suggestions_active());
1806 assert_eq!(
1807 picker
1808 .input
1809 .suggestions()
1810 .iter()
1811 .map(|suggestion| suggestion.value_to_store.as_str())
1812 .collect::<Vec<_>>(),
1813 vec!["%table ", "%profile ", "%dependency "]
1814 );
1815 }
1816
1817 #[test]
1818 fn autocomplete_selected_accepts_scope_suggestion() {
1819 let mut picker = build_picker();
1820 picker.input.set_text("%p");
1821 picker.refresh_filter();
1822
1823 let outcome = picker.autocomplete_selected();
1824
1825 assert_eq!(outcome, TextInputEventOutcome::Handled);
1826 assert_eq!(picker.input.text(), "%profile ");
1827 assert!(!picker.input.is_suggestions_active());
1828 }
1829
1830 #[test]
1831 fn scoped_clause_exposes_trailing_suffix_for_active_scope() {
1832 let mut picker = build_picker();
1833 picker.input.set_text("%profile fin");
1834 picker.refresh_filter();
1835
1836 assert_eq!(picker.input.suggestion_suffix(), Some("ance"));
1837 }
1838
1839 #[test]
1840 fn scoped_clause_with_trailing_space_exposes_value_suffix() {
1841 let mut picker = build_picker();
1842 picker.input.set_text("%profile ");
1843 picker.refresh_filter();
1844
1845 assert_eq!(picker.input.suggestion_suffix(), Some("finance"));
1846
1847 let outcome = picker.autocomplete_selected();
1848
1849 assert_eq!(outcome, TextInputEventOutcome::Handled);
1850 assert_eq!(picker.input.text(), "%profile finance");
1851 }
1852
1853 #[test]
1854 fn refresh_filter_experimentally_normalizes_picker_query_spaces() {
1855 let mut picker = build_picker();
1856 picker.input.set_text(" invoice %profile finance ");
1857 picker.refresh_filter();
1858
1859 assert_eq!(picker.input.text(), "invoice %profile finance ");
1860 }
1861
1862 #[test]
1863 fn experimental_space_normalization_keeps_cursor_in_logical_position() {
1864 let mut picker = build_picker();
1865 picker.input.set_text("invoice %profile");
1866 picker.input.set_cursor_position(9);
1867 picker.refresh_filter();
1868
1869 assert_eq!(picker.input.text(), "invoice %profile");
1870 assert_eq!(picker.input.cursor_position(), 8);
1871 }
1872
1873 #[test]
1874 fn autocomplete_after_extra_spaces_uses_normalized_query() {
1875 let mut picker = build_picker();
1876 picker.input.set_text("%profile ");
1877 picker.refresh_filter();
1878
1879 let outcome = picker.autocomplete_selected();
1880
1881 assert_eq!(outcome, TextInputEventOutcome::Handled);
1882 assert_eq!(picker.input.text(), "%profile finance");
1883 }
1884
1885 #[test]
1886 fn scoped_clause_can_limit_values() {
1887 let entries = vec![
1888 PickerEntry::new("amount", vec![]).with_fields(vec![
1889 PickerField::new("table", "invoice"),
1890 PickerField::new("column", "amount"),
1891 ]),
1892 PickerEntry::new("bonus", vec![]).with_fields(vec![
1893 PickerField::new("table", "department"),
1894 PickerField::new("column", "bonus"),
1895 ]),
1896 ];
1897 let mut picker = PickerData::<()>::with_entries(entries);
1898 picker.set_scopes(vec![
1899 PickerScope::new("table", "Table", vec!["table".to_string()])
1900 .with_completion_key("table")
1901 .with_value_token_limit(1),
1902 PickerScope::new("column", "Column", vec!["column".to_string()])
1903 .with_completion_key("column")
1904 .with_value_token_limit(1),
1905 ]);
1906 picker.input.set_text("%table department bo".to_string());
1907 picker.refresh_filter();
1908
1909 let labels = picker
1910 .visible_entries()
1911 .map(|(_, entry)| entry.label.as_str())
1912 .collect::<Vec<_>>();
1913
1914 assert_eq!(labels, vec!["bonus"]);
1915
1916 picker.input.set_text("%table department ".to_string());
1917 picker.refresh_filter();
1918
1919 assert_eq!(picker.input.suggestion_suffix(), Some("bonus"));
1920 }
1921
1922 #[test]
1923 fn suppressed_scope_value_hides_inline_suffix() {
1924 let entries = vec![
1925 PickerEntry::new("customer:1", vec![])
1926 .with_fields(vec![PickerField::new("target", "customer_id")]),
1927 ];
1928 let mut picker = PickerData::<()>::with_entries(entries);
1929 picker.set_scopes(vec![
1930 PickerScope::new("target", "Target", vec!["target".to_string()])
1931 .with_completion_key("target")
1932 .with_command_argument()
1933 .with_suppressed_value_completion(),
1934 ]);
1935
1936 picker.input.set_text("%target cust".to_string());
1938 picker.refresh_filter();
1939 assert_eq!(picker.input.suggestion_suffix(), None);
1940
1941 picker.input.set_text("%target customer_id ".to_string());
1943 picker.refresh_filter();
1944 assert_eq!(picker.input.suggestion_suffix(), Some("customer:1"));
1945
1946 picker.input.set_text("%target ".to_string());
1948 picker.refresh_filter();
1949 assert_eq!(picker.input.suggestion_suffix(), Some("customer_id"));
1950 }
1951
1952 #[test]
1953 fn suppressed_required_scope_allows_option_suffix_after_argument_space() {
1954 let entries = vec![
1955 PickerEntry::new("customer:1", vec![])
1956 .with_fields(vec![PickerField::new("target", "customer_id")]),
1957 ];
1958 let mut picker = PickerData::<()>::with_entries(entries);
1959 picker.set_scopes(vec![
1960 PickerScope::new("target", "Target", vec!["target".to_string()])
1961 .with_completion_key("target")
1962 .with_command_argument()
1963 .with_suppressed_value_completion(),
1964 ]);
1965
1966 picker.input.set_text("%target customer_id ".to_string());
1967 picker.refresh_filter();
1968
1969 assert_eq!(picker.input.suggestion_suffix(), Some("customer:1"));
1970 }
1971
1972 #[test]
1973 fn replacement_scope_autocomplete_replaces_freeform_value() {
1974 let entries = vec![PickerEntry::new("customer:3", vec![]).with_fields(vec![
1975 PickerField::new("record_id", "3"),
1976 PickerField::new("data", "Acme Bratislava"),
1977 ])];
1978 let mut picker = PickerData::<()>::with_entries(entries);
1979 picker.set_scopes(vec![
1980 PickerScope::new("data", "Data", vec!["data".to_string()])
1981 .with_completion_key("record_id")
1982 .with_suppressed_value_completion()
1983 .with_value_replacement_completion(),
1984 ]);
1985
1986 picker.input.set_text("%data Acme".to_string());
1987 picker.refresh_filter();
1988
1989 assert_eq!(picker.input.suggestion_suffix(), None);
1990 assert_eq!(picker.autocomplete_selected(), TextInputEventOutcome::Handled);
1991 assert_eq!(picker.input.text(), "%data 3");
1992 }
1993
1994 #[test]
1995 fn replacement_scope_autocomplete_preserves_previous_scopes() {
1996 let entries = vec![PickerEntry::new("customer:3", vec![]).with_fields(vec![
1997 PickerField::new("record_id", "3"),
1998 PickerField::new("target", "customer_id"),
1999 PickerField::new("data", "Acme Bratislava"),
2000 ])];
2001 let mut picker = PickerData::<()>::with_entries(entries);
2002 picker.set_scopes(vec![
2003 PickerScope::new("target", "Target", vec!["target".to_string()])
2004 .with_completion_key("target")
2005 .with_command_argument()
2006 .with_suppressed_value_completion(),
2007 PickerScope::new("data", "Data", vec!["data".to_string()])
2008 .with_completion_key("record_id")
2009 .with_suppressed_value_completion()
2010 .with_value_replacement_completion(),
2011 ]);
2012
2013 picker
2014 .input
2015 .set_text("%target customer_id %data Acme".to_string());
2016 picker.refresh_filter();
2017
2018 assert_eq!(picker.autocomplete_selected(), TextInputEventOutcome::Handled);
2019 assert_eq!(picker.input.text(), "%target customer_id %data 3");
2020 }
2021
2022 #[test]
2023 fn fuzzy_query_prefers_exact_label_over_secondary_field_match() {
2024 let entries = vec![
2025 PickerEntry::new("finance", vec![])
2026 .with_fields(vec![PickerField::new("profile", "ops")]),
2027 PickerEntry::new("invoice", vec![])
2028 .with_fields(vec![PickerField::new("profile", "finance")]),
2029 ];
2030
2031 let mut picker = PickerData::<()>::with_entries(entries);
2032 picker.input.set_text("finance");
2033 picker.refresh_filter();
2034
2035 let labels = picker
2036 .visible_entries()
2037 .map(|(_, entry)| entry.label.as_str())
2038 .collect::<Vec<_>>();
2039
2040 assert_eq!(labels, vec!["finance", "invoice"]);
2041 }
2042
2043 #[test]
2044 fn fuzzy_query_matches_abbreviated_label() {
2045 let mut picker = build_picker();
2046 picker.input.set_text("invc");
2047 picker.refresh_filter();
2048
2049 assert_eq!(
2050 picker.selected_entry().map(|entry| entry.label.as_str()),
2051 Some("invoice")
2052 );
2053 }
2054
2055 #[test]
2056 fn implicit_scope_filters_when_query_has_no_explicit_scope() {
2057 let mut picker = build_picker();
2058 picker.set_implicit_scope("profile", "finance");
2059
2060 let mut labels: Vec<_> = picker
2061 .visible_entries()
2062 .map(|(_, entry)| entry.label.as_str())
2063 .collect();
2064 labels.sort(); assert_eq!(labels, vec!["customer", "invoice"]);
2067 }
2068
2069 #[test]
2070 fn explicit_scope_overrides_implicit_scope_of_same_token() {
2071 let mut picker = build_picker();
2072 picker.set_implicit_scope("profile", "finance");
2073 picker.input.set_text("%profile ops");
2074 picker.refresh_filter();
2075
2076 let labels = picker
2077 .visible_entries()
2078 .map(|(_, entry)| entry.label.as_str())
2079 .collect::<Vec<_>>();
2080
2081 assert_eq!(labels, vec!["shipment"]);
2082 }
2083
2084 #[test]
2085 fn bare_scoped_clause_filters_to_entries_with_matching_scope_fields() {
2086 let mut picker = build_picker();
2087 picker.input.set_text("%dependency");
2088 picker.refresh_filter();
2089
2090 let labels = picker
2091 .visible_entries()
2092 .map(|(_, entry)| entry.label.as_str())
2093 .collect::<Vec<_>>();
2094
2095 assert_eq!(labels, vec!["invoice", "shipment"]);
2096 }
2097}