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