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