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