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