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