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