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