//! Project-wide storage of the tasks available, capable of updating itself from the sources set. use std::{ borrow::Cow, cmp::{self, Reverse}, collections::hash_map, path::{Path, PathBuf}, sync::Arc,}; use anyhow::Result;use collections::{HashMap, HashSet, VecDeque};use gpui::{App, AppContext as _, Entity, SharedString, Task};use itertools::Itertools;use language::{ ContextProvider, File, Language, LanguageToolchainStore, Location, language_settings::language_settings,};use lsp::{LanguageServerId, LanguageServerName};use settings::{InvalidSettingsError, TaskKind, parse_json_with_comments};use task::{ DebugTaskTemplate, ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates, TaskVariables, VariableName,};use text::{BufferId, Point, ToPoint};use util::{NumericPrefixWithSuffix, ResultExt as _, paths::PathExt as _, post_inc};use worktree::WorktreeId; use crate::{task_store::TaskSettingsLocation, worktree_store::WorktreeStore}; /// Inventory tracks available tasks for a given project.#[derive(Debug, Default)]pub struct Inventory { last_scheduled_tasks: VecDeque<(TaskSourceKind, ResolvedTask)>, templates_from_settings: ParsedTemplates,} #[derive(Debug, Default)]struct ParsedTemplates { global: HashMap<PathBuf, Vec<TaskTemplate>>, worktree: HashMap<WorktreeId, HashMap<(Arc<Path>, TaskKind), Vec<TaskTemplate>>>,} /// Kind of a source the tasks are fetched from, used to display more source information in the UI.#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]pub enum TaskSourceKind { /// bash-like commands spawned by users, not associated with any path UserInput, /// Tasks from the worktree's .zed/task.json Worktree { id: WorktreeId, directory_in_worktree: PathBuf, id_base: Cow<'static, str>, }, /// ~/.config/zed/task.json - like global files with task definitions, applicable to any path AbsPath { id_base: Cow<'static, str>, abs_path: PathBuf, }, /// Languages-specific tasks coming from extensions. Language { name: SharedString }, /// Language-specific tasks coming from LSP servers. Lsp(LanguageServerId),} /// A collection of task contexts, derived from the current state of the workspace./// Only contains worktrees that are visible and with their root being a directory.#[derive(Debug, Default)]pub struct TaskContexts { /// A context, related to the currently opened item. /// Item can be opened from an invisible worktree, or any other, not necessarily active worktree. pub active_item_context: Option<(Option<WorktreeId>, Option<Location>, TaskContext)>, /// A worktree that corresponds to the active item, or the only worktree in the workspace. pub active_worktree_context: Option<(WorktreeId, TaskContext)>, /// If there are multiple worktrees in the workspace, all non-active ones are included here. pub other_worktree_contexts: Vec<(WorktreeId, TaskContext)>, pub lsp_task_sources: HashMap<LanguageServerName, Vec<BufferId>>, pub latest_selection: Option<text::Anchor>,} impl TaskContexts { pub fn active_context(&self) -> Option<&TaskContext> { self.active_item_context .as_ref() .map(|(_, _, context)| context) .or_else(|| { self.active_worktree_context .as_ref() .map(|(_, context)| context) }) } pub fn location(&self) -> Option<&Location> { self.active_item_context .as_ref() .and_then(|(_, location, _)| location.as_ref()) } pub fn worktree(&self) -> Option<WorktreeId> { self.active_item_context .as_ref() .and_then(|(worktree_id, _, _)| worktree_id.as_ref()) .or_else(|| { self.active_worktree_context .as_ref() .map(|(worktree_id, _)| worktree_id) }) .copied() }} impl TaskSourceKind { pub fn to_id_base(&self) -> String { match self { Self::UserInput => "oneshot".to_string(), Self::AbsPath { id_base, abs_path } => { format!("{id_base}_{}", abs_path.display()) } Self::Worktree { id, id_base, directory_in_worktree, } => { format!("{id_base}_{id}_{}", directory_in_worktree.display()) } Self::Language { name } => format!("language_{name}"), Self::Lsp(server_id) => format!("lsp_{server_id}"), } }} impl Inventory { pub fn new(cx: &mut App) -> Entity<Self> { cx.new(|_| Self::default()) } pub fn list_debug_tasks(&self) -> Vec<&TaskTemplate> { self.templates_from_settings .worktree .values() .flat_map(|tasks| { tasks.iter().filter_map(|(kind, tasks)| { if matches!(kind.1, TaskKind::Debug) { Some(tasks) } else { None } }) }) .flatten() .collect() } /// Pulls its task sources relevant to the worktree and the language given, /// returns all task templates with their source kinds, worktree tasks first, language tasks second /// and global tasks last. No specific order inside source kinds groups. pub fn list_tasks( &self, file: Option<Arc<dyn File>>, language: Option<Arc<Language>>, worktree: Option<WorktreeId>, cx: &App, ) -> Vec<(TaskSourceKind, TaskTemplate)> { let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language { name: language.name().into(), }); let global_tasks = self.global_templates_from_settings(); let language_tasks = language .filter(|language| { language_settings(Some(language.name()), file.as_ref(), cx) .tasks .enabled }) .and_then(|language| language.context_provider()?.associated_tasks(file, cx)) .into_iter() .flat_map(|tasks| tasks.0.into_iter()) .flat_map(|task| Some((task_source_kind.clone()?, task))) .chain(global_tasks); self.worktree_templates_from_settings(worktree) .chain(language_tasks) .collect() } /// Pulls its task sources relevant to the worktree and the language given and resolves them with the [`TaskContexts`] given. /// Joins the new resolutions with the resolved tasks that were used (spawned) before, /// orders them so that the most recently used come first, all equally used ones are ordered so that the most specific tasks come first. /// Deduplicates the tasks by their labels and context and splits the ordered list into two: used tasks and the rest, newly resolved tasks. pub fn used_and_current_resolved_tasks<'a>( &'a self, task_contexts: &'a TaskContexts, cx: &'a App, ) -> ( Vec<(TaskSourceKind, ResolvedTask)>, Vec<(TaskSourceKind, ResolvedTask)>, ) { let worktree = task_contexts.worktree(); let location = task_contexts.location(); let language = location .and_then(|location| location.buffer.read(cx).language_at(location.range.start)); let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language { name: language.name().into(), }); let file = location.and_then(|location| location.buffer.read(cx).file().cloned()); let mut task_labels_to_ids = HashMap::<String, HashSet<TaskId>>::default(); let mut lru_score = 0_u32; let previously_spawned_tasks = self .last_scheduled_tasks .iter() .rev() .filter(|(task_kind, _)| { if matches!(task_kind, TaskSourceKind::Language { .. }) { Some(task_kind) == task_source_kind.as_ref() } else { true } }) .filter(|(_, resolved_task)| { match task_labels_to_ids.entry(resolved_task.resolved_label.clone()) { hash_map::Entry::Occupied(mut o) => { o.get_mut().insert(resolved_task.id.clone()); // Neber allow duplicate reused tasks with the same labels false } hash_map::Entry::Vacant(v) => { v.insert(HashSet::from_iter(Some(resolved_task.id.clone()))); true } } }) .map(|(task_source_kind, resolved_task)| { ( task_source_kind.clone(), resolved_task.clone(), post_inc(&mut lru_score), ) }) .sorted_unstable_by(task_lru_comparator) .map(|(kind, task, _)| (kind, task)) .collect::<Vec<_>>(); let not_used_score = post_inc(&mut lru_score); let global_tasks = self.global_templates_from_settings(); let language_tasks = language .filter(|language| { language_settings(Some(language.name()), file.as_ref(), cx) .tasks .enabled }) .and_then(|language| language.context_provider()?.associated_tasks(file, cx)) .into_iter() .flat_map(|tasks| tasks.0.into_iter()) .flat_map(|task| Some((task_source_kind.clone()?, task))); let worktree_tasks = self .worktree_templates_from_settings(worktree) .chain(language_tasks) .chain(global_tasks); let new_resolved_tasks = worktree_tasks .flat_map(|(kind, task)| { let id_base = kind.to_id_base(); if let TaskSourceKind::Worktree { id, .. } = &kind { None.or_else(|| { let (_, _, item_context) = task_contexts .active_item_context .as_ref() .filter(|(worktree_id, _, _)| Some(id) == worktree_id.as_ref())?; task.resolve_task(&id_base, item_context) }) .or_else(|| { let (_, worktree_context) = task_contexts .active_worktree_context .as_ref() .filter(|(worktree_id, _)| id == worktree_id)?; task.resolve_task(&id_base, worktree_context) }) .or_else(|| { if let TaskSourceKind::Worktree { id, .. } = &kind { let worktree_context = task_contexts .other_worktree_contexts .iter() .find(|(worktree_id, _)| worktree_id == id) .map(|(_, context)| context)?; task.resolve_task(&id_base, worktree_context) } else { None } }) } else { None.or_else(|| { let (_, _, item_context) = task_contexts.active_item_context.as_ref()?; task.resolve_task(&id_base, item_context) }) .or_else(|| { let (_, worktree_context) = task_contexts.active_worktree_context.as_ref()?; task.resolve_task(&id_base, worktree_context) }) } .or_else(|| task.resolve_task(&id_base, &TaskContext::default())) .map(move |resolved_task| (kind.clone(), resolved_task, not_used_score)) }) .filter(|(_, resolved_task, _)| { match task_labels_to_ids.entry(resolved_task.resolved_label.clone()) { hash_map::Entry::Occupied(mut o) => { // Allow new tasks with the same label, if their context is different o.get_mut().insert(resolved_task.id.clone()) } hash_map::Entry::Vacant(v) => { v.insert(HashSet::from_iter(Some(resolved_task.id.clone()))); true } } }) .sorted_unstable_by(task_lru_comparator) .map(|(kind, task, _)| (kind, task)) .collect::<Vec<_>>(); (previously_spawned_tasks, new_resolved_tasks) } /// Returns the last scheduled task by task_id if provided. /// Otherwise, returns the last scheduled task. pub fn last_scheduled_task( &self, task_id: Option<&TaskId>, ) -> Option<(TaskSourceKind, ResolvedTask)> { if let Some(task_id) = task_id { self.last_scheduled_tasks .iter() .find(|(_, task)| &task.id == task_id) .cloned() } else { self.last_scheduled_tasks.back().cloned() } } /// Registers task "usage" as being scheduled – to be used for LRU sorting when listing all tasks. pub fn task_scheduled( &mut self, task_source_kind: TaskSourceKind, resolved_task: ResolvedTask, ) { self.last_scheduled_tasks .push_back((task_source_kind, resolved_task)); if self.last_scheduled_tasks.len() > 5_000 { self.last_scheduled_tasks.pop_front(); } } /// Deletes a resolved task from history, using its id. /// A similar may still resurface in `used_and_current_resolved_tasks` when its [`TaskTemplate`] is resolved again. pub fn delete_previously_used(&mut self, id: &TaskId) { self.last_scheduled_tasks.retain(|(_, task)| &task.id != id); } fn global_templates_from_settings( &self, ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> { self.templates_from_settings .global .iter() .flat_map(|(file_path, templates)| { templates.into_iter().map(|template| { ( TaskSourceKind::AbsPath { id_base: match template.task_type { task::TaskType::Script => Cow::Borrowed("global tasks.json"), task::TaskType::Debug(_) => Cow::Borrowed("global debug.json"), }, abs_path: file_path.clone(), }, template.clone(), ) }) }) } fn worktree_templates_from_settings( &self, worktree: Option<WorktreeId>, ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> { worktree.into_iter().flat_map(|worktree| { self.templates_from_settings .worktree .get(&worktree) .into_iter() .flatten() .flat_map(|(directory, templates)| { templates.iter().map(move |template| (directory, template)) }) .map(move |((directory, _task_kind), template)| { ( TaskSourceKind::Worktree { id: worktree, directory_in_worktree: directory.to_path_buf(), id_base: Cow::Owned(format!( "local worktree tasks from directory {directory:?}" )), }, template.clone(), ) }) }) } /// Updates in-memory task metadata from the JSON string given. /// Will fail if the JSON is not a valid array of objects, but will continue if any object will not parse into a [`TaskTemplate`]. /// /// Global tasks are updated for no worktree provided, otherwise the worktree metadata for a given path will be updated. pub(crate) fn update_file_based_tasks( &mut self, location: TaskSettingsLocation<'_>, raw_tasks_json: Option<&str>, task_kind: TaskKind, ) -> Result<(), InvalidSettingsError> { let raw_tasks = match parse_json_with_comments::<Vec<serde_json::Value>>( raw_tasks_json.unwrap_or("[]"), ) { Ok(tasks) => tasks, Err(e) => { return Err(InvalidSettingsError::Tasks { path: match location { TaskSettingsLocation::Global(path) => path.to_owned(), TaskSettingsLocation::Worktree(settings_location) => { task_kind.config_in_dir(settings_location.path) } }, message: format!("Failed to parse tasks file content as a JSON array: {e}"), }); } }; let new_templates = raw_tasks .into_iter() .filter_map(|raw_template| match &task_kind { TaskKind::Script => serde_json::from_value::<TaskTemplate>(raw_template).log_err(), TaskKind::Debug => serde_json::from_value::<DebugTaskTemplate>(raw_template) .log_err() .map(|content| content.to_zed_format()), }); let parsed_templates = &mut self.templates_from_settings; match location { TaskSettingsLocation::Global(path) => { parsed_templates .global .entry(path.to_owned()) .insert_entry(new_templates.collect()); } TaskSettingsLocation::Worktree(location) => { let new_templates = new_templates.collect::<Vec<_>>(); if new_templates.is_empty() { if let Some(worktree_tasks) = parsed_templates.worktree.get_mut(&location.worktree_id) { worktree_tasks.remove(&(Arc::from(location.path), task_kind)); } } else { parsed_templates .worktree .entry(location.worktree_id) .or_default() .insert((Arc::from(location.path), task_kind), new_templates); } } } Ok(()) }} fn task_lru_comparator( (kind_a, task_a, lru_score_a): &(TaskSourceKind, ResolvedTask, u32), (kind_b, task_b, lru_score_b): &(TaskSourceKind, ResolvedTask, u32),) -> cmp::Ordering { lru_score_a // First, display recently used templates above all. .cmp(lru_score_b) // Then, ensure more specific sources are displayed first. .then(task_source_kind_preference(kind_a).cmp(&task_source_kind_preference(kind_b))) // After that, display first more specific tasks, using more template variables. // Bonus points for tasks with symbol variables. .then(task_variables_preference(task_a).cmp(&task_variables_preference(task_b))) // Finally, sort by the resolved label, but a bit more specifically, to avoid mixing letters and digits. .then({ NumericPrefixWithSuffix::from_numeric_prefixed_str(&task_a.resolved_label) .cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str( &task_b.resolved_label, )) .then(task_a.resolved_label.cmp(&task_b.resolved_label)) .then(kind_a.cmp(kind_b)) })} fn task_source_kind_preference(kind: &TaskSourceKind) -> u32 { match kind { TaskSourceKind::Lsp(..) => 0, TaskSourceKind::Language { .. } => 1, TaskSourceKind::UserInput => 2,Lines 1–500 of 1061. Load more
//! Project-wide storage of the tasks available, capable of updating itself from the sources set. use std::{ borrow::Cow, cmp::{self, Reverse}, collections::hash_map, path::{Path, PathBuf}, sync::Arc, }; use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; use gpui::{App, AppContext as _, Entity, SharedString, Task}; use itertools::Itertools; use language::{ ContextProvider, File, Language, LanguageToolchainStore, Location, language_settings::language_settings, }; use lsp::{LanguageServerId, LanguageServerName}; use settings::{InvalidSettingsError, TaskKind, parse_json_with_comments}; use task::{ DebugTaskTemplate, ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates, TaskVariables, VariableName, }; use text::{BufferId, Point, ToPoint}; use util::{NumericPrefixWithSuffix, ResultExt as _, paths::PathExt as _, post_inc}; use worktree::WorktreeId; use crate::{task_store::TaskSettingsLocation, worktree_store::WorktreeStore}; /// Inventory tracks available tasks for a given project. #[derive(Debug, Default)] pub struct Inventory { last_scheduled_tasks: VecDeque<(TaskSourceKind, ResolvedTask)>, templates_from_settings: ParsedTemplates, } #[derive(Debug, Default)] struct ParsedTemplates { global: HashMap<PathBuf, Vec<TaskTemplate>>, worktree: HashMap<WorktreeId, HashMap<(Arc<Path>, TaskKind), Vec<TaskTemplate>>>, } /// Kind of a source the tasks are fetched from, used to display more source information in the UI. #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum TaskSourceKind { /// bash-like commands spawned by users, not associated with any path UserInput, /// Tasks from the worktree's .zed/task.json Worktree { id: WorktreeId, directory_in_worktree: PathBuf, id_base: Cow<'static, str>, }, /// ~/.config/zed/task.json - like global files with task definitions, applicable to any path AbsPath { id_base: Cow<'static, str>, abs_path: PathBuf, }, /// Languages-specific tasks coming from extensions. Language { name: SharedString }, /// Language-specific tasks coming from LSP servers. Lsp(LanguageServerId), } /// A collection of task contexts, derived from the current state of the workspace. /// Only contains worktrees that are visible and with their root being a directory. #[derive(Debug, Default)] pub struct TaskContexts { /// A context, related to the currently opened item. /// Item can be opened from an invisible worktree, or any other, not necessarily active worktree. pub active_item_context: Option<(Option<WorktreeId>, Option<Location>, TaskContext)>, /// A worktree that corresponds to the active item, or the only worktree in the workspace. pub active_worktree_context: Option<(WorktreeId, TaskContext)>, /// If there are multiple worktrees in the workspace, all non-active ones are included here. pub other_worktree_contexts: Vec<(WorktreeId, TaskContext)>, pub lsp_task_sources: HashMap<LanguageServerName, Vec<BufferId>>, pub latest_selection: Option<text::Anchor>, } impl TaskContexts { pub fn active_context(&self) -> Option<&TaskContext> { self.active_item_context .as_ref() .map(|(_, _, context)| context) .or_else(|| { self.active_worktree_context .as_ref() .map(|(_, context)| context) }) } pub fn location(&self) -> Option<&Location> { self.active_item_context .as_ref() .and_then(|(_, location, _)| location.as_ref()) } pub fn worktree(&self) -> Option<WorktreeId> { self.active_item_context .as_ref() .and_then(|(worktree_id, _, _)| worktree_id.as_ref()) .or_else(|| { self.active_worktree_context .as_ref() .map(|(worktree_id, _)| worktree_id) }) .copied() } } impl TaskSourceKind { pub fn to_id_base(&self) -> String { match self { Self::UserInput => "oneshot".to_string(), Self::AbsPath { id_base, abs_path } => { format!("{id_base}_{}", abs_path.display()) } Self::Worktree { id, id_base, directory_in_worktree, } => { format!("{id_base}_{id}_{}", directory_in_worktree.display()) } Self::Language { name } => format!("language_{name}"), Self::Lsp(server_id) => format!("lsp_{server_id}"), } } } impl Inventory { pub fn new(cx: &mut App) -> Entity<Self> { cx.new(|_| Self::default()) } pub fn list_debug_tasks(&self) -> Vec<&TaskTemplate> { self.templates_from_settings .worktree .values() .flat_map(|tasks| { tasks.iter().filter_map(|(kind, tasks)| { if matches!(kind.1, TaskKind::Debug) { Some(tasks) } else { None } }) }) .flatten() .collect() } /// Pulls its task sources relevant to the worktree and the language given, /// returns all task templates with their source kinds, worktree tasks first, language tasks second /// and global tasks last. No specific order inside source kinds groups. pub fn list_tasks( &self, file: Option<Arc<dyn File>>, language: Option<Arc<Language>>, worktree: Option<WorktreeId>, cx: &App, ) -> Vec<(TaskSourceKind, TaskTemplate)> { let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language { name: language.name().into(), }); let global_tasks = self.global_templates_from_settings(); let language_tasks = language .filter(|language| { language_settings(Some(language.name()), file.as_ref(), cx) .tasks .enabled }) .and_then(|language| language.context_provider()?.associated_tasks(file, cx)) .into_iter() .flat_map(|tasks| tasks.0.into_iter()) .flat_map(|task| Some((task_source_kind.clone()?, task))) .chain(global_tasks); self.worktree_templates_from_settings(worktree) .chain(language_tasks) .collect() } /// Pulls its task sources relevant to the worktree and the language given and resolves them with the [`TaskContexts`] given. /// Joins the new resolutions with the resolved tasks that were used (spawned) before, /// orders them so that the most recently used come first, all equally used ones are ordered so that the most specific tasks come first. /// Deduplicates the tasks by their labels and context and splits the ordered list into two: used tasks and the rest, newly resolved tasks. pub fn used_and_current_resolved_tasks<'a>( &'a self, task_contexts: &'a TaskContexts, cx: &'a App, ) -> ( Vec<(TaskSourceKind, ResolvedTask)>, Vec<(TaskSourceKind, ResolvedTask)>, ) { let worktree = task_contexts.worktree(); let location = task_contexts.location(); let language = location .and_then(|location| location.buffer.read(cx).language_at(location.range.start)); let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language { name: language.name().into(), }); let file = location.and_then(|location| location.buffer.read(cx).file().cloned()); let mut task_labels_to_ids = HashMap::<String, HashSet<TaskId>>::default(); let mut lru_score = 0_u32; let previously_spawned_tasks = self .last_scheduled_tasks .iter() .rev() .filter(|(task_kind, _)| { if matches!(task_kind, TaskSourceKind::Language { .. }) { Some(task_kind) == task_source_kind.as_ref() } else { true } }) .filter(|(_, resolved_task)| { match task_labels_to_ids.entry(resolved_task.resolved_label.clone()) { hash_map::Entry::Occupied(mut o) => { o.get_mut().insert(resolved_task.id.clone()); // Neber allow duplicate reused tasks with the same labels false } hash_map::Entry::Vacant(v) => { v.insert(HashSet::from_iter(Some(resolved_task.id.clone()))); true } } }) .map(|(task_source_kind, resolved_task)| { ( task_source_kind.clone(), resolved_task.clone(), post_inc(&mut lru_score), ) }) .sorted_unstable_by(task_lru_comparator) .map(|(kind, task, _)| (kind, task)) .collect::<Vec<_>>(); let not_used_score = post_inc(&mut lru_score); let global_tasks = self.global_templates_from_settings(); let language_tasks = language .filter(|language| { language_settings(Some(language.name()), file.as_ref(), cx) .tasks .enabled }) .and_then(|language| language.context_provider()?.associated_tasks(file, cx)) .into_iter() .flat_map(|tasks| tasks.0.into_iter()) .flat_map(|task| Some((task_source_kind.clone()?, task))); let worktree_tasks = self .worktree_templates_from_settings(worktree) .chain(language_tasks) .chain(global_tasks); let new_resolved_tasks = worktree_tasks .flat_map(|(kind, task)| { let id_base = kind.to_id_base(); if let TaskSourceKind::Worktree { id, .. } = &kind { None.or_else(|| { let (_, _, item_context) = task_contexts .active_item_context .as_ref() .filter(|(worktree_id, _, _)| Some(id) == worktree_id.as_ref())?; task.resolve_task(&id_base, item_context) }) .or_else(|| { let (_, worktree_context) = task_contexts .active_worktree_context .as_ref() .filter(|(worktree_id, _)| id == worktree_id)?; task.resolve_task(&id_base, worktree_context) }) .or_else(|| { if let TaskSourceKind::Worktree { id, .. } = &kind { let worktree_context = task_contexts .other_worktree_contexts .iter() .find(|(worktree_id, _)| worktree_id == id) .map(|(_, context)| context)?; task.resolve_task(&id_base, worktree_context) } else { None } }) } else { None.or_else(|| { let (_, _, item_context) = task_contexts.active_item_context.as_ref()?; task.resolve_task(&id_base, item_context) }) .or_else(|| { let (_, worktree_context) = task_contexts.active_worktree_context.as_ref()?; task.resolve_task(&id_base, worktree_context) }) } .or_else(|| task.resolve_task(&id_base, &TaskContext::default())) .map(move |resolved_task| (kind.clone(), resolved_task, not_used_score)) }) .filter(|(_, resolved_task, _)| { match task_labels_to_ids.entry(resolved_task.resolved_label.clone()) { hash_map::Entry::Occupied(mut o) => { // Allow new tasks with the same label, if their context is different o.get_mut().insert(resolved_task.id.clone()) } hash_map::Entry::Vacant(v) => { v.insert(HashSet::from_iter(Some(resolved_task.id.clone()))); true } } }) .sorted_unstable_by(task_lru_comparator) .map(|(kind, task, _)| (kind, task)) .collect::<Vec<_>>(); (previously_spawned_tasks, new_resolved_tasks) } /// Returns the last scheduled task by task_id if provided. /// Otherwise, returns the last scheduled task. pub fn last_scheduled_task( &self, task_id: Option<&TaskId>, ) -> Option<(TaskSourceKind, ResolvedTask)> { if let Some(task_id) = task_id { self.last_scheduled_tasks .iter() .find(|(_, task)| &task.id == task_id) .cloned() } else { self.last_scheduled_tasks.back().cloned() } } /// Registers task "usage" as being scheduled – to be used for LRU sorting when listing all tasks. pub fn task_scheduled( &mut self, task_source_kind: TaskSourceKind, resolved_task: ResolvedTask, ) { self.last_scheduled_tasks .push_back((task_source_kind, resolved_task)); if self.last_scheduled_tasks.len() > 5_000 { self.last_scheduled_tasks.pop_front(); } } /// Deletes a resolved task from history, using its id. /// A similar may still resurface in `used_and_current_resolved_tasks` when its [`TaskTemplate`] is resolved again. pub fn delete_previously_used(&mut self, id: &TaskId) { self.last_scheduled_tasks.retain(|(_, task)| &task.id != id); } fn global_templates_from_settings( &self, ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> { self.templates_from_settings .global .iter() .flat_map(|(file_path, templates)| { templates.into_iter().map(|template| { ( TaskSourceKind::AbsPath { id_base: match template.task_type { task::TaskType::Script => Cow::Borrowed("global tasks.json"), task::TaskType::Debug(_) => Cow::Borrowed("global debug.json"), }, abs_path: file_path.clone(), }, template.clone(), ) }) }) } fn worktree_templates_from_settings( &self, worktree: Option<WorktreeId>, ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> { worktree.into_iter().flat_map(|worktree| { self.templates_from_settings .worktree .get(&worktree) .into_iter() .flatten() .flat_map(|(directory, templates)| { templates.iter().map(move |template| (directory, template)) }) .map(move |((directory, _task_kind), template)| { ( TaskSourceKind::Worktree { id: worktree, directory_in_worktree: directory.to_path_buf(), id_base: Cow::Owned(format!( "local worktree tasks from directory {directory:?}" )), }, template.clone(), ) }) }) } /// Updates in-memory task metadata from the JSON string given. /// Will fail if the JSON is not a valid array of objects, but will continue if any object will not parse into a [`TaskTemplate`]. /// /// Global tasks are updated for no worktree provided, otherwise the worktree metadata for a given path will be updated. pub(crate) fn update_file_based_tasks( &mut self, location: TaskSettingsLocation<'_>, raw_tasks_json: Option<&str>, task_kind: TaskKind, ) -> Result<(), InvalidSettingsError> { let raw_tasks = match parse_json_with_comments::<Vec<serde_json::Value>>( raw_tasks_json.unwrap_or("[]"), ) { Ok(tasks) => tasks, Err(e) => { return Err(InvalidSettingsError::Tasks { path: match location { TaskSettingsLocation::Global(path) => path.to_owned(), TaskSettingsLocation::Worktree(settings_location) => { task_kind.config_in_dir(settings_location.path) } }, message: format!("Failed to parse tasks file content as a JSON array: {e}"), }); } }; let new_templates = raw_tasks .into_iter() .filter_map(|raw_template| match &task_kind { TaskKind::Script => serde_json::from_value::<TaskTemplate>(raw_template).log_err(), TaskKind::Debug => serde_json::from_value::<DebugTaskTemplate>(raw_template) .log_err() .map(|content| content.to_zed_format()), }); let parsed_templates = &mut self.templates_from_settings; match location { TaskSettingsLocation::Global(path) => { parsed_templates .global .entry(path.to_owned()) .insert_entry(new_templates.collect()); } TaskSettingsLocation::Worktree(location) => { let new_templates = new_templates.collect::<Vec<_>>(); if new_templates.is_empty() { if let Some(worktree_tasks) = parsed_templates.worktree.get_mut(&location.worktree_id) { worktree_tasks.remove(&(Arc::from(location.path), task_kind)); } } else { parsed_templates .worktree .entry(location.worktree_id) .or_default() .insert((Arc::from(location.path), task_kind), new_templates); } } } Ok(()) } } fn task_lru_comparator( (kind_a, task_a, lru_score_a): &(TaskSourceKind, ResolvedTask, u32), (kind_b, task_b, lru_score_b): &(TaskSourceKind, ResolvedTask, u32), ) -> cmp::Ordering { lru_score_a // First, display recently used templates above all. .cmp(lru_score_b) // Then, ensure more specific sources are displayed first. .then(task_source_kind_preference(kind_a).cmp(&task_source_kind_preference(kind_b))) // After that, display first more specific tasks, using more template variables. // Bonus points for tasks with symbol variables. .then(task_variables_preference(task_a).cmp(&task_variables_preference(task_b))) // Finally, sort by the resolved label, but a bit more specifically, to avoid mixing letters and digits. .then({ NumericPrefixWithSuffix::from_numeric_prefixed_str(&task_a.resolved_label) .cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str( &task_b.resolved_label, )) .then(task_a.resolved_label.cmp(&task_b.resolved_label)) .then(kind_a.cmp(kind_b)) }) } fn task_source_kind_preference(kind: &TaskSourceKind) -> u32 { match kind { TaskSourceKind::Lsp(..) => 0, TaskSourceKind::Language { .. } => 1, TaskSourceKind::UserInput => 2,