stash_picker.rs

  1use fuzzy::StringMatchCandidate;
  2
  3use git::stash::StashEntry;
  4use gpui::{
  5    Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
  6    InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement, Render,
  7    SharedString, Styled, Subscription, Task, WeakEntity, Window, actions, rems,
  8};
  9use picker::{Picker, PickerDelegate};
 10use project::git_store::{Repository, RepositoryEvent};
 11use std::sync::Arc;
 12use time::{OffsetDateTime, UtcOffset};
 13use time_format;
 14use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};
 15use util::ResultExt;
 16use workspace::notifications::DetachAndPromptErr;
 17use workspace::{ModalView, Workspace};
 18
 19use crate::commit_view::CommitView;
 20use crate::stash_picker;
 21
 22actions!(
 23    stash_picker,
 24    [
 25        /// Drop the selected stash entry.
 26        DropStashItem,
 27        /// Show the diff view of the selected stash entry.
 28        ShowStashItem,
 29    ]
 30);
 31
 32pub fn open(
 33    workspace: &mut Workspace,
 34    _: &zed_actions::git::ViewStash,
 35    window: &mut Window,
 36    cx: &mut Context<Workspace>,
 37) {
 38    let repository = workspace.project().read(cx).active_repository(cx);
 39    let weak_workspace = workspace.weak_handle();
 40    workspace.toggle_modal(window, cx, |window, cx| {
 41        StashList::new(repository, weak_workspace, rems(34.), window, cx)
 42    })
 43}
 44
 45pub fn create_embedded(
 46    repository: Option<Entity<Repository>>,
 47    workspace: WeakEntity<Workspace>,
 48    width: Rems,
 49    show_footer: bool,
 50    window: &mut Window,
 51    cx: &mut Context<StashList>,
 52) -> StashList {
 53    StashList::new_embedded(repository, workspace, width, show_footer, window, cx)
 54}
 55
 56pub struct StashList {
 57    width: Rems,
 58    pub picker: Entity<Picker<StashListDelegate>>,
 59    picker_focus_handle: FocusHandle,
 60    _subscriptions: Vec<Subscription>,
 61}
 62
 63impl StashList {
 64    fn new(
 65        repository: Option<Entity<Repository>>,
 66        workspace: WeakEntity<Workspace>,
 67        width: Rems,
 68        window: &mut Window,
 69        cx: &mut Context<Self>,
 70    ) -> Self {
 71        let mut this = Self::new_inner(repository, workspace, width, false, window, cx);
 72        this._subscriptions
 73            .push(cx.subscribe(&this.picker, |_, _, _, cx| {
 74                cx.emit(DismissEvent);
 75            }));
 76        this
 77    }
 78
 79    fn new_inner(
 80        repository: Option<Entity<Repository>>,
 81        workspace: WeakEntity<Workspace>,
 82        width: Rems,
 83        embedded: bool,
 84        window: &mut Window,
 85        cx: &mut Context<Self>,
 86    ) -> Self {
 87        let mut _subscriptions = Vec::new();
 88        let stash_request = repository
 89            .clone()
 90            .map(|repository| repository.read_with(cx, |repo, _| repo.cached_stash()));
 91
 92        if let Some(repo) = repository.clone() {
 93            _subscriptions.push(
 94                cx.subscribe_in(&repo, window, |this, _, event, window, cx| {
 95                    if matches!(event, RepositoryEvent::StashEntriesChanged) {
 96                        let stash_entries = this.picker.read_with(cx, |picker, cx| {
 97                            picker
 98                                .delegate
 99                                .repo
100                                .clone()
101                                .map(|repo| repo.read(cx).cached_stash().entries.to_vec())
102                        });
103                        this.picker.update(cx, |this, cx| {
104                            this.delegate.all_stash_entries = stash_entries;
105                            this.refresh(window, cx);
106                        });
107                    }
108                }),
109            )
110        }
111
112        cx.spawn_in(window, async move |this, cx| {
113            let stash_entries = stash_request
114                .map(|git_stash| git_stash.entries.to_vec())
115                .unwrap_or_default();
116
117            this.update_in(cx, |this, window, cx| {
118                this.picker.update(cx, |picker, cx| {
119                    picker.delegate.all_stash_entries = Some(stash_entries);
120                    picker.refresh(window, cx);
121                })
122            })?;
123
124            anyhow::Ok(())
125        })
126        .detach_and_log_err(cx);
127
128        let delegate = StashListDelegate::new(repository, workspace, window, cx);
129        let picker = cx.new(|cx| {
130            Picker::uniform_list(delegate, window, cx)
131                .show_scrollbar(true)
132                .modal(!embedded)
133        });
134        let picker_focus_handle = picker.focus_handle(cx);
135        picker.update(cx, |picker, _| {
136            picker.delegate.focus_handle = picker_focus_handle.clone();
137            picker.delegate.show_footer = !embedded;
138        });
139
140        Self {
141            picker,
142            picker_focus_handle,
143            width,
144            _subscriptions,
145        }
146    }
147
148    fn new_embedded(
149        repository: Option<Entity<Repository>>,
150        workspace: WeakEntity<Workspace>,
151        width: Rems,
152        show_footer: bool,
153        window: &mut Window,
154        cx: &mut Context<Self>,
155    ) -> Self {
156        let mut this = Self::new_inner(repository, workspace, width, true, window, cx);
157        this.picker.update(cx, |picker, _| {
158            picker.delegate.show_footer = show_footer;
159        });
160        this._subscriptions
161            .push(cx.subscribe(&this.picker, |_, _, _, cx| {
162                cx.emit(DismissEvent);
163            }));
164        this
165    }
166
167    pub fn handle_drop_stash(
168        &mut self,
169        _: &DropStashItem,
170        window: &mut Window,
171        cx: &mut Context<Self>,
172    ) {
173        self.picker.update(cx, |picker, cx| {
174            picker
175                .delegate
176                .drop_stash_at(picker.delegate.selected_index(), window, cx);
177        });
178        cx.notify();
179    }
180
181    pub fn handle_show_stash(
182        &mut self,
183        _: &ShowStashItem,
184        window: &mut Window,
185        cx: &mut Context<Self>,
186    ) {
187        self.picker.update(cx, |picker, cx| {
188            picker
189                .delegate
190                .show_stash_at(picker.delegate.selected_index(), window, cx);
191        });
192
193        cx.emit(DismissEvent);
194    }
195
196    pub fn handle_modifiers_changed(
197        &mut self,
198        ev: &ModifiersChangedEvent,
199        _: &mut Window,
200        cx: &mut Context<Self>,
201    ) {
202        self.picker
203            .update(cx, |picker, _| picker.delegate.modifiers = ev.modifiers)
204    }
205}
206
207impl ModalView for StashList {}
208impl EventEmitter<DismissEvent> for StashList {}
209impl Focusable for StashList {
210    fn focus_handle(&self, _: &App) -> FocusHandle {
211        self.picker_focus_handle.clone()
212    }
213}
214
215impl Render for StashList {
216    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
217        v_flex()
218            .key_context("StashList")
219            .w(self.width)
220            .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
221            .on_action(cx.listener(Self::handle_drop_stash))
222            .on_action(cx.listener(Self::handle_show_stash))
223            .child(self.picker.clone())
224    }
225}
226
227#[derive(Debug, Clone)]
228struct StashEntryMatch {
229    entry: StashEntry,
230    positions: Vec<usize>,
231    formatted_timestamp: String,
232    formatted_absolute_timestamp: String,
233}
234
235pub struct StashListDelegate {
236    matches: Vec<StashEntryMatch>,
237    all_stash_entries: Option<Vec<StashEntry>>,
238    repo: Option<Entity<Repository>>,
239    workspace: WeakEntity<Workspace>,
240    selected_index: usize,
241    last_query: String,
242    modifiers: Modifiers,
243    focus_handle: FocusHandle,
244    timezone: UtcOffset,
245    show_footer: bool,
246}
247
248impl StashListDelegate {
249    fn new(
250        repo: Option<Entity<Repository>>,
251        workspace: WeakEntity<Workspace>,
252        _window: &mut Window,
253        cx: &mut Context<StashList>,
254    ) -> Self {
255        let timezone = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC);
256
257        Self {
258            matches: vec![],
259            repo,
260            workspace,
261            all_stash_entries: None,
262            selected_index: 0,
263            last_query: Default::default(),
264            modifiers: Default::default(),
265            focus_handle: cx.focus_handle(),
266            timezone,
267            show_footer: false,
268        }
269    }
270
271    fn format_message(ix: usize, message: &String) -> String {
272        format!("#{}: {}", ix, message)
273    }
274
275    fn format_timestamp(timestamp: i64, timezone: UtcOffset) -> String {
276        let timestamp =
277            OffsetDateTime::from_unix_timestamp(timestamp).unwrap_or(OffsetDateTime::now_utc());
278        time_format::format_localized_timestamp(
279            timestamp,
280            OffsetDateTime::now_utc(),
281            timezone,
282            time_format::TimestampFormat::Relative,
283        )
284    }
285
286    fn format_absolute_timestamp(timestamp: i64, timezone: UtcOffset) -> String {
287        let timestamp =
288            OffsetDateTime::from_unix_timestamp(timestamp).unwrap_or(OffsetDateTime::now_utc());
289        time_format::format_localized_timestamp(
290            timestamp,
291            OffsetDateTime::now_utc(),
292            timezone,
293            time_format::TimestampFormat::EnhancedAbsolute,
294        )
295    }
296
297    fn drop_stash_at(&self, ix: usize, window: &mut Window, cx: &mut Context<Picker<Self>>) {
298        let Some(entry_match) = self.matches.get(ix) else {
299            return;
300        };
301        let stash_index = entry_match.entry.index;
302        let Some(repo) = self.repo.clone() else {
303            return;
304        };
305
306        cx.spawn(async move |_, cx| {
307            repo.update(cx, |repo, cx| repo.stash_drop(Some(stash_index), cx))
308                .await??;
309            Ok(())
310        })
311        .detach_and_prompt_err("Failed to drop stash", window, cx, |e, _, _| {
312            Some(e.to_string())
313        });
314    }
315
316    fn show_stash_at(&self, ix: usize, window: &mut Window, cx: &mut Context<Picker<Self>>) {
317        let Some(entry_match) = self.matches.get(ix) else {
318            return;
319        };
320        let stash_sha = entry_match.entry.oid.to_string();
321        let stash_index = entry_match.entry.index;
322        let Some(repo) = self.repo.clone() else {
323            return;
324        };
325        CommitView::open(
326            stash_sha,
327            repo.downgrade(),
328            self.workspace.clone(),
329            Some(stash_index),
330            None,
331            window,
332            cx,
333        );
334    }
335
336    fn pop_stash(&self, stash_index: usize, window: &mut Window, cx: &mut Context<Picker<Self>>) {
337        let Some(repo) = self.repo.clone() else {
338            return;
339        };
340
341        cx.spawn(async move |_, cx| {
342            repo.update(cx, |repo, cx| repo.stash_pop(Some(stash_index), cx))
343                .await?;
344            Ok(())
345        })
346        .detach_and_prompt_err("Failed to pop stash", window, cx, |e, _, _| {
347            Some(e.to_string())
348        });
349        cx.emit(DismissEvent);
350    }
351
352    fn apply_stash(&self, stash_index: usize, window: &mut Window, cx: &mut Context<Picker<Self>>) {
353        let Some(repo) = self.repo.clone() else {
354            return;
355        };
356
357        cx.spawn(async move |_, cx| {
358            repo.update(cx, |repo, cx| repo.stash_apply(Some(stash_index), cx))
359                .await?;
360            Ok(())
361        })
362        .detach_and_prompt_err("Failed to apply stash", window, cx, |e, _, _| {
363            Some(e.to_string())
364        });
365        cx.emit(DismissEvent);
366    }
367}
368
369impl PickerDelegate for StashListDelegate {
370    type ListItem = ListItem;
371
372    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
373        "Select a stash…".into()
374    }
375
376    fn match_count(&self) -> usize {
377        self.matches.len()
378    }
379
380    fn selected_index(&self) -> usize {
381        self.selected_index
382    }
383
384    fn set_selected_index(
385        &mut self,
386        ix: usize,
387        _window: &mut Window,
388        _: &mut Context<Picker<Self>>,
389    ) {
390        self.selected_index = ix;
391    }
392
393    fn update_matches(
394        &mut self,
395        query: String,
396        window: &mut Window,
397        cx: &mut Context<Picker<Self>>,
398    ) -> Task<()> {
399        let Some(all_stash_entries) = self.all_stash_entries.clone() else {
400            return Task::ready(());
401        };
402
403        let timezone = self.timezone;
404
405        cx.spawn_in(window, async move |picker, cx| {
406            let matches: Vec<StashEntryMatch> = if query.is_empty() {
407                all_stash_entries
408                    .into_iter()
409                    .map(|entry| {
410                        let formatted_timestamp = Self::format_timestamp(entry.timestamp, timezone);
411                        let formatted_absolute_timestamp =
412                            Self::format_absolute_timestamp(entry.timestamp, timezone);
413
414                        StashEntryMatch {
415                            entry,
416                            positions: Vec::new(),
417                            formatted_timestamp,
418                            formatted_absolute_timestamp,
419                        }
420                    })
421                    .collect()
422            } else {
423                let candidates = all_stash_entries
424                    .iter()
425                    .enumerate()
426                    .map(|(ix, entry)| {
427                        StringMatchCandidate::new(
428                            ix,
429                            &Self::format_message(entry.index, &entry.message),
430                        )
431                    })
432                    .collect::<Vec<StringMatchCandidate>>();
433                fuzzy::match_strings(
434                    &candidates,
435                    &query,
436                    false,
437                    true,
438                    10000,
439                    &Default::default(),
440                    cx.background_executor().clone(),
441                )
442                .await
443                .into_iter()
444                .map(|candidate| {
445                    let entry = all_stash_entries[candidate.candidate_id].clone();
446                    let formatted_timestamp = Self::format_timestamp(entry.timestamp, timezone);
447                    let formatted_absolute_timestamp =
448                        Self::format_absolute_timestamp(entry.timestamp, timezone);
449
450                    StashEntryMatch {
451                        entry,
452                        positions: candidate.positions,
453                        formatted_timestamp,
454                        formatted_absolute_timestamp,
455                    }
456                })
457                .collect()
458            };
459
460            picker
461                .update(cx, |picker, _| {
462                    let delegate = &mut picker.delegate;
463                    delegate.matches = matches;
464                    if delegate.matches.is_empty() {
465                        delegate.selected_index = 0;
466                    } else {
467                        delegate.selected_index =
468                            core::cmp::min(delegate.selected_index, delegate.matches.len() - 1);
469                    }
470                    delegate.last_query = query;
471                })
472                .log_err();
473        })
474    }
475
476    fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
477        let Some(entry_match) = self.matches.get(self.selected_index()) else {
478            return;
479        };
480        let stash_index = entry_match.entry.index;
481        if secondary {
482            self.pop_stash(stash_index, window, cx);
483        } else {
484            self.apply_stash(stash_index, window, cx);
485        }
486    }
487
488    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
489        cx.emit(DismissEvent);
490    }
491
492    fn render_match(
493        &self,
494        ix: usize,
495        selected: bool,
496        _window: &mut Window,
497        cx: &mut Context<Picker<Self>>,
498    ) -> Option<Self::ListItem> {
499        let entry_match = &self.matches[ix];
500
501        let stash_message =
502            Self::format_message(entry_match.entry.index, &entry_match.entry.message);
503        let positions = entry_match.positions.clone();
504        let stash_label = HighlightedLabel::new(stash_message, positions)
505            .truncate()
506            .into_any_element();
507
508        let branch_name = entry_match.entry.branch.clone().unwrap_or_default();
509        let branch_info = h_flex()
510            .gap_1p5()
511            .w_full()
512            .child(
513                Label::new(branch_name)
514                    .truncate()
515                    .color(Color::Muted)
516                    .size(LabelSize::Small),
517            )
518            .child(
519                Label::new("")
520                    .alpha(0.5)
521                    .color(Color::Muted)
522                    .size(LabelSize::Small),
523            )
524            .child(
525                Label::new(entry_match.formatted_timestamp.clone())
526                    .color(Color::Muted)
527                    .size(LabelSize::Small),
528            );
529
530        let view_button = {
531            let focus_handle = self.focus_handle.clone();
532            IconButton::new(("view-stash", ix), IconName::Eye)
533                .icon_size(IconSize::Small)
534                .tooltip(move |_, cx| {
535                    Tooltip::for_action_in("View Stash", &ShowStashItem, &focus_handle, cx)
536                })
537                .on_click(cx.listener(move |this, _, window, cx| {
538                    this.delegate.show_stash_at(ix, window, cx);
539                }))
540        };
541
542        let pop_button = {
543            let focus_handle = self.focus_handle.clone();
544            IconButton::new(("pop-stash", ix), IconName::MaximizeAlt)
545                .icon_size(IconSize::Small)
546                .tooltip(move |_, cx| {
547                    Tooltip::for_action_in("Pop Stash", &menu::SecondaryConfirm, &focus_handle, cx)
548                })
549                .on_click(|_, window, cx| {
550                    window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx);
551                })
552        };
553
554        let drop_button = {
555            let focus_handle = self.focus_handle.clone();
556            IconButton::new(("drop-stash", ix), IconName::Trash)
557                .icon_size(IconSize::Small)
558                .tooltip(move |_, cx| {
559                    Tooltip::for_action_in("Drop Stash", &DropStashItem, &focus_handle, cx)
560                })
561                .on_click(cx.listener(move |this, _, window, cx| {
562                    this.delegate.drop_stash_at(ix, window, cx);
563                }))
564        };
565
566        Some(
567            ListItem::new(format!("stash-{ix}"))
568                .inset(true)
569                .spacing(ListItemSpacing::Sparse)
570                .toggle_state(selected)
571                .child(
572                    h_flex()
573                        .min_w_0()
574                        .w_full()
575                        .gap_2p5()
576                        .child(
577                            Icon::new(IconName::BoxOpen)
578                                .size(IconSize::Small)
579                                .color(Color::Muted),
580                        )
581                        .child(
582                            v_flex()
583                                .id(format!("stash-tooltip-{ix}"))
584                                .min_w_0()
585                                .w_full()
586                                .child(stash_label)
587                                .child(branch_info)
588                                .tooltip({
589                                    let stash_message = Self::format_message(
590                                        entry_match.entry.index,
591                                        &entry_match.entry.message,
592                                    );
593                                    let absolute_timestamp =
594                                        entry_match.formatted_absolute_timestamp.clone();
595
596                                    Tooltip::element(move |_, _| {
597                                        v_flex()
598                                            .child(Label::new(stash_message.clone()))
599                                            .child(
600                                                Label::new(absolute_timestamp.clone())
601                                                    .size(LabelSize::Small)
602                                                    .color(Color::Muted),
603                                            )
604                                            .into_any_element()
605                                    })
606                                }),
607                        ),
608                )
609                .end_slot(
610                    h_flex()
611                        .gap_0p5()
612                        .child(view_button)
613                        .child(pop_button)
614                        .child(drop_button),
615                )
616                .show_end_slot_on_hover(),
617        )
618    }
619
620    fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
621        Some("No stashes found".into())
622    }
623
624    fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
625        if !self.show_footer || self.matches.is_empty() {
626            return None;
627        }
628
629        let focus_handle = self.focus_handle.clone();
630
631        Some(
632            h_flex()
633                .w_full()
634                .p_1p5()
635                .gap_0p5()
636                .justify_end()
637                .flex_wrap()
638                .border_t_1()
639                .border_color(cx.theme().colors().border_variant)
640                .child(
641                    Button::new("drop-stash", "Drop")
642                        .key_binding(
643                            KeyBinding::for_action_in(
644                                &stash_picker::DropStashItem,
645                                &focus_handle,
646                                cx,
647                            )
648                            .map(|kb| kb.size(rems_from_px(12.))),
649                        )
650                        .on_click(|_, window, cx| {
651                            window.dispatch_action(stash_picker::DropStashItem.boxed_clone(), cx)
652                        }),
653                )
654                .child(
655                    Button::new("view-stash", "View")
656                        .key_binding(
657                            KeyBinding::for_action_in(
658                                &stash_picker::ShowStashItem,
659                                &focus_handle,
660                                cx,
661                            )
662                            .map(|kb| kb.size(rems_from_px(12.))),
663                        )
664                        .on_click(cx.listener(move |picker, _, window, cx| {
665                            cx.stop_propagation();
666                            let selected_ix = picker.delegate.selected_index();
667                            picker.delegate.show_stash_at(selected_ix, window, cx);
668                        })),
669                )
670                .child(
671                    Button::new("pop-stash", "Pop")
672                        .key_binding(
673                            KeyBinding::for_action_in(&menu::SecondaryConfirm, &focus_handle, cx)
674                                .map(|kb| kb.size(rems_from_px(12.))),
675                        )
676                        .on_click(|_, window, cx| {
677                            window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx)
678                        }),
679                )
680                .child(
681                    Button::new("apply-stash", "Apply")
682                        .key_binding(
683                            KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
684                                .map(|kb| kb.size(rems_from_px(12.))),
685                        )
686                        .on_click(|_, window, cx| {
687                            window.dispatch_action(menu::Confirm.boxed_clone(), cx)
688                        }),
689                )
690                .into_any(),
691        )
692    }
693}
694
695#[cfg(test)]
696mod tests {
697    use std::str::FromStr;
698
699    use super::*;
700    use git::{Oid, stash::StashEntry};
701    use gpui::{TestAppContext, VisualTestContext, rems};
702    use picker::PickerDelegate;
703    use project::{FakeFs, Project};
704    use settings::SettingsStore;
705    use workspace::MultiWorkspace;
706
707    fn init_test(cx: &mut TestAppContext) {
708        cx.update(|cx| {
709            let settings_store = SettingsStore::test(cx);
710            cx.set_global(settings_store);
711
712            theme_settings::init(theme::LoadThemes::JustBase, cx);
713            editor::init(cx);
714        })
715    }
716
717    /// Convenience function for creating `StashEntry` instances during tests.
718    /// Feel free to update in case you need to provide extra fields.
719    fn stash_entry(index: usize, message: &str, branch: Option<&str>) -> StashEntry {
720        let oid = Oid::from_str(&format!("{:0>40x}", index)).unwrap();
721
722        StashEntry {
723            index,
724            oid,
725            message: message.to_string(),
726            branch: branch.map(Into::into),
727            timestamp: 1000 - index as i64,
728        }
729    }
730
731    #[gpui::test]
732    async fn test_show_stash_dismisses(cx: &mut TestAppContext) {
733        init_test(cx);
734
735        let fs = FakeFs::new(cx.executor());
736        let project = Project::test(fs, [], cx).await;
737        let multi_workspace =
738            cx.add_window(|window, cx| MultiWorkspace::test_new(project, window, cx));
739        let cx = &mut VisualTestContext::from_window(*multi_workspace, cx);
740        let workspace = multi_workspace
741            .update(cx, |workspace, _, _| workspace.workspace().clone())
742            .unwrap();
743        let stash_entries = vec![
744            stash_entry(0, "stash #0", Some("main")),
745            stash_entry(1, "stash #1", Some("develop")),
746        ];
747
748        let stash_list = workspace.update_in(cx, |workspace, window, cx| {
749            let weak_workspace = workspace.weak_handle();
750
751            workspace.toggle_modal(window, cx, move |window, cx| {
752                StashList::new(None, weak_workspace, rems(34.), window, cx)
753            });
754
755            assert!(workspace.active_modal::<StashList>(cx).is_some());
756            workspace.active_modal::<StashList>(cx).unwrap()
757        });
758
759        cx.run_until_parked();
760        stash_list.update(cx, |stash_list, cx| {
761            stash_list.picker.update(cx, |picker, _| {
762                picker.delegate.all_stash_entries = Some(stash_entries);
763            });
764        });
765
766        stash_list
767            .update_in(cx, |stash_list, window, cx| {
768                stash_list.picker.update(cx, |picker, cx| {
769                    picker.delegate.update_matches(String::new(), window, cx)
770                })
771            })
772            .await;
773
774        cx.run_until_parked();
775        stash_list.update_in(cx, |stash_list, window, cx| {
776            assert_eq!(stash_list.picker.read(cx).delegate.matches.len(), 2);
777            stash_list.handle_show_stash(&Default::default(), window, cx);
778        });
779
780        workspace.update(cx, |workspace, cx| {
781            assert!(workspace.active_modal::<StashList>(cx).is_none());
782        });
783    }
784}