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, 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::{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    fn global_templates_from_settings(
 648        &self,
 649    ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
 650        self.templates_from_settings.global_scenarios()
 651    }
 652
 653    fn global_debug_scenarios_from_settings(
 654        &self,
 655    ) -> impl '_ + Iterator<Item = (TaskSourceKind, DebugScenario)> {
 656        self.scenarios_from_settings.global_scenarios()
 657    }
 658
 659    fn worktree_scenarios_from_settings(
 660        &self,
 661        worktree: WorktreeId,
 662    ) -> impl '_ + Iterator<Item = (TaskSourceKind, DebugScenario)> {
 663        self.scenarios_from_settings.worktree_scenarios(worktree)
 664    }
 665
 666    fn worktree_templates_from_settings(
 667        &self,
 668        worktree: WorktreeId,
 669    ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
 670        self.templates_from_settings.worktree_scenarios(worktree)
 671    }
 672
 673    /// Updates in-memory task metadata from the JSON string given.
 674    /// Will fail if the JSON is not a valid array of objects, but will continue if any object will not parse into a [`TaskTemplate`].
 675    ///
 676    /// Global tasks are updated for no worktree provided, otherwise the worktree metadata for a given path will be updated.
 677    pub fn update_file_based_tasks(
 678        &mut self,
 679        location: TaskSettingsLocation<'_>,
 680        raw_tasks_json: Option<&str>,
 681    ) -> Result<(), InvalidSettingsError> {
 682        let raw_tasks = match parse_json_with_comments::<Vec<serde_json::Value>>(
 683            raw_tasks_json.unwrap_or("[]"),
 684        ) {
 685            Ok(tasks) => tasks,
 686            Err(e) => {
 687                return Err(InvalidSettingsError::Tasks {
 688                    path: match location {
 689                        TaskSettingsLocation::Global(path) => path.to_owned(),
 690                        TaskSettingsLocation::Worktree(settings_location) => {
 691                            settings_location.path.as_std_path().join(task_file_name())
 692                        }
 693                    },
 694                    message: format!("Failed to parse tasks file content as a JSON array: {e}"),
 695                });
 696            }
 697        };
 698
 699        let mut validation_errors = Vec::new();
 700        let new_templates = raw_tasks.into_iter().filter_map(|raw_template| {
 701            let template = serde_json::from_value::<TaskTemplate>(raw_template).log_err()?;
 702
 703            // Validate the variable names used in the `TaskTemplate`.
 704            let unknown_variables = template.unknown_variables();
 705            if !unknown_variables.is_empty() {
 706                let variables_list = unknown_variables
 707                    .iter()
 708                    .map(|variable| format!("${variable}"))
 709                    .collect::<Vec<_>>()
 710                    .join(", ");
 711
 712                validation_errors.push(format!(
 713                    "Task '{}' uses unknown variables: {}",
 714                    template.label, variables_list
 715                ));
 716
 717                // Skip this template, since it uses unknown variable names, but
 718                // continue processing others.
 719                return None;
 720            }
 721
 722            Some(template)
 723        });
 724
 725        let parsed_templates = &mut self.templates_from_settings;
 726        match location {
 727            TaskSettingsLocation::Global(path) => {
 728                parsed_templates
 729                    .global
 730                    .entry(path.to_owned())
 731                    .insert_entry(new_templates.collect());
 732                self.last_scheduled_tasks.retain(|(kind, _)| {
 733                    if let TaskSourceKind::AbsPath { abs_path, .. } = kind {
 734                        abs_path != path
 735                    } else {
 736                        true
 737                    }
 738                });
 739            }
 740            TaskSettingsLocation::Worktree(location) => {
 741                let new_templates = new_templates.collect::<Vec<_>>();
 742                if new_templates.is_empty() {
 743                    if let Some(worktree_tasks) =
 744                        parsed_templates.worktree.get_mut(&location.worktree_id)
 745                    {
 746                        worktree_tasks.remove(location.path);
 747                    }
 748                } else {
 749                    parsed_templates
 750                        .worktree
 751                        .entry(location.worktree_id)
 752                        .or_default()
 753                        .insert(Arc::from(location.path), new_templates);
 754                }
 755                self.last_scheduled_tasks.retain(|(kind, _)| {
 756                    if let TaskSourceKind::Worktree {
 757                        directory_in_worktree,
 758                        id,
 759                        ..
 760                    } = kind
 761                    {
 762                        *id != location.worktree_id
 763                            || directory_in_worktree.as_ref() != location.path
 764                    } else {
 765                        true
 766                    }
 767                });
 768            }
 769        }
 770
 771        if !validation_errors.is_empty() {
 772            return Err(InvalidSettingsError::Tasks {
 773                path: match &location {
 774                    TaskSettingsLocation::Global(path) => path.to_path_buf(),
 775                    TaskSettingsLocation::Worktree(location) => {
 776                        location.path.as_std_path().join(task_file_name())
 777                    }
 778                },
 779                message: validation_errors.join("\n"),
 780            });
 781        }
 782
 783        Ok(())
 784    }
 785
 786    /// Updates in-memory task metadata from the JSON string given.
 787    /// Will fail if the JSON is not a valid array of objects, but will continue if any object will not parse into a [`TaskTemplate`].
 788    ///
 789    /// Global tasks are updated for no worktree provided, otherwise the worktree metadata for a given path will be updated.
 790    pub fn update_file_based_scenarios(
 791        &mut self,
 792        location: TaskSettingsLocation<'_>,
 793        raw_tasks_json: Option<&str>,
 794    ) -> Result<(), InvalidSettingsError> {
 795        let raw_tasks = match parse_json_with_comments::<Vec<serde_json::Value>>(
 796            raw_tasks_json.unwrap_or("[]"),
 797        ) {
 798            Ok(tasks) => tasks,
 799            Err(e) => {
 800                return Err(InvalidSettingsError::Debug {
 801                    path: match location {
 802                        TaskSettingsLocation::Global(path) => path.to_owned(),
 803                        TaskSettingsLocation::Worktree(settings_location) => settings_location
 804                            .path
 805                            .as_std_path()
 806                            .join(debug_task_file_name()),
 807                    },
 808                    message: format!("Failed to parse tasks file content as a JSON array: {e}"),
 809                });
 810            }
 811        };
 812
 813        let new_templates = raw_tasks
 814            .into_iter()
 815            .filter_map(|raw_template| {
 816                serde_json::from_value::<DebugScenario>(raw_template).log_err()
 817            })
 818            .collect::<Vec<_>>();
 819
 820        let parsed_scenarios = &mut self.scenarios_from_settings;
 821        let mut new_definitions: HashMap<_, _> = new_templates
 822            .iter()
 823            .map(|template| (template.label.clone(), template.clone()))
 824            .collect();
 825        let previously_existing_scenarios;
 826
 827        match location {
 828            TaskSettingsLocation::Global(path) => {
 829                previously_existing_scenarios = parsed_scenarios
 830                    .global_scenarios()
 831                    .map(|(_, scenario)| scenario.label)
 832                    .collect::<HashSet<_>>();
 833                parsed_scenarios
 834                    .global
 835                    .entry(path.to_owned())
 836                    .insert_entry(new_templates);
 837            }
 838            TaskSettingsLocation::Worktree(location) => {
 839                previously_existing_scenarios = parsed_scenarios
 840                    .worktree_scenarios(location.worktree_id)
 841                    .map(|(_, scenario)| scenario.label)
 842                    .collect::<HashSet<_>>();
 843
 844                if new_templates.is_empty() {
 845                    if let Some(worktree_tasks) =
 846                        parsed_scenarios.worktree.get_mut(&location.worktree_id)
 847                    {
 848                        worktree_tasks.remove(location.path);
 849                    }
 850                } else {
 851                    parsed_scenarios
 852                        .worktree
 853                        .entry(location.worktree_id)
 854                        .or_default()
 855                        .insert(Arc::from(location.path), new_templates);
 856                }
 857            }
 858        }
 859        self.last_scheduled_scenarios.retain_mut(|(scenario, _)| {
 860            if !previously_existing_scenarios.contains(&scenario.label) {
 861                return true;
 862            }
 863            if let Some(new_definition) = new_definitions.remove(&scenario.label) {
 864                *scenario = new_definition;
 865                true
 866            } else {
 867                false
 868            }
 869        });
 870
 871        Ok(())
 872    }
 873}
 874
 875fn task_lru_comparator(
 876    (kind_a, task_a, lru_score_a): &(TaskSourceKind, ResolvedTask, u32),
 877    (kind_b, task_b, lru_score_b): &(TaskSourceKind, ResolvedTask, u32),
 878) -> cmp::Ordering {
 879    lru_score_a
 880        // First, display recently used templates above all.
 881        .cmp(lru_score_b)
 882        // Then, ensure more specific sources are displayed first.
 883        .then(task_source_kind_preference(kind_a).cmp(&task_source_kind_preference(kind_b)))
 884        // After that, display first more specific tasks, using more template variables.
 885        // Bonus points for tasks with symbol variables.
 886        .then(task_variables_preference(task_a).cmp(&task_variables_preference(task_b)))
 887        // Finally, sort by the resolved label, but a bit more specifically, to avoid mixing letters and digits.
 888        .then({
 889            NumericPrefixWithSuffix::from_numeric_prefixed_str(&task_a.resolved_label)
 890                .cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str(
 891                    &task_b.resolved_label,
 892                ))
 893                .then(task_a.resolved_label.cmp(&task_b.resolved_label))
 894                .then(kind_a.cmp(kind_b))
 895        })
 896}
 897
 898pub fn task_source_kind_preference(kind: &TaskSourceKind) -> u32 {
 899    match kind {
 900        TaskSourceKind::Lsp { .. } => 0,
 901        TaskSourceKind::Language { .. } => 1,
 902        TaskSourceKind::UserInput => 2,
 903        TaskSourceKind::Worktree { .. } => 3,
 904        TaskSourceKind::AbsPath { .. } => 4,
 905    }
 906}
 907
 908fn task_variables_preference(task: &ResolvedTask) -> Reverse<usize> {
 909    let task_variables = task.substituted_variables();
 910    Reverse(if task_variables.contains(&VariableName::Symbol) {
 911        task_variables.len() + 1
 912    } else {
 913        task_variables.len()
 914    })
 915}
 916
 917/// A context provided that tries to provide values for all non-custom [`VariableName`] variants for a currently opened file.
 918/// Applied as a base for every custom [`ContextProvider`] unless explicitly oped out.
 919pub struct BasicContextProvider {
 920    worktree_store: Entity<WorktreeStore>,
 921}
 922
 923impl BasicContextProvider {
 924    pub fn new(worktree_store: Entity<WorktreeStore>) -> Self {
 925        Self { worktree_store }
 926    }
 927}
 928
 929impl ContextProvider for BasicContextProvider {
 930    fn build_context(
 931        &self,
 932        _: &TaskVariables,
 933        location: ContextLocation<'_>,
 934        _: Option<HashMap<String, String>>,
 935        _: Arc<dyn LanguageToolchainStore>,
 936        cx: &mut App,
 937    ) -> Task<Result<TaskVariables>> {
 938        let location = location.file_location;
 939        let buffer = location.buffer.read(cx);
 940        let buffer_snapshot = buffer.snapshot();
 941        let symbols = buffer_snapshot.symbols_containing(location.range.start, None);
 942        let symbol = symbols.last().map(|symbol| {
 943            let range = symbol
 944                .name_ranges
 945                .last()
 946                .cloned()
 947                .unwrap_or(0..symbol.text.len());
 948            symbol.text[range].to_string()
 949        });
 950
 951        let current_file = buffer.file().and_then(|file| file.as_local());
 952        let Point { row, column } = location.range.start.to_point(&buffer_snapshot);
 953        let row = row + 1;
 954        let column = column + 1;
 955        let selected_text = buffer
 956            .chars_for_range(location.range.clone())
 957            .collect::<String>();
 958
 959        let mut task_variables = TaskVariables::from_iter([
 960            (VariableName::Row, row.to_string()),
 961            (VariableName::Column, column.to_string()),
 962        ]);
 963
 964        if let Some(symbol) = symbol {
 965            task_variables.insert(VariableName::Symbol, symbol);
 966        }
 967        if !selected_text.trim().is_empty() {
 968            task_variables.insert(VariableName::SelectedText, selected_text);
 969        }
 970        let worktree = buffer
 971            .file()
 972            .map(|file| file.worktree_id(cx))
 973            .and_then(|worktree_id| {
 974                self.worktree_store
 975                    .read(cx)
 976                    .worktree_for_id(worktree_id, cx)
 977            });
 978
 979        if let Some(worktree) = worktree {
 980            let worktree = worktree.read(cx);
 981            let path_style = worktree.path_style();
 982            task_variables.insert(
 983                VariableName::WorktreeRoot,
 984                worktree.abs_path().to_string_lossy().into_owned(),
 985            );
 986            if let Some(current_file) = current_file.as_ref() {
 987                let relative_path = current_file.path();
 988                task_variables.insert(
 989                    VariableName::RelativeFile,
 990                    relative_path.display(path_style).to_string(),
 991                );
 992                if let Some(relative_dir) = relative_path.parent() {
 993                    task_variables.insert(
 994                        VariableName::RelativeDir,
 995                        if relative_dir.is_empty() {
 996                            String::from(".")
 997                        } else {
 998                            relative_dir.display(path_style).to_string()
 999                        },
1000                    );
1001                }
1002            }
1003        }
1004
1005        if let Some(current_file) = current_file {
1006            let path = current_file.abs_path(cx);
1007            if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
1008                task_variables.insert(VariableName::Filename, String::from(filename));
1009            }
1010
1011            if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
1012                task_variables.insert(VariableName::Stem, stem.into());
1013            }
1014
1015            if let Some(dirname) = path.parent().and_then(|s| s.to_str()) {
1016                task_variables.insert(VariableName::Dirname, dirname.into());
1017            }
1018
1019            task_variables.insert(VariableName::File, path.to_string_lossy().into_owned());
1020        }
1021
1022        if let Some(language) = buffer.language() {
1023            task_variables.insert(VariableName::Language, language.name().to_string());
1024        }
1025
1026        Task::ready(Ok(task_variables))
1027    }
1028}
1029
1030/// A ContextProvider that doesn't provide any task variables on it's own, though it has some associated tasks.
1031pub struct ContextProviderWithTasks {
1032    templates: TaskTemplates,
1033}
1034
1035impl ContextProviderWithTasks {
1036    pub fn new(definitions: TaskTemplates) -> Self {
1037        Self {
1038            templates: definitions,
1039        }
1040    }
1041}
1042
1043impl ContextProvider for ContextProviderWithTasks {
1044    fn associated_tasks(&self, _: Option<Entity<Buffer>>, _: &App) -> Task<Option<TaskTemplates>> {
1045        Task::ready(Some(self.templates.clone()))
1046    }
1047}