task_inventory.rs

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