1//! Project-wide storage of the tasks available, capable of updating itself from the sources set.
2
3use std::{
4 any::TypeId,
5 cmp,
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};
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.clone())?))
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 .cmp(&lru_score_b)
303 .then(task_source_kind_preference(kind_a).cmp(&task_source_kind_preference(kind_b)))
304 .then(
305 kind_a
306 .worktree()
307 .is_none()
308 .cmp(&kind_b.worktree().is_none()),
309 )
310 .then(kind_a.worktree().cmp(&kind_b.worktree()))
311 .then(
312 kind_a
313 .abs_path()
314 .is_none()
315 .cmp(&kind_b.abs_path().is_none()),
316 )
317 .then(kind_a.abs_path().cmp(&kind_b.abs_path()))
318 .then({
319 NumericPrefixWithSuffix::from_numeric_prefixed_str(&task_a.resolved_label)
320 .cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str(
321 &task_b.resolved_label,
322 ))
323 .then(task_a.resolved_label.cmp(&task_b.resolved_label))
324 })
325}
326
327fn task_source_kind_preference(kind: &TaskSourceKind) -> u32 {
328 match kind {
329 TaskSourceKind::Language { .. } => 1,
330 TaskSourceKind::UserInput => 2,
331 TaskSourceKind::Worktree { .. } => 3,
332 TaskSourceKind::AbsPath { .. } => 4,
333 }
334}
335
336#[cfg(test)]
337mod test_inventory {
338 use gpui::{AppContext, Context as _, Model, ModelContext, TestAppContext};
339 use itertools::Itertools;
340 use task::{TaskContext, TaskId, TaskSource, TaskTemplate, TaskTemplates};
341 use worktree::WorktreeId;
342
343 use crate::Inventory;
344
345 use super::{task_source_kind_preference, TaskSourceKind};
346
347 #[derive(Debug, Clone, PartialEq, Eq)]
348 pub struct TestTask {
349 id: task::TaskId,
350 name: String,
351 }
352
353 pub struct StaticTestSource {
354 pub tasks: Vec<TestTask>,
355 }
356
357 impl StaticTestSource {
358 pub(super) fn new(
359 task_names: impl IntoIterator<Item = String>,
360 cx: &mut AppContext,
361 ) -> Model<Box<dyn TaskSource>> {
362 cx.new_model(|_| {
363 Box::new(Self {
364 tasks: task_names
365 .into_iter()
366 .enumerate()
367 .map(|(i, name)| TestTask {
368 id: TaskId(format!("task_{i}_{name}")),
369 name,
370 })
371 .collect(),
372 }) as Box<dyn TaskSource>
373 })
374 }
375 }
376
377 impl TaskSource for StaticTestSource {
378 fn tasks_to_schedule(
379 &mut self,
380 _cx: &mut ModelContext<Box<dyn TaskSource>>,
381 ) -> TaskTemplates {
382 TaskTemplates(
383 self.tasks
384 .clone()
385 .into_iter()
386 .map(|task| TaskTemplate {
387 label: task.name,
388 command: "test command".to_string(),
389 ..TaskTemplate::default()
390 })
391 .collect(),
392 )
393 }
394
395 fn as_any(&mut self) -> &mut dyn std::any::Any {
396 self
397 }
398 }
399
400 pub(super) fn task_template_names(
401 inventory: &Model<Inventory>,
402 worktree: Option<WorktreeId>,
403 cx: &mut TestAppContext,
404 ) -> Vec<String> {
405 inventory.update(cx, |inventory, cx| {
406 inventory
407 .list_tasks(None, worktree, cx)
408 .into_iter()
409 .map(|(_, task)| task.label)
410 .sorted()
411 .collect()
412 })
413 }
414
415 pub(super) fn resolved_task_names(
416 inventory: &Model<Inventory>,
417 worktree: Option<WorktreeId>,
418 cx: &mut TestAppContext,
419 ) -> Vec<String> {
420 inventory.update(cx, |inventory, cx| {
421 let (used, current) = inventory.used_and_current_resolved_tasks(
422 None,
423 worktree,
424 TaskContext::default(),
425 cx,
426 );
427 used.into_iter()
428 .chain(current)
429 .map(|(_, task)| task.original_task.label)
430 .collect()
431 })
432 }
433
434 pub(super) fn register_task_used(
435 inventory: &Model<Inventory>,
436 task_name: &str,
437 cx: &mut TestAppContext,
438 ) {
439 inventory.update(cx, |inventory, cx| {
440 let (task_source_kind, task) = inventory
441 .list_tasks(None, None, cx)
442 .into_iter()
443 .find(|(_, task)| task.label == task_name)
444 .unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
445 let id_base = task_source_kind.to_id_base();
446 inventory.task_scheduled(
447 task_source_kind.clone(),
448 task.resolve_task(&id_base, TaskContext::default())
449 .unwrap_or_else(|| panic!("Failed to resolve task with name {task_name}")),
450 );
451 });
452 }
453
454 pub(super) fn list_tasks(
455 inventory: &Model<Inventory>,
456 worktree: Option<WorktreeId>,
457 cx: &mut TestAppContext,
458 ) -> Vec<(TaskSourceKind, String)> {
459 inventory.update(cx, |inventory, cx| {
460 let (used, current) = inventory.used_and_current_resolved_tasks(
461 None,
462 worktree,
463 TaskContext::default(),
464 cx,
465 );
466 let mut all = used;
467 all.extend(current);
468 all.into_iter()
469 .map(|(source_kind, task)| (source_kind, task.resolved_label))
470 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
471 .collect()
472 })
473 }
474}
475
476#[cfg(test)]
477mod tests {
478 use gpui::TestAppContext;
479
480 use super::test_inventory::*;
481 use super::*;
482
483 #[gpui::test]
484 fn test_task_list_sorting(cx: &mut TestAppContext) {
485 let inventory = cx.update(Inventory::new);
486 let initial_tasks = resolved_task_names(&inventory, None, cx);
487 assert!(
488 initial_tasks.is_empty(),
489 "No tasks expected for empty inventory, but got {initial_tasks:?}"
490 );
491 let initial_tasks = task_template_names(&inventory, None, cx);
492 assert!(
493 initial_tasks.is_empty(),
494 "No tasks expected for empty inventory, but got {initial_tasks:?}"
495 );
496
497 inventory.update(cx, |inventory, cx| {
498 inventory.add_source(
499 TaskSourceKind::UserInput,
500 |cx| StaticTestSource::new(vec!["3_task".to_string()], cx),
501 cx,
502 );
503 });
504 inventory.update(cx, |inventory, cx| {
505 inventory.add_source(
506 TaskSourceKind::UserInput,
507 |cx| {
508 StaticTestSource::new(
509 vec![
510 "1_task".to_string(),
511 "2_task".to_string(),
512 "1_a_task".to_string(),
513 ],
514 cx,
515 )
516 },
517 cx,
518 );
519 });
520
521 let expected_initial_state = [
522 "1_a_task".to_string(),
523 "1_task".to_string(),
524 "2_task".to_string(),
525 "3_task".to_string(),
526 ];
527 assert_eq!(
528 task_template_names(&inventory, None, cx),
529 &expected_initial_state,
530 );
531 assert_eq!(
532 resolved_task_names(&inventory, None, cx),
533 &expected_initial_state,
534 "Tasks with equal amount of usages should be sorted alphanumerically"
535 );
536
537 register_task_used(&inventory, "2_task", cx);
538 assert_eq!(
539 task_template_names(&inventory, None, cx),
540 &expected_initial_state,
541 );
542 assert_eq!(
543 resolved_task_names(&inventory, None, cx),
544 vec![
545 "2_task".to_string(),
546 "1_a_task".to_string(),
547 "1_task".to_string(),
548 "3_task".to_string()
549 ],
550 );
551
552 register_task_used(&inventory, "1_task", cx);
553 register_task_used(&inventory, "1_task", cx);
554 register_task_used(&inventory, "1_task", cx);
555 register_task_used(&inventory, "3_task", cx);
556 assert_eq!(
557 task_template_names(&inventory, None, cx),
558 &expected_initial_state,
559 );
560 assert_eq!(
561 resolved_task_names(&inventory, None, cx),
562 vec![
563 "3_task".to_string(),
564 "1_task".to_string(),
565 "2_task".to_string(),
566 "1_a_task".to_string(),
567 ],
568 );
569
570 inventory.update(cx, |inventory, cx| {
571 inventory.add_source(
572 TaskSourceKind::UserInput,
573 |cx| {
574 StaticTestSource::new(vec!["10_hello".to_string(), "11_hello".to_string()], cx)
575 },
576 cx,
577 );
578 });
579 let expected_updated_state = [
580 "10_hello".to_string(),
581 "11_hello".to_string(),
582 "1_a_task".to_string(),
583 "1_task".to_string(),
584 "2_task".to_string(),
585 "3_task".to_string(),
586 ];
587 assert_eq!(
588 task_template_names(&inventory, None, cx),
589 &expected_updated_state,
590 );
591 assert_eq!(
592 resolved_task_names(&inventory, None, cx),
593 vec![
594 "3_task".to_string(),
595 "1_task".to_string(),
596 "2_task".to_string(),
597 "1_a_task".to_string(),
598 "10_hello".to_string(),
599 "11_hello".to_string(),
600 ],
601 );
602
603 register_task_used(&inventory, "11_hello", cx);
604 assert_eq!(
605 task_template_names(&inventory, None, cx),
606 &expected_updated_state,
607 );
608 assert_eq!(
609 resolved_task_names(&inventory, None, cx),
610 vec![
611 "11_hello".to_string(),
612 "3_task".to_string(),
613 "1_task".to_string(),
614 "2_task".to_string(),
615 "1_a_task".to_string(),
616 "10_hello".to_string(),
617 ],
618 );
619 }
620
621 #[gpui::test]
622 fn test_inventory_static_task_filters(cx: &mut TestAppContext) {
623 let inventory_with_statics = cx.update(Inventory::new);
624 let common_name = "common_task_name";
625 let path_1 = Path::new("path_1");
626 let path_2 = Path::new("path_2");
627 let worktree_1 = WorktreeId::from_usize(1);
628 let worktree_path_1 = Path::new("worktree_path_1");
629 let worktree_2 = WorktreeId::from_usize(2);
630 let worktree_path_2 = Path::new("worktree_path_2");
631 inventory_with_statics.update(cx, |inventory, cx| {
632 inventory.add_source(
633 TaskSourceKind::UserInput,
634 |cx| {
635 StaticTestSource::new(
636 vec!["user_input".to_string(), common_name.to_string()],
637 cx,
638 )
639 },
640 cx,
641 );
642 inventory.add_source(
643 TaskSourceKind::AbsPath {
644 id_base: "test source",
645 abs_path: path_1.to_path_buf(),
646 },
647 |cx| {
648 StaticTestSource::new(
649 vec!["static_source_1".to_string(), common_name.to_string()],
650 cx,
651 )
652 },
653 cx,
654 );
655 inventory.add_source(
656 TaskSourceKind::AbsPath {
657 id_base: "test source",
658 abs_path: path_2.to_path_buf(),
659 },
660 |cx| {
661 StaticTestSource::new(
662 vec!["static_source_2".to_string(), common_name.to_string()],
663 cx,
664 )
665 },
666 cx,
667 );
668 inventory.add_source(
669 TaskSourceKind::Worktree {
670 id: worktree_1,
671 abs_path: worktree_path_1.to_path_buf(),
672 id_base: "test_source",
673 },
674 |cx| {
675 StaticTestSource::new(
676 vec!["worktree_1".to_string(), common_name.to_string()],
677 cx,
678 )
679 },
680 cx,
681 );
682 inventory.add_source(
683 TaskSourceKind::Worktree {
684 id: worktree_2,
685 abs_path: worktree_path_2.to_path_buf(),
686 id_base: "test_source",
687 },
688 |cx| {
689 StaticTestSource::new(
690 vec!["worktree_2".to_string(), common_name.to_string()],
691 cx,
692 )
693 },
694 cx,
695 );
696 });
697
698 let worktree_independent_tasks = vec![
699 (
700 TaskSourceKind::AbsPath {
701 id_base: "test source",
702 abs_path: path_1.to_path_buf(),
703 },
704 common_name.to_string(),
705 ),
706 (
707 TaskSourceKind::AbsPath {
708 id_base: "test source",
709 abs_path: path_1.to_path_buf(),
710 },
711 "static_source_1".to_string(),
712 ),
713 (
714 TaskSourceKind::AbsPath {
715 id_base: "test source",
716 abs_path: path_2.to_path_buf(),
717 },
718 common_name.to_string(),
719 ),
720 (
721 TaskSourceKind::AbsPath {
722 id_base: "test source",
723 abs_path: path_2.to_path_buf(),
724 },
725 "static_source_2".to_string(),
726 ),
727 (TaskSourceKind::UserInput, common_name.to_string()),
728 (TaskSourceKind::UserInput, "user_input".to_string()),
729 ];
730 let worktree_1_tasks = [
731 (
732 TaskSourceKind::Worktree {
733 id: worktree_1,
734 abs_path: worktree_path_1.to_path_buf(),
735 id_base: "test_source",
736 },
737 common_name.to_string(),
738 ),
739 (
740 TaskSourceKind::Worktree {
741 id: worktree_1,
742 abs_path: worktree_path_1.to_path_buf(),
743 id_base: "test_source",
744 },
745 "worktree_1".to_string(),
746 ),
747 ];
748 let worktree_2_tasks = [
749 (
750 TaskSourceKind::Worktree {
751 id: worktree_2,
752 abs_path: worktree_path_2.to_path_buf(),
753 id_base: "test_source",
754 },
755 common_name.to_string(),
756 ),
757 (
758 TaskSourceKind::Worktree {
759 id: worktree_2,
760 abs_path: worktree_path_2.to_path_buf(),
761 id_base: "test_source",
762 },
763 "worktree_2".to_string(),
764 ),
765 ];
766
767 let all_tasks = worktree_1_tasks
768 .iter()
769 .chain(worktree_2_tasks.iter())
770 // worktree-less tasks come later in the list
771 .chain(worktree_independent_tasks.iter())
772 .cloned()
773 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
774 .collect::<Vec<_>>();
775
776 assert_eq!(list_tasks(&inventory_with_statics, None, cx), all_tasks);
777 assert_eq!(
778 list_tasks(&inventory_with_statics, Some(worktree_1), cx),
779 worktree_1_tasks
780 .iter()
781 .chain(worktree_independent_tasks.iter())
782 .cloned()
783 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
784 .collect::<Vec<_>>(),
785 );
786 assert_eq!(
787 list_tasks(&inventory_with_statics, Some(worktree_2), cx),
788 worktree_2_tasks
789 .iter()
790 .chain(worktree_independent_tasks.iter())
791 .cloned()
792 .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
793 .collect::<Vec<_>>(),
794 );
795 }
796}