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, Task};
 14use itertools::Itertools;
 15use language::{ContextProvider, File, Language, LanguageToolchainStore, Location};
 16use settings::{parse_json_with_comments, SettingsLocation};
 17use task::{
 18    ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates, TaskVariables, VariableName,
 19};
 20use text::{Point, ToPoint};
 21use util::{post_inc, NumericPrefixWithSuffix, ResultExt as _};
 22use worktree::WorktreeId;
 23
 24use crate::worktree_store::WorktreeStore;
 25
 26/// Inventory tracks available tasks for a given project.
 27#[derive(Debug, Default)]
 28pub struct Inventory {
 29    last_scheduled_tasks: VecDeque<(TaskSourceKind, ResolvedTask)>,
 30    templates_from_settings: ParsedTemplates,
 31}
 32
 33#[derive(Debug, Default)]
 34struct ParsedTemplates {
 35    global: Vec<TaskTemplate>,
 36    worktree: HashMap<WorktreeId, HashMap<Arc<Path>, Vec<TaskTemplate>>>,
 37}
 38
 39/// Kind of a source the tasks are fetched from, used to display more source information in the UI.
 40#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
 41pub enum TaskSourceKind {
 42    /// bash-like commands spawned by users, not associated with any path
 43    UserInput,
 44    /// Tasks from the worktree's .zed/task.json
 45    Worktree {
 46        id: WorktreeId,
 47        directory_in_worktree: PathBuf,
 48        id_base: Cow<'static, str>,
 49    },
 50    /// ~/.config/zed/task.json - like global files with task definitions, applicable to any path
 51    AbsPath {
 52        id_base: Cow<'static, str>,
 53        abs_path: PathBuf,
 54    },
 55    /// Languages-specific tasks coming from extensions.
 56    Language { name: Arc<str> },
 57}
 58
 59impl TaskSourceKind {
 60    pub fn to_id_base(&self) -> String {
 61        match self {
 62            TaskSourceKind::UserInput => "oneshot".to_string(),
 63            TaskSourceKind::AbsPath { id_base, abs_path } => {
 64                format!("{id_base}_{}", abs_path.display())
 65            }
 66            TaskSourceKind::Worktree {
 67                id,
 68                id_base,
 69                directory_in_worktree,
 70            } => {
 71                format!("{id_base}_{id}_{}", directory_in_worktree.display())
 72            }
 73            TaskSourceKind::Language { name } => format!("language_{name}"),
 74        }
 75    }
 76}
 77
 78impl Inventory {
 79    pub fn new(cx: &mut AppContext) -> Model<Self> {
 80        cx.new_model(|_| Self::default())
 81    }
 82
 83    /// Pulls its task sources relevant to the worktree and the language given,
 84    /// returns all task templates with their source kinds, 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: &AppContext,
 92    ) -> Vec<(TaskSourceKind, TaskTemplate)> {
 93        let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
 94            name: language.name().0,
 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 contenxt 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: &AppContext,
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().0,
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, Default::default(), 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::{Model, TestAppContext};
375    use itertools::Itertools;
376    use task::TaskContext;
377    use worktree::WorktreeId;
378
379    use crate::Inventory;
380
381    use super::{task_source_kind_preference, TaskSourceKind};
382
383    pub(super) fn task_template_names(
384        inventory: &Model<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: &Model<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, Default::default(), &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: &Model<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    pub(super) async fn list_tasks_sorted_by_last_used(
438        inventory: &Model<Inventory>,
439        worktree: Option<WorktreeId>,
440        cx: &mut TestAppContext,
441    ) -> Vec<(TaskSourceKind, String)> {
442        let (used, current) = inventory.update(cx, |inventory, cx| {
443            inventory.used_and_current_resolved_tasks(worktree, None, &TaskContext::default(), cx)
444        });
445        let mut all = used;
446        all.extend(current);
447        all.into_iter()
448            .map(|(source_kind, task)| (source_kind, task.resolved_label))
449            .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
450            .collect()
451    }
452}
453
454/// A context provided that tries to provide values for all non-custom [`VariableName`] variants for a currently opened file.
455/// Applied as a base for every custom [`ContextProvider`] unless explicitly oped out.
456pub struct BasicContextProvider {
457    worktree_store: Model<WorktreeStore>,
458}
459
460impl BasicContextProvider {
461    pub fn new(worktree_store: Model<WorktreeStore>) -> Self {
462        Self { worktree_store }
463    }
464}
465impl ContextProvider for BasicContextProvider {
466    fn build_context(
467        &self,
468        _: &TaskVariables,
469        location: &Location,
470        _: Option<HashMap<String, String>>,
471        _: Arc<dyn LanguageToolchainStore>,
472        cx: &mut AppContext,
473    ) -> Task<Result<TaskVariables>> {
474        let buffer = location.buffer.read(cx);
475        let buffer_snapshot = buffer.snapshot();
476        let symbols = buffer_snapshot.symbols_containing(location.range.start, None);
477        let symbol = symbols.unwrap_or_default().last().map(|symbol| {
478            let range = symbol
479                .name_ranges
480                .last()
481                .cloned()
482                .unwrap_or(0..symbol.text.len());
483            symbol.text[range].to_string()
484        });
485
486        let current_file = buffer
487            .file()
488            .and_then(|file| file.as_local())
489            .map(|file| file.abs_path(cx).to_string_lossy().to_string());
490        let Point { row, column } = location.range.start.to_point(&buffer_snapshot);
491        let row = row + 1;
492        let column = column + 1;
493        let selected_text = buffer
494            .chars_for_range(location.range.clone())
495            .collect::<String>();
496
497        let mut task_variables = TaskVariables::from_iter([
498            (VariableName::Row, row.to_string()),
499            (VariableName::Column, column.to_string()),
500        ]);
501
502        if let Some(symbol) = symbol {
503            task_variables.insert(VariableName::Symbol, symbol);
504        }
505        if !selected_text.trim().is_empty() {
506            task_variables.insert(VariableName::SelectedText, selected_text);
507        }
508        let worktree_abs_path =
509            buffer
510                .file()
511                .map(|file| file.worktree_id(cx))
512                .and_then(|worktree_id| {
513                    self.worktree_store
514                        .read(cx)
515                        .worktree_for_id(worktree_id, cx)
516                        .map(|worktree| worktree.read(cx).abs_path())
517                });
518        if let Some(worktree_path) = worktree_abs_path {
519            task_variables.insert(
520                VariableName::WorktreeRoot,
521                worktree_path.to_string_lossy().to_string(),
522            );
523            if let Some(full_path) = current_file.as_ref() {
524                let relative_path = pathdiff::diff_paths(full_path, worktree_path);
525                if let Some(relative_path) = relative_path {
526                    task_variables.insert(
527                        VariableName::RelativeFile,
528                        relative_path.to_string_lossy().into_owned(),
529                    );
530                }
531            }
532        }
533
534        if let Some(path_as_string) = current_file {
535            let path = Path::new(&path_as_string);
536            if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
537                task_variables.insert(VariableName::Filename, String::from(filename));
538            }
539
540            if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
541                task_variables.insert(VariableName::Stem, stem.into());
542            }
543
544            if let Some(dirname) = path.parent().and_then(|s| s.to_str()) {
545                task_variables.insert(VariableName::Dirname, dirname.into());
546            }
547
548            task_variables.insert(VariableName::File, path_as_string);
549        }
550
551        Task::ready(Ok(task_variables))
552    }
553}
554
555/// A ContextProvider that doesn't provide any task variables on it's own, though it has some associated tasks.
556pub struct ContextProviderWithTasks {
557    templates: TaskTemplates,
558}
559
560impl ContextProviderWithTasks {
561    pub fn new(definitions: TaskTemplates) -> Self {
562        Self {
563            templates: definitions,
564        }
565    }
566}
567
568impl ContextProvider for ContextProviderWithTasks {
569    fn associated_tasks(
570        &self,
571        _: Option<Arc<dyn language::File>>,
572        _: &AppContext,
573    ) -> Option<TaskTemplates> {
574        Some(self.templates.clone())
575    }
576}
577
578#[cfg(test)]
579mod tests {
580    use gpui::TestAppContext;
581    use pretty_assertions::assert_eq;
582    use serde_json::json;
583
584    use crate::task_store::TaskStore;
585
586    use super::test_inventory::*;
587    use super::*;
588
589    #[gpui::test]
590    async fn test_task_list_sorting(cx: &mut TestAppContext) {
591        init_test(cx);
592        let inventory = cx.update(Inventory::new);
593        let initial_tasks = resolved_task_names(&inventory, None, cx).await;
594        assert!(
595            initial_tasks.is_empty(),
596            "No tasks expected for empty inventory, but got {initial_tasks:?}"
597        );
598        let initial_tasks = task_template_names(&inventory, None, cx);
599        assert!(
600            initial_tasks.is_empty(),
601            "No tasks expected for empty inventory, but got {initial_tasks:?}"
602        );
603        cx.run_until_parked();
604        let expected_initial_state = [
605            "1_a_task".to_string(),
606            "1_task".to_string(),
607            "2_task".to_string(),
608            "3_task".to_string(),
609        ];
610
611        inventory.update(cx, |inventory, _| {
612            inventory
613                .update_file_based_tasks(
614                    None,
615                    Some(&mock_tasks_from_names(
616                        expected_initial_state.iter().map(|name| name.as_str()),
617                    )),
618                )
619                .unwrap();
620        });
621        assert_eq!(
622            task_template_names(&inventory, None, cx),
623            &expected_initial_state,
624        );
625        assert_eq!(
626            resolved_task_names(&inventory, None, cx).await,
627            &expected_initial_state,
628            "Tasks with equal amount of usages should be sorted alphanumerically"
629        );
630
631        register_task_used(&inventory, "2_task", cx);
632        assert_eq!(
633            task_template_names(&inventory, None, cx),
634            &expected_initial_state,
635        );
636        assert_eq!(
637            resolved_task_names(&inventory, None, cx).await,
638            vec![
639                "2_task".to_string(),
640                "1_a_task".to_string(),
641                "1_task".to_string(),
642                "3_task".to_string()
643            ],
644        );
645
646        register_task_used(&inventory, "1_task", cx);
647        register_task_used(&inventory, "1_task", cx);
648        register_task_used(&inventory, "1_task", cx);
649        register_task_used(&inventory, "3_task", cx);
650        assert_eq!(
651            task_template_names(&inventory, None, cx),
652            &expected_initial_state,
653        );
654        assert_eq!(
655            resolved_task_names(&inventory, None, cx).await,
656            vec![
657                "3_task".to_string(),
658                "1_task".to_string(),
659                "2_task".to_string(),
660                "1_a_task".to_string(),
661            ],
662        );
663
664        inventory.update(cx, |inventory, _| {
665            inventory
666                .update_file_based_tasks(
667                    None,
668                    Some(&mock_tasks_from_names(
669                        ["10_hello", "11_hello"]
670                            .into_iter()
671                            .chain(expected_initial_state.iter().map(|name| name.as_str())),
672                    )),
673                )
674                .unwrap();
675        });
676        cx.run_until_parked();
677        let expected_updated_state = [
678            "10_hello".to_string(),
679            "11_hello".to_string(),
680            "1_a_task".to_string(),
681            "1_task".to_string(),
682            "2_task".to_string(),
683            "3_task".to_string(),
684        ];
685        assert_eq!(
686            task_template_names(&inventory, None, cx),
687            &expected_updated_state,
688        );
689        assert_eq!(
690            resolved_task_names(&inventory, None, cx).await,
691            vec![
692                "3_task".to_string(),
693                "1_task".to_string(),
694                "2_task".to_string(),
695                "1_a_task".to_string(),
696                "10_hello".to_string(),
697                "11_hello".to_string(),
698            ],
699        );
700
701        register_task_used(&inventory, "11_hello", cx);
702        assert_eq!(
703            task_template_names(&inventory, None, cx),
704            &expected_updated_state,
705        );
706        assert_eq!(
707            resolved_task_names(&inventory, None, cx).await,
708            vec![
709                "11_hello".to_string(),
710                "3_task".to_string(),
711                "1_task".to_string(),
712                "2_task".to_string(),
713                "1_a_task".to_string(),
714                "10_hello".to_string(),
715            ],
716        );
717    }
718
719    #[gpui::test]
720    async fn test_inventory_static_task_filters(cx: &mut TestAppContext) {
721        init_test(cx);
722        let inventory = cx.update(Inventory::new);
723        let common_name = "common_task_name";
724        let worktree_1 = WorktreeId::from_usize(1);
725        let worktree_2 = WorktreeId::from_usize(2);
726
727        cx.run_until_parked();
728        let worktree_independent_tasks = vec![
729            (
730                TaskSourceKind::AbsPath {
731                    id_base: "global tasks.json".into(),
732                    abs_path: paths::tasks_file().clone(),
733                },
734                common_name.to_string(),
735            ),
736            (
737                TaskSourceKind::AbsPath {
738                    id_base: "global tasks.json".into(),
739                    abs_path: paths::tasks_file().clone(),
740                },
741                "static_source_1".to_string(),
742            ),
743            (
744                TaskSourceKind::AbsPath {
745                    id_base: "global tasks.json".into(),
746                    abs_path: paths::tasks_file().clone(),
747                },
748                "static_source_2".to_string(),
749            ),
750        ];
751        let worktree_1_tasks = [
752            (
753                TaskSourceKind::Worktree {
754                    id: worktree_1,
755                    directory_in_worktree: PathBuf::from(".zed"),
756                    id_base: "local worktree tasks from directory \".zed\"".into(),
757                },
758                common_name.to_string(),
759            ),
760            (
761                TaskSourceKind::Worktree {
762                    id: worktree_1,
763                    directory_in_worktree: PathBuf::from(".zed"),
764                    id_base: "local worktree tasks from directory \".zed\"".into(),
765                },
766                "worktree_1".to_string(),
767            ),
768        ];
769        let worktree_2_tasks = [
770            (
771                TaskSourceKind::Worktree {
772                    id: worktree_2,
773                    directory_in_worktree: PathBuf::from(".zed"),
774                    id_base: "local worktree tasks from directory \".zed\"".into(),
775                },
776                common_name.to_string(),
777            ),
778            (
779                TaskSourceKind::Worktree {
780                    id: worktree_2,
781                    directory_in_worktree: PathBuf::from(".zed"),
782                    id_base: "local worktree tasks from directory \".zed\"".into(),
783                },
784                "worktree_2".to_string(),
785            ),
786        ];
787
788        inventory.update(cx, |inventory, _| {
789            inventory
790                .update_file_based_tasks(
791                    None,
792                    Some(&mock_tasks_from_names(
793                        worktree_independent_tasks
794                            .iter()
795                            .map(|(_, name)| name.as_str()),
796                    )),
797                )
798                .unwrap();
799            inventory
800                .update_file_based_tasks(
801                    Some(SettingsLocation {
802                        worktree_id: worktree_1,
803                        path: Path::new(".zed"),
804                    }),
805                    Some(&mock_tasks_from_names(
806                        worktree_1_tasks.iter().map(|(_, name)| name.as_str()),
807                    )),
808                )
809                .unwrap();
810            inventory
811                .update_file_based_tasks(
812                    Some(SettingsLocation {
813                        worktree_id: worktree_2,
814                        path: Path::new(".zed"),
815                    }),
816                    Some(&mock_tasks_from_names(
817                        worktree_2_tasks.iter().map(|(_, name)| name.as_str()),
818                    )),
819                )
820                .unwrap();
821        });
822
823        assert_eq!(
824            list_tasks_sorted_by_last_used(&inventory, None, cx).await,
825            worktree_independent_tasks,
826            "Without a worktree, only worktree-independent tasks should be listed"
827        );
828        assert_eq!(
829            list_tasks_sorted_by_last_used(&inventory, Some(worktree_1), cx).await,
830            worktree_1_tasks
831                .iter()
832                .chain(worktree_independent_tasks.iter())
833                .cloned()
834                .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
835                .collect::<Vec<_>>(),
836        );
837        assert_eq!(
838            list_tasks_sorted_by_last_used(&inventory, Some(worktree_2), cx).await,
839            worktree_2_tasks
840                .iter()
841                .chain(worktree_independent_tasks.iter())
842                .cloned()
843                .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
844                .collect::<Vec<_>>(),
845        );
846
847        assert_eq!(
848            list_tasks(&inventory, None, cx).await,
849            worktree_independent_tasks,
850            "Without a worktree, only worktree-independent tasks should be listed"
851        );
852        assert_eq!(
853            list_tasks(&inventory, Some(worktree_1), cx).await,
854            worktree_1_tasks
855                .iter()
856                .chain(worktree_independent_tasks.iter())
857                .cloned()
858                .collect::<Vec<_>>(),
859        );
860        assert_eq!(
861            list_tasks(&inventory, Some(worktree_2), cx).await,
862            worktree_2_tasks
863                .iter()
864                .chain(worktree_independent_tasks.iter())
865                .cloned()
866                .collect::<Vec<_>>(),
867        );
868    }
869
870    fn init_test(_cx: &mut TestAppContext) {
871        if std::env::var("RUST_LOG").is_ok() {
872            env_logger::try_init().ok();
873        }
874        TaskStore::init(None);
875    }
876
877    pub(super) async fn resolved_task_names(
878        inventory: &Model<Inventory>,
879        worktree: Option<WorktreeId>,
880        cx: &mut TestAppContext,
881    ) -> Vec<String> {
882        let (used, current) = inventory.update(cx, |inventory, cx| {
883            inventory.used_and_current_resolved_tasks(worktree, None, &TaskContext::default(), cx)
884        });
885        used.into_iter()
886            .chain(current)
887            .map(|(_, task)| task.original_task().label.clone())
888            .collect()
889    }
890
891    fn mock_tasks_from_names<'a>(task_names: impl Iterator<Item = &'a str> + 'a) -> String {
892        serde_json::to_string(&serde_json::Value::Array(
893            task_names
894                .map(|task_name| {
895                    json!({
896                        "label": task_name,
897                        "command": "echo",
898                        "args": vec![task_name],
899                    })
900                })
901                .collect::<Vec<_>>(),
902        ))
903        .unwrap()
904    }
905}