1262 lines · 48.0 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::PathBuf,    sync::Arc,}; use anyhow::Result;use collections::{HashMap, HashSet, VecDeque};use dap::DapRegistry;use gpui::{App, AppContext as _, Context, Entity, SharedString, Task, WeakEntity};use itertools::Itertools;use language::{    Buffer, ContextLocation, ContextProvider, File, Language, LanguageToolchainStore, Location,    language_settings::LanguageSettings,};use lsp::{LanguageServerId, LanguageServerName};use paths::{debug_task_file_name, task_file_name};use settings::{InvalidSettingsError, parse_json_with_comments};use task::{    DebugScenario, ResolvedTask, SharedTaskContext, TaskContext, TaskHook, TaskId, TaskTemplate,    TaskTemplates, TaskVariables, VariableName,};use text::{BufferId, Point, ToPoint};use util::{NumericPrefixWithSuffix, ResultExt as _, post_inc, rel_path::RelPath};use worktree::WorktreeId; use crate::{git_store::GitStore, task_store::TaskSettingsLocation, worktree_store::WorktreeStore}; pub const GIT_COMMAND_TASK_TAG: &str = "git-command"; #[derive(Clone, Debug, Default)]pub struct DebugScenarioContext {    pub task_context: SharedTaskContext,    pub worktree_id: Option<WorktreeId>,    pub active_buffer: Option<WeakEntity<Buffer>>,} /// Inventory tracks available tasks for a given project.pub struct Inventory {    last_scheduled_tasks: VecDeque<(TaskSourceKind, ResolvedTask)>,    last_scheduled_scenarios: VecDeque<(DebugScenario, DebugScenarioContext)>,    templates_from_settings: InventoryFor<TaskTemplate>,    scenarios_from_settings: InventoryFor<DebugScenario>,} impl std::fmt::Debug for Inventory {    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {        f.debug_struct("Inventory")            .field("last_scheduled_tasks", &self.last_scheduled_tasks)            .field("last_scheduled_scenarios", &self.last_scheduled_scenarios)            .field("templates_from_settings", &self.templates_from_settings)            .field("scenarios_from_settings", &self.scenarios_from_settings)            .finish()    }} // 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<RelPath>, Vec<T>>>,} impl<T: InventoryContents> InventoryFor<T> {    fn worktree_scenarios(        &self,        worktree: WorktreeId,    ) -> impl '_ + Iterator<Item = (TaskSourceKind, T)> {        let worktree_dirs = self.worktree.get(&worktree);        let has_zed_dir = worktree_dirs            .map(|dirs| {                dirs.keys()                    .any(|dir| dir.file_name().is_some_and(|name| name == ".zed"))            })            .unwrap_or(false);         worktree_dirs            .into_iter()            .flatten()            .filter(move |(directory, _)| {                !(has_zed_dir && directory.file_name().is_some_and(|name| name == ".vscode"))            })            .flat_map(|(directory, templates)| {                templates.iter().map(move |template| (directory, template))            })            .map(move |(directory, template)| {                (                    TaskSourceKind::Worktree {                        id: worktree,                        directory_in_worktree: directory.clone(),                        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.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: Arc<RelPath>,        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)    }     /// Re-resolves a previously resolved task against the current contexts, so that    /// any context-dependent variables (e.g. the active file or selection) reflect the    /// editor's current state rather than the state from when the task was first run.    ///    /// For worktree tasks the search is scoped to the task's own worktree; other tasks    /// fall back through the active item, active worktree, and finally an empty context.    /// If none of the contexts resolve, the original `resolved_task` is returned unchanged.    fn reresolve_task(&self, kind: &TaskSourceKind, resolved_task: &ResolvedTask) -> ResolvedTask {        let id_base = kind.to_id_base();        let task = resolved_task.original_task();         let candidate_contexts: Vec<Option<&TaskContext>> = match kind {            TaskSourceKind::Worktree { id, .. } => vec![                self.active_item_context                    .as_ref()                    .filter(|(worktree_id, _, _)| worktree_id.as_ref() == Some(id))                    .map(|(_, _, context)| context),                self.active_worktree_context                    .as_ref()                    .filter(|(worktree_id, _)| worktree_id == id)                    .map(|(_, context)| context),                self.other_worktree_contexts                    .iter()                    .find(|(worktree_id, _)| worktree_id == id)                    .map(|(_, context)| context),            ],            _ => vec![                self.active_item_context                    .as_ref()                    .map(|(_, _, context)| context),                self.active_worktree_context                    .as_ref()                    .map(|(_, context)| context),            ],        };         candidate_contexts            .into_iter()            .flatten()            .find_map(|context| task.resolve_task(&id_base, context))            .or_else(|| task.resolve_task(&id_base, &TaskContext::default()))            .unwrap_or_else(|| resolved_task.clone())    }} 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.as_unix_str())            }            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 {            last_scheduled_tasks: VecDeque::default(),            last_scheduled_scenarios: VecDeque::default(),            templates_from_settings: InventoryFor::default(),            scenarios_from_settings: InventoryFor::default(),        })    }     pub fn scenario_scheduled(        &mut self,        scenario: DebugScenario,        task_context: SharedTaskContext,        worktree_id: Option<WorktreeId>,        active_buffer: Option<WeakEntity<Buffer>>,    ) {        self.last_scheduled_scenarios            .retain(|(s, _)| s.label != scenario.label);        self.last_scheduled_scenarios.push_front((            scenario,            DebugScenarioContext {                task_context,                worktree_id,                active_buffer,            },        ));        if self.last_scheduled_scenarios.len() > 5_000 {            self.last_scheduled_scenarios.pop_front();        }    }     pub fn last_scheduled_scenario(&self) -> Option<&(DebugScenario, DebugScenarioContext)> {        self.last_scheduled_scenarios.back()    }     pub fn list_debug_scenarios(        &self,        task_contexts: &TaskContexts,        lsp_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>,        current_resolved_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>,        add_current_language_tasks: bool,        cx: &mut App,    ) -> Task<(        Vec<(DebugScenario, DebugScenarioContext)>,        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 last_scheduled_scenarios = self.last_scheduled_scenarios.iter().cloned().collect();         let adapter = task_contexts.location().and_then(|location| {            let buffer = location.buffer.read(cx);            let adapter = LanguageSettings::for_buffer(&buffer, cx)                .debuggers                .first()                .map(SharedString::from)                .or_else(|| {                    buffer                        .language()                        .and_then(|l| l.config().debuggers.first().map(SharedString::from))                });            adapter.map(|adapter| (adapter, DapRegistry::global(cx).locators()))        });        cx.background_spawn(async move {            if let Some((adapter, locators)) = adapter {                for (kind, task) in                    lsp_tasks                        .into_iter()                        .chain(current_resolved_tasks.into_iter().filter(|(kind, _)| {                            add_current_language_tasks                                || !matches!(kind, TaskSourceKind::Language { .. })                        }))                {                    let adapter = adapter.clone().into();                     for locator in locators.values() {                        if let Some(scenario) = locator                            .create_scenario(task.original_task(), task.display_label(), &adapter)                            .await                        {                            scenarios.push((kind, scenario));                            break;                        }                    }                }            }            (last_scheduled_scenarios, scenarios)        })    }     pub fn task_template_by_label(        &self,        buffer: Option<Entity<Buffer>>,        worktree_id: Option<WorktreeId>,        label: &str,        cx: &App,    ) -> Task<Option<TaskTemplate>> {        let (buffer_worktree_id, language) = buffer            .as_ref()            .map(|buffer| {                let buffer = buffer.read(cx);                (                    buffer.file().as_ref().map(|file| file.worktree_id(cx)),                    buffer.language().cloned(),                )            })            .unwrap_or((None, None));         let tasks = self.list_tasks(buffer, language, worktree_id.or(buffer_worktree_id), cx);        let label = label.to_owned();        cx.background_spawn(async move {            tasks                .await                .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,        buffer: Option<Entity<Buffer>>,        language: Option<Arc<Language>>,        worktree: Option<WorktreeId>,        cx: &App,    ) -> Task<Vec<(TaskSourceKind, TaskTemplate)>> {        let global_tasks = self.global_templates_from_settings().collect::<Vec<_>>();        let mut worktree_tasks = worktree            .into_iter()            .flat_map(|worktree| self.worktree_templates_from_settings(worktree))            .collect::<Vec<_>>();         let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {            name: language.name().into(),        });        let language_tasks = language            .filter(|language| {                LanguageSettings::resolve(                    buffer.as_ref().map(|b| b.read(cx)),                    Some(&language.name()),                    cx,                )                .tasks                .enabled            })            .and_then(|language| {                language                    .context_provider()                    .map(|provider| provider.associated_tasks(buffer, cx))            });        cx.background_spawn(async move {            if let Some(t) = language_tasks {                worktree_tasks.extend(t.await.into_iter().flat_map(|tasks| {                    tasks                        .0                        .into_iter()                        .filter_map(|task| Some((task_source_kind.clone()?, task)))                }));            }            worktree_tasks.extend(global_tasks);            worktree_tasks        })    }     /// 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(        &self,        task_contexts: Arc<TaskContexts>,        cx: &mut Context<Self>,    ) -> Task<(        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());        let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {            name: language.name().into(),        });        let buffer = location.map(|location| location.buffer.clone());         let worktrees_with_zed_tasks: HashSet<WorktreeId> = self            .templates_from_settings            .worktree            .iter()

Lines 1–500 of 1262.