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