task_inventory.rs

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