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