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