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