task_inventory.rs

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