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