1360 lines · 49.9 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 dap::DapRegistry;use gpui::{App, AppContext as _, Entity, SharedString, Task};use itertools::Itertools;use language::{    Buffer, ContextLocation, ContextProvider, File, Language, LanguageToolchainStore, Location,    language_settings::language_settings,};use lsp::{LanguageServerId, LanguageServerName};use paths::{debug_task_file_name, task_file_name};use settings::{InvalidSettingsError, parse_json_with_comments};use task::{    DebugScenario, 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)>,    last_scheduled_scenarios: VecDeque<DebugScenario>,    templates_from_settings: InventoryFor<TaskTemplate>,    scenarios_from_settings: InventoryFor<DebugScenario>,} // Helper trait for better error messages in [InventoryFor]trait InventoryContents: Clone {    const GLOBAL_SOURCE_FILE: &'static str;    const LABEL: &'static str;} impl InventoryContents for TaskTemplate {    const GLOBAL_SOURCE_FILE: &'static str = "tasks.json";    const LABEL: &'static str = "tasks";} impl InventoryContents for DebugScenario {    const GLOBAL_SOURCE_FILE: &'static str = "debug.json";     const LABEL: &'static str = "debug scenarios";} #[derive(Debug)]struct InventoryFor<T> {    global: HashMap<PathBuf, Vec<T>>,    worktree: HashMap<WorktreeId, HashMap<Arc<Path>, Vec<T>>>,} impl<T: InventoryContents> InventoryFor<T> {    fn worktree_scenarios(        &self,        worktree: WorktreeId,    ) -> impl '_ + Iterator<Item = (TaskSourceKind, T)> {        self.worktree            .get(&worktree)            .into_iter()            .flatten()            .flat_map(|(directory, templates)| {                templates.iter().map(move |template| (directory, template))            })            .map(move |(directory, template)| {                (                    TaskSourceKind::Worktree {                        id: worktree,                        directory_in_worktree: directory.to_path_buf(),                        id_base: Cow::Owned(format!(                            "local worktree {} from directory {directory:?}",                            T::LABEL                        )),                    },                    template.clone(),                )            })    }     fn global_scenarios(&self) -> impl '_ + Iterator<Item = (TaskSourceKind, T)> {        self.global.iter().flat_map(|(file_path, templates)| {            templates.into_iter().map(|template| {                (                    TaskSourceKind::AbsPath {                        id_base: Cow::Owned(format!("global {}", T::GLOBAL_SOURCE_FILE)),                        abs_path: file_path.clone(),                    },                    template.clone(),                )            })        })    }} impl<T> Default for InventoryFor<T> {    fn default() -> Self {        Self {            global: HashMap::default(),            worktree: HashMap::default(),        }    }} /// 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 {        language_name: SharedString,        server: 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 file(&self, cx: &App) -> Option<Arc<dyn File>> {        self.active_item_context            .as_ref()            .and_then(|(_, location, _)| location.as_ref())            .and_then(|location| location.buffer.read(cx).file().cloned())    }     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()    }     pub fn task_context_for_worktree_id(&self, worktree_id: WorktreeId) -> Option<&TaskContext> {        self.active_worktree_context            .iter()            .chain(self.other_worktree_contexts.iter())            .find(|(id, _)| *id == worktree_id)            .map(|(_, context)| context)    }} 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,                language_name,            } => format!("lsp_{language_name}_{server}"),        }    }} impl Inventory {    pub fn new(cx: &mut App) -> Entity<Self> {        cx.new(|_| Self::default())    }     pub fn scenario_scheduled(&mut self, scenario: DebugScenario) {        self.last_scheduled_scenarios            .retain(|s| s.label != scenario.label);        self.last_scheduled_scenarios.push_back(scenario);        if self.last_scheduled_scenarios.len() > 5_000 {            self.last_scheduled_scenarios.pop_front();        }    }     pub fn last_scheduled_scenario(&self) -> Option<&DebugScenario> {        self.last_scheduled_scenarios.back()    }     pub fn list_debug_scenarios(        &self,        task_contexts: &TaskContexts,        cx: &mut App,    ) -> (Vec<DebugScenario>, Vec<(TaskSourceKind, DebugScenario)>) {        let mut scenarios = Vec::new();         if let Some(worktree_id) = task_contexts            .active_worktree_context            .iter()            .chain(task_contexts.other_worktree_contexts.iter())            .map(|context| context.0)            .next()        {            scenarios.extend(self.worktree_scenarios_from_settings(worktree_id));        }        scenarios.extend(self.global_debug_scenarios_from_settings());         let (_, new) = self.used_and_current_resolved_tasks(task_contexts, cx);        if let Some(location) = task_contexts.location() {            let file = location.buffer.read(cx).file();            let language = location.buffer.read(cx).language();            let language_name = language.as_ref().map(|l| l.name());            let adapter = language_settings(language_name, file, cx)                .debuggers                .first()                .map(SharedString::from)                .or_else(|| {                    language.and_then(|l| l.config().debuggers.first().map(SharedString::from))                });            if let Some(adapter) = adapter {                for (kind, task) in new {                    if let Some(scenario) =                        DapRegistry::global(cx)                            .locators()                            .values()                            .find_map(|locator| {                                locator.create_scenario(                                    &task.original_task().clone(),                                    &task.display_label(),                                    adapter.clone().into(),                                )                            })                    {                        scenarios.push((kind, scenario));                    }                }            }        }         (            self.last_scheduled_scenarios.iter().cloned().collect(),            scenarios,        )    }     pub fn task_template_by_label(        &self,        buffer: Option<Entity<Buffer>>,        worktree_id: Option<WorktreeId>,        label: &str,        cx: &App,    ) -> Option<TaskTemplate> {        let (buffer_worktree_id, file, language) = buffer            .map(|buffer| {                let buffer = buffer.read(cx);                let file = buffer.file().cloned();                (                    file.as_ref().map(|file| file.worktree_id(cx)),                    file,                    buffer.language().cloned(),                )            })            .unwrap_or((None, None, None));         self.list_tasks(file, language, worktree_id.or(buffer_worktree_id), cx)            .into_iter()            .find(|(_, template)| template.label == label)            .map(|val| val.1)    }     /// 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 global_tasks = self.global_templates_from_settings();        let worktree_tasks = worktree            .into_iter()            .flat_map(|worktree| self.worktree_templates_from_settings(worktree));        let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {            name: language.name().into(),        });        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)));         worktree_tasks            .chain(language_tasks)            .chain(global_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 = worktree            .into_iter()            .flat_map(|worktree| 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(

Lines 1–500 of 1360.