Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:33:58.327Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

stash_picker.rs

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