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::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, 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 (file, language) = {
 316                let buffer = location.buffer.read(cx);
 317                (buffer.file(), buffer.language())
 318            };
 319            let language_name = language.as_ref().map(|l| l.name());
 320            let adapter = language_settings(language_name, file, cx)
 321                .debuggers
 322                .first()
 323                .map(SharedString::from)
 324                .or_else(|| {
 325                    language.and_then(|l| l.config().debuggers.first().map(SharedString::from))
 326                });
 327            adapter.map(|adapter| (adapter, DapRegistry::global(cx).locators()))
 328        });
 329        cx.background_spawn(async move {
 330            if let Some((adapter, locators)) = adapter {
 331                for (kind, task) in
 332                    lsp_tasks
 333                        .into_iter()
 334                        .chain(current_resolved_tasks.into_iter().filter(|(kind, _)| {
 335                            add_current_language_tasks
 336                                || !matches!(kind, TaskSourceKind::Language { .. })
 337                        }))
 338                {
 339                    let adapter = adapter.clone().into();
 340
 341                    for locator in locators.values() {
 342                        if let Some(scenario) = locator
 343                            .create_scenario(task.original_task(), task.display_label(), &adapter)
 344                            .await
 345                        {
 346                            scenarios.push((kind, scenario));
 347                            break;
 348                        }
 349                    }
 350                }
 351            }
 352            (last_scheduled_scenarios, scenarios)
 353        })
 354    }
 355
 356    pub fn task_template_by_label(
 357        &self,
 358        buffer: Option<Entity<Buffer>>,
 359        worktree_id: Option<WorktreeId>,
 360        label: &str,
 361        cx: &App,
 362    ) -> Task<Option<TaskTemplate>> {
 363        let (buffer_worktree_id, file, language) = buffer
 364            .map(|buffer| {
 365                let buffer = buffer.read(cx);
 366                let file = buffer.file().cloned();
 367                (
 368                    file.as_ref().map(|file| file.worktree_id(cx)),
 369                    file,
 370                    buffer.language().cloned(),
 371                )
 372            })
 373            .unwrap_or((None, None, None));
 374
 375        let tasks = self.list_tasks(file, language, worktree_id.or(buffer_worktree_id), cx);
 376        let label = label.to_owned();
 377        cx.background_spawn(async move {
 378            tasks
 379                .await
 380                .into_iter()
 381                .find(|(_, template)| template.label == label)
 382                .map(|val| val.1)
 383        })
 384    }
 385
 386    /// Pulls its task sources relevant to the worktree and the language given,
 387    /// returns all task templates with their source kinds, worktree tasks first, language tasks second
 388    /// and global tasks last. No specific order inside source kinds groups.
 389    pub fn list_tasks(
 390        &self,
 391        file: Option<Arc<dyn File>>,
 392        language: Option<Arc<Language>>,
 393        worktree: Option<WorktreeId>,
 394        cx: &App,
 395    ) -> Task<Vec<(TaskSourceKind, TaskTemplate)>> {
 396        let global_tasks = self.global_templates_from_settings().collect::<Vec<_>>();
 397        let mut worktree_tasks = worktree
 398            .into_iter()
 399            .flat_map(|worktree| self.worktree_templates_from_settings(worktree))
 400            .collect::<Vec<_>>();
 401
 402        let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
 403            name: language.name().into(),
 404        });
 405        let language_tasks = language
 406            .filter(|language| {
 407                language_settings(Some(language.name()), file.as_ref(), cx)
 408                    .tasks
 409                    .enabled
 410            })
 411            .and_then(|language| {
 412                language
 413                    .context_provider()
 414                    .map(|provider| provider.associated_tasks(file, cx))
 415            });
 416        cx.background_spawn(async move {
 417            if let Some(t) = language_tasks {
 418                worktree_tasks.extend(t.await.into_iter().flat_map(|tasks| {
 419                    tasks
 420                        .0
 421                        .into_iter()
 422                        .filter_map(|task| Some((task_source_kind.clone()?, task)))
 423                }));
 424            }
 425            worktree_tasks.extend(global_tasks);
 426            worktree_tasks
 427        })
 428    }
 429
 430    /// Pulls its task sources relevant to the worktree and the language given and resolves them with the [`TaskContexts`] given.
 431    /// Joins the new resolutions with the resolved tasks that were used (spawned) before,
 432    /// orders them so that the most recently used come first, all equally used ones are ordered so that the most specific tasks come first.
 433    /// Deduplicates the tasks by their labels and context and splits the ordered list into two: used tasks and the rest, newly resolved tasks.
 434    pub fn used_and_current_resolved_tasks(
 435        &self,
 436        task_contexts: Arc<TaskContexts>,
 437        cx: &mut Context<Self>,
 438    ) -> Task<(
 439        Vec<(TaskSourceKind, ResolvedTask)>,
 440        Vec<(TaskSourceKind, ResolvedTask)>,
 441    )> {
 442        let worktree = task_contexts.worktree();
 443        let location = task_contexts.location();
 444        let language = location.and_then(|location| location.buffer.read(cx).language());
 445        let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
 446            name: language.name().into(),
 447        });
 448        let file = location.and_then(|location| location.buffer.read(cx).file().cloned());
 449
 450        let worktrees_with_zed_tasks: HashSet<WorktreeId> = self
 451            .templates_from_settings
 452            .worktree
 453            .iter()
 454            .filter(|(_, dirs)| {
 455                dirs.keys()
 456                    .any(|dir| dir.file_name().is_some_and(|name| name == ".zed"))
 457            })
 458            .map(|(id, _)| *id)
 459            .collect();
 460
 461        let mut task_labels_to_ids = HashMap::<String, HashSet<TaskId>>::default();
 462        let mut lru_score = 0_u32;
 463        let previously_spawned_tasks = self
 464            .last_scheduled_tasks
 465            .iter()
 466            .rev()
 467            .filter(|(task_kind, _)| {
 468                if matches!(task_kind, TaskSourceKind::Language { .. }) {
 469                    Some(task_kind) == task_source_kind.as_ref()
 470                } else if let TaskSourceKind::Worktree {
 471                    id,
 472                    directory_in_worktree: dir,
 473                    ..
 474                } = task_kind
 475                {
 476                    !(worktrees_with_zed_tasks.contains(id)
 477                        && dir.file_name().is_some_and(|name| name == ".vscode"))
 478                } else {
 479                    true
 480                }
 481            })
 482            .filter(|(_, resolved_task)| {
 483                match task_labels_to_ids.entry(resolved_task.resolved_label.clone()) {
 484                    hash_map::Entry::Occupied(mut o) => {
 485                        o.get_mut().insert(resolved_task.id.clone());
 486                        // Neber allow duplicate reused tasks with the same labels
 487                        false
 488                    }
 489                    hash_map::Entry::Vacant(v) => {
 490                        v.insert(HashSet::from_iter(Some(resolved_task.id.clone())));
 491                        true
 492                    }
 493                }
 494            })
 495            .map(|(task_source_kind, resolved_task)| {
 496                (
 497                    task_source_kind.clone(),
 498                    resolved_task.clone(),
 499                    post_inc(&mut lru_score),
 500                )
 501            })
 502            .sorted_unstable_by(task_lru_comparator)
 503            .map(|(kind, task, _)| (kind, task))
 504            .collect::<Vec<_>>();
 505
 506        let not_used_score = post_inc(&mut lru_score);
 507        let global_tasks = self.global_templates_from_settings().collect::<Vec<_>>();
 508        let associated_tasks = language
 509            .filter(|language| {
 510                language_settings(Some(language.name()), file.as_ref(), cx)
 511                    .tasks
 512                    .enabled
 513            })
 514            .and_then(|language| {
 515                language
 516                    .context_provider()
 517                    .map(|provider| provider.associated_tasks(file, cx))
 518            });
 519        let worktree_tasks = worktree
 520            .into_iter()
 521            .flat_map(|worktree| self.worktree_templates_from_settings(worktree))
 522            .collect::<Vec<_>>();
 523        let task_contexts = task_contexts.clone();
 524        cx.background_spawn(async move {
 525            let language_tasks = if let Some(task) = associated_tasks {
 526                task.await.map(|templates| {
 527                    templates
 528                        .0
 529                        .into_iter()
 530                        .flat_map(|task| Some((task_source_kind.clone()?, task)))
 531                })
 532            } else {
 533                None
 534            };
 535
 536            let worktree_tasks = worktree_tasks
 537                .into_iter()
 538                .chain(language_tasks.into_iter().flatten())
 539                .chain(global_tasks);
 540
 541            let new_resolved_tasks = worktree_tasks
 542                .flat_map(|(kind, task)| {
 543                    let id_base = kind.to_id_base();
 544
 545                    if let TaskSourceKind::Worktree { id, .. } = &kind {
 546                        None.or_else(|| {
 547                            let (_, _, item_context) =
 548                                task_contexts.active_item_context.as_ref().filter(
 549                                    |(worktree_id, _, _)| Some(id) == worktree_id.as_ref(),
 550                                )?;
 551                            task.resolve_task(&id_base, item_context)
 552                        })
 553                        .or_else(|| {
 554                            let (_, worktree_context) = task_contexts
 555                                .active_worktree_context
 556                                .as_ref()
 557                                .filter(|(worktree_id, _)| id == worktree_id)?;
 558                            task.resolve_task(&id_base, worktree_context)
 559                        })
 560                        .or_else(|| {
 561                            if let TaskSourceKind::Worktree { id, .. } = &kind {
 562                                let worktree_context = task_contexts
 563                                    .other_worktree_contexts
 564                                    .iter()
 565                                    .find(|(worktree_id, _)| worktree_id == id)
 566                                    .map(|(_, context)| context)?;
 567                                task.resolve_task(&id_base, worktree_context)
 568                            } else {
 569                                None
 570                            }
 571                        })
 572                    } else {
 573                        None.or_else(|| {
 574                            let (_, _, item_context) =
 575                                task_contexts.active_item_context.as_ref()?;
 576                            task.resolve_task(&id_base, item_context)
 577                        })
 578                        .or_else(|| {
 579                            let (_, worktree_context) =
 580                                task_contexts.active_worktree_context.as_ref()?;
 581                            task.resolve_task(&id_base, worktree_context)
 582                        })
 583                    }
 584                    .or_else(|| task.resolve_task(&id_base, &TaskContext::default()))
 585                    .map(move |resolved_task| (kind.clone(), resolved_task, not_used_score))
 586                })
 587                .filter(|(_, resolved_task, _)| {
 588                    match task_labels_to_ids.entry(resolved_task.resolved_label.clone()) {
 589                        hash_map::Entry::Occupied(mut o) => {
 590                            // Allow new tasks with the same label, if their context is different
 591                            o.get_mut().insert(resolved_task.id.clone())
 592                        }
 593                        hash_map::Entry::Vacant(v) => {
 594                            v.insert(HashSet::from_iter(Some(resolved_task.id.clone())));
 595                            true
 596                        }
 597                    }
 598                })
 599                .sorted_unstable_by(task_lru_comparator)
 600                .map(|(kind, task, _)| (kind, task))
 601                .collect::<Vec<_>>();
 602
 603            (previously_spawned_tasks, new_resolved_tasks)
 604        })
 605    }
 606
 607    /// Returns the last scheduled task by task_id if provided.
 608    /// Otherwise, returns the last scheduled task.
 609    pub fn last_scheduled_task(
 610        &self,
 611        task_id: Option<&TaskId>,
 612    ) -> Option<(TaskSourceKind, ResolvedTask)> {
 613        if let Some(task_id) = task_id {
 614            self.last_scheduled_tasks
 615                .iter()
 616                .find(|(_, task)| &task.id == task_id)
 617                .cloned()
 618        } else {
 619            self.last_scheduled_tasks.back().cloned()
 620        }
 621    }
 622
 623    /// Registers task "usage" as being scheduled – to be used for LRU sorting when listing all tasks.
 624    pub fn task_scheduled(
 625        &mut self,
 626        task_source_kind: TaskSourceKind,
 627        resolved_task: ResolvedTask,
 628    ) {
 629        self.last_scheduled_tasks
 630            .push_back((task_source_kind, resolved_task));
 631        if self.last_scheduled_tasks.len() > 5_000 {
 632            self.last_scheduled_tasks.pop_front();
 633        }
 634    }
 635
 636    /// Deletes a resolved task from history, using its id.
 637    /// A similar may still resurface in `used_and_current_resolved_tasks` when its [`TaskTemplate`] is resolved again.
 638    pub fn delete_previously_used(&mut self, id: &TaskId) {
 639        self.last_scheduled_tasks.retain(|(_, task)| &task.id != id);
 640    }
 641
 642    fn global_templates_from_settings(
 643        &self,
 644    ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
 645        self.templates_from_settings.global_scenarios()
 646    }
 647
 648    fn global_debug_scenarios_from_settings(
 649        &self,
 650    ) -> impl '_ + Iterator<Item = (TaskSourceKind, DebugScenario)> {
 651        self.scenarios_from_settings.global_scenarios()
 652    }
 653
 654    fn worktree_scenarios_from_settings(
 655        &self,
 656        worktree: WorktreeId,
 657    ) -> impl '_ + Iterator<Item = (TaskSourceKind, DebugScenario)> {
 658        self.scenarios_from_settings.worktree_scenarios(worktree)
 659    }
 660
 661    fn worktree_templates_from_settings(
 662        &self,
 663        worktree: WorktreeId,
 664    ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
 665        self.templates_from_settings.worktree_scenarios(worktree)
 666    }
 667
 668    /// Updates in-memory task metadata from the JSON string given.
 669    /// Will fail if the JSON is not a valid array of objects, but will continue if any object will not parse into a [`TaskTemplate`].
 670    ///
 671    /// Global tasks are updated for no worktree provided, otherwise the worktree metadata for a given path will be updated.
 672    pub fn update_file_based_tasks(
 673        &mut self,
 674        location: TaskSettingsLocation<'_>,
 675        raw_tasks_json: Option<&str>,
 676    ) -> Result<(), InvalidSettingsError> {
 677        let raw_tasks = match parse_json_with_comments::<Vec<serde_json::Value>>(
 678            raw_tasks_json.unwrap_or("[]"),
 679        ) {
 680            Ok(tasks) => tasks,
 681            Err(e) => {
 682                return Err(InvalidSettingsError::Tasks {
 683                    path: match location {
 684                        TaskSettingsLocation::Global(path) => path.to_owned(),
 685                        TaskSettingsLocation::Worktree(settings_location) => {
 686                            settings_location.path.as_std_path().join(task_file_name())
 687                        }
 688                    },
 689                    message: format!("Failed to parse tasks file content as a JSON array: {e}"),
 690                });
 691            }
 692        };
 693
 694        let mut validation_errors = Vec::new();
 695        let new_templates = raw_tasks.into_iter().filter_map(|raw_template| {
 696            let template = serde_json::from_value::<TaskTemplate>(raw_template).log_err()?;
 697
 698            // Validate the variable names used in the `TaskTemplate`.
 699            let unknown_variables = template.unknown_variables();
 700            if !unknown_variables.is_empty() {
 701                let variables_list = unknown_variables
 702                    .iter()
 703                    .map(|variable| format!("${variable}"))
 704                    .collect::<Vec<_>>()
 705                    .join(", ");
 706
 707                validation_errors.push(format!(
 708                    "Task '{}' uses unknown variables: {}",
 709                    template.label, variables_list
 710                ));
 711
 712                // Skip this template, since it uses unknown variable names, but
 713                // continue processing others.
 714                return None;
 715            }
 716
 717            Some(template)
 718        });
 719
 720        let parsed_templates = &mut self.templates_from_settings;
 721        match location {
 722            TaskSettingsLocation::Global(path) => {
 723                parsed_templates
 724                    .global
 725                    .entry(path.to_owned())
 726                    .insert_entry(new_templates.collect());
 727                self.last_scheduled_tasks.retain(|(kind, _)| {
 728                    if let TaskSourceKind::AbsPath { abs_path, .. } = kind {
 729                        abs_path != path
 730                    } else {
 731                        true
 732                    }
 733                });
 734            }
 735            TaskSettingsLocation::Worktree(location) => {
 736                let new_templates = new_templates.collect::<Vec<_>>();
 737                if new_templates.is_empty() {
 738                    if let Some(worktree_tasks) =
 739                        parsed_templates.worktree.get_mut(&location.worktree_id)
 740                    {
 741                        worktree_tasks.remove(location.path);
 742                    }
 743                } else {
 744                    parsed_templates
 745                        .worktree
 746                        .entry(location.worktree_id)
 747                        .or_default()
 748                        .insert(Arc::from(location.path), new_templates);
 749                }
 750                self.last_scheduled_tasks.retain(|(kind, _)| {
 751                    if let TaskSourceKind::Worktree {
 752                        directory_in_worktree,
 753                        id,
 754                        ..
 755                    } = kind
 756                    {
 757                        *id != location.worktree_id
 758                            || directory_in_worktree.as_ref() != location.path
 759                    } else {
 760                        true
 761                    }
 762                });
 763            }
 764        }
 765
 766        if !validation_errors.is_empty() {
 767            return Err(InvalidSettingsError::Tasks {
 768                path: match &location {
 769                    TaskSettingsLocation::Global(path) => path.to_path_buf(),
 770                    TaskSettingsLocation::Worktree(location) => {
 771                        location.path.as_std_path().join(task_file_name())
 772                    }
 773                },
 774                message: validation_errors.join("\n"),
 775            });
 776        }
 777
 778        Ok(())
 779    }
 780
 781    /// Updates in-memory task metadata from the JSON string given.
 782    /// Will fail if the JSON is not a valid array of objects, but will continue if any object will not parse into a [`TaskTemplate`].
 783    ///
 784    /// Global tasks are updated for no worktree provided, otherwise the worktree metadata for a given path will be updated.
 785    pub fn update_file_based_scenarios(
 786        &mut self,
 787        location: TaskSettingsLocation<'_>,
 788        raw_tasks_json: Option<&str>,
 789    ) -> Result<(), InvalidSettingsError> {
 790        let raw_tasks = match parse_json_with_comments::<Vec<serde_json::Value>>(
 791            raw_tasks_json.unwrap_or("[]"),
 792        ) {
 793            Ok(tasks) => tasks,
 794            Err(e) => {
 795                return Err(InvalidSettingsError::Debug {
 796                    path: match location {
 797                        TaskSettingsLocation::Global(path) => path.to_owned(),
 798                        TaskSettingsLocation::Worktree(settings_location) => settings_location
 799                            .path
 800                            .as_std_path()
 801                            .join(debug_task_file_name()),
 802                    },
 803                    message: format!("Failed to parse tasks file content as a JSON array: {e}"),
 804                });
 805            }
 806        };
 807
 808        let new_templates = raw_tasks
 809            .into_iter()
 810            .filter_map(|raw_template| {
 811                serde_json::from_value::<DebugScenario>(raw_template).log_err()
 812            })
 813            .collect::<Vec<_>>();
 814
 815        let parsed_scenarios = &mut self.scenarios_from_settings;
 816        let mut new_definitions: HashMap<_, _> = new_templates
 817            .iter()
 818            .map(|template| (template.label.clone(), template.clone()))
 819            .collect();
 820        let previously_existing_scenarios;
 821
 822        match location {
 823            TaskSettingsLocation::Global(path) => {
 824                previously_existing_scenarios = parsed_scenarios
 825                    .global_scenarios()
 826                    .map(|(_, scenario)| scenario.label)
 827                    .collect::<HashSet<_>>();
 828                parsed_scenarios
 829                    .global
 830                    .entry(path.to_owned())
 831                    .insert_entry(new_templates);
 832            }
 833            TaskSettingsLocation::Worktree(location) => {
 834                previously_existing_scenarios = parsed_scenarios
 835                    .worktree_scenarios(location.worktree_id)
 836                    .map(|(_, scenario)| scenario.label)
 837                    .collect::<HashSet<_>>();
 838
 839                if new_templates.is_empty() {
 840                    if let Some(worktree_tasks) =
 841                        parsed_scenarios.worktree.get_mut(&location.worktree_id)
 842                    {
 843                        worktree_tasks.remove(location.path);
 844                    }
 845                } else {
 846                    parsed_scenarios
 847                        .worktree
 848                        .entry(location.worktree_id)
 849                        .or_default()
 850                        .insert(Arc::from(location.path), new_templates);
 851                }
 852            }
 853        }
 854        self.last_scheduled_scenarios.retain_mut(|(scenario, _)| {
 855            if !previously_existing_scenarios.contains(&scenario.label) {
 856                return true;
 857            }
 858            if let Some(new_definition) = new_definitions.remove(&scenario.label) {
 859                *scenario = new_definition;
 860                true
 861            } else {
 862                false
 863            }
 864        });
 865
 866        Ok(())
 867    }
 868}
 869
 870fn task_lru_comparator(
 871    (kind_a, task_a, lru_score_a): &(TaskSourceKind, ResolvedTask, u32),
 872    (kind_b, task_b, lru_score_b): &(TaskSourceKind, ResolvedTask, u32),
 873) -> cmp::Ordering {
 874    lru_score_a
 875        // First, display recently used templates above all.
 876        .cmp(lru_score_b)
 877        // Then, ensure more specific sources are displayed first.
 878        .then(task_source_kind_preference(kind_a).cmp(&task_source_kind_preference(kind_b)))
 879        // After that, display first more specific tasks, using more template variables.
 880        // Bonus points for tasks with symbol variables.
 881        .then(task_variables_preference(task_a).cmp(&task_variables_preference(task_b)))
 882        // Finally, sort by the resolved label, but a bit more specifically, to avoid mixing letters and digits.
 883        .then({
 884            NumericPrefixWithSuffix::from_numeric_prefixed_str(&task_a.resolved_label)
 885                .cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str(
 886                    &task_b.resolved_label,
 887                ))
 888                .then(task_a.resolved_label.cmp(&task_b.resolved_label))
 889                .then(kind_a.cmp(kind_b))
 890        })
 891}
 892
 893pub fn task_source_kind_preference(kind: &TaskSourceKind) -> u32 {
 894    match kind {
 895        TaskSourceKind::Lsp { .. } => 0,
 896        TaskSourceKind::Language { .. } => 1,
 897        TaskSourceKind::UserInput => 2,
 898        TaskSourceKind::Worktree { .. } => 3,
 899        TaskSourceKind::AbsPath { .. } => 4,
 900    }
 901}
 902
 903fn task_variables_preference(task: &ResolvedTask) -> Reverse<usize> {
 904    let task_variables = task.substituted_variables();
 905    Reverse(if task_variables.contains(&VariableName::Symbol) {
 906        task_variables.len() + 1
 907    } else {
 908        task_variables.len()
 909    })
 910}
 911
 912/// A context provided that tries to provide values for all non-custom [`VariableName`] variants for a currently opened file.
 913/// Applied as a base for every custom [`ContextProvider`] unless explicitly oped out.
 914pub struct BasicContextProvider {
 915    worktree_store: Entity<WorktreeStore>,
 916}
 917
 918impl BasicContextProvider {
 919    pub fn new(worktree_store: Entity<WorktreeStore>) -> Self {
 920        Self { worktree_store }
 921    }
 922}
 923
 924impl ContextProvider for BasicContextProvider {
 925    fn build_context(
 926        &self,
 927        _: &TaskVariables,
 928        location: ContextLocation<'_>,
 929        _: Option<HashMap<String, String>>,
 930        _: Arc<dyn LanguageToolchainStore>,
 931        cx: &mut App,
 932    ) -> Task<Result<TaskVariables>> {
 933        let location = location.file_location;
 934        let buffer = location.buffer.read(cx);
 935        let buffer_snapshot = buffer.snapshot();
 936        let symbols = buffer_snapshot.symbols_containing(location.range.start, None);
 937        let symbol = symbols.last().map(|symbol| {
 938            let range = symbol
 939                .name_ranges
 940                .last()
 941                .cloned()
 942                .unwrap_or(0..symbol.text.len());
 943            symbol.text[range].to_string()
 944        });
 945
 946        let current_file = buffer.file().and_then(|file| file.as_local());
 947        let Point { row, column } = location.range.start.to_point(&buffer_snapshot);
 948        let row = row + 1;
 949        let column = column + 1;
 950        let selected_text = buffer
 951            .chars_for_range(location.range.clone())
 952            .collect::<String>();
 953
 954        let mut task_variables = TaskVariables::from_iter([
 955            (VariableName::Row, row.to_string()),
 956            (VariableName::Column, column.to_string()),
 957        ]);
 958
 959        if let Some(symbol) = symbol {
 960            task_variables.insert(VariableName::Symbol, symbol);
 961        }
 962        if !selected_text.trim().is_empty() {
 963            task_variables.insert(VariableName::SelectedText, selected_text);
 964        }
 965        let worktree = buffer
 966            .file()
 967            .map(|file| file.worktree_id(cx))
 968            .and_then(|worktree_id| {
 969                self.worktree_store
 970                    .read(cx)
 971                    .worktree_for_id(worktree_id, cx)
 972            });
 973
 974        if let Some(worktree) = worktree {
 975            let worktree = worktree.read(cx);
 976            let path_style = worktree.path_style();
 977            task_variables.insert(
 978                VariableName::WorktreeRoot,
 979                worktree.abs_path().to_string_lossy().into_owned(),
 980            );
 981            if let Some(current_file) = current_file.as_ref() {
 982                let relative_path = current_file.path();
 983                task_variables.insert(
 984                    VariableName::RelativeFile,
 985                    relative_path.display(path_style).to_string(),
 986                );
 987                if let Some(relative_dir) = relative_path.parent() {
 988                    task_variables.insert(
 989                        VariableName::RelativeDir,
 990                        if relative_dir.is_empty() {
 991                            String::from(".")
 992                        } else {
 993                            relative_dir.display(path_style).to_string()
 994                        },
 995                    );
 996                }
 997            }
 998        }
 999
1000        if let Some(current_file) = current_file {
1001            let path = current_file.abs_path(cx);
1002            if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
1003                task_variables.insert(VariableName::Filename, String::from(filename));
1004            }
1005
1006            if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
1007                task_variables.insert(VariableName::Stem, stem.into());
1008            }
1009
1010            if let Some(dirname) = path.parent().and_then(|s| s.to_str()) {
1011                task_variables.insert(VariableName::Dirname, dirname.into());
1012            }
1013
1014            task_variables.insert(VariableName::File, path.to_string_lossy().into_owned());
1015        }
1016
1017        if let Some(language) = buffer.language() {
1018            task_variables.insert(VariableName::Language, language.name().to_string());
1019        }
1020
1021        Task::ready(Ok(task_variables))
1022    }
1023}
1024
1025/// A ContextProvider that doesn't provide any task variables on it's own, though it has some associated tasks.
1026pub struct ContextProviderWithTasks {
1027    templates: TaskTemplates,
1028}
1029
1030impl ContextProviderWithTasks {
1031    pub fn new(definitions: TaskTemplates) -> Self {
1032        Self {
1033            templates: definitions,
1034        }
1035    }
1036}
1037
1038impl ContextProvider for ContextProviderWithTasks {
1039    fn associated_tasks(&self, _: Option<Arc<dyn File>>, _: &App) -> Task<Option<TaskTemplates>> {
1040        Task::ready(Some(self.templates.clone()))
1041    }
1042}