1061 lines · 39.2 KB · Rust
RawDownload
//! 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.