task_inventory.rs

  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};
 14use itertools::Itertools;
 15use language::{ContextProvider, File, Language, 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}
434
435impl ContextProvider for BasicContextProvider {
436    fn build_context(
437        &self,
438        _: &TaskVariables,
439        location: &Location,
440        _: Option<&HashMap<String, String>>,
441        cx: &mut AppContext,
442    ) -> 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        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}