use std::sync::Arc; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ actions, rems, AppContext, DismissEvent, EventEmitter, FocusableView, InteractiveElement, Model, ParentElement, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; use project::Inventory; use task::{oneshot_source::OneshotSource, Task}; use ui::{v_flex, HighlightedLabel, ListItem, ListItemSpacing, Selectable, WindowContext}; use util::ResultExt; use workspace::{ModalView, Workspace}; use crate::schedule_task; actions!(task, [Spawn, Rerun]); /// A modal used to spawn new tasks. pub(crate) struct TasksModalDelegate { inventory: Model, candidates: Vec>, matches: Vec, selected_index: usize, workspace: WeakView, prompt: String, } impl TasksModalDelegate { fn new(inventory: Model, workspace: WeakView) -> Self { Self { inventory, workspace, candidates: Vec::new(), matches: Vec::new(), selected_index: 0, prompt: String::default(), } } fn spawn_oneshot(&mut self, cx: &mut AppContext) -> Option> { self.inventory .update(cx, |inventory, _| inventory.source::())? .update(cx, |oneshot_source, _| { Some( oneshot_source .as_any() .downcast_mut::()? .spawn(self.prompt.clone()), ) }) } } pub(crate) struct TasksModal { picker: View>, _subscription: Subscription, } impl TasksModal { pub(crate) fn new( inventory: Model, workspace: WeakView, cx: &mut ViewContext, ) -> Self { let picker = cx .new_view(|cx| Picker::uniform_list(TasksModalDelegate::new(inventory, workspace), cx)); let _subscription = cx.subscribe(&picker, |_, _, _, cx| { cx.emit(DismissEvent); }); Self { picker, _subscription, } } } impl Render for TasksModal { fn render(&mut self, cx: &mut ViewContext) -> impl gpui::prelude::IntoElement { v_flex() .w(rems(34.)) .child(self.picker.clone()) .on_mouse_down_out(cx.listener(|modal, _, cx| { modal.picker.update(cx, |picker, cx| { picker.cancel(&Default::default(), cx); }) })) } } impl EventEmitter for TasksModal {} impl FocusableView for TasksModal { fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle { self.picker.read(cx).focus_handle(cx) } } impl ModalView for TasksModal {} impl PickerDelegate for TasksModalDelegate { type ListItem = ListItem; fn match_count(&self) -> usize { self.matches.len() } fn selected_index(&self) -> usize { self.selected_index } fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext>) { self.selected_index = ix; } fn placeholder_text(&self, cx: &mut WindowContext) -> Arc { Arc::from(format!( "{} runs the selected task, {} spawns a bash-like task from the prompt", cx.keystroke_text_for(&menu::Confirm), cx.keystroke_text_for(&menu::SecondaryConfirm), )) } fn update_matches( &mut self, query: String, cx: &mut ViewContext>, ) -> gpui::Task<()> { cx.spawn(move |picker, mut cx| async move { let Some(candidates) = picker .update(&mut cx, |picker, cx| { picker.delegate.candidates = picker .delegate .inventory .update(cx, |inventory, cx| inventory.list_tasks(None, true, cx)); picker .delegate .candidates .iter() .enumerate() .map(|(index, candidate)| StringMatchCandidate { id: index, char_bag: candidate.name().chars().collect(), string: candidate.name().into(), }) .collect::>() }) .ok() else { return; }; let matches = fuzzy::match_strings( &candidates, &query, true, 1000, &Default::default(), cx.background_executor().clone(), ) .await; picker .update(&mut cx, |picker, _| { let delegate = &mut picker.delegate; delegate.matches = matches; delegate.prompt = query; if delegate.matches.is_empty() { delegate.selected_index = 0; } else { delegate.selected_index = delegate.selected_index.min(delegate.matches.len() - 1); } }) .log_err(); }) } fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>) { let current_match_index = self.selected_index(); let task = if secondary { if !self.prompt.trim().is_empty() { self.spawn_oneshot(cx) } else { None } } else { self.matches.get(current_match_index).map(|current_match| { let ix = current_match.candidate_id; self.candidates[ix].clone() }) }; let Some(task) = task else { return; }; self.workspace .update(cx, |workspace, cx| { schedule_task(workspace, task.as_ref(), cx); }) .ok(); cx.emit(DismissEvent); } fn dismissed(&mut self, cx: &mut ViewContext>) { cx.emit(DismissEvent); } fn render_match( &self, ix: usize, selected: bool, _cx: &mut ViewContext>, ) -> Option { let hit = &self.matches[ix]; let highlights: Vec<_> = hit.positions.iter().copied().collect(); Some( ListItem::new(SharedString::from(format!("tasks-modal-{ix}"))) .inset(true) .spacing(ListItemSpacing::Sparse) .selected(selected) .start_slot(HighlightedLabel::new(hit.string.clone(), highlights)), ) } }