task_inventory.rs

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