task_inventory.rs

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