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