task_inventory.rs

  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}