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, SettingsLocation, 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::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: 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            .clone()
 328            .into_iter()
 329            .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: match template.task_type {
 337                            task::TaskType::Script => paths::tasks_file().clone(),
 338                            task::TaskType::Debug(_) => paths::debug_tasks_file().clone(),
 339                        },
 340                    },
 341                    template,
 342                )
 343            })
 344    }
 345
 346    fn worktree_templates_from_settings(
 347        &self,
 348        worktree: Option<WorktreeId>,
 349    ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
 350        worktree.into_iter().flat_map(|worktree| {
 351            self.templates_from_settings
 352                .worktree
 353                .get(&worktree)
 354                .into_iter()
 355                .flatten()
 356                .flat_map(|(directory, templates)| {
 357                    templates.iter().map(move |template| (directory, template))
 358                })
 359                .map(move |((directory, _task_kind), template)| {
 360                    (
 361                        TaskSourceKind::Worktree {
 362                            id: worktree,
 363                            directory_in_worktree: directory.to_path_buf(),
 364                            id_base: Cow::Owned(format!(
 365                                "local worktree tasks from directory {directory:?}"
 366                            )),
 367                        },
 368                        template.clone(),
 369                    )
 370                })
 371        })
 372    }
 373
 374    /// Updates in-memory task metadata from the JSON string given.
 375    /// Will fail if the JSON is not a valid array of objects, but will continue if any object will not parse into a [`TaskTemplate`].
 376    ///
 377    /// Global tasks are updated for no worktree provided, otherwise the worktree metadata for a given path will be updated.
 378    pub(crate) fn update_file_based_tasks(
 379        &mut self,
 380        location: Option<SettingsLocation<'_>>,
 381        raw_tasks_json: Option<&str>,
 382        task_kind: TaskKind,
 383    ) -> anyhow::Result<()> {
 384        let raw_tasks =
 385            parse_json_with_comments::<Vec<serde_json::Value>>(raw_tasks_json.unwrap_or("[]"))
 386                .context("parsing tasks file content as a JSON array")?;
 387        let new_templates = raw_tasks
 388            .into_iter()
 389            .filter_map(|raw_template| match &task_kind {
 390                TaskKind::Script => serde_json::from_value::<TaskTemplate>(raw_template).log_err(),
 391                TaskKind::Debug => serde_json::from_value::<DebugTaskDefinition>(raw_template)
 392                    .log_err()
 393                    .and_then(|content| content.to_zed_format().log_err()),
 394            });
 395
 396        let parsed_templates = &mut self.templates_from_settings;
 397        match location {
 398            Some(location) => {
 399                let new_templates = new_templates.collect::<Vec<_>>();
 400                if new_templates.is_empty() {
 401                    if let Some(worktree_tasks) =
 402                        parsed_templates.worktree.get_mut(&location.worktree_id)
 403                    {
 404                        worktree_tasks.remove(&(Arc::from(location.path), task_kind));
 405                    }
 406                } else {
 407                    parsed_templates
 408                        .worktree
 409                        .entry(location.worktree_id)
 410                        .or_default()
 411                        .insert((Arc::from(location.path), task_kind), new_templates);
 412                }
 413            }
 414            None => parsed_templates.global = new_templates.collect(),
 415        }
 416        Ok(())
 417    }
 418}
 419
 420fn task_lru_comparator(
 421    (kind_a, task_a, lru_score_a): &(TaskSourceKind, ResolvedTask, u32),
 422    (kind_b, task_b, lru_score_b): &(TaskSourceKind, ResolvedTask, u32),
 423) -> cmp::Ordering {
 424    lru_score_a
 425        // First, display recently used templates above all.
 426        .cmp(lru_score_b)
 427        // Then, ensure more specific sources are displayed first.
 428        .then(task_source_kind_preference(kind_a).cmp(&task_source_kind_preference(kind_b)))
 429        // After that, display first more specific tasks, using more template variables.
 430        // Bonus points for tasks with symbol variables.
 431        .then(task_variables_preference(task_a).cmp(&task_variables_preference(task_b)))
 432        // Finally, sort by the resolved label, but a bit more specifically, to avoid mixing letters and digits.
 433        .then({
 434            NumericPrefixWithSuffix::from_numeric_prefixed_str(&task_a.resolved_label)
 435                .cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str(
 436                    &task_b.resolved_label,
 437                ))
 438                .then(task_a.resolved_label.cmp(&task_b.resolved_label))
 439                .then(kind_a.cmp(kind_b))
 440        })
 441}
 442
 443fn task_source_kind_preference(kind: &TaskSourceKind) -> u32 {
 444    match kind {
 445        TaskSourceKind::Language { .. } => 1,
 446        TaskSourceKind::UserInput => 2,
 447        TaskSourceKind::Worktree { .. } => 3,
 448        TaskSourceKind::AbsPath { .. } => 4,
 449    }
 450}
 451
 452fn task_variables_preference(task: &ResolvedTask) -> Reverse<usize> {
 453    let task_variables = task.substituted_variables();
 454    Reverse(if task_variables.contains(&VariableName::Symbol) {
 455        task_variables.len() + 1
 456    } else {
 457        task_variables.len()
 458    })
 459}
 460
 461#[cfg(test)]
 462mod test_inventory {
 463    use gpui::{Entity, TestAppContext};
 464    use itertools::Itertools;
 465    use task::TaskContext;
 466    use worktree::WorktreeId;
 467
 468    use crate::Inventory;
 469
 470    use super::TaskSourceKind;
 471
 472    pub(super) fn task_template_names(
 473        inventory: &Entity<Inventory>,
 474        worktree: Option<WorktreeId>,
 475        cx: &mut TestAppContext,
 476    ) -> Vec<String> {
 477        inventory.update(cx, |inventory, cx| {
 478            inventory
 479                .list_tasks(None, None, worktree, cx)
 480                .into_iter()
 481                .map(|(_, task)| task.label)
 482                .sorted()
 483                .collect()
 484        })
 485    }
 486
 487    pub(super) fn register_task_used(
 488        inventory: &Entity<Inventory>,
 489        task_name: &str,
 490        cx: &mut TestAppContext,
 491    ) {
 492        inventory.update(cx, |inventory, cx| {
 493            let (task_source_kind, task) = inventory
 494                .list_tasks(None, None, None, cx)
 495                .into_iter()
 496                .find(|(_, task)| task.label == task_name)
 497                .unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
 498            let id_base = task_source_kind.to_id_base();
 499            inventory.task_scheduled(
 500                task_source_kind.clone(),
 501                task.resolve_task(&id_base, &TaskContext::default())
 502                    .unwrap_or_else(|| panic!("Failed to resolve task with name {task_name}")),
 503            );
 504        });
 505    }
 506
 507    pub(super) async fn list_tasks(
 508        inventory: &Entity<Inventory>,
 509        worktree: Option<WorktreeId>,
 510        cx: &mut TestAppContext,
 511    ) -> Vec<(TaskSourceKind, String)> {
 512        inventory.update(cx, |inventory, cx| {
 513            let task_context = &TaskContext::default();
 514            inventory
 515                .list_tasks(None, None, worktree, cx)
 516                .into_iter()
 517                .filter_map(|(source_kind, task)| {
 518                    let id_base = source_kind.to_id_base();
 519                    Some((source_kind, task.resolve_task(&id_base, task_context)?))
 520                })
 521                .map(|(source_kind, resolved_task)| (source_kind, resolved_task.resolved_label))
 522                .collect()
 523        })
 524    }
 525}
 526
 527/// A context provided that tries to provide values for all non-custom [`VariableName`] variants for a currently opened file.
 528/// Applied as a base for every custom [`ContextProvider`] unless explicitly oped out.
 529pub struct BasicContextProvider {
 530    worktree_store: Entity<WorktreeStore>,
 531}
 532
 533impl BasicContextProvider {
 534    pub fn new(worktree_store: Entity<WorktreeStore>) -> Self {
 535        Self { worktree_store }
 536    }
 537}
 538impl ContextProvider for BasicContextProvider {
 539    fn build_context(
 540        &self,
 541        _: &TaskVariables,
 542        location: &Location,
 543        _: Option<HashMap<String, String>>,
 544        _: Arc<dyn LanguageToolchainStore>,
 545        cx: &mut App,
 546    ) -> Task<Result<TaskVariables>> {
 547        let buffer = location.buffer.read(cx);
 548        let buffer_snapshot = buffer.snapshot();
 549        let symbols = buffer_snapshot.symbols_containing(location.range.start, None);
 550        let symbol = symbols.unwrap_or_default().last().map(|symbol| {
 551            let range = symbol
 552                .name_ranges
 553                .last()
 554                .cloned()
 555                .unwrap_or(0..symbol.text.len());
 556            symbol.text[range].to_string()
 557        });
 558
 559        let current_file = buffer
 560            .file()
 561            .and_then(|file| file.as_local())
 562            .map(|file| file.abs_path(cx).to_sanitized_string());
 563        let Point { row, column } = location.range.start.to_point(&buffer_snapshot);
 564        let row = row + 1;
 565        let column = column + 1;
 566        let selected_text = buffer
 567            .chars_for_range(location.range.clone())
 568            .collect::<String>();
 569
 570        let mut task_variables = TaskVariables::from_iter([
 571            (VariableName::Row, row.to_string()),
 572            (VariableName::Column, column.to_string()),
 573        ]);
 574
 575        if let Some(symbol) = symbol {
 576            task_variables.insert(VariableName::Symbol, symbol);
 577        }
 578        if !selected_text.trim().is_empty() {
 579            task_variables.insert(VariableName::SelectedText, selected_text);
 580        }
 581        let worktree_root_dir =
 582            buffer
 583                .file()
 584                .map(|file| file.worktree_id(cx))
 585                .and_then(|worktree_id| {
 586                    self.worktree_store
 587                        .read(cx)
 588                        .worktree_for_id(worktree_id, cx)
 589                        .and_then(|worktree| worktree.read(cx).root_dir())
 590                });
 591        if let Some(worktree_path) = worktree_root_dir {
 592            task_variables.insert(
 593                VariableName::WorktreeRoot,
 594                worktree_path.to_sanitized_string(),
 595            );
 596            if let Some(full_path) = current_file.as_ref() {
 597                let relative_path = pathdiff::diff_paths(full_path, worktree_path);
 598                if let Some(relative_path) = relative_path {
 599                    task_variables.insert(
 600                        VariableName::RelativeFile,
 601                        relative_path.to_sanitized_string(),
 602                    );
 603                }
 604            }
 605        }
 606
 607        if let Some(path_as_string) = current_file {
 608            let path = Path::new(&path_as_string);
 609            if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
 610                task_variables.insert(VariableName::Filename, String::from(filename));
 611            }
 612
 613            if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
 614                task_variables.insert(VariableName::Stem, stem.into());
 615            }
 616
 617            if let Some(dirname) = path.parent().and_then(|s| s.to_str()) {
 618                task_variables.insert(VariableName::Dirname, dirname.into());
 619            }
 620
 621            task_variables.insert(VariableName::File, path_as_string);
 622        }
 623
 624        Task::ready(Ok(task_variables))
 625    }
 626}
 627
 628/// A ContextProvider that doesn't provide any task variables on it's own, though it has some associated tasks.
 629pub struct ContextProviderWithTasks {
 630    templates: TaskTemplates,
 631}
 632
 633impl ContextProviderWithTasks {
 634    pub fn new(definitions: TaskTemplates) -> Self {
 635        Self {
 636            templates: definitions,
 637        }
 638    }
 639}
 640
 641impl ContextProvider for ContextProviderWithTasks {
 642    fn associated_tasks(
 643        &self,
 644        _: Option<Arc<dyn language::File>>,
 645        _: &App,
 646    ) -> Option<TaskTemplates> {
 647        Some(self.templates.clone())
 648    }
 649}
 650
 651#[cfg(test)]
 652mod tests {
 653    use gpui::TestAppContext;
 654    use pretty_assertions::assert_eq;
 655    use serde_json::json;
 656
 657    use crate::task_store::TaskStore;
 658
 659    use super::test_inventory::*;
 660    use super::*;
 661
 662    #[gpui::test]
 663    async fn test_task_list_sorting(cx: &mut TestAppContext) {
 664        init_test(cx);
 665        let inventory = cx.update(Inventory::new);
 666        let initial_tasks = resolved_task_names(&inventory, None, cx).await;
 667        assert!(
 668            initial_tasks.is_empty(),
 669            "No tasks expected for empty inventory, but got {initial_tasks:?}"
 670        );
 671        let initial_tasks = task_template_names(&inventory, None, cx);
 672        assert!(
 673            initial_tasks.is_empty(),
 674            "No tasks expected for empty inventory, but got {initial_tasks:?}"
 675        );
 676        cx.run_until_parked();
 677        let expected_initial_state = [
 678            "1_a_task".to_string(),
 679            "1_task".to_string(),
 680            "2_task".to_string(),
 681            "3_task".to_string(),
 682        ];
 683
 684        inventory.update(cx, |inventory, _| {
 685            inventory
 686                .update_file_based_tasks(
 687                    None,
 688                    Some(&mock_tasks_from_names(
 689                        expected_initial_state.iter().map(|name| name.as_str()),
 690                    )),
 691                    settings::TaskKind::Script,
 692                )
 693                .unwrap();
 694        });
 695        assert_eq!(
 696            task_template_names(&inventory, None, cx),
 697            &expected_initial_state,
 698        );
 699        assert_eq!(
 700            resolved_task_names(&inventory, None, cx).await,
 701            &expected_initial_state,
 702            "Tasks with equal amount of usages should be sorted alphanumerically"
 703        );
 704
 705        register_task_used(&inventory, "2_task", cx);
 706        assert_eq!(
 707            task_template_names(&inventory, None, cx),
 708            &expected_initial_state,
 709        );
 710        assert_eq!(
 711            resolved_task_names(&inventory, None, cx).await,
 712            vec![
 713                "2_task".to_string(),
 714                "1_a_task".to_string(),
 715                "1_task".to_string(),
 716                "3_task".to_string()
 717            ],
 718        );
 719
 720        register_task_used(&inventory, "1_task", cx);
 721        register_task_used(&inventory, "1_task", cx);
 722        register_task_used(&inventory, "1_task", cx);
 723        register_task_used(&inventory, "3_task", cx);
 724        assert_eq!(
 725            task_template_names(&inventory, None, cx),
 726            &expected_initial_state,
 727        );
 728        assert_eq!(
 729            resolved_task_names(&inventory, None, cx).await,
 730            vec![
 731                "3_task".to_string(),
 732                "1_task".to_string(),
 733                "2_task".to_string(),
 734                "1_a_task".to_string(),
 735            ],
 736        );
 737
 738        inventory.update(cx, |inventory, _| {
 739            inventory
 740                .update_file_based_tasks(
 741                    None,
 742                    Some(&mock_tasks_from_names(
 743                        ["10_hello", "11_hello"]
 744                            .into_iter()
 745                            .chain(expected_initial_state.iter().map(|name| name.as_str())),
 746                    )),
 747                    settings::TaskKind::Script,
 748                )
 749                .unwrap();
 750        });
 751        cx.run_until_parked();
 752        let expected_updated_state = [
 753            "10_hello".to_string(),
 754            "11_hello".to_string(),
 755            "1_a_task".to_string(),
 756            "1_task".to_string(),
 757            "2_task".to_string(),
 758            "3_task".to_string(),
 759        ];
 760        assert_eq!(
 761            task_template_names(&inventory, None, cx),
 762            &expected_updated_state,
 763        );
 764        assert_eq!(
 765            resolved_task_names(&inventory, None, cx).await,
 766            vec![
 767                "3_task".to_string(),
 768                "1_task".to_string(),
 769                "2_task".to_string(),
 770                "1_a_task".to_string(),
 771                "10_hello".to_string(),
 772                "11_hello".to_string(),
 773            ],
 774        );
 775
 776        register_task_used(&inventory, "11_hello", cx);
 777        assert_eq!(
 778            task_template_names(&inventory, None, cx),
 779            &expected_updated_state,
 780        );
 781        assert_eq!(
 782            resolved_task_names(&inventory, None, cx).await,
 783            vec![
 784                "11_hello".to_string(),
 785                "3_task".to_string(),
 786                "1_task".to_string(),
 787                "2_task".to_string(),
 788                "1_a_task".to_string(),
 789                "10_hello".to_string(),
 790            ],
 791        );
 792    }
 793
 794    #[gpui::test]
 795    async fn test_inventory_static_task_filters(cx: &mut TestAppContext) {
 796        init_test(cx);
 797        let inventory = cx.update(Inventory::new);
 798        let common_name = "common_task_name";
 799        let worktree_1 = WorktreeId::from_usize(1);
 800        let worktree_2 = WorktreeId::from_usize(2);
 801
 802        cx.run_until_parked();
 803        let worktree_independent_tasks = vec![
 804            (
 805                TaskSourceKind::AbsPath {
 806                    id_base: "global tasks.json".into(),
 807                    abs_path: paths::tasks_file().clone(),
 808                },
 809                common_name.to_string(),
 810            ),
 811            (
 812                TaskSourceKind::AbsPath {
 813                    id_base: "global tasks.json".into(),
 814                    abs_path: paths::tasks_file().clone(),
 815                },
 816                "static_source_1".to_string(),
 817            ),
 818            (
 819                TaskSourceKind::AbsPath {
 820                    id_base: "global tasks.json".into(),
 821                    abs_path: paths::tasks_file().clone(),
 822                },
 823                "static_source_2".to_string(),
 824            ),
 825        ];
 826        let worktree_1_tasks = [
 827            (
 828                TaskSourceKind::Worktree {
 829                    id: worktree_1,
 830                    directory_in_worktree: PathBuf::from(".zed"),
 831                    id_base: "local worktree tasks from directory \".zed\"".into(),
 832                },
 833                common_name.to_string(),
 834            ),
 835            (
 836                TaskSourceKind::Worktree {
 837                    id: worktree_1,
 838                    directory_in_worktree: PathBuf::from(".zed"),
 839                    id_base: "local worktree tasks from directory \".zed\"".into(),
 840                },
 841                "worktree_1".to_string(),
 842            ),
 843        ];
 844        let worktree_2_tasks = [
 845            (
 846                TaskSourceKind::Worktree {
 847                    id: worktree_2,
 848                    directory_in_worktree: PathBuf::from(".zed"),
 849                    id_base: "local worktree tasks from directory \".zed\"".into(),
 850                },
 851                common_name.to_string(),
 852            ),
 853            (
 854                TaskSourceKind::Worktree {
 855                    id: worktree_2,
 856                    directory_in_worktree: PathBuf::from(".zed"),
 857                    id_base: "local worktree tasks from directory \".zed\"".into(),
 858                },
 859                "worktree_2".to_string(),
 860            ),
 861        ];
 862
 863        inventory.update(cx, |inventory, _| {
 864            inventory
 865                .update_file_based_tasks(
 866                    None,
 867                    Some(&mock_tasks_from_names(
 868                        worktree_independent_tasks
 869                            .iter()
 870                            .map(|(_, name)| name.as_str()),
 871                    )),
 872                    settings::TaskKind::Script,
 873                )
 874                .unwrap();
 875            inventory
 876                .update_file_based_tasks(
 877                    Some(SettingsLocation {
 878                        worktree_id: worktree_1,
 879                        path: Path::new(".zed"),
 880                    }),
 881                    Some(&mock_tasks_from_names(
 882                        worktree_1_tasks.iter().map(|(_, name)| name.as_str()),
 883                    )),
 884                    settings::TaskKind::Script,
 885                )
 886                .unwrap();
 887            inventory
 888                .update_file_based_tasks(
 889                    Some(SettingsLocation {
 890                        worktree_id: worktree_2,
 891                        path: Path::new(".zed"),
 892                    }),
 893                    Some(&mock_tasks_from_names(
 894                        worktree_2_tasks.iter().map(|(_, name)| name.as_str()),
 895                    )),
 896                    settings::TaskKind::Script,
 897                )
 898                .unwrap();
 899        });
 900
 901        assert_eq!(
 902            list_tasks_sorted_by_last_used(&inventory, None, cx).await,
 903            worktree_independent_tasks,
 904            "Without a worktree, only worktree-independent tasks should be listed"
 905        );
 906        assert_eq!(
 907            list_tasks_sorted_by_last_used(&inventory, Some(worktree_1), cx).await,
 908            worktree_1_tasks
 909                .iter()
 910                .chain(worktree_independent_tasks.iter())
 911                .cloned()
 912                .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
 913                .collect::<Vec<_>>(),
 914        );
 915        assert_eq!(
 916            list_tasks_sorted_by_last_used(&inventory, Some(worktree_2), cx).await,
 917            worktree_2_tasks
 918                .iter()
 919                .chain(worktree_independent_tasks.iter())
 920                .cloned()
 921                .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
 922                .collect::<Vec<_>>(),
 923        );
 924
 925        assert_eq!(
 926            list_tasks(&inventory, None, cx).await,
 927            worktree_independent_tasks,
 928            "Without a worktree, only worktree-independent tasks should be listed"
 929        );
 930        assert_eq!(
 931            list_tasks(&inventory, Some(worktree_1), cx).await,
 932            worktree_1_tasks
 933                .iter()
 934                .chain(worktree_independent_tasks.iter())
 935                .cloned()
 936                .collect::<Vec<_>>(),
 937        );
 938        assert_eq!(
 939            list_tasks(&inventory, Some(worktree_2), cx).await,
 940            worktree_2_tasks
 941                .iter()
 942                .chain(worktree_independent_tasks.iter())
 943                .cloned()
 944                .collect::<Vec<_>>(),
 945        );
 946    }
 947
 948    fn init_test(_cx: &mut TestAppContext) {
 949        if std::env::var("RUST_LOG").is_ok() {
 950            env_logger::try_init().ok();
 951        }
 952        TaskStore::init(None);
 953    }
 954
 955    async fn resolved_task_names(
 956        inventory: &Entity<Inventory>,
 957        worktree: Option<WorktreeId>,
 958        cx: &mut TestAppContext,
 959    ) -> Vec<String> {
 960        let (used, current) = inventory.update(cx, |inventory, cx| {
 961            let mut task_contexts = TaskContexts::default();
 962            task_contexts.active_worktree_context =
 963                worktree.map(|worktree| (worktree, TaskContext::default()));
 964            inventory.used_and_current_resolved_tasks(&task_contexts, cx)
 965        });
 966        used.into_iter()
 967            .chain(current)
 968            .map(|(_, task)| task.original_task().label.clone())
 969            .collect()
 970    }
 971
 972    fn mock_tasks_from_names<'a>(task_names: impl Iterator<Item = &'a str> + 'a) -> String {
 973        serde_json::to_string(&serde_json::Value::Array(
 974            task_names
 975                .map(|task_name| {
 976                    json!({
 977                        "label": task_name,
 978                        "command": "echo",
 979                        "args": vec![task_name],
 980                    })
 981                })
 982                .collect::<Vec<_>>(),
 983        ))
 984        .unwrap()
 985    }
 986
 987    async fn list_tasks_sorted_by_last_used(
 988        inventory: &Entity<Inventory>,
 989        worktree: Option<WorktreeId>,
 990        cx: &mut TestAppContext,
 991    ) -> Vec<(TaskSourceKind, String)> {
 992        let (used, current) = inventory.update(cx, |inventory, cx| {
 993            let mut task_contexts = TaskContexts::default();
 994            task_contexts.active_worktree_context =
 995                worktree.map(|worktree| (worktree, TaskContext::default()));
 996            inventory.used_and_current_resolved_tasks(&task_contexts, cx)
 997        });
 998        let mut all = used;
 999        all.extend(current);
1000        all.into_iter()
1001            .map(|(source_kind, task)| (source_kind, task.resolved_label))
1002            .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
1003            .collect()
1004    }
1005}