task_inventory.rs

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