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