Skip to repository content790 lines · 27.1 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:34:33.559Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
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, 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