modal.rs

  1use std::sync::Arc;
  2
  3use fuzzy::{StringMatch, StringMatchCandidate};
  4use gpui::{
  5    actions, rems, DismissEvent, EventEmitter, FocusableView, InteractiveElement, Model,
  6    ParentElement, Render, SharedString, Styled, Subscription, Task, View, ViewContext,
  7    VisualContext, WeakView,
  8};
  9use picker::{Picker, PickerDelegate};
 10use project::Inventory;
 11use runnable::Runnable;
 12use ui::{v_flex, HighlightedLabel, ListItem, ListItemSpacing, Selectable};
 13use util::ResultExt;
 14use workspace::{ModalView, Workspace};
 15
 16use crate::schedule_runnable;
 17
 18actions!(runnables, [Spawn, Rerun]);
 19
 20/// A modal used to spawn new runnables.
 21pub(crate) struct RunnablesModalDelegate {
 22    inventory: Model<Inventory>,
 23    candidates: Vec<Arc<dyn Runnable>>,
 24    matches: Vec<StringMatch>,
 25    selected_index: usize,
 26    placeholder_text: Arc<str>,
 27    workspace: WeakView<Workspace>,
 28}
 29
 30impl RunnablesModalDelegate {
 31    fn new(inventory: Model<Inventory>, workspace: WeakView<Workspace>) -> Self {
 32        Self {
 33            inventory,
 34            workspace,
 35            candidates: Vec::new(),
 36            matches: Vec::new(),
 37            selected_index: 0,
 38            placeholder_text: Arc::from("Select runnable..."),
 39        }
 40    }
 41}
 42
 43pub(crate) struct RunnablesModal {
 44    picker: View<Picker<RunnablesModalDelegate>>,
 45    _subscription: Subscription,
 46}
 47
 48impl RunnablesModal {
 49    pub(crate) fn new(
 50        inventory: Model<Inventory>,
 51        workspace: WeakView<Workspace>,
 52        cx: &mut ViewContext<Self>,
 53    ) -> Self {
 54        let picker = cx.new_view(|cx| {
 55            Picker::uniform_list(RunnablesModalDelegate::new(inventory, workspace), cx)
 56        });
 57        let _subscription = cx.subscribe(&picker, |_, _, _, cx| {
 58            cx.emit(DismissEvent);
 59        });
 60        Self {
 61            picker,
 62            _subscription,
 63        }
 64    }
 65}
 66impl Render for RunnablesModal {
 67    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl gpui::prelude::IntoElement {
 68        v_flex()
 69            .w(rems(34.))
 70            .child(self.picker.clone())
 71            .on_mouse_down_out(cx.listener(|modal, _, cx| {
 72                modal.picker.update(cx, |picker, cx| {
 73                    picker.cancel(&Default::default(), cx);
 74                })
 75            }))
 76    }
 77}
 78
 79impl EventEmitter<DismissEvent> for RunnablesModal {}
 80impl FocusableView for RunnablesModal {
 81    fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
 82        self.picker.read(cx).focus_handle(cx)
 83    }
 84}
 85impl ModalView for RunnablesModal {}
 86
 87impl PickerDelegate for RunnablesModalDelegate {
 88    type ListItem = ListItem;
 89
 90    fn match_count(&self) -> usize {
 91        self.matches.len()
 92    }
 93
 94    fn selected_index(&self) -> usize {
 95        self.selected_index
 96    }
 97
 98    fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<picker::Picker<Self>>) {
 99        self.selected_index = ix;
100    }
101
102    fn placeholder_text(&self) -> Arc<str> {
103        self.placeholder_text.clone()
104    }
105
106    fn update_matches(
107        &mut self,
108        query: String,
109        cx: &mut ViewContext<picker::Picker<Self>>,
110    ) -> Task<()> {
111        cx.spawn(move |picker, mut cx| async move {
112            let Some(candidates) = picker
113                .update(&mut cx, |picker, cx| {
114                    picker.delegate.candidates = picker
115                        .delegate
116                        .inventory
117                        .update(cx, |inventory, cx| inventory.list_runnables(None, cx));
118                    picker
119                        .delegate
120                        .candidates
121                        .sort_by(|a, b| a.name().cmp(&b.name()));
122
123                    picker
124                        .delegate
125                        .candidates
126                        .iter()
127                        .enumerate()
128                        .map(|(index, candidate)| StringMatchCandidate {
129                            id: index,
130                            char_bag: candidate.name().chars().collect(),
131                            string: candidate.name().into(),
132                        })
133                        .collect::<Vec<_>>()
134                })
135                .ok()
136            else {
137                return;
138            };
139            let matches = fuzzy::match_strings(
140                &candidates,
141                &query,
142                true,
143                1000,
144                &Default::default(),
145                cx.background_executor().clone(),
146            )
147            .await;
148            picker
149                .update(&mut cx, |picker, _| {
150                    let delegate = &mut picker.delegate;
151                    delegate.matches = matches;
152
153                    if delegate.matches.is_empty() {
154                        delegate.selected_index = 0;
155                    } else {
156                        delegate.selected_index =
157                            delegate.selected_index.min(delegate.matches.len() - 1);
158                    }
159                })
160                .log_err();
161        })
162    }
163
164    fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<picker::Picker<Self>>) {
165        let current_match_index = self.selected_index();
166        let Some(current_match) = self.matches.get(current_match_index) else {
167            return;
168        };
169
170        let ix = current_match.candidate_id;
171        let runnable = &self.candidates[ix];
172        self.workspace
173            .update(cx, |workspace, cx| {
174                schedule_runnable(workspace, runnable.as_ref(), cx);
175            })
176            .ok();
177        cx.emit(DismissEvent);
178    }
179
180    fn dismissed(&mut self, cx: &mut ViewContext<picker::Picker<Self>>) {
181        cx.emit(DismissEvent);
182    }
183
184    fn render_match(
185        &self,
186        ix: usize,
187        selected: bool,
188        _cx: &mut ViewContext<picker::Picker<Self>>,
189    ) -> Option<Self::ListItem> {
190        let hit = &self.matches[ix];
191        //let runnable = self.candidates[target_index].metadata();
192        let highlights: Vec<_> = hit.positions.iter().copied().collect();
193        Some(
194            ListItem::new(SharedString::from(format!("runnables-modal-{ix}")))
195                .inset(true)
196                .spacing(ListItemSpacing::Sparse)
197                .selected(selected)
198                .start_slot(HighlightedLabel::new(hit.string.clone(), highlights)),
199        )
200    }
201}