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