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::Result;
  12use collections::{HashMap, HashSet, VecDeque};
  13use dap::DapRegistry;
  14use gpui::{App, AppContext as _, Entity, SharedString, Task};
  15use itertools::Itertools;
  16use language::{
  17    Buffer, ContextLocation, ContextProvider, File, Language, LanguageToolchainStore, Location,
  18    language_settings::language_settings,
  19};
  20use lsp::{LanguageServerId, LanguageServerName};
  21use paths::{debug_task_file_name, task_file_name};
  22use settings::{InvalidSettingsError, parse_json_with_comments};
  23use task::{
  24    DebugScenario, ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates, TaskVariables,
  25    VariableName,
  26};
  27use text::{BufferId, Point, ToPoint};
  28use util::{NumericPrefixWithSuffix, ResultExt as _, paths::PathExt as _, post_inc};
  29use worktree::WorktreeId;
  30
  31use crate::{task_store::TaskSettingsLocation, worktree_store::WorktreeStore};
  32
  33/// Inventory tracks available tasks for a given project.
  34#[derive(Debug, Default)]
  35pub struct Inventory {
  36    last_scheduled_tasks: VecDeque<(TaskSourceKind, ResolvedTask)>,
  37    last_scheduled_scenarios: VecDeque<DebugScenario>,
  38    templates_from_settings: InventoryFor<TaskTemplate>,
  39    scenarios_from_settings: InventoryFor<DebugScenario>,
  40}
  41
  42// Helper trait for better error messages in [InventoryFor]
  43trait InventoryContents: Clone {
  44    const GLOBAL_SOURCE_FILE: &'static str;
  45    const LABEL: &'static str;
  46}
  47
  48impl InventoryContents for TaskTemplate {
  49    const GLOBAL_SOURCE_FILE: &'static str = "tasks.json";
  50    const LABEL: &'static str = "tasks";
  51}
  52
  53impl InventoryContents for DebugScenario {
  54    const GLOBAL_SOURCE_FILE: &'static str = "debug.json";
  55
  56    const LABEL: &'static str = "debug scenarios";
  57}
  58
  59#[derive(Debug)]
  60struct InventoryFor<T> {
  61    global: HashMap<PathBuf, Vec<T>>,
  62    worktree: HashMap<WorktreeId, HashMap<Arc<Path>, Vec<T>>>,
  63}
  64
  65impl<T: InventoryContents> InventoryFor<T> {
  66    fn worktree_scenarios(
  67        &self,
  68        worktree: WorktreeId,
  69    ) -> impl '_ + Iterator<Item = (TaskSourceKind, T)> {
  70        self.worktree
  71            .get(&worktree)
  72            .into_iter()
  73            .flatten()
  74            .flat_map(|(directory, templates)| {
  75                templates.iter().map(move |template| (directory, template))
  76            })
  77            .map(move |(directory, template)| {
  78                (
  79                    TaskSourceKind::Worktree {
  80                        id: worktree,
  81                        directory_in_worktree: directory.to_path_buf(),
  82                        id_base: Cow::Owned(format!(
  83                            "local worktree {} from directory {directory:?}",
  84                            T::LABEL
  85                        )),
  86                    },
  87                    template.clone(),
  88                )
  89            })
  90    }
  91
  92    fn global_scenarios(&self) -> impl '_ + Iterator<Item = (TaskSourceKind, T)> {
  93        self.global.iter().flat_map(|(file_path, templates)| {
  94            templates.into_iter().map(|template| {
  95                (
  96                    TaskSourceKind::AbsPath {
  97                        id_base: Cow::Owned(format!("global {}", T::GLOBAL_SOURCE_FILE)),
  98                        abs_path: file_path.clone(),
  99                    },
 100                    template.clone(),
 101                )
 102            })
 103        })
 104    }
 105}
 106
 107impl<T> Default for InventoryFor<T> {
 108    fn default() -> Self {
 109        Self {
 110            global: HashMap::default(),
 111            worktree: HashMap::default(),
 112        }
 113    }
 114}
 115
 116/// Kind of a source the tasks are fetched from, used to display more source information in the UI.
 117#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
 118pub enum TaskSourceKind {
 119    /// bash-like commands spawned by users, not associated with any path
 120    UserInput,
 121    /// Tasks from the worktree's .zed/task.json
 122    Worktree {
 123        id: WorktreeId,
 124        directory_in_worktree: PathBuf,
 125        id_base: Cow<'static, str>,
 126    },
 127    /// ~/.config/zed/task.json - like global files with task definitions, applicable to any path
 128    AbsPath {
 129        id_base: Cow<'static, str>,
 130        abs_path: PathBuf,
 131    },
 132    /// Languages-specific tasks coming from extensions.
 133    Language { name: SharedString },
 134    /// Language-specific tasks coming from LSP servers.
 135    Lsp(LanguageServerId),
 136}
 137
 138/// A collection of task contexts, derived from the current state of the workspace.
 139/// Only contains worktrees that are visible and with their root being a directory.
 140#[derive(Debug, Default)]
 141pub struct TaskContexts {
 142    /// A context, related to the currently opened item.
 143    /// Item can be opened from an invisible worktree, or any other, not necessarily active worktree.
 144    pub active_item_context: Option<(Option<WorktreeId>, Option<Location>, TaskContext)>,
 145    /// A worktree that corresponds to the active item, or the only worktree in the workspace.
 146    pub active_worktree_context: Option<(WorktreeId, TaskContext)>,
 147    /// If there are multiple worktrees in the workspace, all non-active ones are included here.
 148    pub other_worktree_contexts: Vec<(WorktreeId, TaskContext)>,
 149    pub lsp_task_sources: HashMap<LanguageServerName, Vec<BufferId>>,
 150    pub latest_selection: Option<text::Anchor>,
 151}
 152
 153impl TaskContexts {
 154    pub fn active_context(&self) -> Option<&TaskContext> {
 155        self.active_item_context
 156            .as_ref()
 157            .map(|(_, _, context)| context)
 158            .or_else(|| {
 159                self.active_worktree_context
 160                    .as_ref()
 161                    .map(|(_, context)| context)
 162            })
 163    }
 164
 165    pub fn location(&self) -> Option<&Location> {
 166        self.active_item_context
 167            .as_ref()
 168            .and_then(|(_, location, _)| location.as_ref())
 169    }
 170
 171    pub fn file(&self, cx: &App) -> Option<Arc<dyn File>> {
 172        self.active_item_context
 173            .as_ref()
 174            .and_then(|(_, location, _)| location.as_ref())
 175            .and_then(|location| location.buffer.read(cx).file().cloned())
 176    }
 177
 178    pub fn worktree(&self) -> Option<WorktreeId> {
 179        self.active_item_context
 180            .as_ref()
 181            .and_then(|(worktree_id, _, _)| worktree_id.as_ref())
 182            .or_else(|| {
 183                self.active_worktree_context
 184                    .as_ref()
 185                    .map(|(worktree_id, _)| worktree_id)
 186            })
 187            .copied()
 188    }
 189
 190    pub fn task_context_for_worktree_id(&self, worktree_id: WorktreeId) -> Option<&TaskContext> {
 191        self.active_worktree_context
 192            .iter()
 193            .chain(self.other_worktree_contexts.iter())
 194            .find(|(id, _)| *id == worktree_id)
 195            .map(|(_, context)| context)
 196    }
 197}
 198
 199impl TaskSourceKind {
 200    pub fn to_id_base(&self) -> String {
 201        match self {
 202            Self::UserInput => "oneshot".to_string(),
 203            Self::AbsPath { id_base, abs_path } => {
 204                format!("{id_base}_{}", abs_path.display())
 205            }
 206            Self::Worktree {
 207                id,
 208                id_base,
 209                directory_in_worktree,
 210            } => {
 211                format!("{id_base}_{id}_{}", directory_in_worktree.display())
 212            }
 213            Self::Language { name } => format!("language_{name}"),
 214            Self::Lsp(server_id) => format!("lsp_{server_id}"),
 215        }
 216    }
 217}
 218
 219impl Inventory {
 220    pub fn new(cx: &mut App) -> Entity<Self> {
 221        cx.new(|_| Self::default())
 222    }
 223
 224    pub fn scenario_scheduled(&mut self, scenario: DebugScenario) {
 225        self.last_scheduled_scenarios
 226            .retain(|s| s.label != scenario.label);
 227        self.last_scheduled_scenarios.push_back(scenario);
 228        if self.last_scheduled_scenarios.len() > 5_000 {
 229            self.last_scheduled_scenarios.pop_front();
 230        }
 231    }
 232
 233    pub fn last_scheduled_scenario(&self) -> Option<&DebugScenario> {
 234        self.last_scheduled_scenarios.back()
 235    }
 236
 237    pub fn list_debug_scenarios(
 238        &self,
 239        task_contexts: &TaskContexts,
 240        cx: &mut App,
 241    ) -> (Vec<DebugScenario>, Vec<(TaskSourceKind, DebugScenario)>) {
 242        let mut scenarios = Vec::new();
 243
 244        if let Some(worktree_id) = task_contexts
 245            .active_worktree_context
 246            .iter()
 247            .chain(task_contexts.other_worktree_contexts.iter())
 248            .map(|context| context.0)
 249            .next()
 250        {
 251            scenarios.extend(self.worktree_scenarios_from_settings(worktree_id));
 252        }
 253        scenarios.extend(self.global_debug_scenarios_from_settings());
 254
 255        let (_, new) = self.used_and_current_resolved_tasks(task_contexts, cx);
 256        if let Some(location) = task_contexts.location() {
 257            let file = location.buffer.read(cx).file();
 258            let language = location.buffer.read(cx).language();
 259            let language_name = language.as_ref().map(|l| l.name());
 260            let adapter = language_settings(language_name, file, cx)
 261                .debuggers
 262                .first()
 263                .map(SharedString::from)
 264                .or_else(|| {
 265                    language.and_then(|l| l.config().debuggers.first().map(SharedString::from))
 266                });
 267            if let Some(adapter) = adapter {
 268                for (kind, task) in new {
 269                    if let Some(scenario) =
 270                        DapRegistry::global(cx)
 271                            .locators()
 272                            .values()
 273                            .find_map(|locator| {
 274                                locator.create_scenario(
 275                                    &task.original_task().clone(),
 276                                    &task.display_label(),
 277                                    adapter.clone().into(),
 278                                )
 279                            })
 280                    {
 281                        scenarios.push((kind, scenario));
 282                    }
 283                }
 284            }
 285        }
 286
 287        (
 288            self.last_scheduled_scenarios.iter().cloned().collect(),
 289            scenarios,
 290        )
 291    }
 292
 293    pub fn task_template_by_label(
 294        &self,
 295        buffer: Option<Entity<Buffer>>,
 296        worktree_id: Option<WorktreeId>,
 297        label: &str,
 298        cx: &App,
 299    ) -> Option<TaskTemplate> {
 300        let (buffer_worktree_id, file, language) = buffer
 301            .map(|buffer| {
 302                let buffer = buffer.read(cx);
 303                let file = buffer.file().cloned();
 304                (
 305                    file.as_ref().map(|file| file.worktree_id(cx)),
 306                    file,
 307                    buffer.language().cloned(),
 308                )
 309            })
 310            .unwrap_or((None, None, None));
 311
 312        self.list_tasks(file, language, worktree_id.or(buffer_worktree_id), cx)
 313            .into_iter()
 314            .find(|(_, template)| template.label == label)
 315            .map(|val| val.1)
 316    }
 317
 318    /// Pulls its task sources relevant to the worktree and the language given,
 319    /// returns all task templates with their source kinds, worktree tasks first, language tasks second
 320    /// and global tasks last. No specific order inside source kinds groups.
 321    pub fn list_tasks(
 322        &self,
 323        file: Option<Arc<dyn File>>,
 324        language: Option<Arc<Language>>,
 325        worktree: Option<WorktreeId>,
 326        cx: &App,
 327    ) -> Vec<(TaskSourceKind, TaskTemplate)> {
 328        let global_tasks = self.global_templates_from_settings();
 329        let worktree_tasks = worktree
 330            .into_iter()
 331            .flat_map(|worktree| self.worktree_templates_from_settings(worktree));
 332        let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
 333            name: language.name().into(),
 334        });
 335        let language_tasks = language
 336            .filter(|language| {
 337                language_settings(Some(language.name()), file.as_ref(), cx)
 338                    .tasks
 339                    .enabled
 340            })
 341            .and_then(|language| language.context_provider()?.associated_tasks(file, cx))
 342            .into_iter()
 343            .flat_map(|tasks| tasks.0.into_iter())
 344            .flat_map(|task| Some((task_source_kind.clone()?, task)));
 345
 346        worktree_tasks
 347            .chain(language_tasks)
 348            .chain(global_tasks)
 349            .collect()
 350    }
 351
 352    /// Pulls its task sources relevant to the worktree and the language given and resolves them with the [`TaskContexts`] given.
 353    /// Joins the new resolutions with the resolved tasks that were used (spawned) before,
 354    /// orders them so that the most recently used come first, all equally used ones are ordered so that the most specific tasks come first.
 355    /// Deduplicates the tasks by their labels and context and splits the ordered list into two: used tasks and the rest, newly resolved tasks.
 356    pub fn used_and_current_resolved_tasks<'a>(
 357        &'a self,
 358        task_contexts: &'a TaskContexts,
 359        cx: &'a App,
 360    ) -> (
 361        Vec<(TaskSourceKind, ResolvedTask)>,
 362        Vec<(TaskSourceKind, ResolvedTask)>,
 363    ) {
 364        let worktree = task_contexts.worktree();
 365        let location = task_contexts.location();
 366        let language = location
 367            .and_then(|location| location.buffer.read(cx).language_at(location.range.start));
 368        let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
 369            name: language.name().into(),
 370        });
 371        let file = location.and_then(|location| location.buffer.read(cx).file().cloned());
 372
 373        let mut task_labels_to_ids = HashMap::<String, HashSet<TaskId>>::default();
 374        let mut lru_score = 0_u32;
 375        let previously_spawned_tasks = self
 376            .last_scheduled_tasks
 377            .iter()
 378            .rev()
 379            .filter(|(task_kind, _)| {
 380                if matches!(task_kind, TaskSourceKind::Language { .. }) {
 381                    Some(task_kind) == task_source_kind.as_ref()
 382                } else {
 383                    true
 384                }
 385            })
 386            .filter(|(_, resolved_task)| {
 387                match task_labels_to_ids.entry(resolved_task.resolved_label.clone()) {
 388                    hash_map::Entry::Occupied(mut o) => {
 389                        o.get_mut().insert(resolved_task.id.clone());
 390                        // Neber allow duplicate reused tasks with the same labels
 391                        false
 392                    }
 393                    hash_map::Entry::Vacant(v) => {
 394                        v.insert(HashSet::from_iter(Some(resolved_task.id.clone())));
 395                        true
 396                    }
 397                }
 398            })
 399            .map(|(task_source_kind, resolved_task)| {
 400                (
 401                    task_source_kind.clone(),
 402                    resolved_task.clone(),
 403                    post_inc(&mut lru_score),
 404                )
 405            })
 406            .sorted_unstable_by(task_lru_comparator)
 407            .map(|(kind, task, _)| (kind, task))
 408            .collect::<Vec<_>>();
 409
 410        let not_used_score = post_inc(&mut lru_score);
 411        let global_tasks = self.global_templates_from_settings();
 412
 413        let language_tasks = language
 414            .filter(|language| {
 415                language_settings(Some(language.name()), file.as_ref(), cx)
 416                    .tasks
 417                    .enabled
 418            })
 419            .and_then(|language| language.context_provider()?.associated_tasks(file, cx))
 420            .into_iter()
 421            .flat_map(|tasks| tasks.0.into_iter())
 422            .flat_map(|task| Some((task_source_kind.clone()?, task)));
 423        let worktree_tasks = worktree
 424            .into_iter()
 425            .flat_map(|worktree| self.worktree_templates_from_settings(worktree))
 426            .chain(language_tasks)
 427            .chain(global_tasks);
 428
 429        let new_resolved_tasks = worktree_tasks
 430            .flat_map(|(kind, task)| {
 431                let id_base = kind.to_id_base();
 432                if let TaskSourceKind::Worktree { id, .. } = &kind {
 433                    None.or_else(|| {
 434                        let (_, _, item_context) = task_contexts
 435                            .active_item_context
 436                            .as_ref()
 437                            .filter(|(worktree_id, _, _)| Some(id) == worktree_id.as_ref())?;
 438                        task.resolve_task(&id_base, item_context)
 439                    })
 440                    .or_else(|| {
 441                        let (_, worktree_context) = task_contexts
 442                            .active_worktree_context
 443                            .as_ref()
 444                            .filter(|(worktree_id, _)| id == worktree_id)?;
 445                        task.resolve_task(&id_base, worktree_context)
 446                    })
 447                    .or_else(|| {
 448                        if let TaskSourceKind::Worktree { id, .. } = &kind {
 449                            let worktree_context = task_contexts
 450                                .other_worktree_contexts
 451                                .iter()
 452                                .find(|(worktree_id, _)| worktree_id == id)
 453                                .map(|(_, context)| context)?;
 454                            task.resolve_task(&id_base, worktree_context)
 455                        } else {
 456                            None
 457                        }
 458                    })
 459                } else {
 460                    None.or_else(|| {
 461                        let (_, _, item_context) = task_contexts.active_item_context.as_ref()?;
 462                        task.resolve_task(&id_base, item_context)
 463                    })
 464                    .or_else(|| {
 465                        let (_, worktree_context) =
 466                            task_contexts.active_worktree_context.as_ref()?;
 467                        task.resolve_task(&id_base, worktree_context)
 468                    })
 469                }
 470                .or_else(|| task.resolve_task(&id_base, &TaskContext::default()))
 471                .map(move |resolved_task| (kind.clone(), resolved_task, not_used_score))
 472            })
 473            .filter(|(_, resolved_task, _)| {
 474                match task_labels_to_ids.entry(resolved_task.resolved_label.clone()) {
 475                    hash_map::Entry::Occupied(mut o) => {
 476                        // Allow new tasks with the same label, if their context is different
 477                        o.get_mut().insert(resolved_task.id.clone())
 478                    }
 479                    hash_map::Entry::Vacant(v) => {
 480                        v.insert(HashSet::from_iter(Some(resolved_task.id.clone())));
 481                        true
 482                    }
 483                }
 484            })
 485            .sorted_unstable_by(task_lru_comparator)
 486            .map(|(kind, task, _)| (kind, task))
 487            .collect::<Vec<_>>();
 488
 489        (previously_spawned_tasks, new_resolved_tasks)
 490    }
 491
 492    /// Returns the last scheduled task by task_id if provided.
 493    /// Otherwise, returns the last scheduled task.
 494    pub fn last_scheduled_task(
 495        &self,
 496        task_id: Option<&TaskId>,
 497    ) -> Option<(TaskSourceKind, ResolvedTask)> {
 498        if let Some(task_id) = task_id {
 499            self.last_scheduled_tasks
 500                .iter()
 501                .find(|(_, task)| &task.id == task_id)
 502                .cloned()
 503        } else {
 504            self.last_scheduled_tasks.back().cloned()
 505        }
 506    }
 507
 508    /// Registers task "usage" as being scheduled – to be used for LRU sorting when listing all tasks.
 509    pub fn task_scheduled(
 510        &mut self,
 511        task_source_kind: TaskSourceKind,
 512        resolved_task: ResolvedTask,
 513    ) {
 514        self.last_scheduled_tasks
 515            .push_back((task_source_kind, resolved_task));
 516        if self.last_scheduled_tasks.len() > 5_000 {
 517            self.last_scheduled_tasks.pop_front();
 518        }
 519    }
 520
 521    /// Deletes a resolved task from history, using its id.
 522    /// A similar may still resurface in `used_and_current_resolved_tasks` when its [`TaskTemplate`] is resolved again.
 523    pub fn delete_previously_used(&mut self, id: &TaskId) {
 524        self.last_scheduled_tasks.retain(|(_, task)| &task.id != id);
 525    }
 526
 527    fn global_templates_from_settings(
 528        &self,
 529    ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
 530        self.templates_from_settings.global_scenarios()
 531    }
 532
 533    fn global_debug_scenarios_from_settings(
 534        &self,
 535    ) -> impl '_ + Iterator<Item = (TaskSourceKind, DebugScenario)> {
 536        self.scenarios_from_settings.global_scenarios()
 537    }
 538
 539    fn worktree_scenarios_from_settings(
 540        &self,
 541        worktree: WorktreeId,
 542    ) -> impl '_ + Iterator<Item = (TaskSourceKind, DebugScenario)> {
 543        self.scenarios_from_settings.worktree_scenarios(worktree)
 544    }
 545
 546    fn worktree_templates_from_settings(
 547        &self,
 548        worktree: WorktreeId,
 549    ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
 550        self.templates_from_settings.worktree_scenarios(worktree)
 551    }
 552
 553    /// Updates in-memory task metadata from the JSON string given.
 554    /// Will fail if the JSON is not a valid array of objects, but will continue if any object will not parse into a [`TaskTemplate`].
 555    ///
 556    /// Global tasks are updated for no worktree provided, otherwise the worktree metadata for a given path will be updated.
 557    pub(crate) fn update_file_based_tasks(
 558        &mut self,
 559        location: TaskSettingsLocation<'_>,
 560        raw_tasks_json: Option<&str>,
 561    ) -> Result<(), InvalidSettingsError> {
 562        let raw_tasks = match parse_json_with_comments::<Vec<serde_json::Value>>(
 563            raw_tasks_json.unwrap_or("[]"),
 564        ) {
 565            Ok(tasks) => tasks,
 566            Err(e) => {
 567                return Err(InvalidSettingsError::Tasks {
 568                    path: match location {
 569                        TaskSettingsLocation::Global(path) => path.to_owned(),
 570                        TaskSettingsLocation::Worktree(settings_location) => {
 571                            settings_location.path.join(task_file_name())
 572                        }
 573                    },
 574                    message: format!("Failed to parse tasks file content as a JSON array: {e}"),
 575                });
 576            }
 577        };
 578        let new_templates = raw_tasks.into_iter().filter_map(|raw_template| {
 579            serde_json::from_value::<TaskTemplate>(raw_template).log_err()
 580        });
 581
 582        let parsed_templates = &mut self.templates_from_settings;
 583        match location {
 584            TaskSettingsLocation::Global(path) => {
 585                parsed_templates
 586                    .global
 587                    .entry(path.to_owned())
 588                    .insert_entry(new_templates.collect());
 589                self.last_scheduled_tasks.retain(|(kind, _)| {
 590                    if let TaskSourceKind::AbsPath { abs_path, .. } = kind {
 591                        abs_path != path
 592                    } else {
 593                        true
 594                    }
 595                });
 596            }
 597            TaskSettingsLocation::Worktree(location) => {
 598                let new_templates = new_templates.collect::<Vec<_>>();
 599                if new_templates.is_empty() {
 600                    if let Some(worktree_tasks) =
 601                        parsed_templates.worktree.get_mut(&location.worktree_id)
 602                    {
 603                        worktree_tasks.remove(location.path);
 604                    }
 605                } else {
 606                    parsed_templates
 607                        .worktree
 608                        .entry(location.worktree_id)
 609                        .or_default()
 610                        .insert(Arc::from(location.path), new_templates);
 611                }
 612                self.last_scheduled_tasks.retain(|(kind, _)| {
 613                    if let TaskSourceKind::Worktree {
 614                        directory_in_worktree,
 615                        id,
 616                        ..
 617                    } = kind
 618                    {
 619                        *id != location.worktree_id || directory_in_worktree != location.path
 620                    } else {
 621                        true
 622                    }
 623                });
 624            }
 625        }
 626
 627        Ok(())
 628    }
 629
 630    /// Updates in-memory task metadata from the JSON string given.
 631    /// Will fail if the JSON is not a valid array of objects, but will continue if any object will not parse into a [`TaskTemplate`].
 632    ///
 633    /// Global tasks are updated for no worktree provided, otherwise the worktree metadata for a given path will be updated.
 634    pub(crate) fn update_file_based_scenarios(
 635        &mut self,
 636        location: TaskSettingsLocation<'_>,
 637        raw_tasks_json: Option<&str>,
 638    ) -> Result<(), InvalidSettingsError> {
 639        let raw_tasks = match parse_json_with_comments::<Vec<serde_json::Value>>(
 640            raw_tasks_json.unwrap_or("[]"),
 641        ) {
 642            Ok(tasks) => tasks,
 643            Err(e) => {
 644                return Err(InvalidSettingsError::Debug {
 645                    path: match location {
 646                        TaskSettingsLocation::Global(path) => path.to_owned(),
 647                        TaskSettingsLocation::Worktree(settings_location) => {
 648                            settings_location.path.join(debug_task_file_name())
 649                        }
 650                    },
 651                    message: format!("Failed to parse tasks file content as a JSON array: {e}"),
 652                });
 653            }
 654        };
 655
 656        let new_templates = raw_tasks.into_iter().filter_map(|raw_template| {
 657            serde_json::from_value::<DebugScenario>(raw_template).log_err()
 658        });
 659
 660        let parsed_scenarios = &mut self.scenarios_from_settings;
 661        match location {
 662            TaskSettingsLocation::Global(path) => {
 663                parsed_scenarios
 664                    .global
 665                    .entry(path.to_owned())
 666                    .insert_entry(new_templates.collect());
 667            }
 668            TaskSettingsLocation::Worktree(location) => {
 669                let new_templates = new_templates.collect::<Vec<_>>();
 670                if new_templates.is_empty() {
 671                    if let Some(worktree_tasks) =
 672                        parsed_scenarios.worktree.get_mut(&location.worktree_id)
 673                    {
 674                        worktree_tasks.remove(location.path);
 675                    }
 676                } else {
 677                    parsed_scenarios
 678                        .worktree
 679                        .entry(location.worktree_id)
 680                        .or_default()
 681                        .insert(Arc::from(location.path), new_templates);
 682                }
 683            }
 684        }
 685
 686        Ok(())
 687    }
 688}
 689
 690fn task_lru_comparator(
 691    (kind_a, task_a, lru_score_a): &(TaskSourceKind, ResolvedTask, u32),
 692    (kind_b, task_b, lru_score_b): &(TaskSourceKind, ResolvedTask, u32),
 693) -> cmp::Ordering {
 694    lru_score_a
 695        // First, display recently used templates above all.
 696        .cmp(lru_score_b)
 697        // Then, ensure more specific sources are displayed first.
 698        .then(task_source_kind_preference(kind_a).cmp(&task_source_kind_preference(kind_b)))
 699        // After that, display first more specific tasks, using more template variables.
 700        // Bonus points for tasks with symbol variables.
 701        .then(task_variables_preference(task_a).cmp(&task_variables_preference(task_b)))
 702        // Finally, sort by the resolved label, but a bit more specifically, to avoid mixing letters and digits.
 703        .then({
 704            NumericPrefixWithSuffix::from_numeric_prefixed_str(&task_a.resolved_label)
 705                .cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str(
 706                    &task_b.resolved_label,
 707                ))
 708                .then(task_a.resolved_label.cmp(&task_b.resolved_label))
 709                .then(kind_a.cmp(kind_b))
 710        })
 711}
 712
 713fn task_source_kind_preference(kind: &TaskSourceKind) -> u32 {
 714    match kind {
 715        TaskSourceKind::Lsp(..) => 0,
 716        TaskSourceKind::Language { .. } => 1,
 717        TaskSourceKind::UserInput => 2,
 718        TaskSourceKind::Worktree { .. } => 3,
 719        TaskSourceKind::AbsPath { .. } => 4,
 720    }
 721}
 722
 723fn task_variables_preference(task: &ResolvedTask) -> Reverse<usize> {
 724    let task_variables = task.substituted_variables();
 725    Reverse(if task_variables.contains(&VariableName::Symbol) {
 726        task_variables.len() + 1
 727    } else {
 728        task_variables.len()
 729    })
 730}
 731
 732#[cfg(test)]
 733mod test_inventory {
 734    use gpui::{Entity, TestAppContext};
 735    use itertools::Itertools;
 736    use task::TaskContext;
 737    use worktree::WorktreeId;
 738
 739    use crate::Inventory;
 740
 741    use super::TaskSourceKind;
 742
 743    pub(super) fn task_template_names(
 744        inventory: &Entity<Inventory>,
 745        worktree: Option<WorktreeId>,
 746        cx: &mut TestAppContext,
 747    ) -> Vec<String> {
 748        inventory.update(cx, |inventory, cx| {
 749            inventory
 750                .list_tasks(None, None, worktree, cx)
 751                .into_iter()
 752                .map(|(_, task)| task.label)
 753                .sorted()
 754                .collect()
 755        })
 756    }
 757
 758    pub(super) fn register_task_used(
 759        inventory: &Entity<Inventory>,
 760        task_name: &str,
 761        cx: &mut TestAppContext,
 762    ) {
 763        inventory.update(cx, |inventory, cx| {
 764            let (task_source_kind, task) = inventory
 765                .list_tasks(None, None, None, cx)
 766                .into_iter()
 767                .find(|(_, task)| task.label == task_name)
 768                .unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
 769            let id_base = task_source_kind.to_id_base();
 770            inventory.task_scheduled(
 771                task_source_kind.clone(),
 772                task.resolve_task(&id_base, &TaskContext::default())
 773                    .unwrap_or_else(|| panic!("Failed to resolve task with name {task_name}")),
 774            );
 775        });
 776    }
 777
 778    pub(super) fn register_worktree_task_used(
 779        inventory: &Entity<Inventory>,
 780        worktree_id: WorktreeId,
 781        task_name: &str,
 782        cx: &mut TestAppContext,
 783    ) {
 784        inventory.update(cx, |inventory, cx| {
 785            let (task_source_kind, task) = inventory
 786                .list_tasks(None, None, Some(worktree_id), cx)
 787                .into_iter()
 788                .find(|(_, task)| task.label == task_name)
 789                .unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
 790            let id_base = task_source_kind.to_id_base();
 791            inventory.task_scheduled(
 792                task_source_kind.clone(),
 793                task.resolve_task(&id_base, &TaskContext::default())
 794                    .unwrap_or_else(|| panic!("Failed to resolve task with name {task_name}")),
 795            );
 796        });
 797    }
 798
 799    pub(super) async fn list_tasks(
 800        inventory: &Entity<Inventory>,
 801        worktree: Option<WorktreeId>,
 802        cx: &mut TestAppContext,
 803    ) -> Vec<(TaskSourceKind, String)> {
 804        inventory.update(cx, |inventory, cx| {
 805            let task_context = &TaskContext::default();
 806            inventory
 807                .list_tasks(None, None, worktree, cx)
 808                .into_iter()
 809                .filter_map(|(source_kind, task)| {
 810                    let id_base = source_kind.to_id_base();
 811                    Some((source_kind, task.resolve_task(&id_base, task_context)?))
 812                })
 813                .map(|(source_kind, resolved_task)| (source_kind, resolved_task.resolved_label))
 814                .collect()
 815        })
 816    }
 817}
 818
 819/// A context provided that tries to provide values for all non-custom [`VariableName`] variants for a currently opened file.
 820/// Applied as a base for every custom [`ContextProvider`] unless explicitly oped out.
 821pub struct BasicContextProvider {
 822    worktree_store: Entity<WorktreeStore>,
 823}
 824
 825impl BasicContextProvider {
 826    pub fn new(worktree_store: Entity<WorktreeStore>) -> Self {
 827        Self { worktree_store }
 828    }
 829}
 830impl ContextProvider for BasicContextProvider {
 831    fn build_context(
 832        &self,
 833        _: &TaskVariables,
 834        location: ContextLocation<'_>,
 835        _: Option<HashMap<String, String>>,
 836        _: Arc<dyn LanguageToolchainStore>,
 837        cx: &mut App,
 838    ) -> Task<Result<TaskVariables>> {
 839        let location = location.file_location;
 840        let buffer = location.buffer.read(cx);
 841        let buffer_snapshot = buffer.snapshot();
 842        let symbols = buffer_snapshot.symbols_containing(location.range.start, None);
 843        let symbol = symbols.unwrap_or_default().last().map(|symbol| {
 844            let range = symbol
 845                .name_ranges
 846                .last()
 847                .cloned()
 848                .unwrap_or(0..symbol.text.len());
 849            symbol.text[range].to_string()
 850        });
 851
 852        let current_file = buffer
 853            .file()
 854            .and_then(|file| file.as_local())
 855            .map(|file| file.abs_path(cx).to_sanitized_string());
 856        let Point { row, column } = location.range.start.to_point(&buffer_snapshot);
 857        let row = row + 1;
 858        let column = column + 1;
 859        let selected_text = buffer
 860            .chars_for_range(location.range.clone())
 861            .collect::<String>();
 862
 863        let mut task_variables = TaskVariables::from_iter([
 864            (VariableName::Row, row.to_string()),
 865            (VariableName::Column, column.to_string()),
 866        ]);
 867
 868        if let Some(symbol) = symbol {
 869            task_variables.insert(VariableName::Symbol, symbol);
 870        }
 871        if !selected_text.trim().is_empty() {
 872            task_variables.insert(VariableName::SelectedText, selected_text);
 873        }
 874        let worktree_root_dir =
 875            buffer
 876                .file()
 877                .map(|file| file.worktree_id(cx))
 878                .and_then(|worktree_id| {
 879                    self.worktree_store
 880                        .read(cx)
 881                        .worktree_for_id(worktree_id, cx)
 882                        .and_then(|worktree| worktree.read(cx).root_dir())
 883                });
 884        if let Some(worktree_path) = worktree_root_dir {
 885            task_variables.insert(
 886                VariableName::WorktreeRoot,
 887                worktree_path.to_sanitized_string(),
 888            );
 889            if let Some(full_path) = current_file.as_ref() {
 890                let relative_path = pathdiff::diff_paths(full_path, worktree_path);
 891                if let Some(relative_file) = relative_path {
 892                    task_variables.insert(
 893                        VariableName::RelativeFile,
 894                        relative_file.to_sanitized_string(),
 895                    );
 896                    if let Some(relative_dir) = relative_file.parent() {
 897                        task_variables.insert(
 898                            VariableName::RelativeDir,
 899                            if relative_dir.as_os_str().is_empty() {
 900                                String::from(".")
 901                            } else {
 902                                relative_dir.to_sanitized_string()
 903                            },
 904                        );
 905                    }
 906                }
 907            }
 908        }
 909
 910        if let Some(path_as_string) = current_file {
 911            let path = Path::new(&path_as_string);
 912            if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
 913                task_variables.insert(VariableName::Filename, String::from(filename));
 914            }
 915
 916            if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
 917                task_variables.insert(VariableName::Stem, stem.into());
 918            }
 919
 920            if let Some(dirname) = path.parent().and_then(|s| s.to_str()) {
 921                task_variables.insert(VariableName::Dirname, dirname.into());
 922            }
 923
 924            task_variables.insert(VariableName::File, path_as_string);
 925        }
 926
 927        Task::ready(Ok(task_variables))
 928    }
 929}
 930
 931/// A ContextProvider that doesn't provide any task variables on it's own, though it has some associated tasks.
 932pub struct ContextProviderWithTasks {
 933    templates: TaskTemplates,
 934}
 935
 936impl ContextProviderWithTasks {
 937    pub fn new(definitions: TaskTemplates) -> Self {
 938        Self {
 939            templates: definitions,
 940        }
 941    }
 942}
 943
 944impl ContextProvider for ContextProviderWithTasks {
 945    fn associated_tasks(
 946        &self,
 947        _: Option<Arc<dyn language::File>>,
 948        _: &App,
 949    ) -> Option<TaskTemplates> {
 950        Some(self.templates.clone())
 951    }
 952}
 953
 954#[cfg(test)]
 955mod tests {
 956    use gpui::TestAppContext;
 957    use paths::tasks_file;
 958    use pretty_assertions::assert_eq;
 959    use serde_json::json;
 960    use settings::SettingsLocation;
 961
 962    use crate::task_store::TaskStore;
 963
 964    use super::test_inventory::*;
 965    use super::*;
 966
 967    #[gpui::test]
 968    async fn test_task_list_sorting(cx: &mut TestAppContext) {
 969        init_test(cx);
 970        let inventory = cx.update(Inventory::new);
 971        let initial_tasks = resolved_task_names(&inventory, None, cx);
 972        assert!(
 973            initial_tasks.is_empty(),
 974            "No tasks expected for empty inventory, but got {initial_tasks:?}"
 975        );
 976        let initial_tasks = task_template_names(&inventory, None, cx);
 977        assert!(
 978            initial_tasks.is_empty(),
 979            "No tasks expected for empty inventory, but got {initial_tasks:?}"
 980        );
 981        cx.run_until_parked();
 982        let expected_initial_state = [
 983            "1_a_task".to_string(),
 984            "1_task".to_string(),
 985            "2_task".to_string(),
 986            "3_task".to_string(),
 987        ];
 988
 989        inventory.update(cx, |inventory, _| {
 990            inventory
 991                .update_file_based_tasks(
 992                    TaskSettingsLocation::Global(tasks_file()),
 993                    Some(&mock_tasks_from_names(
 994                        expected_initial_state.iter().map(|name| name.as_str()),
 995                    )),
 996                )
 997                .unwrap();
 998        });
 999        assert_eq!(
1000            task_template_names(&inventory, None, cx),
1001            &expected_initial_state,
1002        );
1003        assert_eq!(
1004            resolved_task_names(&inventory, None, cx),
1005            &expected_initial_state,
1006            "Tasks with equal amount of usages should be sorted alphanumerically"
1007        );
1008
1009        register_task_used(&inventory, "2_task", cx);
1010        assert_eq!(
1011            task_template_names(&inventory, None, cx),
1012            &expected_initial_state,
1013        );
1014        assert_eq!(
1015            resolved_task_names(&inventory, None, cx),
1016            vec![
1017                "2_task".to_string(),
1018                "1_a_task".to_string(),
1019                "1_task".to_string(),
1020                "3_task".to_string()
1021            ],
1022        );
1023
1024        register_task_used(&inventory, "1_task", cx);
1025        register_task_used(&inventory, "1_task", cx);
1026        register_task_used(&inventory, "1_task", cx);
1027        register_task_used(&inventory, "3_task", cx);
1028        assert_eq!(
1029            task_template_names(&inventory, None, cx),
1030            &expected_initial_state,
1031        );
1032        assert_eq!(
1033            resolved_task_names(&inventory, None, cx),
1034            vec![
1035                "3_task".to_string(),
1036                "1_task".to_string(),
1037                "2_task".to_string(),
1038                "1_a_task".to_string(),
1039            ],
1040            "Most recently used task should be at the top"
1041        );
1042
1043        let worktree_id = WorktreeId::from_usize(0);
1044        let local_worktree_location = SettingsLocation {
1045            worktree_id,
1046            path: Path::new("foo"),
1047        };
1048        inventory.update(cx, |inventory, _| {
1049            inventory
1050                .update_file_based_tasks(
1051                    TaskSettingsLocation::Worktree(local_worktree_location),
1052                    Some(&mock_tasks_from_names(["worktree_task_1"])),
1053                )
1054                .unwrap();
1055        });
1056        assert_eq!(
1057            resolved_task_names(&inventory, None, cx),
1058            vec![
1059                "3_task".to_string(),
1060                "1_task".to_string(),
1061                "2_task".to_string(),
1062                "1_a_task".to_string(),
1063            ],
1064            "Most recently used task should be at the top"
1065        );
1066        assert_eq!(
1067            resolved_task_names(&inventory, Some(worktree_id), cx),
1068            vec![
1069                "3_task".to_string(),
1070                "1_task".to_string(),
1071                "2_task".to_string(),
1072                "worktree_task_1".to_string(),
1073                "1_a_task".to_string(),
1074            ],
1075        );
1076        register_worktree_task_used(&inventory, worktree_id, "worktree_task_1", cx);
1077        assert_eq!(
1078            resolved_task_names(&inventory, Some(worktree_id), cx),
1079            vec![
1080                "worktree_task_1".to_string(),
1081                "3_task".to_string(),
1082                "1_task".to_string(),
1083                "2_task".to_string(),
1084                "1_a_task".to_string(),
1085            ],
1086            "Most recently used worktree task should be at the top"
1087        );
1088
1089        inventory.update(cx, |inventory, _| {
1090            inventory
1091                .update_file_based_tasks(
1092                    TaskSettingsLocation::Global(tasks_file()),
1093                    Some(&mock_tasks_from_names(
1094                        ["10_hello", "11_hello"]
1095                            .into_iter()
1096                            .chain(expected_initial_state.iter().map(|name| name.as_str())),
1097                    )),
1098                )
1099                .unwrap();
1100        });
1101        cx.run_until_parked();
1102        let expected_updated_state = [
1103            "10_hello".to_string(),
1104            "11_hello".to_string(),
1105            "1_a_task".to_string(),
1106            "1_task".to_string(),
1107            "2_task".to_string(),
1108            "3_task".to_string(),
1109        ];
1110        assert_eq!(
1111            task_template_names(&inventory, None, cx),
1112            &expected_updated_state,
1113        );
1114        assert_eq!(
1115            resolved_task_names(&inventory, None, cx),
1116            vec![
1117                "worktree_task_1".to_string(),
1118                "1_a_task".to_string(),
1119                "1_task".to_string(),
1120                "2_task".to_string(),
1121                "3_task".to_string(),
1122                "10_hello".to_string(),
1123                "11_hello".to_string(),
1124            ],
1125            "After global tasks update, worktree task usage is not erased and it's the first still; global task is back to regular order as its file was updated"
1126        );
1127
1128        register_task_used(&inventory, "11_hello", cx);
1129        assert_eq!(
1130            task_template_names(&inventory, None, cx),
1131            &expected_updated_state,
1132        );
1133        assert_eq!(
1134            resolved_task_names(&inventory, None, cx),
1135            vec![
1136                "11_hello".to_string(),
1137                "worktree_task_1".to_string(),
1138                "1_a_task".to_string(),
1139                "1_task".to_string(),
1140                "2_task".to_string(),
1141                "3_task".to_string(),
1142                "10_hello".to_string(),
1143            ],
1144        );
1145    }
1146
1147    #[gpui::test]
1148    async fn test_inventory_static_task_filters(cx: &mut TestAppContext) {
1149        init_test(cx);
1150        let inventory = cx.update(Inventory::new);
1151        let common_name = "common_task_name";
1152        let worktree_1 = WorktreeId::from_usize(1);
1153        let worktree_2 = WorktreeId::from_usize(2);
1154
1155        cx.run_until_parked();
1156        let worktree_independent_tasks = vec![
1157            (
1158                TaskSourceKind::AbsPath {
1159                    id_base: "global tasks.json".into(),
1160                    abs_path: paths::tasks_file().clone(),
1161                },
1162                common_name.to_string(),
1163            ),
1164            (
1165                TaskSourceKind::AbsPath {
1166                    id_base: "global tasks.json".into(),
1167                    abs_path: paths::tasks_file().clone(),
1168                },
1169                "static_source_1".to_string(),
1170            ),
1171            (
1172                TaskSourceKind::AbsPath {
1173                    id_base: "global tasks.json".into(),
1174                    abs_path: paths::tasks_file().clone(),
1175                },
1176                "static_source_2".to_string(),
1177            ),
1178        ];
1179        let worktree_1_tasks = [
1180            (
1181                TaskSourceKind::Worktree {
1182                    id: worktree_1,
1183                    directory_in_worktree: PathBuf::from(".zed"),
1184                    id_base: "local worktree tasks from directory \".zed\"".into(),
1185                },
1186                common_name.to_string(),
1187            ),
1188            (
1189                TaskSourceKind::Worktree {
1190                    id: worktree_1,
1191                    directory_in_worktree: PathBuf::from(".zed"),
1192                    id_base: "local worktree tasks from directory \".zed\"".into(),
1193                },
1194                "worktree_1".to_string(),
1195            ),
1196        ];
1197        let worktree_2_tasks = [
1198            (
1199                TaskSourceKind::Worktree {
1200                    id: worktree_2,
1201                    directory_in_worktree: PathBuf::from(".zed"),
1202                    id_base: "local worktree tasks from directory \".zed\"".into(),
1203                },
1204                common_name.to_string(),
1205            ),
1206            (
1207                TaskSourceKind::Worktree {
1208                    id: worktree_2,
1209                    directory_in_worktree: PathBuf::from(".zed"),
1210                    id_base: "local worktree tasks from directory \".zed\"".into(),
1211                },
1212                "worktree_2".to_string(),
1213            ),
1214        ];
1215
1216        inventory.update(cx, |inventory, _| {
1217            inventory
1218                .update_file_based_tasks(
1219                    TaskSettingsLocation::Global(tasks_file()),
1220                    Some(&mock_tasks_from_names(
1221                        worktree_independent_tasks
1222                            .iter()
1223                            .map(|(_, name)| name.as_str()),
1224                    )),
1225                )
1226                .unwrap();
1227            inventory
1228                .update_file_based_tasks(
1229                    TaskSettingsLocation::Worktree(SettingsLocation {
1230                        worktree_id: worktree_1,
1231                        path: Path::new(".zed"),
1232                    }),
1233                    Some(&mock_tasks_from_names(
1234                        worktree_1_tasks.iter().map(|(_, name)| name.as_str()),
1235                    )),
1236                )
1237                .unwrap();
1238            inventory
1239                .update_file_based_tasks(
1240                    TaskSettingsLocation::Worktree(SettingsLocation {
1241                        worktree_id: worktree_2,
1242                        path: Path::new(".zed"),
1243                    }),
1244                    Some(&mock_tasks_from_names(
1245                        worktree_2_tasks.iter().map(|(_, name)| name.as_str()),
1246                    )),
1247                )
1248                .unwrap();
1249        });
1250
1251        assert_eq!(
1252            list_tasks_sorted_by_last_used(&inventory, None, cx).await,
1253            worktree_independent_tasks,
1254            "Without a worktree, only worktree-independent tasks should be listed"
1255        );
1256        assert_eq!(
1257            list_tasks_sorted_by_last_used(&inventory, Some(worktree_1), cx).await,
1258            worktree_1_tasks
1259                .iter()
1260                .chain(worktree_independent_tasks.iter())
1261                .cloned()
1262                .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
1263                .collect::<Vec<_>>(),
1264        );
1265        assert_eq!(
1266            list_tasks_sorted_by_last_used(&inventory, Some(worktree_2), cx).await,
1267            worktree_2_tasks
1268                .iter()
1269                .chain(worktree_independent_tasks.iter())
1270                .cloned()
1271                .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
1272                .collect::<Vec<_>>(),
1273        );
1274
1275        assert_eq!(
1276            list_tasks(&inventory, None, cx).await,
1277            worktree_independent_tasks,
1278            "Without a worktree, only worktree-independent tasks should be listed"
1279        );
1280        assert_eq!(
1281            list_tasks(&inventory, Some(worktree_1), cx).await,
1282            worktree_1_tasks
1283                .iter()
1284                .chain(worktree_independent_tasks.iter())
1285                .cloned()
1286                .collect::<Vec<_>>(),
1287        );
1288        assert_eq!(
1289            list_tasks(&inventory, Some(worktree_2), cx).await,
1290            worktree_2_tasks
1291                .iter()
1292                .chain(worktree_independent_tasks.iter())
1293                .cloned()
1294                .collect::<Vec<_>>(),
1295        );
1296    }
1297
1298    fn init_test(_cx: &mut TestAppContext) {
1299        zlog::init_test();
1300        TaskStore::init(None);
1301    }
1302
1303    fn resolved_task_names(
1304        inventory: &Entity<Inventory>,
1305        worktree: Option<WorktreeId>,
1306        cx: &mut TestAppContext,
1307    ) -> Vec<String> {
1308        inventory.update(cx, |inventory, cx| {
1309            let mut task_contexts = TaskContexts::default();
1310            task_contexts.active_worktree_context =
1311                worktree.map(|worktree| (worktree, TaskContext::default()));
1312            let (used, current) = inventory.used_and_current_resolved_tasks(&task_contexts, cx);
1313            used.into_iter()
1314                .chain(current)
1315                .map(|(_, task)| task.original_task().label.clone())
1316                .collect()
1317        })
1318    }
1319
1320    fn mock_tasks_from_names<'a>(task_names: impl IntoIterator<Item = &'a str> + 'a) -> String {
1321        serde_json::to_string(&serde_json::Value::Array(
1322            task_names
1323                .into_iter()
1324                .map(|task_name| {
1325                    json!({
1326                        "label": task_name,
1327                        "command": "echo",
1328                        "args": vec![task_name],
1329                    })
1330                })
1331                .collect::<Vec<_>>(),
1332        ))
1333        .unwrap()
1334    }
1335
1336    async fn list_tasks_sorted_by_last_used(
1337        inventory: &Entity<Inventory>,
1338        worktree: Option<WorktreeId>,
1339        cx: &mut TestAppContext,
1340    ) -> Vec<(TaskSourceKind, String)> {
1341        inventory.update(cx, |inventory, cx| {
1342            let mut task_contexts = TaskContexts::default();
1343            task_contexts.active_worktree_context =
1344                worktree.map(|worktree| (worktree, TaskContext::default()));
1345            let (used, current) = inventory.used_and_current_resolved_tasks(&task_contexts, cx);
1346            let mut all = used;
1347            all.extend(current);
1348            all.into_iter()
1349                .map(|(source_kind, task)| (source_kind, task.resolved_label))
1350                .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
1351                .collect()
1352        })
1353    }
1354}