1//! Project-wide storage of the tasks available, capable of updating itself from the sources set.
2
3use std::{any::TypeId, path::Path, sync::Arc};
4
5use collections::{HashMap, VecDeque};
6use gpui::{AppContext, Context, Model, ModelContext, Subscription};
7use itertools::Itertools;
8use task::{Task, TaskId, TaskSource};
9use util::{post_inc, NumericPrefixWithSuffix};
10
11/// Inventory tracks available tasks for a given project.
12pub struct Inventory {
13 sources: Vec<SourceInInventory>,
14 last_scheduled_tasks: VecDeque<TaskId>,
15}
16
17struct SourceInInventory {
18 source: Model<Box<dyn TaskSource>>,
19 _subscription: Subscription,
20 type_id: TypeId,
21}
22
23impl Inventory {
24 pub(crate) fn new(cx: &mut AppContext) -> Model<Self> {
25 cx.new_model(|_| Self {
26 sources: Vec::new(),
27 last_scheduled_tasks: VecDeque::new(),
28 })
29 }
30
31 /// Registers a new tasks source, that would be fetched for available tasks.
32 pub fn add_source(&mut self, source: Model<Box<dyn TaskSource>>, cx: &mut ModelContext<Self>) {
33 let _subscription = cx.observe(&source, |_, _, cx| {
34 cx.notify();
35 });
36 let type_id = source.read(cx).type_id();
37 let source = SourceInInventory {
38 source,
39 _subscription,
40 type_id,
41 };
42 self.sources.push(source);
43 cx.notify();
44 }
45
46 pub fn source<T: TaskSource>(&self) -> Option<Model<Box<dyn TaskSource>>> {
47 let target_type_id = std::any::TypeId::of::<T>();
48 self.sources.iter().find_map(
49 |SourceInInventory {
50 type_id, source, ..
51 }| {
52 if &target_type_id == type_id {
53 Some(source.clone())
54 } else {
55 None
56 }
57 },
58 )
59 }
60
61 /// Pulls its sources to list runanbles for the path given (up to the source to decide what to return for no path).
62 pub fn list_tasks(
63 &self,
64 path: Option<&Path>,
65 lru: bool,
66 cx: &mut AppContext,
67 ) -> Vec<Arc<dyn Task>> {
68 let mut lru_score = 0_u32;
69 let tasks_by_usage = if lru {
70 self.last_scheduled_tasks
71 .iter()
72 .rev()
73 .fold(HashMap::default(), |mut tasks, id| {
74 tasks.entry(id).or_insert_with(|| post_inc(&mut lru_score));
75 tasks
76 })
77 } else {
78 HashMap::default()
79 };
80 let not_used_score = post_inc(&mut lru_score);
81
82 self.sources
83 .iter()
84 .flat_map(|source| {
85 source
86 .source
87 .update(cx, |source, cx| source.tasks_for_path(path, cx))
88 })
89 .map(|task| {
90 let usages = if lru {
91 tasks_by_usage
92 .get(&task.id())
93 .copied()
94 .unwrap_or(not_used_score)
95 } else {
96 not_used_score
97 };
98 (task, usages)
99 })
100 .sorted_unstable_by(|(task_a, usages_a), (task_b, usages_b)| {
101 usages_a.cmp(usages_b).then({
102 NumericPrefixWithSuffix::from_numeric_prefixed_str(task_a.name())
103 .cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str(
104 task_b.name(),
105 ))
106 .then(task_a.name().cmp(task_b.name()))
107 })
108 })
109 .map(|(task, _)| task)
110 .collect()
111 }
112
113 /// Returns the last scheduled task, if any of the sources contains one with the matching id.
114 pub fn last_scheduled_task(&self, cx: &mut AppContext) -> Option<Arc<dyn Task>> {
115 self.last_scheduled_tasks.back().and_then(|id| {
116 // TODO straighten the `Path` story to understand what has to be passed here: or it will break in the future.
117 self.list_tasks(None, false, cx)
118 .into_iter()
119 .find(|task| task.id() == id)
120 })
121 }
122
123 /// Registers task "usage" as being scheduled – to be used for LRU sorting when listing all tasks.
124 pub fn task_scheduled(&mut self, id: TaskId) {
125 self.last_scheduled_tasks.push_back(id);
126 if self.last_scheduled_tasks.len() > 5_000 {
127 self.last_scheduled_tasks.pop_front();
128 }
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use std::path::PathBuf;
135
136 use gpui::TestAppContext;
137
138 use super::*;
139
140 #[gpui::test]
141 fn test_task_list_sorting(cx: &mut TestAppContext) {
142 let inventory = cx.update(Inventory::new);
143 let initial_tasks = list_task_names(&inventory, None, true, cx);
144 assert!(
145 initial_tasks.is_empty(),
146 "No tasks expected for empty inventory, but got {initial_tasks:?}"
147 );
148 let initial_tasks = list_task_names(&inventory, None, false, cx);
149 assert!(
150 initial_tasks.is_empty(),
151 "No tasks expected for empty inventory, but got {initial_tasks:?}"
152 );
153
154 inventory.update(cx, |inventory, cx| {
155 inventory.add_source(TestSource::new(vec!["3_task".to_string()], cx), cx);
156 });
157 inventory.update(cx, |inventory, cx| {
158 inventory.add_source(
159 TestSource::new(
160 vec![
161 "1_task".to_string(),
162 "2_task".to_string(),
163 "1_a_task".to_string(),
164 ],
165 cx,
166 ),
167 cx,
168 );
169 });
170
171 let expected_initial_state = [
172 "1_a_task".to_string(),
173 "1_task".to_string(),
174 "2_task".to_string(),
175 "3_task".to_string(),
176 ];
177 assert_eq!(
178 list_task_names(&inventory, None, false, cx),
179 &expected_initial_state,
180 "Task list without lru sorting, should be sorted alphanumerically"
181 );
182 assert_eq!(
183 list_task_names(&inventory, None, true, cx),
184 &expected_initial_state,
185 "Tasks with equal amount of usages should be sorted alphanumerically"
186 );
187
188 register_task_used(&inventory, "2_task", cx);
189 assert_eq!(
190 list_task_names(&inventory, None, false, cx),
191 &expected_initial_state,
192 "Task list without lru sorting, should be sorted alphanumerically"
193 );
194 assert_eq!(
195 list_task_names(&inventory, None, true, cx),
196 vec![
197 "2_task".to_string(),
198 "1_a_task".to_string(),
199 "1_task".to_string(),
200 "3_task".to_string()
201 ],
202 );
203
204 register_task_used(&inventory, "1_task", cx);
205 register_task_used(&inventory, "1_task", cx);
206 register_task_used(&inventory, "1_task", cx);
207 register_task_used(&inventory, "3_task", cx);
208 assert_eq!(
209 list_task_names(&inventory, None, false, cx),
210 &expected_initial_state,
211 "Task list without lru sorting, should be sorted alphanumerically"
212 );
213 assert_eq!(
214 list_task_names(&inventory, None, true, cx),
215 vec![
216 "3_task".to_string(),
217 "1_task".to_string(),
218 "2_task".to_string(),
219 "1_a_task".to_string(),
220 ],
221 );
222
223 inventory.update(cx, |inventory, cx| {
224 inventory.add_source(
225 TestSource::new(vec!["10_hello".to_string(), "11_hello".to_string()], cx),
226 cx,
227 );
228 });
229 let expected_updated_state = [
230 "1_a_task".to_string(),
231 "1_task".to_string(),
232 "2_task".to_string(),
233 "3_task".to_string(),
234 "10_hello".to_string(),
235 "11_hello".to_string(),
236 ];
237 assert_eq!(
238 list_task_names(&inventory, None, false, cx),
239 &expected_updated_state,
240 "Task list without lru sorting, should be sorted alphanumerically"
241 );
242 assert_eq!(
243 list_task_names(&inventory, None, true, cx),
244 vec![
245 "3_task".to_string(),
246 "1_task".to_string(),
247 "2_task".to_string(),
248 "1_a_task".to_string(),
249 "10_hello".to_string(),
250 "11_hello".to_string(),
251 ],
252 );
253
254 register_task_used(&inventory, "11_hello", cx);
255 assert_eq!(
256 list_task_names(&inventory, None, false, cx),
257 &expected_updated_state,
258 "Task list without lru sorting, should be sorted alphanumerically"
259 );
260 assert_eq!(
261 list_task_names(&inventory, None, true, cx),
262 vec![
263 "11_hello".to_string(),
264 "3_task".to_string(),
265 "1_task".to_string(),
266 "2_task".to_string(),
267 "1_a_task".to_string(),
268 "10_hello".to_string(),
269 ],
270 );
271 }
272
273 #[derive(Debug, Clone, PartialEq, Eq)]
274 struct TestTask {
275 id: TaskId,
276 name: String,
277 }
278
279 impl Task for TestTask {
280 fn id(&self) -> &TaskId {
281 &self.id
282 }
283
284 fn name(&self) -> &str {
285 &self.name
286 }
287
288 fn cwd(&self) -> Option<&Path> {
289 None
290 }
291
292 fn exec(&self, _cwd: Option<PathBuf>) -> Option<task::SpawnInTerminal> {
293 None
294 }
295 }
296
297 struct TestSource {
298 tasks: Vec<TestTask>,
299 }
300
301 impl TestSource {
302 fn new(
303 task_names: impl IntoIterator<Item = String>,
304 cx: &mut AppContext,
305 ) -> Model<Box<dyn TaskSource>> {
306 cx.new_model(|_| {
307 Box::new(Self {
308 tasks: task_names
309 .into_iter()
310 .enumerate()
311 .map(|(i, name)| TestTask {
312 id: TaskId(format!("task_{i}_{name}")),
313 name,
314 })
315 .collect(),
316 }) as Box<dyn TaskSource>
317 })
318 }
319 }
320
321 impl TaskSource for TestSource {
322 fn tasks_for_path(
323 &mut self,
324 _path: Option<&Path>,
325 _cx: &mut ModelContext<Box<dyn TaskSource>>,
326 ) -> Vec<Arc<dyn Task>> {
327 self.tasks
328 .clone()
329 .into_iter()
330 .map(|task| Arc::new(task) as Arc<dyn Task>)
331 .collect()
332 }
333
334 fn as_any(&mut self) -> &mut dyn std::any::Any {
335 self
336 }
337 }
338
339 fn list_task_names(
340 inventory: &Model<Inventory>,
341 path: Option<&Path>,
342 lru: bool,
343 cx: &mut TestAppContext,
344 ) -> Vec<String> {
345 inventory.update(cx, |inventory, cx| {
346 inventory
347 .list_tasks(path, lru, cx)
348 .into_iter()
349 .map(|task| task.name().to_string())
350 .collect()
351 })
352 }
353
354 fn register_task_used(inventory: &Model<Inventory>, task_name: &str, cx: &mut TestAppContext) {
355 inventory.update(cx, |inventory, cx| {
356 let task = inventory
357 .list_tasks(None, false, cx)
358 .into_iter()
359 .find(|task| task.name() == task_name)
360 .unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
361 inventory.task_scheduled(task.id().clone());
362 });
363 }
364}