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