modal.rs

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