1//! Project-wide storage of the tasks available, capable of updating itself from the sources set.
2
3use std::{
4 any::TypeId,
5 path::{Path, PathBuf},
6 sync::Arc,
7};
8
9use collections::{HashMap, VecDeque};
10use gpui::{AppContext, Context, Model, ModelContext, Subscription};
11use itertools::Itertools;
12use project_core::worktree::WorktreeId;
13use task::{Task, TaskContext, TaskId, TaskSource};
14use util::{post_inc, NumericPrefixWithSuffix};
15
16/// Inventory tracks available tasks for a given project.
17pub struct Inventory {
18 sources: Vec<SourceInInventory>,
19 last_scheduled_tasks: VecDeque<(TaskId, TaskContext)>,
20}
21
22struct SourceInInventory {
23 source: Model<Box<dyn TaskSource>>,
24 _subscription: Subscription,
25 type_id: TypeId,
26 kind: TaskSourceKind,
27}
28
29/// Kind of a source the tasks are fetched from, used to display more source information in the UI.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum TaskSourceKind {
32 /// bash-like commands spawned by users, not associated with any path
33 UserInput,
34 /// ~/.config/zed/task.json - like global files with task definitions, applicable to any path
35 AbsPath(PathBuf),
36 /// Worktree-specific task definitions, e.g. dynamic tasks from open worktree file, or tasks from the worktree's .zed/task.json
37 Worktree { id: WorktreeId, abs_path: PathBuf },
38}
39
40impl TaskSourceKind {
41 fn abs_path(&self) -> Option<&Path> {
42 match self {
43 Self::AbsPath(abs_path) | Self::Worktree { abs_path, .. } => Some(abs_path),
44 Self::UserInput => None,
45 }
46 }
47
48 fn worktree(&self) -> Option<WorktreeId> {
49 match self {
50 Self::Worktree { id, .. } => Some(*id),
51 _ => None,
52 }
53 }
54}
55
56impl Inventory {
57 pub fn new(cx: &mut AppContext) -> Model<Self> {
58 cx.new_model(|_| Self {
59 sources: Vec::new(),
60 last_scheduled_tasks: VecDeque::new(),
61 })
62 }
63
64 /// If the task with the same path was not added yet,
65 /// registers a new tasks source to fetch for available tasks later.
66 /// Unless a source is removed, ignores future additions for the same path.
67 pub fn add_source(
68 &mut self,
69 kind: TaskSourceKind,
70 create_source: impl FnOnce(&mut ModelContext<Self>) -> Model<Box<dyn TaskSource>>,
71 cx: &mut ModelContext<Self>,
72 ) {
73 let abs_path = kind.abs_path();
74 if abs_path.is_some() {
75 if let Some(a) = self.sources.iter().find(|s| s.kind.abs_path() == abs_path) {
76 log::debug!("Source for path {abs_path:?} already exists, not adding. Old kind: {OLD_KIND:?}, new kind: {kind:?}", OLD_KIND = a.kind);
77 return;
78 }
79 }
80
81 let source = create_source(cx);
82 let type_id = source.read(cx).type_id();
83 let source = SourceInInventory {
84 _subscription: cx.observe(&source, |_, _, cx| {
85 cx.notify();
86 }),
87 source,
88 type_id,
89 kind,
90 };
91 self.sources.push(source);
92 cx.notify();
93 }
94
95 /// If present, removes the local static source entry that has the given path,
96 /// making corresponding task definitions unavailable in the fetch results.
97 ///
98 /// Now, entry for this path can be re-added again.
99 pub fn remove_local_static_source(&mut self, abs_path: &Path) {
100 self.sources.retain(|s| s.kind.abs_path() != Some(abs_path));
101 }
102
103 /// If present, removes the worktree source entry that has the given worktree id,
104 /// making corresponding task definitions unavailable in the fetch results.
105 ///
106 /// Now, entry for this path can be re-added again.
107 pub fn remove_worktree_sources(&mut self, worktree: WorktreeId) {
108 self.sources.retain(|s| s.kind.worktree() != Some(worktree));
109 }
110
111 pub fn source<T: TaskSource>(&self) -> Option<Model<Box<dyn TaskSource>>> {
112 let target_type_id = std::any::TypeId::of::<T>();
113 self.sources.iter().find_map(
114 |SourceInInventory {
115 type_id, source, ..
116 }| {
117 if &target_type_id == type_id {
118 Some(source.clone())
119 } else {
120 None
121 }
122 },
123 )
124 }
125
126 /// Pulls its sources to list runanbles for the path given (up to the source to decide what to return for no path).
127 pub fn list_tasks(
128 &self,
129 path: Option<&Path>,
130 worktree: Option<WorktreeId>,
131 lru: bool,
132 cx: &mut AppContext,
133 ) -> Vec<(TaskSourceKind, Arc<dyn Task>)> {
134 let mut lru_score = 0_u32;
135 let tasks_by_usage = if lru {
136 self.last_scheduled_tasks.iter().rev().fold(
137 HashMap::default(),
138 |mut tasks, (id, context)| {
139 tasks
140 .entry(id)
141 .or_insert_with(|| (post_inc(&mut lru_score), Some(context)));
142 tasks
143 },
144 )
145 } else {
146 HashMap::default()
147 };
148 let not_used_task_context = None;
149 let not_used_score = (post_inc(&mut lru_score), not_used_task_context);
150 self.sources
151 .iter()
152 .filter(|source| {
153 let source_worktree = source.kind.worktree();
154 worktree.is_none() || source_worktree.is_none() || source_worktree == worktree
155 })
156 .flat_map(|source| {
157 source
158 .source
159 .update(cx, |source, cx| source.tasks_for_path(path, cx))
160 .into_iter()
161 .map(|task| (&source.kind, task))
162 })
163 .map(|task| {
164 let usages = if lru {
165 tasks_by_usage
166 .get(&task.1.id())
167 .copied()
168 .unwrap_or(not_used_score)
169 } else {
170 not_used_score
171 };
172 (task, usages)
173 })
174 .sorted_unstable_by(
175 |((kind_a, task_a), usages_a), ((kind_b, task_b), usages_b)| {
176 usages_a
177 .0
178 .cmp(&usages_b.0)
179 .then(
180 kind_a
181 .worktree()
182 .is_none()
183 .cmp(&kind_b.worktree().is_none()),
184 )
185 .then(kind_a.worktree().cmp(&kind_b.worktree()))
186 .then(
187 kind_a
188 .abs_path()
189 .is_none()
190 .cmp(&kind_b.abs_path().is_none()),
191 )
192 .then(kind_a.abs_path().cmp(&kind_b.abs_path()))
193 .then({
194 NumericPrefixWithSuffix::from_numeric_prefixed_str(task_a.name())
195 .cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str(
196 task_b.name(),
197 ))
198 .then(task_a.name().cmp(task_b.name()))
199 })
200 },
201 )
202 .map(|((kind, task), _)| (kind.clone(), task))
203 .collect()
204 }
205
206 /// Returns the last scheduled task, if any of the sources contains one with the matching id.
207 pub fn last_scheduled_task(&self, cx: &mut AppContext) -> Option<(Arc<dyn Task>, TaskContext)> {
208 self.last_scheduled_tasks
209 .back()
210 .and_then(|(id, task_context)| {
211 // TODO straighten the `Path` story to understand what has to be passed here: or it will break in the future.
212 self.list_tasks(None, None, false, cx)
213 .into_iter()
214 .find(|(_, task)| task.id() == id)
215 .map(|(_, task)| (task, task_context.clone()))
216 })
217 }
218
219 /// Registers task "usage" as being scheduled – to be used for LRU sorting when listing all tasks.
220 pub fn task_scheduled(&mut self, id: TaskId, task_context: TaskContext) {
221 self.last_scheduled_tasks.push_back((id, task_context));
222 if self.last_scheduled_tasks.len() > 5_000 {
223 self.last_scheduled_tasks.pop_front();
224 }
225 }
226}
227
228#[cfg(any(test, feature = "test-support"))]
229pub mod test_inventory {
230 use std::{path::Path, sync::Arc};
231
232 use gpui::{AppContext, Context as _, Model, ModelContext, TestAppContext};
233 use project_core::worktree::WorktreeId;
234 use task::{Task, TaskContext, TaskId, TaskSource};
235
236 use crate::Inventory;
237
238 use super::TaskSourceKind;
239
240 #[derive(Debug, Clone, PartialEq, Eq)]
241 pub struct TestTask {
242 pub id: task::TaskId,
243 pub name: String,
244 }
245
246 impl Task for TestTask {
247 fn id(&self) -> &TaskId {
248 &self.id
249 }
250
251 fn name(&self) -> &str {
252 &self.name
253 }
254
255 fn cwd(&self) -> Option<&str> {
256 None
257 }
258
259 fn exec(&self, _cwd: TaskContext) -> Option<task::SpawnInTerminal> {
260 None
261 }
262 }
263
264 pub struct StaticTestSource {
265 pub tasks: Vec<TestTask>,
266 }
267
268 impl StaticTestSource {
269 pub fn new(
270 task_names: impl IntoIterator<Item = String>,
271 cx: &mut AppContext,
272 ) -> Model<Box<dyn TaskSource>> {
273 cx.new_model(|_| {
274 Box::new(Self {
275 tasks: task_names
276 .into_iter()
277 .enumerate()
278 .map(|(i, name)| TestTask {
279 id: TaskId(format!("task_{i}_{name}")),
280 name,
281 })
282 .collect(),
283 }) as Box<dyn TaskSource>
284 })
285 }
286 }
287
288 impl TaskSource for StaticTestSource {
289 fn tasks_for_path(
290 &mut self,
291 _path: Option<&Path>,
292 _cx: &mut ModelContext<Box<dyn TaskSource>>,
293 ) -> Vec<Arc<dyn Task>> {
294 self.tasks
295 .clone()
296 .into_iter()
297 .map(|task| Arc::new(task) as Arc<dyn Task>)
298 .collect()
299 }
300
301 fn as_any(&mut self) -> &mut dyn std::any::Any {
302 self
303 }
304 }
305
306 pub fn list_task_names(
307 inventory: &Model<Inventory>,
308 path: Option<&Path>,
309 worktree: Option<WorktreeId>,
310 lru: bool,
311 cx: &mut TestAppContext,
312 ) -> Vec<String> {
313 inventory.update(cx, |inventory, cx| {
314 inventory
315 .list_tasks(path, worktree, lru, cx)
316 .into_iter()
317 .map(|(_, task)| task.name().to_string())
318 .collect()
319 })
320 }
321
322 pub fn register_task_used(
323 inventory: &Model<Inventory>,
324 task_name: &str,
325 cx: &mut TestAppContext,
326 ) {
327 inventory.update(cx, |inventory, cx| {
328 let task = inventory
329 .list_tasks(None, None, false, cx)
330 .into_iter()
331 .find(|(_, task)| task.name() == task_name)
332 .unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
333 inventory.task_scheduled(task.1.id().clone(), TaskContext::default());
334 });
335 }
336
337 pub fn list_tasks(
338 inventory: &Model<Inventory>,
339 path: Option<&Path>,
340 worktree: Option<WorktreeId>,
341 lru: bool,
342 cx: &mut TestAppContext,
343 ) -> Vec<(TaskSourceKind, String)> {
344 inventory.update(cx, |inventory, cx| {
345 inventory
346 .list_tasks(path, worktree, lru, cx)
347 .into_iter()
348 .map(|(source_kind, task)| (source_kind, task.name().to_string()))
349 .collect()
350 })
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use gpui::TestAppContext;
357
358 use super::test_inventory::*;
359 use super::*;
360
361 #[gpui::test]
362 fn test_task_list_sorting(cx: &mut TestAppContext) {
363 let inventory = cx.update(Inventory::new);
364 let initial_tasks = list_task_names(&inventory, None, None, true, cx);
365 assert!(
366 initial_tasks.is_empty(),
367 "No tasks expected for empty inventory, but got {initial_tasks:?}"
368 );
369 let initial_tasks = list_task_names(&inventory, None, None, false, cx);
370 assert!(
371 initial_tasks.is_empty(),
372 "No tasks expected for empty inventory, but got {initial_tasks:?}"
373 );
374
375 inventory.update(cx, |inventory, cx| {
376 inventory.add_source(
377 TaskSourceKind::UserInput,
378 |cx| StaticTestSource::new(vec!["3_task".to_string()], cx),
379 cx,
380 );
381 });
382 inventory.update(cx, |inventory, cx| {
383 inventory.add_source(
384 TaskSourceKind::UserInput,
385 |cx| {
386 StaticTestSource::new(
387 vec![
388 "1_task".to_string(),
389 "2_task".to_string(),
390 "1_a_task".to_string(),
391 ],
392 cx,
393 )
394 },
395 cx,
396 );
397 });
398
399 let expected_initial_state = [
400 "1_a_task".to_string(),
401 "1_task".to_string(),
402 "2_task".to_string(),
403 "3_task".to_string(),
404 ];
405 assert_eq!(
406 list_task_names(&inventory, None, None, false, cx),
407 &expected_initial_state,
408 "Task list without lru sorting, should be sorted alphanumerically"
409 );
410 assert_eq!(
411 list_task_names(&inventory, None, None, true, cx),
412 &expected_initial_state,
413 "Tasks with equal amount of usages should be sorted alphanumerically"
414 );
415
416 register_task_used(&inventory, "2_task", cx);
417 assert_eq!(
418 list_task_names(&inventory, None, None, false, cx),
419 &expected_initial_state,
420 "Task list without lru sorting, should be sorted alphanumerically"
421 );
422 assert_eq!(
423 list_task_names(&inventory, None, None, true, cx),
424 vec![
425 "2_task".to_string(),
426 "1_a_task".to_string(),
427 "1_task".to_string(),
428 "3_task".to_string()
429 ],
430 );
431
432 register_task_used(&inventory, "1_task", cx);
433 register_task_used(&inventory, "1_task", cx);
434 register_task_used(&inventory, "1_task", cx);
435 register_task_used(&inventory, "3_task", cx);
436 assert_eq!(
437 list_task_names(&inventory, None, None, false, cx),
438 &expected_initial_state,
439 "Task list without lru sorting, should be sorted alphanumerically"
440 );
441 assert_eq!(
442 list_task_names(&inventory, None, None, true, cx),
443 vec![
444 "3_task".to_string(),
445 "1_task".to_string(),
446 "2_task".to_string(),
447 "1_a_task".to_string(),
448 ],
449 );
450
451 inventory.update(cx, |inventory, cx| {
452 inventory.add_source(
453 TaskSourceKind::UserInput,
454 |cx| {
455 StaticTestSource::new(vec!["10_hello".to_string(), "11_hello".to_string()], cx)
456 },
457 cx,
458 );
459 });
460 let expected_updated_state = [
461 "1_a_task".to_string(),
462 "1_task".to_string(),
463 "2_task".to_string(),
464 "3_task".to_string(),
465 "10_hello".to_string(),
466 "11_hello".to_string(),
467 ];
468 assert_eq!(
469 list_task_names(&inventory, None, None, false, cx),
470 &expected_updated_state,
471 "Task list without lru sorting, should be sorted alphanumerically"
472 );
473 assert_eq!(
474 list_task_names(&inventory, None, None, true, cx),
475 vec![
476 "3_task".to_string(),
477 "1_task".to_string(),
478 "2_task".to_string(),
479 "1_a_task".to_string(),
480 "10_hello".to_string(),
481 "11_hello".to_string(),
482 ],
483 );
484
485 register_task_used(&inventory, "11_hello", cx);
486 assert_eq!(
487 list_task_names(&inventory, None, None, false, cx),
488 &expected_updated_state,
489 "Task list without lru sorting, should be sorted alphanumerically"
490 );
491 assert_eq!(
492 list_task_names(&inventory, None, None, true, cx),
493 vec![
494 "11_hello".to_string(),
495 "3_task".to_string(),
496 "1_task".to_string(),
497 "2_task".to_string(),
498 "1_a_task".to_string(),
499 "10_hello".to_string(),
500 ],
501 );
502 }
503
504 #[gpui::test]
505 fn test_inventory_static_task_filters(cx: &mut TestAppContext) {
506 let inventory_with_statics = cx.update(Inventory::new);
507 let common_name = "common_task_name";
508 let path_1 = Path::new("path_1");
509 let path_2 = Path::new("path_2");
510 let worktree_1 = WorktreeId::from_usize(1);
511 let worktree_path_1 = Path::new("worktree_path_1");
512 let worktree_2 = WorktreeId::from_usize(2);
513 let worktree_path_2 = Path::new("worktree_path_2");
514 inventory_with_statics.update(cx, |inventory, cx| {
515 inventory.add_source(
516 TaskSourceKind::UserInput,
517 |cx| {
518 StaticTestSource::new(
519 vec!["user_input".to_string(), common_name.to_string()],
520 cx,
521 )
522 },
523 cx,
524 );
525 inventory.add_source(
526 TaskSourceKind::AbsPath(path_1.to_path_buf()),
527 |cx| {
528 StaticTestSource::new(
529 vec!["static_source_1".to_string(), common_name.to_string()],
530 cx,
531 )
532 },
533 cx,
534 );
535 inventory.add_source(
536 TaskSourceKind::AbsPath(path_2.to_path_buf()),
537 |cx| {
538 StaticTestSource::new(
539 vec!["static_source_2".to_string(), common_name.to_string()],
540 cx,
541 )
542 },
543 cx,
544 );
545 inventory.add_source(
546 TaskSourceKind::Worktree {
547 id: worktree_1,
548 abs_path: worktree_path_1.to_path_buf(),
549 },
550 |cx| {
551 StaticTestSource::new(
552 vec!["worktree_1".to_string(), common_name.to_string()],
553 cx,
554 )
555 },
556 cx,
557 );
558 inventory.add_source(
559 TaskSourceKind::Worktree {
560 id: worktree_2,
561 abs_path: worktree_path_2.to_path_buf(),
562 },
563 |cx| {
564 StaticTestSource::new(
565 vec!["worktree_2".to_string(), common_name.to_string()],
566 cx,
567 )
568 },
569 cx,
570 );
571 });
572
573 let worktree_independent_tasks = vec![
574 (
575 TaskSourceKind::AbsPath(path_1.to_path_buf()),
576 common_name.to_string(),
577 ),
578 (
579 TaskSourceKind::AbsPath(path_1.to_path_buf()),
580 "static_source_1".to_string(),
581 ),
582 (
583 TaskSourceKind::AbsPath(path_2.to_path_buf()),
584 common_name.to_string(),
585 ),
586 (
587 TaskSourceKind::AbsPath(path_2.to_path_buf()),
588 "static_source_2".to_string(),
589 ),
590 (TaskSourceKind::UserInput, common_name.to_string()),
591 (TaskSourceKind::UserInput, "user_input".to_string()),
592 ];
593 let worktree_1_tasks = vec![
594 (
595 TaskSourceKind::Worktree {
596 id: worktree_1,
597 abs_path: worktree_path_1.to_path_buf(),
598 },
599 common_name.to_string(),
600 ),
601 (
602 TaskSourceKind::Worktree {
603 id: worktree_1,
604 abs_path: worktree_path_1.to_path_buf(),
605 },
606 "worktree_1".to_string(),
607 ),
608 ];
609 let worktree_2_tasks = vec![
610 (
611 TaskSourceKind::Worktree {
612 id: worktree_2,
613 abs_path: worktree_path_2.to_path_buf(),
614 },
615 common_name.to_string(),
616 ),
617 (
618 TaskSourceKind::Worktree {
619 id: worktree_2,
620 abs_path: worktree_path_2.to_path_buf(),
621 },
622 "worktree_2".to_string(),
623 ),
624 ];
625
626 let all_tasks = worktree_1_tasks
627 .iter()
628 .chain(worktree_2_tasks.iter())
629 // worktree-less tasks come later in the list
630 .chain(worktree_independent_tasks.iter())
631 .cloned()
632 .collect::<Vec<_>>();
633
634 for path in [
635 None,
636 Some(path_1),
637 Some(path_2),
638 Some(worktree_path_1),
639 Some(worktree_path_2),
640 ] {
641 assert_eq!(
642 list_tasks(&inventory_with_statics, path, None, false, cx),
643 all_tasks,
644 "Path {path:?} choice should not adjust static runnables"
645 );
646 assert_eq!(
647 list_tasks(&inventory_with_statics, path, Some(worktree_1), false, cx),
648 worktree_1_tasks
649 .iter()
650 .chain(worktree_independent_tasks.iter())
651 .cloned()
652 .collect::<Vec<_>>(),
653 "Path {path:?} choice should not adjust static runnables for worktree_1"
654 );
655 assert_eq!(
656 list_tasks(&inventory_with_statics, path, Some(worktree_2), false, cx),
657 worktree_2_tasks
658 .iter()
659 .chain(worktree_independent_tasks.iter())
660 .cloned()
661 .collect::<Vec<_>>(),
662 "Path {path:?} choice should not adjust static runnables for worktree_2"
663 );
664 }
665 }
666}