Skip to main content

tui_pages/picker/
state.rs

1//! Picker content, fuzzy-search engine, and the scope-aware text-input
2//! provider.
3//!
4//! [`PickerData`] is the turnkey state for a fuzzy-search overlay: a list of
5//! [`PickerEntry`] rows, a canvas text input with scope (`%token`) parsing and
6//! autocompletion, and a [`nucleo`]-backed ranking engine. It is generic over an
7//! application-owned mode payload `M` (see [`PickerData::set_mode`]) that the
8//! library stores but never inspects — mirroring how
9//! [`DialogData`](crate::dialog::DialogData) carries an opaque purpose.
10
11use 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    /// When set, the inline completion suffix is suppressed while the cursor is
46    /// editing this scope's value. Use it for scopes whose value is free-form
47    /// (e.g. a row search) rather than a known candidate to complete toward.
48    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    /// Suppress the inline completion suffix while editing this scope's value.
106    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/// Per-field-key scoring weights for the fuzzy matcher.
248///
249/// A higher weight makes matches in that field rank above matches in other
250/// fields. Every entry's `label` field is always present, so the default keeps
251/// `label` heaviest; applications layer their own domain keys on top via
252/// [`PickerFieldWeights::with_override`].
253#[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    /// Weights with a single fallback applied to every field key.
270    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
298/// A generic, scope-aware fuzzy picker.
299///
300/// `M` is an application-owned mode payload stored opaquely (see
301/// [`set_mode`](PickerData::set_mode)); the library never inspects it. Use
302/// `PickerData<()>` (the default) when you don't need one.
303pub 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
473// Experimental picker-query whitespace normalization. Keep this isolated so the
474// behavior can be removed if command-query editing should return to raw input.
475fn 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    /// Attach an application-owned mode payload. The library stores it verbatim
570    /// and never inspects it; read it back with [`mode_ref`](PickerData::mode_ref)
571    /// / [`mode_mut`](PickerData::mode_mut).
572    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        // A scope can opt out of inline value completion (e.g. a free-form row
1140        // search). Suppress the suffix while the cursor is editing such a
1141        // scope's non-empty value.
1142        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        // Typing a value for the suppressed scope must not surface a suffix.
1937        picker.input.set_text("%target cust".to_string());
1938        picker.refresh_filter();
1939        assert_eq!(picker.input.suggestion_suffix(), None);
1940
1941        // Trailing space after a required argument starts final-option completion.
1942        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        // The bare scope token (no value yet) still completes.
1947        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(); // Make comparison order-independent
2065
2066        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}