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