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