settings_profile_selector.rs

  1use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
  2use gpui::{
  3    App, Context, DismissEvent, Entity, EventEmitter, Focusable, Render, Task, WeakEntity, Window,
  4};
  5use picker::{Picker, PickerDelegate};
  6use settings::{ActiveSettingsProfileName, SettingsStore};
  7use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*};
  8use workspace::{ModalView, Workspace};
  9
 10pub fn init(cx: &mut App) {
 11    cx.on_action(|_: &zed_actions::settings_profile_selector::Toggle, cx| {
 12        workspace::with_active_or_new_workspace(cx, |workspace, window, cx| {
 13            toggle_settings_profile_selector(workspace, window, cx);
 14        });
 15    });
 16}
 17
 18fn toggle_settings_profile_selector(
 19    workspace: &mut Workspace,
 20    window: &mut Window,
 21    cx: &mut Context<Workspace>,
 22) {
 23    workspace.toggle_modal(window, cx, |window, cx| {
 24        let delegate = SettingsProfileSelectorDelegate::new(cx.entity().downgrade(), window, cx);
 25        SettingsProfileSelector::new(delegate, window, cx)
 26    });
 27}
 28
 29pub struct SettingsProfileSelector {
 30    picker: Entity<Picker<SettingsProfileSelectorDelegate>>,
 31}
 32
 33impl ModalView for SettingsProfileSelector {}
 34
 35impl EventEmitter<DismissEvent> for SettingsProfileSelector {}
 36
 37impl Focusable for SettingsProfileSelector {
 38    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
 39        self.picker.focus_handle(cx)
 40    }
 41}
 42
 43impl Render for SettingsProfileSelector {
 44    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
 45        v_flex().w(rems(22.)).child(self.picker.clone())
 46    }
 47}
 48
 49impl SettingsProfileSelector {
 50    pub fn new(
 51        delegate: SettingsProfileSelectorDelegate,
 52        window: &mut Window,
 53        cx: &mut Context<Self>,
 54    ) -> Self {
 55        let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
 56        Self { picker }
 57    }
 58}
 59
 60pub struct SettingsProfileSelectorDelegate {
 61    matches: Vec<StringMatch>,
 62    profile_names: Vec<Option<String>>,
 63    original_profile_name: Option<String>,
 64    selected_profile_name: Option<String>,
 65    selected_index: usize,
 66    selection_completed: bool,
 67    selector: WeakEntity<SettingsProfileSelector>,
 68}
 69
 70impl SettingsProfileSelectorDelegate {
 71    fn new(
 72        selector: WeakEntity<SettingsProfileSelector>,
 73        _: &mut Window,
 74        cx: &mut Context<SettingsProfileSelector>,
 75    ) -> Self {
 76        let settings_store = cx.global::<SettingsStore>();
 77        let mut profile_names: Vec<Option<String>> = settings_store
 78            .configured_settings_profiles()
 79            .map(|s| Some(s.to_string()))
 80            .collect();
 81        profile_names.insert(0, None);
 82
 83        let matches = profile_names
 84            .iter()
 85            .enumerate()
 86            .map(|(ix, profile_name)| StringMatch {
 87                candidate_id: ix,
 88                score: 0.0,
 89                positions: Default::default(),
 90                string: display_name(profile_name),
 91            })
 92            .collect();
 93
 94        let profile_name = cx
 95            .try_global::<ActiveSettingsProfileName>()
 96            .map(|p| p.0.clone());
 97
 98        let mut this = Self {
 99            matches,
100            profile_names,
101            original_profile_name: profile_name.clone(),
102            selected_profile_name: None,
103            selected_index: 0,
104            selection_completed: false,
105            selector,
106        };
107
108        if let Some(profile_name) = profile_name {
109            this.select_if_matching(&profile_name);
110        }
111
112        this
113    }
114
115    fn select_if_matching(&mut self, profile_name: &str) {
116        self.selected_index = self
117            .matches
118            .iter()
119            .position(|mat| mat.string == profile_name)
120            .unwrap_or(self.selected_index);
121    }
122
123    fn set_selected_profile(
124        &self,
125        cx: &mut Context<Picker<SettingsProfileSelectorDelegate>>,
126    ) -> Option<String> {
127        let mat = self.matches.get(self.selected_index)?;
128        let profile_name = self.profile_names.get(mat.candidate_id)?;
129        Self::update_active_profile_name_global(profile_name.clone(), cx)
130    }
131
132    fn update_active_profile_name_global(
133        profile_name: Option<String>,
134        cx: &mut Context<Picker<SettingsProfileSelectorDelegate>>,
135    ) -> Option<String> {
136        if let Some(profile_name) = profile_name {
137            cx.set_global(ActiveSettingsProfileName(profile_name.clone()));
138            return Some(profile_name);
139        }
140
141        if cx.has_global::<ActiveSettingsProfileName>() {
142            cx.remove_global::<ActiveSettingsProfileName>();
143        }
144
145        None
146    }
147}
148
149impl PickerDelegate for SettingsProfileSelectorDelegate {
150    type ListItem = ListItem;
151
152    fn placeholder_text(&self, _: &mut Window, _: &mut App) -> std::sync::Arc<str> {
153        "Select a settings profile...".into()
154    }
155
156    fn match_count(&self) -> usize {
157        self.matches.len()
158    }
159
160    fn selected_index(&self) -> usize {
161        self.selected_index
162    }
163
164    fn set_selected_index(
165        &mut self,
166        ix: usize,
167        _: &mut Window,
168        cx: &mut Context<Picker<SettingsProfileSelectorDelegate>>,
169    ) {
170        self.selected_index = ix;
171        self.selected_profile_name = self.set_selected_profile(cx);
172    }
173
174    fn update_matches(
175        &mut self,
176        query: String,
177        window: &mut Window,
178        cx: &mut Context<Picker<SettingsProfileSelectorDelegate>>,
179    ) -> Task<()> {
180        let background = cx.background_executor().clone();
181        let candidates = self
182            .profile_names
183            .iter()
184            .enumerate()
185            .map(|(id, profile_name)| StringMatchCandidate::new(id, &display_name(profile_name)))
186            .collect::<Vec<_>>();
187
188        cx.spawn_in(window, async move |this, cx| {
189            let matches = if query.is_empty() {
190                candidates
191                    .into_iter()
192                    .enumerate()
193                    .map(|(index, candidate)| StringMatch {
194                        candidate_id: index,
195                        string: candidate.string,
196                        positions: Vec::new(),
197                        score: 0.0,
198                    })
199                    .collect()
200            } else {
201                match_strings(
202                    &candidates,
203                    &query,
204                    false,
205                    true,
206                    100,
207                    &Default::default(),
208                    background,
209                )
210                .await
211            };
212
213            this.update_in(cx, |this, _, cx| {
214                this.delegate.matches = matches;
215                this.delegate.selected_index = this
216                    .delegate
217                    .selected_index
218                    .min(this.delegate.matches.len().saturating_sub(1));
219                this.delegate.selected_profile_name = this.delegate.set_selected_profile(cx);
220            })
221            .ok();
222        })
223    }
224
225    fn confirm(
226        &mut self,
227        _: bool,
228        _: &mut Window,
229        cx: &mut Context<Picker<SettingsProfileSelectorDelegate>>,
230    ) {
231        self.selection_completed = true;
232        self.selector
233            .update(cx, |_, cx| {
234                cx.emit(DismissEvent);
235            })
236            .ok();
237    }
238
239    fn dismissed(
240        &mut self,
241        _: &mut Window,
242        cx: &mut Context<Picker<SettingsProfileSelectorDelegate>>,
243    ) {
244        if !self.selection_completed {
245            SettingsProfileSelectorDelegate::update_active_profile_name_global(
246                self.original_profile_name.clone(),
247                cx,
248            );
249        }
250        self.selector.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
251    }
252
253    fn render_match(
254        &self,
255        ix: usize,
256        selected: bool,
257        _: &mut Window,
258        _: &mut Context<Picker<Self>>,
259    ) -> Option<Self::ListItem> {
260        let mat = &self.matches.get(ix)?;
261        let profile_name = &self.profile_names.get(mat.candidate_id)?;
262
263        Some(
264            ListItem::new(ix)
265                .inset(true)
266                .spacing(ListItemSpacing::Sparse)
267                .toggle_state(selected)
268                .child(HighlightedLabel::new(
269                    display_name(profile_name),
270                    mat.positions.clone(),
271                )),
272        )
273    }
274}
275
276fn display_name(profile_name: &Option<String>) -> String {
277    profile_name.clone().unwrap_or("Disabled".into())
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use editor;
284    use gpui::{TestAppContext, UpdateGlobal, VisualTestContext};
285    use menu::{Cancel, Confirm, SelectNext, SelectPrevious};
286    use project::{FakeFs, Project};
287    use serde_json::json;
288    use settings::Settings;
289    use theme_settings::ThemeSettings;
290    use workspace::{self, AppState, MultiWorkspace};
291    use zed_actions::settings_profile_selector;
292
293    async fn init_test(
294        user_settings_json: serde_json::Value,
295        cx: &mut TestAppContext,
296    ) -> (Entity<Workspace>, &mut VisualTestContext) {
297        cx.update(|cx| {
298            let state = AppState::test(cx);
299            let settings_store = SettingsStore::test(cx);
300            cx.set_global(settings_store);
301            settings::init(cx);
302            theme_settings::init(theme::LoadThemes::JustBase, cx);
303            super::init(cx);
304            editor::init(cx);
305            state
306        });
307
308        cx.update(|cx| {
309            SettingsStore::update_global(cx, |store, cx| {
310                store
311                    .set_user_settings(&user_settings_json.to_string(), cx)
312                    .unwrap();
313            });
314        });
315
316        let fs = FakeFs::new(cx.executor());
317        let project = Project::test(fs, ["/test".as_ref()], cx).await;
318        let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
319        let cx = VisualTestContext::from_window(*window, cx).into_mut();
320        let workspace = window
321            .read_with(cx, |mw, _| mw.workspace().clone())
322            .unwrap();
323
324        cx.update(|_, cx| {
325            assert!(!cx.has_global::<ActiveSettingsProfileName>());
326        });
327
328        (workspace, cx)
329    }
330
331    #[track_caller]
332    fn active_settings_profile_picker(
333        workspace: &Entity<Workspace>,
334        cx: &mut VisualTestContext,
335    ) -> Entity<Picker<SettingsProfileSelectorDelegate>> {
336        workspace.update(cx, |workspace, cx| {
337            workspace
338                .active_modal::<SettingsProfileSelector>(cx)
339                .expect("settings profile selector is not open")
340                .read(cx)
341                .picker
342                .clone()
343        })
344    }
345
346    #[gpui::test]
347    async fn test_settings_profile_selector_state(cx: &mut TestAppContext) {
348        let classroom_and_streaming_profile_name = "Classroom / Streaming".to_string();
349        let demo_videos_profile_name = "Demo Videos".to_string();
350
351        let user_settings_json = json!({
352            "buffer_font_size": 10.0,
353            "profiles": {
354                classroom_and_streaming_profile_name.clone(): {
355                    "settings": {
356                        "buffer_font_size": 20.0,
357                    }
358                },
359                demo_videos_profile_name.clone(): {
360                    "settings": {
361                        "buffer_font_size": 15.0
362                    }
363                }
364            }
365        });
366        let (workspace, cx) = init_test(user_settings_json, cx).await;
367
368        cx.dispatch_action(settings_profile_selector::Toggle);
369        let picker = active_settings_profile_picker(&workspace, cx);
370
371        picker.read_with(cx, |picker, cx| {
372            assert_eq!(picker.delegate.matches.len(), 3);
373            assert_eq!(picker.delegate.matches[0].string, display_name(&None));
374            assert_eq!(
375                picker.delegate.matches[1].string,
376                classroom_and_streaming_profile_name
377            );
378            assert_eq!(picker.delegate.matches[2].string, demo_videos_profile_name);
379            assert_eq!(picker.delegate.matches.get(3), None);
380
381            assert_eq!(picker.delegate.selected_index, 0);
382            assert_eq!(picker.delegate.selected_profile_name, None);
383
384            assert_eq!(cx.try_global::<ActiveSettingsProfileName>(), None);
385            assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(10.0));
386        });
387
388        cx.dispatch_action(Confirm);
389
390        cx.update(|_, cx| {
391            assert_eq!(cx.try_global::<ActiveSettingsProfileName>(), None);
392        });
393
394        cx.dispatch_action(settings_profile_selector::Toggle);
395        let picker = active_settings_profile_picker(&workspace, cx);
396        cx.dispatch_action(SelectNext);
397
398        picker.read_with(cx, |picker, cx| {
399            assert_eq!(picker.delegate.selected_index, 1);
400            assert_eq!(
401                picker.delegate.selected_profile_name,
402                Some(classroom_and_streaming_profile_name.clone())
403            );
404
405            assert_eq!(
406                cx.try_global::<ActiveSettingsProfileName>()
407                    .map(|p| p.0.clone()),
408                Some(classroom_and_streaming_profile_name.clone())
409            );
410
411            assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(20.0));
412        });
413
414        cx.dispatch_action(Cancel);
415
416        cx.update(|_, cx| {
417            assert_eq!(cx.try_global::<ActiveSettingsProfileName>(), None);
418            assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(10.0));
419        });
420
421        cx.dispatch_action(settings_profile_selector::Toggle);
422        let picker = active_settings_profile_picker(&workspace, cx);
423
424        cx.dispatch_action(SelectNext);
425
426        picker.read_with(cx, |picker, cx| {
427            assert_eq!(picker.delegate.selected_index, 1);
428            assert_eq!(
429                picker.delegate.selected_profile_name,
430                Some(classroom_and_streaming_profile_name.clone())
431            );
432
433            assert_eq!(
434                cx.try_global::<ActiveSettingsProfileName>()
435                    .map(|p| p.0.clone()),
436                Some(classroom_and_streaming_profile_name.clone())
437            );
438
439            assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(20.0));
440        });
441
442        cx.dispatch_action(SelectNext);
443
444        picker.read_with(cx, |picker, cx| {
445            assert_eq!(picker.delegate.selected_index, 2);
446            assert_eq!(
447                picker.delegate.selected_profile_name,
448                Some(demo_videos_profile_name.clone())
449            );
450
451            assert_eq!(
452                cx.try_global::<ActiveSettingsProfileName>()
453                    .map(|p| p.0.clone()),
454                Some(demo_videos_profile_name.clone())
455            );
456
457            assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(15.0));
458        });
459
460        cx.dispatch_action(Confirm);
461
462        cx.update(|_, cx| {
463            assert_eq!(
464                cx.try_global::<ActiveSettingsProfileName>()
465                    .map(|p| p.0.clone()),
466                Some(demo_videos_profile_name.clone())
467            );
468            assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(15.0));
469        });
470
471        cx.dispatch_action(settings_profile_selector::Toggle);
472        let picker = active_settings_profile_picker(&workspace, cx);
473
474        picker.read_with(cx, |picker, cx| {
475            assert_eq!(picker.delegate.selected_index, 2);
476            assert_eq!(
477                picker.delegate.selected_profile_name,
478                Some(demo_videos_profile_name.clone())
479            );
480
481            assert_eq!(
482                cx.try_global::<ActiveSettingsProfileName>()
483                    .map(|p| p.0.clone()),
484                Some(demo_videos_profile_name.clone())
485            );
486            assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(15.0));
487        });
488
489        cx.dispatch_action(SelectPrevious);
490
491        picker.read_with(cx, |picker, cx| {
492            assert_eq!(picker.delegate.selected_index, 1);
493            assert_eq!(
494                picker.delegate.selected_profile_name,
495                Some(classroom_and_streaming_profile_name.clone())
496            );
497
498            assert_eq!(
499                cx.try_global::<ActiveSettingsProfileName>()
500                    .map(|p| p.0.clone()),
501                Some(classroom_and_streaming_profile_name.clone())
502            );
503
504            assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(20.0));
505        });
506
507        cx.dispatch_action(Cancel);
508
509        cx.update(|_, cx| {
510            assert_eq!(
511                cx.try_global::<ActiveSettingsProfileName>()
512                    .map(|p| p.0.clone()),
513                Some(demo_videos_profile_name.clone())
514            );
515
516            assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(15.0));
517        });
518
519        cx.dispatch_action(settings_profile_selector::Toggle);
520        let picker = active_settings_profile_picker(&workspace, cx);
521
522        picker.read_with(cx, |picker, cx| {
523            assert_eq!(picker.delegate.selected_index, 2);
524            assert_eq!(
525                picker.delegate.selected_profile_name,
526                Some(demo_videos_profile_name.clone())
527            );
528
529            assert_eq!(
530                cx.try_global::<ActiveSettingsProfileName>()
531                    .map(|p| p.0.clone()),
532                Some(demo_videos_profile_name)
533            );
534
535            assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(15.0));
536        });
537
538        cx.dispatch_action(SelectPrevious);
539
540        picker.read_with(cx, |picker, cx| {
541            assert_eq!(picker.delegate.selected_index, 1);
542            assert_eq!(
543                picker.delegate.selected_profile_name,
544                Some(classroom_and_streaming_profile_name.clone())
545            );
546
547            assert_eq!(
548                cx.try_global::<ActiveSettingsProfileName>()
549                    .map(|p| p.0.clone()),
550                Some(classroom_and_streaming_profile_name)
551            );
552
553            assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(20.0));
554        });
555
556        cx.dispatch_action(SelectPrevious);
557
558        picker.read_with(cx, |picker, cx| {
559            assert_eq!(picker.delegate.selected_index, 0);
560            assert_eq!(picker.delegate.selected_profile_name, None);
561
562            assert_eq!(
563                cx.try_global::<ActiveSettingsProfileName>()
564                    .map(|p| p.0.clone()),
565                None
566            );
567
568            assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(10.0));
569        });
570
571        cx.dispatch_action(Confirm);
572
573        cx.update(|_, cx| {
574            assert_eq!(cx.try_global::<ActiveSettingsProfileName>(), None);
575            assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(10.0));
576        });
577    }
578
579    #[gpui::test]
580    async fn test_settings_profile_with_user_base(cx: &mut TestAppContext) {
581        let user_settings_json = json!({
582            "buffer_font_size": 10.0,
583            "profiles": {
584                "Explicit User": {
585                    "base": "user",
586                    "settings": {
587                        "buffer_font_size": 20.0
588                    }
589                },
590                "Implicit User": {
591                    "settings": {
592                        "buffer_font_size": 20.0
593                    }
594                }
595            }
596        });
597        let (workspace, cx) = init_test(user_settings_json, cx).await;
598
599        // Select "Explicit User" (index 1) — profile applies on top of user settings.
600        cx.dispatch_action(settings_profile_selector::Toggle);
601        let picker = active_settings_profile_picker(&workspace, cx);
602        cx.dispatch_action(SelectNext);
603
604        picker.read_with(cx, |picker, cx| {
605            assert_eq!(
606                picker.delegate.selected_profile_name.as_deref(),
607                Some("Explicit User")
608            );
609            assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(20.0));
610        });
611
612        cx.dispatch_action(Confirm);
613
614        // Select "Implicit User" (index 2) — no base specified, same behavior.
615        cx.dispatch_action(settings_profile_selector::Toggle);
616        let picker = active_settings_profile_picker(&workspace, cx);
617        cx.dispatch_action(SelectNext);
618
619        picker.read_with(cx, |picker, cx| {
620            assert_eq!(
621                picker.delegate.selected_profile_name.as_deref(),
622                Some("Implicit User")
623            );
624            assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(20.0));
625        });
626
627        cx.dispatch_action(Confirm);
628    }
629
630    #[gpui::test]
631    async fn test_settings_profile_with_default_base(cx: &mut TestAppContext) {
632        let user_settings_json = json!({
633            "buffer_font_size": 10.0,
634            "profiles": {
635                "Clean Slate": {
636                    "base": "default"
637                },
638                "Custom on Defaults": {
639                    "base": "default",
640                    "settings": {
641                        "buffer_font_size": 30.0
642                    }
643                }
644            }
645        });
646        let (workspace, cx) = init_test(user_settings_json, cx).await;
647
648        // User has buffer_font_size: 10, factory default is 15.
649        cx.update(|_, cx| {
650            assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(10.0));
651        });
652
653        // "Clean Slate" has base: "default" with no settings overrides,
654        // so we get the factory default (15), not the user's value (10).
655        cx.dispatch_action(settings_profile_selector::Toggle);
656        let picker = active_settings_profile_picker(&workspace, cx);
657        cx.dispatch_action(SelectNext);
658
659        picker.read_with(cx, |picker, cx| {
660            assert_eq!(
661                picker.delegate.selected_profile_name.as_deref(),
662                Some("Clean Slate")
663            );
664            assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(15.0));
665        });
666
667        // "Custom on Defaults" has base: "default" with buffer_font_size: 30,
668        // so the profile's override (30) applies on top of the factory default,
669        // not on top of the user's value (10).
670        cx.dispatch_action(SelectNext);
671
672        picker.read_with(cx, |picker, cx| {
673            assert_eq!(
674                picker.delegate.selected_profile_name.as_deref(),
675                Some("Custom on Defaults")
676            );
677            assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(30.0));
678        });
679
680        cx.dispatch_action(Confirm);
681
682        cx.update(|_, cx| {
683            assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(30.0));
684        });
685    }
686
687    #[gpui::test]
688    async fn test_settings_profile_selector_is_in_user_configuration_order(
689        cx: &mut TestAppContext,
690    ) {
691        // Must be unique names (HashMap)
692        let user_settings_json = json!({
693            "profiles": {
694                "z": { "settings": {} },
695                "e": { "settings": {} },
696                "d": { "settings": {} },
697                " ": { "settings": {} },
698                "r": { "settings": {} },
699                "u": { "settings": {} },
700                "l": { "settings": {} },
701                "3": { "settings": {} },
702                "s": { "settings": {} },
703                "!": { "settings": {} },
704            }
705        });
706        let (workspace, cx) = init_test(user_settings_json, cx).await;
707
708        cx.dispatch_action(settings_profile_selector::Toggle);
709        let picker = active_settings_profile_picker(&workspace, cx);
710
711        picker.read_with(cx, |picker, _| {
712            assert_eq!(picker.delegate.matches.len(), 11);
713            assert_eq!(picker.delegate.matches[0].string, display_name(&None));
714            assert_eq!(picker.delegate.matches[1].string, "z");
715            assert_eq!(picker.delegate.matches[2].string, "e");
716            assert_eq!(picker.delegate.matches[3].string, "d");
717            assert_eq!(picker.delegate.matches[4].string, " ");
718            assert_eq!(picker.delegate.matches[5].string, "r");
719            assert_eq!(picker.delegate.matches[6].string, "u");
720            assert_eq!(picker.delegate.matches[7].string, "l");
721            assert_eq!(picker.delegate.matches[8].string, "3");
722            assert_eq!(picker.delegate.matches[9].string, "s");
723            assert_eq!(picker.delegate.matches[10].string, "!");
724        });
725    }
726}