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, View, ViewContext,
  7    VisualContext, WeakView,
  8};
  9use picker::{Picker, PickerDelegate};
 10use project::Inventory;
 11use task::{oneshot_source::OneshotSource, Task};
 12use ui::{v_flex, HighlightedLabel, ListItem, ListItemSpacing, Selectable};
 13use util::ResultExt;
 14use workspace::{ModalView, Workspace};
 15
 16use crate::schedule_task;
 17
 18actions!(task, [Spawn, Rerun]);
 19
 20/// A modal used to spawn new tasks.
 21pub(crate) struct TasksModalDelegate {
 22    inventory: Model<Inventory>,
 23    candidates: Vec<Arc<dyn Task>>,
 24    matches: Vec<StringMatch>,
 25    selected_index: usize,
 26    placeholder_text: Arc<str>,
 27    workspace: WeakView<Workspace>,
 28    last_prompt: String,
 29}
 30
 31impl TasksModalDelegate {
 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 task..."),
 40            last_prompt: String::default(),
 41        }
 42    }
 43
 44    fn spawn_oneshot(&mut self, cx: &mut AppContext) -> Option<Arc<dyn Task>> {
 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 TasksModal {
 58    picker: View<Picker<TasksModalDelegate>>,
 59    _subscription: Subscription,
 60}
 61
 62impl TasksModal {
 63    pub(crate) fn new(
 64        inventory: Model<Inventory>,
 65        workspace: WeakView<Workspace>,
 66        cx: &mut ViewContext<Self>,
 67    ) -> Self {
 68        let picker = cx
 69            .new_view(|cx| Picker::uniform_list(TasksModalDelegate::new(inventory, workspace), cx));
 70        let _subscription = cx.subscribe(&picker, |_, _, _, cx| {
 71            cx.emit(DismissEvent);
 72        });
 73        Self {
 74            picker,
 75            _subscription,
 76        }
 77    }
 78}
 79
 80impl Render for TasksModal {
 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 TasksModal {}
 94
 95impl FocusableView for TasksModal {
 96    fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
 97        self.picker.read(cx).focus_handle(cx)
 98    }
 99}
100
101impl ModalView for TasksModal {}
102
103impl PickerDelegate for TasksModalDelegate {
104    type ListItem = ListItem;
105
106    fn match_count(&self) -> usize {
107        self.matches.len()
108    }
109
110    fn selected_index(&self) -> usize {
111        self.selected_index
112    }
113
114    fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<picker::Picker<Self>>) {
115        self.selected_index = ix;
116    }
117
118    fn placeholder_text(&self) -> Arc<str> {
119        self.placeholder_text.clone()
120    }
121
122    fn update_matches(
123        &mut self,
124        query: String,
125        cx: &mut ViewContext<picker::Picker<Self>>,
126    ) -> gpui::Task<()> {
127        cx.spawn(move |picker, mut cx| async move {
128            let Some(candidates) = picker
129                .update(&mut cx, |picker, cx| {
130                    picker.delegate.candidates = picker
131                        .delegate
132                        .inventory
133                        .update(cx, |inventory, cx| inventory.list_tasks(None, cx));
134                    picker
135                        .delegate
136                        .candidates
137                        .sort_by(|a, b| a.name().cmp(&b.name()));
138
139                    picker
140                        .delegate
141                        .candidates
142                        .iter()
143                        .enumerate()
144                        .map(|(index, candidate)| StringMatchCandidate {
145                            id: index,
146                            char_bag: candidate.name().chars().collect(),
147                            string: candidate.name().into(),
148                        })
149                        .collect::<Vec<_>>()
150                })
151                .ok()
152            else {
153                return;
154            };
155            let matches = fuzzy::match_strings(
156                &candidates,
157                &query,
158                true,
159                1000,
160                &Default::default(),
161                cx.background_executor().clone(),
162            )
163            .await;
164            picker
165                .update(&mut cx, |picker, _| {
166                    let delegate = &mut picker.delegate;
167                    delegate.matches = matches;
168                    delegate.last_prompt = query;
169
170                    if delegate.matches.is_empty() {
171                        delegate.selected_index = 0;
172                    } else {
173                        delegate.selected_index =
174                            delegate.selected_index.min(delegate.matches.len() - 1);
175                    }
176                })
177                .log_err();
178        })
179    }
180
181    fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<picker::Picker<Self>>) {
182        let current_match_index = self.selected_index();
183        let Some(task) = secondary
184            .then(|| self.spawn_oneshot(cx))
185            .flatten()
186            .or_else(|| {
187                self.matches.get(current_match_index).map(|current_match| {
188                    let ix = current_match.candidate_id;
189                    self.candidates[ix].clone()
190                })
191            })
192        else {
193            return;
194        };
195
196        self.workspace
197            .update(cx, |workspace, cx| {
198                schedule_task(workspace, task.as_ref(), cx);
199            })
200            .ok();
201        cx.emit(DismissEvent);
202    }
203
204    fn dismissed(&mut self, cx: &mut ViewContext<picker::Picker<Self>>) {
205        cx.emit(DismissEvent);
206    }
207
208    fn render_match(
209        &self,
210        ix: usize,
211        selected: bool,
212        _cx: &mut ViewContext<picker::Picker<Self>>,
213    ) -> Option<Self::ListItem> {
214        let hit = &self.matches[ix];
215        let highlights: Vec<_> = hit.positions.iter().copied().collect();
216        Some(
217            ListItem::new(SharedString::from(format!("tasks-modal-{ix}")))
218                .inset(true)
219                .spacing(ListItemSpacing::Sparse)
220                .selected(selected)
221                .start_slot(HighlightedLabel::new(hit.string.clone(), highlights)),
222        )
223    }
224}