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