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