1//! Project-wide storage of the tasks available, capable of updating itself from the sources set.
2
3use std::{
4 any::TypeId,
5 cmp::{self, Reverse},
6 path::{Path, PathBuf},
7 sync::Arc,
8};
9
10use collections::{hash_map, HashMap, VecDeque};
11use gpui::{AppContext, Context, Model, ModelContext, Subscription};
12use itertools::{Either, Itertools};
13use language::Language;
14use task::{ResolvedTask, TaskContext, TaskId, TaskSource, TaskTemplate, VariableName};
15use util::{post_inc, NumericPrefixWithSuffix};
16use worktree::WorktreeId;
17
18/// Inventory tracks available tasks for a given project.
19pub struct Inventory {
20 sources: Vec<SourceInInventory>,
21 last_scheduled_tasks: VecDeque<(TaskSourceKind, ResolvedTask)>,
22}
23
24struct SourceInInventory {
25 source: Model<Box<dyn TaskSource>>,
26 _subscription: Subscription,
27 type_id: TypeId,
28 kind: TaskSourceKind,
29}
30
31/// Kind of a source the tasks are fetched from, used to display more source information in the UI.
32#[derive(Debug, Clone, PartialEq, Eq, Hash)]
33pub enum TaskSourceKind {
34 /// bash-like commands spawned by users, not associated with any path
35 UserInput,
36 /// ~/.config/zed/task.json - like global files with task definitions, applicable to any path
37 AbsPath {
38 id_base: &'static str,
39 abs_path: PathBuf,
40 },
41 /// Tasks from the worktree's .zed/task.json
42 Worktree {
43 id: WorktreeId,
44 abs_path: PathBuf,
45 id_base: &'static str,
46 },
47 /// Languages-specific tasks coming from extensions.
48 Language { name: Arc<str> },
49}
50
51impl TaskSourceKind {
52 pub fn abs_path(&self) -> Option<&Path> {
53 match self {
54 Self::AbsPath { abs_path, .. } | Self::Worktree { abs_path, .. } => Some(abs_path),
55 Self::UserInput | Self::Language { .. } => None,
56 }
57 }
58
59 pub fn worktree(&self) -> Option<WorktreeId> {
60 match self {
61 Self::Worktree { id, .. } => Some(*id),
62 _ => None,
63 }
64 }
65
66 pub fn to_id_base(&self) -> String {
67 match self {
68 TaskSourceKind::UserInput => "oneshot".to_string(),
69 TaskSourceKind::AbsPath { id_base, abs_path } => {
70 format!("{id_base}_{}", abs_path.display())
71 }
72 TaskSourceKind::Worktree {
73 id,
74 id_base,
75 abs_path,
76 } => {
77 format!("{id_base}_{id}_{}", abs_path.display())
78 }
79 TaskSourceKind::Language { name } => format!("language_{name}"),
80 }
81 }
82}
83
84impl Inventory {
85 pub fn new(cx: &mut AppContext) -> Model<Self> {
86 cx.new_model(|_| Self {
87 sources: Vec::new(),
88 last_scheduled_tasks: VecDeque::new(),
89 })
90 }
91
92 /// If the task with the same path was not added yet,
93 /// registers a new tasks source to fetch for available tasks later.
94 /// Unless a source is removed, ignores future additions for the same path.
95 pub fn add_source(
96 &mut self,
97 kind: TaskSourceKind,
98 create_source: impl FnOnce(&mut ModelContext<Self>) -> Model<Box<dyn TaskSource>>,
99 cx: &mut ModelContext<Self>,
100 ) {
101 let abs_path = kind.abs_path();
102 if abs_path.is_some() {
103 if let Some(a) = self.sources.iter().find(|s| s.kind.abs_path() == abs_path) {
104 log::debug!("Source for path {abs_path:?} already exists, not adding. Old kind: {OLD_KIND:?}, new kind: {kind:?}", OLD_KIND = a.kind);
105 return;
106 }
107 }
108
109 let source = create_source(cx);
110 let type_id = source.read(cx).type_id();
111 let source = SourceInInventory {
112 _subscription: cx.observe(&source, |_, _, cx| {
113 cx.notify();
114 }),
115 source,
116 type_id,
117 kind,
118 };
119 self.sources.push(source);
120 cx.notify();
121 }
122
123 /// If present, removes the local static source entry that has the given path,
124 /// making corresponding task definitions unavailable in the fetch results.
125 ///
126 /// Now, entry for this path can be re-added again.
127 pub fn remove_local_static_source(&mut self, abs_path: &Path) {
128 self.sources.retain(|s| s.kind.abs_path() != Some(abs_path));
129 }
130
131 /// If present, removes the worktree source entry that has the given worktree id,
132 /// making corresponding task definitions unavailable in the fetch results.
133 ///
134 /// Now, entry for this path can be re-added again.
135 pub fn remove_worktree_sources(&mut self, worktree: WorktreeId) {
136 self.sources.retain(|s| s.kind.worktree() != Some(worktree));
137 }
138
139 pub fn source<T: TaskSource>(&self) -> Option<(Model<Box<dyn TaskSource>>, TaskSourceKind)> {
140 let target_type_id = std::any::TypeId::of::<T>();
141 self.sources.iter().find_map(
142 |SourceInInventory {
143 type_id,
144 source,
145 kind,
146 ..
147 }| {
148 if &target_type_id == type_id {
149 Some((source.clone(), kind.clone()))
150 } else {
151 None
152 }
153 },
154 )
155 }
156
157 /// Pulls its task sources relevant to the worktree and the language given,
158 /// returns all task templates with their source kinds, in no specific order.
159 pub fn list_tasks(
160 &self,
161 language: Option<Arc<Language>>,
162 worktree: Option<WorktreeId>,
163 cx: &mut AppContext,
164 ) -> Vec<(TaskSourceKind, TaskTemplate)> {
165 let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
166 name: language.name(),
167 });
168 let language_tasks = language
169 .and_then(|language| language.context_provider()?.associated_tasks())
170 .into_iter()
171 .flat_map(|tasks| tasks.0.into_iter())
172 .flat_map(|task| Some((task_source_kind.as_ref()?, task)));
173
174 self.sources
175 .iter()
176 .filter(|source| {
177 let source_worktree = source.kind.worktree();
178 worktree.is_none() || source_worktree.is_none() || source_worktree == worktree
179 })
180 .flat_map(|source| {
181 source
182 .source
183 .update(cx, |source, cx| source.tasks_to_schedule(cx))
184 .0
185 .into_iter()
186 .map(|task| (&source.kind, task))
187 })
188 .chain(language_tasks)
189 .map(|(task_source_kind, task)| (task_source_kind.clone(), task))
190 .collect()
191 }
192
193 /// Pulls its task sources relevant to the worktree and the language given and resolves them with the [`TaskContext`] given.
194 /// Joins the new resolutions with the resolved tasks that were used (spawned) before,
195 /// orders them so that the most recently used come first, all equally used ones are ordered so that the most specific tasks come first.
196 /// Deduplicates the tasks by their labels and splits the ordered list into two: used tasks and the rest, newly resolved tasks.
197 pub fn used_and_current_resolved_tasks(
198 &self,
199 language: Option<Arc<Language>>,
200 worktree: Option<WorktreeId>,
201 task_context: &TaskContext,
202 cx: &mut AppContext,
203 ) -> (
204 Vec<(TaskSourceKind, ResolvedTask)>,
205 Vec<(TaskSourceKind, ResolvedTask)>,
206 ) {
207 let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
208 name: language.name(),
209 });
210 let language_tasks = language
211 .and_then(|language| language.context_provider()?.associated_tasks())
212 .into_iter()
213 .flat_map(|tasks| tasks.0.into_iter())
214 .flat_map(|task| Some((task_source_kind.as_ref()?, task)));
215
216 let mut lru_score = 0_u32;
217 let mut task_usage = self
218 .last_scheduled_tasks
219 .iter()
220 .rev()
221 .filter(|(_, task)| !task.original_task().ignore_previously_resolved)
222 .filter(|(task_kind, _)| {
223 if matches!(task_kind, TaskSourceKind::Language { .. }) {
224 Some(task_kind) == task_source_kind.as_ref()
225 } else {
226 true
227 }
228 })
229 .fold(
230 HashMap::default(),
231 |mut tasks, (task_source_kind, resolved_task)| {
232 tasks.entry(&resolved_task.id).or_insert_with(|| {
233 (task_source_kind, resolved_task, post_inc(&mut lru_score))
234 });
235 tasks
236 },
237 );
238 let not_used_score = post_inc(&mut lru_score);
239 let currently_resolved_tasks = self
240 .sources
241 .iter()
242 .filter(|source| {
243 let source_worktree = source.kind.worktree();
244 worktree.is_none() || source_worktree.is_none() || source_worktree == worktree
245 })
246 .flat_map(|source| {
247 source
248 .source
249 .update(cx, |source, cx| source.tasks_to_schedule(cx))
250 .0
251 .into_iter()
252 .map(|task| (&source.kind, task))
253 })
254 .chain(language_tasks)
255 .filter_map(|(kind, task)| {
256 let id_base = kind.to_id_base();
257 Some((kind, task.resolve_task(&id_base, task_context)?))
258 })
259 .map(|(kind, task)| {
260 let lru_score = task_usage
261 .remove(&task.id)
262 .map(|(_, _, lru_score)| lru_score)
263 .unwrap_or(not_used_score);
264 (kind.clone(), task, lru_score)
265 })
266 .collect::<Vec<_>>();
267 let previously_spawned_tasks = task_usage
268 .into_iter()
269 .map(|(_, (kind, task, lru_score))| (kind.clone(), task.clone(), lru_score));
270
271 let mut tasks_by_label = HashMap::default();
272 tasks_by_label = previously_spawned_tasks.into_iter().fold(
273 tasks_by_label,
274 |mut tasks_by_label, (source, task, lru_score)| {
275 match tasks_by_label.entry((source, task.resolved_label.clone())) {
276 hash_map::Entry::Occupied(mut o) => {
277 let (_, previous_lru_score) = o.get();
278 if previous_lru_score >= &lru_score {
279 o.insert((task, lru_score));
280 }
281 }
282 hash_map::Entry::Vacant(v) => {
283 v.insert((task, lru_score));
284 }
285 }
286 tasks_by_label
287 },
288 );
289 tasks_by_label = currently_resolved_tasks.into_iter().fold(
290 tasks_by_label,
291 |mut tasks_by_label, (source, task, lru_score)| {
292 match tasks_by_label.entry((source, task.resolved_label.clone())) {
293 hash_map::Entry::Occupied(mut o) => {
294 let (previous_task, _) = o.get();
295 let new_template = task.original_task();
296 if new_template.ignore_previously_resolved
297 || new_template != previous_task.original_task()
298 {
299 o.insert((task, lru_score));
300 }
301 }
302 hash_map::Entry::Vacant(v) => {
303 v.insert((task, lru_score));
304 }
305 }
306 tasks_by_label
307 },
308 );
309
310 tasks_by_label
311 .into_iter()
312 .map(|((kind, _), (task, lru_score))| (kind, task, lru_score))
313 .sorted_unstable_by(task_lru_comparator)
314 .partition_map(|(kind, task, lru_score)| {
315 if lru_score < not_used_score {
316 Either::Left((kind, task))
317 } else {
318 Either::Right((kind, task))
319 }
320 })
321 }
322
323 /// Returns the last scheduled task, if any of the sources contains one with the matching id.
324 pub fn last_scheduled_task(&self) -> Option<(TaskSourceKind, ResolvedTask)> {
325 self.last_scheduled_tasks.back().cloned()
326 }
327
328 /// Registers task "usage" as being scheduled – to be used for LRU sorting when listing all tasks.
329 pub fn task_scheduled(
330 &mut self,
331 task_source_kind: TaskSourceKind,
332 resolved_task: ResolvedTask,
333 ) {
334 self.last_scheduled_tasks
335 .push_back((task_source_kind, resolved_task));
336 if self.last_scheduled_tasks.len() > 5_000 {
337 self.last_scheduled_tasks.pop_front();
338 }
339 }
340
341 /// Deletes a resolved task from history, using its id.
342 /// A similar may still resurface in `used_and_current_resolved_tasks` when its [`TaskTemplate`] is resolved again.
343 pub fn delete_previously_used(&mut self, id: &TaskId) {
344 self.last_scheduled_tasks.retain(|(_, task)| &task.id != id);
345 }
346}
347
348fn task_lru_comparator(
349 (kind_a, task_a, lru_score_a): &(TaskSourceKind, ResolvedTask, u32),
350 (kind_b, task_b, lru_score_b): &(TaskSourceKind, ResolvedTask, u32),
351) -> cmp::Ordering {
352 lru_score_a
353 // First, display recently used templates above all.
354 .cmp(&lru_score_b)
355 // Then, ensure more specific sources are displayed first.
356 .then(task_source_kind_preference(kind_a).cmp(&task_source_kind_preference(kind_b)))
357 // After that, display first more specific tasks, using more template variables.
358 // Bonus points for tasks with symbol variables.
359 .then(task_variables_preference(task_a).cmp(&task_variables_preference(task_b)))
360 // Finally, sort by the resolved label, but a bit more specifically, to avoid mixing letters and digits.
361 .then({
362 NumericPrefixWithSuffix::from_numeric_prefixed_str(&task_a.resolved_label)
363 .cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str(
364 &task_b.resolved_label,
365 ))
366 .then(task_a.resolved_label.cmp(&task_b.resolved_label))
367 })
368}
369
370fn task_source_kind_preference(kind: &TaskSourceKind) -> u32 {
371 match kind {
372 TaskSourceKind::Language { .. } => 1,
373 TaskSourceKind::UserInput => 2,
374 TaskSourceKind::Worktree { .. } => 3,
375 TaskSourceKind::AbsPath { .. } => 4,
376 }
377}
378
379fn task_variables_preference(task: &ResolvedTask) -> Reverse<usize> {
380 let task_variables = task.substituted_variables();
381 Reverse(if task_variables.contains(&VariableName::Symbol) {
382 task_variables.len() + 1
383 } else {
384 task_variables.len()
385 })
386}
387
388#[cfg(test)]
389mod test_inventory {
390 use gpui::{AppContext, Context as _, Model, ModelContext, TestAppContext};
391 use itertools::Itertools;
392 use task::{TaskContext, TaskId, TaskSource, TaskTemplate, TaskTemplates};
393 use worktree::WorktreeId;
394
395 use crate::Inventory;
396
397 use super::{task_source_kind_preference, TaskSourceKind};
398
399 #[derive(Debug, Clone, PartialEq, Eq)]
400 pub struct TestTask {
401 id: task::TaskId,
402 name: String,
403 }
404
405 pub struct StaticTestSource {
406 pub tasks: Vec<TestTask>,
407 }
408
409 impl StaticTestSource {
410 pub(super) fn new(
411 task_names: impl IntoIterator<Item = String>,
412 cx: &mut AppContext,
413 ) -> Model<Box<dyn TaskSource>> {
414 cx.new_model(|_| {
415 Box::new(Self {
416 tasks: task_names
417 .into_iter()
418 .enumerate()
419 .map(|(i, name)| TestTask {
420 id: TaskId(format!("task_{i}_{name}")),
421 name,
422 })
423 .collect(),
424 }) as Box<dyn TaskSource>
425 })
426 }
427 }
428
429 impl TaskSource for StaticTestSource {
430 fn tasks_to_schedule(
431 &mut self,
432 _cx: &mut ModelContext<Box<dyn TaskSource>>,
433 ) -> TaskTemplates {
434 TaskTemplates(
435 self.tasks
436 .clone()
437 .into_iter()
438 .map(|task| TaskTemplate {
439 label: task.name,
440 command: "test command".to_string(),
441 ..TaskTemplate::default()
442 })
443 .collect(),
444 )
445 }
446
447 fn as_any(&mut self) -> &mut dyn std::any::Any {
448 self
449 }
450 }
451
452 pub(super) fn task_template_names(
453 inventory: &Model<Inventory>,
454 worktree: Option<WorktreeId>,
455 cx: &mut TestAppContext,
456 ) -> Vec<String> {
457 inventory.update(cx, |inventory, cx| {
458 inventory
459 .list_tasks(None, worktree, cx)
460 .into_iter()
461 .map(|(_, task)| task.label)
462 .sorted()
463 .collect()
464 })
465 }
466
467 pub(super) fn resolved_task_names(
468 inventory: &Model<Inventory>,
469 worktree: Option<WorktreeId>,
470 cx: &mut TestAppContext,
471 ) -> Vec<String> {
472 inventory.update(cx, |inventory, cx| {
473 let (used, current) = inventory.used_and_current_resolved_tasks(
474 None,
475 worktree,
476 &TaskContext::default(),
477 cx,
478 );
479 used.into_iter()
480 .chain(current)
481 .map(|(_, task)| task.original_task().label.clone())
482 .collect()
483 })
484 }
485
486 pub(super) fn register_task_used(
487 inventory: &Model<Inventory>,
488 task_name: &str,
489 cx: &mut TestAppContext,
490 ) {
491 inventory.update(cx, |inventory, cx| {
492 let (task_source_kind, task) = inventory
493 .list_tasks(None, None, cx)
494 .into_iter()
495 .find(|(_, task)| task.label == task_name)
496 .unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
497 let id_base = task_source_kind.to_id_base();
498 inventory.task_scheduled(
499 task_source_kind.clone(),
500 task.resolve_task(&id_base, &TaskContext::default())
501 .unwrap_or_else(|| panic!("Failed to resolve task with name {task_name}")),
502 );
503 });
504 }
505
506 pub(super) fn list_tasks(
507 inventory: &Model<Inventory>,
508 worktree: Option<WorktreeId>,
509 cx: &mut TestAppContext,
510 ) -> Vec<(TaskSourceKind, String)> {
511 inventory.update(cx, |inventory, cx| {
512 let (used, current) = inventory.used_and_current_resolved_tasks(
513 None,
514 worktree,
515 &TaskContext::default(),
516 cx,
517 );
518 let mut all = used;
519 all.extend(current);
520 all.into_iter()
521 .map(|(source_kind, task)| (source_kind, task.resolved_label))
522 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
523 .collect()
524 })
525 }
526}
527
528#[cfg(test)]
529mod tests {
530 use gpui::TestAppContext;
531
532 use super::test_inventory::*;
533 use super::*;
534
535 #[gpui::test]
536 fn test_task_list_sorting(cx: &mut TestAppContext) {
537 let inventory = cx.update(Inventory::new);
538 let initial_tasks = resolved_task_names(&inventory, None, cx);
539 assert!(
540 initial_tasks.is_empty(),
541 "No tasks expected for empty inventory, but got {initial_tasks:?}"
542 );
543 let initial_tasks = task_template_names(&inventory, None, cx);
544 assert!(
545 initial_tasks.is_empty(),
546 "No tasks expected for empty inventory, but got {initial_tasks:?}"
547 );
548
549 inventory.update(cx, |inventory, cx| {
550 inventory.add_source(
551 TaskSourceKind::UserInput,
552 |cx| StaticTestSource::new(vec!["3_task".to_string()], cx),
553 cx,
554 );
555 });
556 inventory.update(cx, |inventory, cx| {
557 inventory.add_source(
558 TaskSourceKind::UserInput,
559 |cx| {
560 StaticTestSource::new(
561 vec![
562 "1_task".to_string(),
563 "2_task".to_string(),
564 "1_a_task".to_string(),
565 ],
566 cx,
567 )
568 },
569 cx,
570 );
571 });
572
573 let expected_initial_state = [
574 "1_a_task".to_string(),
575 "1_task".to_string(),
576 "2_task".to_string(),
577 "3_task".to_string(),
578 ];
579 assert_eq!(
580 task_template_names(&inventory, None, cx),
581 &expected_initial_state,
582 );
583 assert_eq!(
584 resolved_task_names(&inventory, None, cx),
585 &expected_initial_state,
586 "Tasks with equal amount of usages should be sorted alphanumerically"
587 );
588
589 register_task_used(&inventory, "2_task", cx);
590 assert_eq!(
591 task_template_names(&inventory, None, cx),
592 &expected_initial_state,
593 );
594 assert_eq!(
595 resolved_task_names(&inventory, None, cx),
596 vec![
597 "2_task".to_string(),
598 "1_a_task".to_string(),
599 "1_task".to_string(),
600 "3_task".to_string()
601 ],
602 );
603
604 register_task_used(&inventory, "1_task", cx);
605 register_task_used(&inventory, "1_task", cx);
606 register_task_used(&inventory, "1_task", cx);
607 register_task_used(&inventory, "3_task", cx);
608 assert_eq!(
609 task_template_names(&inventory, None, cx),
610 &expected_initial_state,
611 );
612 assert_eq!(
613 resolved_task_names(&inventory, None, cx),
614 vec![
615 "3_task".to_string(),
616 "1_task".to_string(),
617 "2_task".to_string(),
618 "1_a_task".to_string(),
619 ],
620 );
621
622 inventory.update(cx, |inventory, cx| {
623 inventory.add_source(
624 TaskSourceKind::UserInput,
625 |cx| {
626 StaticTestSource::new(vec!["10_hello".to_string(), "11_hello".to_string()], cx)
627 },
628 cx,
629 );
630 });
631 let expected_updated_state = [
632 "10_hello".to_string(),
633 "11_hello".to_string(),
634 "1_a_task".to_string(),
635 "1_task".to_string(),
636 "2_task".to_string(),
637 "3_task".to_string(),
638 ];
639 assert_eq!(
640 task_template_names(&inventory, None, cx),
641 &expected_updated_state,
642 );
643 assert_eq!(
644 resolved_task_names(&inventory, None, cx),
645 vec![
646 "3_task".to_string(),
647 "1_task".to_string(),
648 "2_task".to_string(),
649 "1_a_task".to_string(),
650 "10_hello".to_string(),
651 "11_hello".to_string(),
652 ],
653 );
654
655 register_task_used(&inventory, "11_hello", cx);
656 assert_eq!(
657 task_template_names(&inventory, None, cx),
658 &expected_updated_state,
659 );
660 assert_eq!(
661 resolved_task_names(&inventory, None, cx),
662 vec![
663 "11_hello".to_string(),
664 "3_task".to_string(),
665 "1_task".to_string(),
666 "2_task".to_string(),
667 "1_a_task".to_string(),
668 "10_hello".to_string(),
669 ],
670 );
671 }
672
673 #[gpui::test]
674 fn test_inventory_static_task_filters(cx: &mut TestAppContext) {
675 let inventory_with_statics = cx.update(Inventory::new);
676 let common_name = "common_task_name";
677 let path_1 = Path::new("path_1");
678 let path_2 = Path::new("path_2");
679 let worktree_1 = WorktreeId::from_usize(1);
680 let worktree_path_1 = Path::new("worktree_path_1");
681 let worktree_2 = WorktreeId::from_usize(2);
682 let worktree_path_2 = Path::new("worktree_path_2");
683 inventory_with_statics.update(cx, |inventory, cx| {
684 inventory.add_source(
685 TaskSourceKind::UserInput,
686 |cx| {
687 StaticTestSource::new(
688 vec!["user_input".to_string(), common_name.to_string()],
689 cx,
690 )
691 },
692 cx,
693 );
694 inventory.add_source(
695 TaskSourceKind::AbsPath {
696 id_base: "test source",
697 abs_path: path_1.to_path_buf(),
698 },
699 |cx| {
700 StaticTestSource::new(
701 vec!["static_source_1".to_string(), common_name.to_string()],
702 cx,
703 )
704 },
705 cx,
706 );
707 inventory.add_source(
708 TaskSourceKind::AbsPath {
709 id_base: "test source",
710 abs_path: path_2.to_path_buf(),
711 },
712 |cx| {
713 StaticTestSource::new(
714 vec!["static_source_2".to_string(), common_name.to_string()],
715 cx,
716 )
717 },
718 cx,
719 );
720 inventory.add_source(
721 TaskSourceKind::Worktree {
722 id: worktree_1,
723 abs_path: worktree_path_1.to_path_buf(),
724 id_base: "test_source",
725 },
726 |cx| {
727 StaticTestSource::new(
728 vec!["worktree_1".to_string(), common_name.to_string()],
729 cx,
730 )
731 },
732 cx,
733 );
734 inventory.add_source(
735 TaskSourceKind::Worktree {
736 id: worktree_2,
737 abs_path: worktree_path_2.to_path_buf(),
738 id_base: "test_source",
739 },
740 |cx| {
741 StaticTestSource::new(
742 vec!["worktree_2".to_string(), common_name.to_string()],
743 cx,
744 )
745 },
746 cx,
747 );
748 });
749
750 let worktree_independent_tasks = vec![
751 (
752 TaskSourceKind::AbsPath {
753 id_base: "test source",
754 abs_path: path_2.to_path_buf(),
755 },
756 common_name.to_string(),
757 ),
758 (
759 TaskSourceKind::AbsPath {
760 id_base: "test source",
761 abs_path: path_1.to_path_buf(),
762 },
763 "static_source_1".to_string(),
764 ),
765 (
766 TaskSourceKind::AbsPath {
767 id_base: "test source",
768 abs_path: path_1.to_path_buf(),
769 },
770 common_name.to_string(),
771 ),
772 (
773 TaskSourceKind::AbsPath {
774 id_base: "test source",
775 abs_path: path_2.to_path_buf(),
776 },
777 "static_source_2".to_string(),
778 ),
779 (TaskSourceKind::UserInput, common_name.to_string()),
780 (TaskSourceKind::UserInput, "user_input".to_string()),
781 ];
782 let worktree_1_tasks = [
783 (
784 TaskSourceKind::Worktree {
785 id: worktree_1,
786 abs_path: worktree_path_1.to_path_buf(),
787 id_base: "test_source",
788 },
789 common_name.to_string(),
790 ),
791 (
792 TaskSourceKind::Worktree {
793 id: worktree_1,
794 abs_path: worktree_path_1.to_path_buf(),
795 id_base: "test_source",
796 },
797 "worktree_1".to_string(),
798 ),
799 ];
800 let worktree_2_tasks = [
801 (
802 TaskSourceKind::Worktree {
803 id: worktree_2,
804 abs_path: worktree_path_2.to_path_buf(),
805 id_base: "test_source",
806 },
807 common_name.to_string(),
808 ),
809 (
810 TaskSourceKind::Worktree {
811 id: worktree_2,
812 abs_path: worktree_path_2.to_path_buf(),
813 id_base: "test_source",
814 },
815 "worktree_2".to_string(),
816 ),
817 ];
818
819 let all_tasks = worktree_1_tasks
820 .iter()
821 .chain(worktree_2_tasks.iter())
822 // worktree-less tasks come later in the list
823 .chain(worktree_independent_tasks.iter())
824 .cloned()
825 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
826 .collect::<Vec<_>>();
827
828 assert_eq!(list_tasks(&inventory_with_statics, None, cx), all_tasks);
829 assert_eq!(
830 list_tasks(&inventory_with_statics, Some(worktree_1), cx),
831 worktree_1_tasks
832 .iter()
833 .chain(worktree_independent_tasks.iter())
834 .cloned()
835 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
836 .collect::<Vec<_>>(),
837 );
838 assert_eq!(
839 list_tasks(&inventory_with_statics, Some(worktree_2), cx),
840 worktree_2_tasks
841 .iter()
842 .chain(worktree_independent_tasks.iter())
843 .cloned()
844 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
845 .collect::<Vec<_>>(),
846 );
847 }
848}