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