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::{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.last_scheduled_tasks.iter().rev().fold(
218 HashMap::default(),
219 |mut tasks, (task_source_kind, resolved_task)| {
220 tasks
221 .entry(&resolved_task.id)
222 .or_insert_with(|| (task_source_kind, resolved_task, post_inc(&mut lru_score)));
223 tasks
224 },
225 );
226 let not_used_score = post_inc(&mut lru_score);
227 let current_resolved_tasks = self
228 .sources
229 .iter()
230 .filter(|source| {
231 let source_worktree = source.kind.worktree();
232 worktree.is_none() || source_worktree.is_none() || source_worktree == worktree
233 })
234 .flat_map(|source| {
235 source
236 .source
237 .update(cx, |source, cx| source.tasks_to_schedule(cx))
238 .0
239 .into_iter()
240 .map(|task| (&source.kind, task))
241 })
242 .chain(language_tasks)
243 .filter_map(|(kind, task)| {
244 let id_base = kind.to_id_base();
245 Some((kind, task.resolve_task(&id_base, task_context)?))
246 })
247 .map(|(kind, task)| {
248 let lru_score = task_usage
249 .remove(&task.id)
250 .map(|(_, _, lru_score)| lru_score)
251 .unwrap_or(not_used_score);
252 (kind.clone(), task, lru_score)
253 })
254 .collect::<Vec<_>>();
255 let previous_resolved_tasks = task_usage
256 .into_iter()
257 .map(|(_, (kind, task, lru_score))| (kind.clone(), task.clone(), lru_score));
258
259 previous_resolved_tasks
260 .chain(current_resolved_tasks)
261 .sorted_unstable_by(task_lru_comparator)
262 .unique_by(|(kind, task, _)| (kind.clone(), task.resolved_label.clone()))
263 .partition_map(|(kind, task, lru_index)| {
264 if lru_index < not_used_score {
265 Either::Left((kind, task))
266 } else {
267 Either::Right((kind, task))
268 }
269 })
270 }
271
272 /// Returns the last scheduled task, if any of the sources contains one with the matching id.
273 pub fn last_scheduled_task(&self) -> Option<(TaskSourceKind, ResolvedTask)> {
274 self.last_scheduled_tasks.back().cloned()
275 }
276
277 /// Registers task "usage" as being scheduled – to be used for LRU sorting when listing all tasks.
278 pub fn task_scheduled(
279 &mut self,
280 task_source_kind: TaskSourceKind,
281 resolved_task: ResolvedTask,
282 ) {
283 self.last_scheduled_tasks
284 .push_back((task_source_kind, resolved_task));
285 if self.last_scheduled_tasks.len() > 5_000 {
286 self.last_scheduled_tasks.pop_front();
287 }
288 }
289
290 /// Deletes a resolved task from history, using its id.
291 /// A similar may still resurface in `used_and_current_resolved_tasks` when its [`TaskTemplate`] is resolved again.
292 pub fn delete_previously_used(&mut self, id: &TaskId) {
293 self.last_scheduled_tasks.retain(|(_, task)| &task.id != id);
294 }
295}
296
297fn task_lru_comparator(
298 (kind_a, task_a, lru_score_a): &(TaskSourceKind, ResolvedTask, u32),
299 (kind_b, task_b, lru_score_b): &(TaskSourceKind, ResolvedTask, u32),
300) -> cmp::Ordering {
301 lru_score_a
302 // First, display recently used templates above all.
303 .cmp(&lru_score_b)
304 // Then, ensure more specific sources are displayed first.
305 .then(task_source_kind_preference(kind_a).cmp(&task_source_kind_preference(kind_b)))
306 // After that, display first more specific tasks, using more template variables.
307 // Bonus points for tasks with symbol variables.
308 .then(task_variables_preference(task_a).cmp(&task_variables_preference(task_b)))
309 // Finally, sort by the resolved label, but a bit more specifically, to avoid mixing letters and digits.
310 .then({
311 NumericPrefixWithSuffix::from_numeric_prefixed_str(&task_a.resolved_label)
312 .cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str(
313 &task_b.resolved_label,
314 ))
315 .then(task_a.resolved_label.cmp(&task_b.resolved_label))
316 })
317}
318
319fn task_source_kind_preference(kind: &TaskSourceKind) -> u32 {
320 match kind {
321 TaskSourceKind::Language { .. } => 1,
322 TaskSourceKind::UserInput => 2,
323 TaskSourceKind::Worktree { .. } => 3,
324 TaskSourceKind::AbsPath { .. } => 4,
325 }
326}
327
328fn task_variables_preference(task: &ResolvedTask) -> Reverse<usize> {
329 let task_variables = task.substituted_variables();
330 Reverse(if task_variables.contains(&VariableName::Symbol) {
331 task_variables.len() + 1
332 } else {
333 task_variables.len()
334 })
335}
336
337#[cfg(test)]
338mod test_inventory {
339 use gpui::{AppContext, Context as _, Model, ModelContext, TestAppContext};
340 use itertools::Itertools;
341 use task::{TaskContext, TaskId, TaskSource, TaskTemplate, TaskTemplates};
342 use worktree::WorktreeId;
343
344 use crate::Inventory;
345
346 use super::{task_source_kind_preference, TaskSourceKind};
347
348 #[derive(Debug, Clone, PartialEq, Eq)]
349 pub struct TestTask {
350 id: task::TaskId,
351 name: String,
352 }
353
354 pub struct StaticTestSource {
355 pub tasks: Vec<TestTask>,
356 }
357
358 impl StaticTestSource {
359 pub(super) fn new(
360 task_names: impl IntoIterator<Item = String>,
361 cx: &mut AppContext,
362 ) -> Model<Box<dyn TaskSource>> {
363 cx.new_model(|_| {
364 Box::new(Self {
365 tasks: task_names
366 .into_iter()
367 .enumerate()
368 .map(|(i, name)| TestTask {
369 id: TaskId(format!("task_{i}_{name}")),
370 name,
371 })
372 .collect(),
373 }) as Box<dyn TaskSource>
374 })
375 }
376 }
377
378 impl TaskSource for StaticTestSource {
379 fn tasks_to_schedule(
380 &mut self,
381 _cx: &mut ModelContext<Box<dyn TaskSource>>,
382 ) -> TaskTemplates {
383 TaskTemplates(
384 self.tasks
385 .clone()
386 .into_iter()
387 .map(|task| TaskTemplate {
388 label: task.name,
389 command: "test command".to_string(),
390 ..TaskTemplate::default()
391 })
392 .collect(),
393 )
394 }
395
396 fn as_any(&mut self) -> &mut dyn std::any::Any {
397 self
398 }
399 }
400
401 pub(super) fn task_template_names(
402 inventory: &Model<Inventory>,
403 worktree: Option<WorktreeId>,
404 cx: &mut TestAppContext,
405 ) -> Vec<String> {
406 inventory.update(cx, |inventory, cx| {
407 inventory
408 .list_tasks(None, worktree, cx)
409 .into_iter()
410 .map(|(_, task)| task.label)
411 .sorted()
412 .collect()
413 })
414 }
415
416 pub(super) fn resolved_task_names(
417 inventory: &Model<Inventory>,
418 worktree: Option<WorktreeId>,
419 cx: &mut TestAppContext,
420 ) -> Vec<String> {
421 inventory.update(cx, |inventory, cx| {
422 let (used, current) = inventory.used_and_current_resolved_tasks(
423 None,
424 worktree,
425 &TaskContext::default(),
426 cx,
427 );
428 used.into_iter()
429 .chain(current)
430 .map(|(_, task)| task.original_task().label.clone())
431 .collect()
432 })
433 }
434
435 pub(super) fn register_task_used(
436 inventory: &Model<Inventory>,
437 task_name: &str,
438 cx: &mut TestAppContext,
439 ) {
440 inventory.update(cx, |inventory, cx| {
441 let (task_source_kind, task) = inventory
442 .list_tasks(None, None, cx)
443 .into_iter()
444 .find(|(_, task)| task.label == task_name)
445 .unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
446 let id_base = task_source_kind.to_id_base();
447 inventory.task_scheduled(
448 task_source_kind.clone(),
449 task.resolve_task(&id_base, &TaskContext::default())
450 .unwrap_or_else(|| panic!("Failed to resolve task with name {task_name}")),
451 );
452 });
453 }
454
455 pub(super) fn list_tasks(
456 inventory: &Model<Inventory>,
457 worktree: Option<WorktreeId>,
458 cx: &mut TestAppContext,
459 ) -> Vec<(TaskSourceKind, String)> {
460 inventory.update(cx, |inventory, cx| {
461 let (used, current) = inventory.used_and_current_resolved_tasks(
462 None,
463 worktree,
464 &TaskContext::default(),
465 cx,
466 );
467 let mut all = used;
468 all.extend(current);
469 all.into_iter()
470 .map(|(source_kind, task)| (source_kind, task.resolved_label))
471 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
472 .collect()
473 })
474 }
475}
476
477#[cfg(test)]
478mod tests {
479 use gpui::TestAppContext;
480
481 use super::test_inventory::*;
482 use super::*;
483
484 #[gpui::test]
485 fn test_task_list_sorting(cx: &mut TestAppContext) {
486 let inventory = cx.update(Inventory::new);
487 let initial_tasks = resolved_task_names(&inventory, None, cx);
488 assert!(
489 initial_tasks.is_empty(),
490 "No tasks expected for empty inventory, but got {initial_tasks:?}"
491 );
492 let initial_tasks = task_template_names(&inventory, None, cx);
493 assert!(
494 initial_tasks.is_empty(),
495 "No tasks expected for empty inventory, but got {initial_tasks:?}"
496 );
497
498 inventory.update(cx, |inventory, cx| {
499 inventory.add_source(
500 TaskSourceKind::UserInput,
501 |cx| StaticTestSource::new(vec!["3_task".to_string()], cx),
502 cx,
503 );
504 });
505 inventory.update(cx, |inventory, cx| {
506 inventory.add_source(
507 TaskSourceKind::UserInput,
508 |cx| {
509 StaticTestSource::new(
510 vec![
511 "1_task".to_string(),
512 "2_task".to_string(),
513 "1_a_task".to_string(),
514 ],
515 cx,
516 )
517 },
518 cx,
519 );
520 });
521
522 let expected_initial_state = [
523 "1_a_task".to_string(),
524 "1_task".to_string(),
525 "2_task".to_string(),
526 "3_task".to_string(),
527 ];
528 assert_eq!(
529 task_template_names(&inventory, None, cx),
530 &expected_initial_state,
531 );
532 assert_eq!(
533 resolved_task_names(&inventory, None, cx),
534 &expected_initial_state,
535 "Tasks with equal amount of usages should be sorted alphanumerically"
536 );
537
538 register_task_used(&inventory, "2_task", cx);
539 assert_eq!(
540 task_template_names(&inventory, None, cx),
541 &expected_initial_state,
542 );
543 assert_eq!(
544 resolved_task_names(&inventory, None, cx),
545 vec![
546 "2_task".to_string(),
547 "1_a_task".to_string(),
548 "1_task".to_string(),
549 "3_task".to_string()
550 ],
551 );
552
553 register_task_used(&inventory, "1_task", cx);
554 register_task_used(&inventory, "1_task", cx);
555 register_task_used(&inventory, "1_task", cx);
556 register_task_used(&inventory, "3_task", cx);
557 assert_eq!(
558 task_template_names(&inventory, None, cx),
559 &expected_initial_state,
560 );
561 assert_eq!(
562 resolved_task_names(&inventory, None, cx),
563 vec![
564 "3_task".to_string(),
565 "1_task".to_string(),
566 "2_task".to_string(),
567 "1_a_task".to_string(),
568 ],
569 );
570
571 inventory.update(cx, |inventory, cx| {
572 inventory.add_source(
573 TaskSourceKind::UserInput,
574 |cx| {
575 StaticTestSource::new(vec!["10_hello".to_string(), "11_hello".to_string()], cx)
576 },
577 cx,
578 );
579 });
580 let expected_updated_state = [
581 "10_hello".to_string(),
582 "11_hello".to_string(),
583 "1_a_task".to_string(),
584 "1_task".to_string(),
585 "2_task".to_string(),
586 "3_task".to_string(),
587 ];
588 assert_eq!(
589 task_template_names(&inventory, None, cx),
590 &expected_updated_state,
591 );
592 assert_eq!(
593 resolved_task_names(&inventory, None, cx),
594 vec![
595 "3_task".to_string(),
596 "1_task".to_string(),
597 "2_task".to_string(),
598 "1_a_task".to_string(),
599 "10_hello".to_string(),
600 "11_hello".to_string(),
601 ],
602 );
603
604 register_task_used(&inventory, "11_hello", cx);
605 assert_eq!(
606 task_template_names(&inventory, None, cx),
607 &expected_updated_state,
608 );
609 assert_eq!(
610 resolved_task_names(&inventory, None, cx),
611 vec![
612 "11_hello".to_string(),
613 "3_task".to_string(),
614 "1_task".to_string(),
615 "2_task".to_string(),
616 "1_a_task".to_string(),
617 "10_hello".to_string(),
618 ],
619 );
620 }
621
622 #[gpui::test]
623 fn test_inventory_static_task_filters(cx: &mut TestAppContext) {
624 let inventory_with_statics = cx.update(Inventory::new);
625 let common_name = "common_task_name";
626 let path_1 = Path::new("path_1");
627 let path_2 = Path::new("path_2");
628 let worktree_1 = WorktreeId::from_usize(1);
629 let worktree_path_1 = Path::new("worktree_path_1");
630 let worktree_2 = WorktreeId::from_usize(2);
631 let worktree_path_2 = Path::new("worktree_path_2");
632 inventory_with_statics.update(cx, |inventory, cx| {
633 inventory.add_source(
634 TaskSourceKind::UserInput,
635 |cx| {
636 StaticTestSource::new(
637 vec!["user_input".to_string(), common_name.to_string()],
638 cx,
639 )
640 },
641 cx,
642 );
643 inventory.add_source(
644 TaskSourceKind::AbsPath {
645 id_base: "test source",
646 abs_path: path_1.to_path_buf(),
647 },
648 |cx| {
649 StaticTestSource::new(
650 vec!["static_source_1".to_string(), common_name.to_string()],
651 cx,
652 )
653 },
654 cx,
655 );
656 inventory.add_source(
657 TaskSourceKind::AbsPath {
658 id_base: "test source",
659 abs_path: path_2.to_path_buf(),
660 },
661 |cx| {
662 StaticTestSource::new(
663 vec!["static_source_2".to_string(), common_name.to_string()],
664 cx,
665 )
666 },
667 cx,
668 );
669 inventory.add_source(
670 TaskSourceKind::Worktree {
671 id: worktree_1,
672 abs_path: worktree_path_1.to_path_buf(),
673 id_base: "test_source",
674 },
675 |cx| {
676 StaticTestSource::new(
677 vec!["worktree_1".to_string(), common_name.to_string()],
678 cx,
679 )
680 },
681 cx,
682 );
683 inventory.add_source(
684 TaskSourceKind::Worktree {
685 id: worktree_2,
686 abs_path: worktree_path_2.to_path_buf(),
687 id_base: "test_source",
688 },
689 |cx| {
690 StaticTestSource::new(
691 vec!["worktree_2".to_string(), common_name.to_string()],
692 cx,
693 )
694 },
695 cx,
696 );
697 });
698
699 let worktree_independent_tasks = vec![
700 (
701 TaskSourceKind::AbsPath {
702 id_base: "test source",
703 abs_path: path_1.to_path_buf(),
704 },
705 common_name.to_string(),
706 ),
707 (
708 TaskSourceKind::AbsPath {
709 id_base: "test source",
710 abs_path: path_1.to_path_buf(),
711 },
712 "static_source_1".to_string(),
713 ),
714 (
715 TaskSourceKind::AbsPath {
716 id_base: "test source",
717 abs_path: path_2.to_path_buf(),
718 },
719 common_name.to_string(),
720 ),
721 (
722 TaskSourceKind::AbsPath {
723 id_base: "test source",
724 abs_path: path_2.to_path_buf(),
725 },
726 "static_source_2".to_string(),
727 ),
728 (TaskSourceKind::UserInput, common_name.to_string()),
729 (TaskSourceKind::UserInput, "user_input".to_string()),
730 ];
731 let worktree_1_tasks = [
732 (
733 TaskSourceKind::Worktree {
734 id: worktree_1,
735 abs_path: worktree_path_1.to_path_buf(),
736 id_base: "test_source",
737 },
738 common_name.to_string(),
739 ),
740 (
741 TaskSourceKind::Worktree {
742 id: worktree_1,
743 abs_path: worktree_path_1.to_path_buf(),
744 id_base: "test_source",
745 },
746 "worktree_1".to_string(),
747 ),
748 ];
749 let worktree_2_tasks = [
750 (
751 TaskSourceKind::Worktree {
752 id: worktree_2,
753 abs_path: worktree_path_2.to_path_buf(),
754 id_base: "test_source",
755 },
756 common_name.to_string(),
757 ),
758 (
759 TaskSourceKind::Worktree {
760 id: worktree_2,
761 abs_path: worktree_path_2.to_path_buf(),
762 id_base: "test_source",
763 },
764 "worktree_2".to_string(),
765 ),
766 ];
767
768 let all_tasks = worktree_1_tasks
769 .iter()
770 .chain(worktree_2_tasks.iter())
771 // worktree-less tasks come later in the list
772 .chain(worktree_independent_tasks.iter())
773 .cloned()
774 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
775 .collect::<Vec<_>>();
776
777 assert_eq!(list_tasks(&inventory_with_statics, None, cx), all_tasks);
778 assert_eq!(
779 list_tasks(&inventory_with_statics, Some(worktree_1), cx),
780 worktree_1_tasks
781 .iter()
782 .chain(worktree_independent_tasks.iter())
783 .cloned()
784 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
785 .collect::<Vec<_>>(),
786 );
787 assert_eq!(
788 list_tasks(&inventory_with_statics, Some(worktree_2), cx),
789 worktree_2_tasks
790 .iter()
791 .chain(worktree_independent_tasks.iter())
792 .cloned()
793 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
794 .collect::<Vec<_>>(),
795 );
796 }
797}