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