diff --git a/.zed/tasks.json b/.zed/tasks.json index b6a9d9f4cd794d205d028f12bd8300e70f988f55..be2ccefedca46406713d9abf116c5efa9390fdb8 100644 --- a/.zed/tasks.json +++ b/.zed/tasks.json @@ -4,13 +4,13 @@ "command": "./script/clippy", "args": [], "allow_concurrent_runs": true, - "use_new_terminal": false + "use_new_terminal": false, }, { "label": "cargo run --profile release-fast", "command": "cargo", "args": ["run", "--profile", "release-fast"], "allow_concurrent_runs": true, - "use_new_terminal": false - } + "use_new_terminal": false, + }, ] diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 0ed0aeb78bf8889136a479ed2dac5caba633db55..d440ddfcbbaf673a9eb9cba58767a330e45f9ef0 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -2588,7 +2588,7 @@ impl AgentPanel { anyhow::Ok(()) }); - self._worktree_creation_task = Some(cx.foreground_executor().spawn(async move { + self._worktree_creation_task = Some(cx.background_spawn(async move { task.await.log_err(); })); } @@ -2745,6 +2745,10 @@ impl AgentPanel { new_window_handle.update(cx, |multi_workspace, window, cx| { multi_workspace.activate(new_workspace.clone(), window, cx); + + new_workspace.update(cx, |workspace, cx| { + workspace.run_create_worktree_tasks(window, cx); + }) })?; this.update_in(cx, |this, window, cx| { diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index fc1458979c06fd32aea95056d09703e01ebce897..6f838f02768a38d1c84935f5a7e7a303e682847d 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -1799,6 +1799,26 @@ impl GitStore { &self.repositories } + /// Returns the original (main) repository working directory for the given worktree. + /// For normal checkouts this equals the worktree's own path; for linked + /// worktrees it points back to the original repo. + pub fn original_repo_path_for_worktree( + &self, + worktree_id: WorktreeId, + cx: &App, + ) -> Option> { + self.active_repo_id + .iter() + .chain(self.worktree_ids.keys()) + .find(|repo_id| { + self.worktree_ids + .get(repo_id) + .is_some_and(|ids| ids.contains(&worktree_id)) + }) + .and_then(|repo_id| self.repositories.get(repo_id)) + .map(|repo| repo.read(cx).snapshot().original_repo_abs_path) + } + pub fn status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option { let (repo, path) = self.repository_and_path_for_buffer_id(buffer_id, cx)?; let status = repo.read(cx).snapshot.status_for_path(&path)?; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 0c1b8942cc26976d51d406bfa9f67da714110623..bbfa7ffe208c198e76a9838695765c912977385d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1230,12 +1230,23 @@ impl Project { ) }); + let git_store = cx.new(|cx| { + GitStore::local( + &worktree_store, + buffer_store.clone(), + environment.clone(), + fs.clone(), + cx, + ) + }); + let task_store = cx.new(|cx| { TaskStore::local( buffer_store.downgrade(), worktree_store.clone(), toolchain_store.read(cx).as_language_toolchain_store(), environment.clone(), + git_store.clone(), cx, ) }); @@ -1271,16 +1282,6 @@ impl Project { ) }); - let git_store = cx.new(|cx| { - GitStore::local( - &worktree_store, - buffer_store.clone(), - environment.clone(), - fs.clone(), - cx, - ) - }); - let agent_server_store = cx.new(|cx| { AgentServerStore::local( node.clone(), @@ -1415,30 +1416,6 @@ impl Project { ) }); - let task_store = cx.new(|cx| { - TaskStore::remote( - buffer_store.downgrade(), - worktree_store.clone(), - toolchain_store.read(cx).as_language_toolchain_store(), - remote.read(cx).proto_client(), - REMOTE_SERVER_PROJECT_ID, - cx, - ) - }); - - let settings_observer = cx.new(|cx| { - SettingsObserver::new_remote( - fs.clone(), - worktree_store.clone(), - task_store.clone(), - Some(remote_proto.clone()), - false, - cx, - ) - }); - cx.subscribe(&settings_observer, Self::on_settings_observer_event) - .detach(); - let context_server_store = cx.new(|cx| { ContextServerStore::remote( rpc::proto::REMOTE_SERVER_PROJECT_ID, @@ -1503,6 +1480,31 @@ impl Project { ) }); + let task_store = cx.new(|cx| { + TaskStore::remote( + buffer_store.downgrade(), + worktree_store.clone(), + toolchain_store.read(cx).as_language_toolchain_store(), + remote.read(cx).proto_client(), + REMOTE_SERVER_PROJECT_ID, + git_store.clone(), + cx, + ) + }); + + let settings_observer = cx.new(|cx| { + SettingsObserver::new_remote( + fs.clone(), + worktree_store.clone(), + task_store.clone(), + Some(remote_proto.clone()), + false, + cx, + ) + }); + cx.subscribe(&settings_observer, Self::on_settings_observer_event) + .detach(); + let agent_server_store = cx.new(|_| { AgentServerStore::remote( REMOTE_SERVER_PROJECT_ID, @@ -1732,6 +1734,17 @@ impl Project { ) }); + let git_store = cx.new(|cx| { + GitStore::remote( + // In this remote case we pass None for the environment + &worktree_store, + buffer_store.clone(), + client.clone().into(), + remote_id, + cx, + ) + }); + let task_store = cx.new(|cx| { if run_tasks { TaskStore::remote( @@ -1740,6 +1753,7 @@ impl Project { Arc::new(EmptyToolchainStore), client.clone().into(), remote_id, + git_store.clone(), cx, ) } else { @@ -1758,17 +1772,6 @@ impl Project { ) }); - let git_store = cx.new(|cx| { - GitStore::remote( - // In this remote case we pass None for the environment - &worktree_store, - buffer_store.clone(), - client.clone().into(), - remote_id, - cx, - ) - }); - let agent_server_store = cx.new(|_cx| AgentServerStore::collab()); let replica_id = ReplicaId::new(response.payload.replica_id as u16); diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index 46999b2b7024c6035732b64de30a3e64cd65460c..663380181015d52c9a91f1a23c7bd0d48d8ac57d 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -21,14 +21,14 @@ 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, TaskId, TaskTemplate, + 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::{task_store::TaskSettingsLocation, worktree_store::WorktreeStore}; +use crate::{git_store::GitStore, task_store::TaskSettingsLocation, worktree_store::WorktreeStore}; #[derive(Clone, Debug, Default)] pub struct DebugScenarioContext { @@ -644,6 +644,19 @@ impl Inventory { self.last_scheduled_tasks.retain(|(_, task)| &task.id != id); } + /// Returns all task templates (worktree and global) that have at least one + /// hook in the provided set. + pub fn templates_with_hooks( + &self, + hooks: &HashSet, + worktree: WorktreeId, + ) -> Vec<(TaskSourceKind, TaskTemplate)> { + self.worktree_templates_from_settings(worktree) + .chain(self.global_templates_from_settings()) + .filter(|(_, template)| !template.hooks.is_disjoint(hooks)) + .collect() + } + fn global_templates_from_settings( &self, ) -> impl '_ + Iterator { @@ -918,11 +931,15 @@ fn task_variables_preference(task: &ResolvedTask) -> Reverse { /// Applied as a base for every custom [`ContextProvider`] unless explicitly oped out. pub struct BasicContextProvider { worktree_store: Entity, + git_store: Entity, } impl BasicContextProvider { - pub fn new(worktree_store: Entity) -> Self { - Self { worktree_store } + pub fn new(worktree_store: Entity, git_store: Entity) -> Self { + Self { + worktree_store, + git_store, + } } } @@ -1002,6 +1019,19 @@ impl ContextProvider for BasicContextProvider { } } + if let Some(worktree_id) = location.buffer.read(cx).file().map(|f| f.worktree_id(cx)) { + if let Some(path) = self + .git_store + .read(cx) + .original_repo_path_for_worktree(worktree_id, cx) + { + task_variables.insert( + VariableName::MainGitWorktree, + path.to_string_lossy().into_owned(), + ); + } + } + if let Some(current_file) = current_file { let path = current_file.abs_path(cx); if let Some(filename) = path.file_name().and_then(|f| f.to_str()) { diff --git a/crates/project/src/task_store.rs b/crates/project/src/task_store.rs index 7aec460aeb9917eb9c1c58668ece4a10033a7ac9..5b91a3a8901d63e7311fb7ec81a69767b68e02d4 100644 --- a/crates/project/src/task_store.rs +++ b/crates/project/src/task_store.rs @@ -19,7 +19,7 @@ use util::ResultExt; use crate::{ BasicContextProvider, Inventory, ProjectEnvironment, buffer_store::BufferStore, - worktree_store::WorktreeStore, + git_store::GitStore, worktree_store::WorktreeStore, }; // platform-dependent warning @@ -33,6 +33,7 @@ pub struct StoreState { task_inventory: Entity, buffer_store: WeakEntity, worktree_store: Entity, + git_store: Entity, toolchain_store: Arc, } @@ -163,6 +164,7 @@ impl TaskStore { worktree_store: Entity, toolchain_store: Arc, environment: Entity, + git_store: Entity, cx: &mut Context, ) -> Self { Self::Functional(StoreState { @@ -172,6 +174,7 @@ impl TaskStore { }, task_inventory: Inventory::new(cx), buffer_store, + git_store, toolchain_store, worktree_store, }) @@ -183,6 +186,7 @@ impl TaskStore { toolchain_store: Arc, upstream_client: AnyProtoClient, project_id: u64, + git_store: Entity, cx: &mut Context, ) -> Self { Self::Functional(StoreState { @@ -192,6 +196,7 @@ impl TaskStore { }, task_inventory: Inventory::new(cx), buffer_store, + git_store, toolchain_store, worktree_store, }) @@ -207,6 +212,7 @@ impl TaskStore { TaskStore::Functional(state) => match &state.mode { StoreMode::Local { environment, .. } => local_task_context_for_location( state.worktree_store.clone(), + state.git_store.clone(), state.toolchain_store.clone(), environment.clone(), captured_variables, @@ -220,6 +226,7 @@ impl TaskStore { *project_id, upstream_client.clone(), state.worktree_store.clone(), + state.git_store.clone(), captured_variables, location, state.toolchain_store.clone(), @@ -302,6 +309,7 @@ impl TaskStore { fn local_task_context_for_location( worktree_store: Entity, + git_store: Entity, toolchain_store: Arc, environment: Entity, captured_variables: TaskVariables, @@ -329,7 +337,7 @@ fn local_task_context_for_location( worktree_store.clone(), location, project_env.clone(), - BasicContextProvider::new(worktree_store), + BasicContextProvider::new(worktree_store, git_store), toolchain_store, cx, ) @@ -351,6 +359,7 @@ fn remote_task_context_for_location( project_id: u64, upstream_client: AnyProtoClient, worktree_store: Entity, + git_store: Entity, captured_variables: TaskVariables, location: Location, toolchain_store: Arc, @@ -362,7 +371,7 @@ fn remote_task_context_for_location( .update(|cx| { let worktree_root = worktree_root(&worktree_store, &location, cx); - BasicContextProvider::new(worktree_store).build_context( + BasicContextProvider::new(worktree_store, git_store).build_context( &TaskVariables::default(), ContextLocation { fs: None, diff --git a/crates/project/tests/integration/debugger.rs b/crates/project/tests/integration/debugger.rs index 6cdc126d9750ae38f36e27879e5e9b635295015c..61bba78c74baec2e48b172043b3b504ccf32dba9 100644 --- a/crates/project/tests/integration/debugger.rs +++ b/crates/project/tests/integration/debugger.rs @@ -23,6 +23,7 @@ mod go_locator { show_summary: true, show_command: true, save: SaveStrategy::default(), + hooks: Default::default(), }; let scenario = locator @@ -51,6 +52,7 @@ mod go_locator { show_summary: true, show_command: true, save: SaveStrategy::default(), + hooks: Default::default(), }; let scenario = locator @@ -190,6 +192,7 @@ mod go_locator { show_summary: true, show_command: true, save: SaveStrategy::default(), + hooks: Default::default(), }; let scenario = locator @@ -225,6 +228,7 @@ mod python_locator { show_summary: false, show_command: false, save: task::SaveStrategy::default(), + hooks: Default::default(), }; let expected_scenario = DebugScenario { diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index c725f8177648ea0ca16106251e65908255a38d6d..7bdbbad796bd2ced34ed7ccab690555457a0842b 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -191,6 +191,7 @@ impl HeadlessProject { worktree_store.clone(), toolchain_store.read(cx).as_language_toolchain_store(), environment.clone(), + git_store.clone(), cx, ); task_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index f16f59390939171394684c9fc51e011a8f77a956..daaa8eb6b89c8954ef335f59692fbb059195e627 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -1431,9 +1431,7 @@ impl std::fmt::Display for InvalidSettingsError { | InvalidSettingsError::DefaultSettings { message } | InvalidSettingsError::Tasks { message, .. } | InvalidSettingsError::Editorconfig { message, .. } - | InvalidSettingsError::Debug { message, .. } => { - write!(f, "{message}") - } + | InvalidSettingsError::Debug { message, .. } => write!(f, "{message}"), } } } diff --git a/crates/task/src/task.rs b/crates/task/src/task.rs index ba5f4ae4fed9e676add2eafc8dc14f47cb2200ed..5126d5e89f723f0a9612c2033a789c569111b20a 100644 --- a/crates/task/src/task.rs +++ b/crates/task/src/task.rs @@ -23,8 +23,8 @@ pub use debug_format::{ Request, TcpArgumentsTemplate, ZedDebugConfig, }; pub use task_template::{ - DebugArgsRequest, HideStrategy, RevealStrategy, SaveStrategy, TaskTemplate, TaskTemplates, - substitute_variables_in_map, substitute_variables_in_str, + DebugArgsRequest, HideStrategy, RevealStrategy, SaveStrategy, TaskHook, TaskTemplate, + TaskTemplates, substitute_variables_in_map, substitute_variables_in_str, }; pub use util::shell::{Shell, ShellKind}; pub use util::shell_builder::ShellBuilder; @@ -181,6 +181,10 @@ pub enum VariableName { /// Open a Picker to select a process ID to use in place /// Can only be used to debug configurations PickProcessId, + /// An absolute path of the main (original) git worktree for the current repository. + /// For normal checkouts, this equals the worktree root. For linked worktrees, + /// this is the original repo's working directory. + MainGitWorktree, /// Custom variable, provided by the plugin or other external source. /// Will be printed with `CUSTOM_` prefix to avoid potential conflicts with other variables. Custom(Cow<'static, str>), @@ -216,6 +220,7 @@ impl FromStr for VariableName { "LANGUAGE" => Self::Language, "ROW" => Self::Row, "COLUMN" => Self::Column, + "MAIN_GIT_WORKTREE" => Self::MainGitWorktree, _ => { if let Some(custom_name) = without_prefix.strip_prefix(ZED_CUSTOM_VARIABLE_NAME_PREFIX) @@ -251,6 +256,7 @@ impl std::fmt::Display for VariableName { Self::Language => write!(f, "{ZED_VARIABLE_NAME_PREFIX}LANGUAGE"), Self::RunnableSymbol => write!(f, "{ZED_VARIABLE_NAME_PREFIX}RUNNABLE_SYMBOL"), Self::PickProcessId => write!(f, "{ZED_VARIABLE_NAME_PREFIX}PICK_PID"), + Self::MainGitWorktree => write!(f, "{ZED_VARIABLE_NAME_PREFIX}MAIN_GIT_WORKTREE"), Self::Custom(s) => write!( f, "{ZED_VARIABLE_NAME_PREFIX}{ZED_CUSTOM_VARIABLE_NAME_PREFIX}{s}" diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index cee6024ca62fb1ed74489f55ae99f6334db3d0f0..2f74d84e500e5151014aa2a71686cd68ac3a87a5 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -75,6 +75,9 @@ pub struct TaskTemplate { /// Which edited buffers to save before running the task. #[serde(default)] pub save: SaveStrategy, + /// Hooks that this task runs when emitted. + #[serde(default)] + pub hooks: HashSet, } #[derive(Deserialize, Eq, PartialEq, Clone, Debug)] @@ -86,6 +89,14 @@ pub enum DebugArgsRequest { Attach(AttachRequest), } +/// What to do with the terminal pane and tab, after the command was started. +#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum TaskHook { + #[serde(alias = "create_git_worktree")] + CreateWorktree, +} + /// What to do with the terminal pane and tab, after the command was started. #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] diff --git a/crates/tasks_ui/src/tasks_ui.rs b/crates/tasks_ui/src/tasks_ui.rs index d83cdfc830fc1abb19b8d05261aba711dbb14c1d..ca8ebb5248e4e6d77a05efab8d43dbfbd8d02eca 100644 --- a/crates/tasks_ui/src/tasks_ui.rs +++ b/crates/tasks_ui/src/tasks_ui.rs @@ -434,7 +434,9 @@ mod tests { ) .await; let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; - let worktree_store = project.read_with(cx, |project, _| project.worktree_store()); + let (worktree_store, git_store) = project.read_with(cx, |project, _| { + (project.worktree_store(), project.git_store().clone()) + }); let rust_language = Arc::new( Language::new( LanguageConfig { @@ -451,6 +453,7 @@ mod tests { .unwrap() .with_context_provider(Some(Arc::new(BasicContextProvider::new( worktree_store.clone(), + git_store.clone(), )))), ); @@ -474,6 +477,7 @@ mod tests { .unwrap() .with_context_provider(Some(Arc::new(BasicContextProvider::new( worktree_store.clone(), + git_store.clone(), )))), ); diff --git a/crates/workspace/src/tasks.rs b/crates/workspace/src/tasks.rs index 0ebb97b9d75543986bb6727546aad872a11a4f87..98421365532a8fdd4fc36f0f5c68e83b0814ae8e 100644 --- a/crates/workspace/src/tasks.rs +++ b/crates/workspace/src/tasks.rs @@ -1,13 +1,14 @@ use std::process::ExitStatus; use anyhow::Result; +use collections::HashSet; use gpui::{AppContext, Context, Entity, Task}; use language::Buffer; use project::{TaskSourceKind, WorktreeId}; use remote::ConnectionState; use task::{ DebugScenario, ResolvedTask, SaveStrategy, SharedTaskContext, SpawnInTerminal, TaskContext, - TaskTemplate, + TaskHook, TaskTemplate, TaskVariables, VariableName, }; use ui::Window; use util::TryFutureExt; @@ -164,6 +165,111 @@ impl Workspace { Task::ready(None) } } + + pub fn run_create_worktree_tasks(&mut self, window: &mut Window, cx: &mut Context) { + let project = self.project().clone(); + let hooks = HashSet::from_iter([TaskHook::CreateWorktree]); + + let worktree_tasks: Vec<(WorktreeId, TaskContext, Vec)> = { + let project = project.read(cx); + let task_store = project.task_store(); + let Some(inventory) = task_store.read(cx).task_inventory().cloned() else { + return; + }; + + let git_store = project.git_store().read(cx); + + let mut worktree_tasks = Vec::new(); + for worktree in project.worktrees(cx) { + let worktree = worktree.read(cx); + let worktree_id = worktree.id(); + let worktree_abs_path = worktree.abs_path(); + + let templates: Vec = inventory + .read(cx) + .templates_with_hooks(&hooks, worktree_id) + .into_iter() + .map(|(_, template)| template) + .collect(); + + if templates.is_empty() { + continue; + } + + let mut task_variables = TaskVariables::default(); + task_variables.insert( + VariableName::WorktreeRoot, + worktree_abs_path.to_string_lossy().into_owned(), + ); + + if let Some(path) = git_store.original_repo_path_for_worktree(worktree_id, cx) { + task_variables.insert( + VariableName::MainGitWorktree, + path.to_string_lossy().into_owned(), + ); + } + + let task_context = TaskContext { + cwd: Some(worktree_abs_path.to_path_buf()), + task_variables, + project_env: Default::default(), + }; + + worktree_tasks.push((worktree_id, task_context, templates)); + } + worktree_tasks + }; + + if worktree_tasks.is_empty() { + return; + } + + let task = cx.spawn_in(window, async move |workspace, cx| { + let mut tasks = Vec::new(); + for (worktree_id, task_context, templates) in worktree_tasks { + let id_base = format!("worktree_setup_{worktree_id}"); + + tasks.push(cx.spawn({ + let workspace = workspace.clone(); + async move |cx| { + for task_template in templates { + let Some(resolved) = + task_template.resolve_task(&id_base, &task_context) + else { + continue; + }; + + let status = workspace.update_in(cx, |workspace, window, cx| { + workspace.spawn_in_terminal(resolved.resolved, window, cx) + })?; + + if let Some(result) = status.await { + match result { + Ok(exit_status) if !exit_status.success() => { + log::error!( + "Git worktree setup task failed with status: {:?}", + exit_status.code() + ); + break; + } + Err(error) => { + log::error!("Git worktree setup task error: {error:#}"); + break; + } + _ => {} + } + } + } + anyhow::Ok(()) + } + })); + } + + futures::future::join_all(tasks).await; + anyhow::Ok(()) + }); + task.detach_and_log_err(cx); + } } #[cfg(test)]