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