task_inventory.rs

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