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}