task_inventory.rs

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