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