Detailed changes
@@ -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,
+ },
]
@@ -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| {
@@ -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<Arc<Path>> {
+ 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<FileStatus> {
let (repo, path) = self.repository_and_path_for_buffer_id(buffer_id, cx)?;
let status = repo.read(cx).snapshot.status_for_path(&path)?;
@@ -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);
@@ -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<TaskHook>,
+ 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<Item = (TaskSourceKind, TaskTemplate)> {
@@ -918,11 +931,15 @@ fn task_variables_preference(task: &ResolvedTask) -> Reverse<usize> {
/// Applied as a base for every custom [`ContextProvider`] unless explicitly oped out.
pub struct BasicContextProvider {
worktree_store: Entity<WorktreeStore>,
+ git_store: Entity<GitStore>,
}
impl BasicContextProvider {
- pub fn new(worktree_store: Entity<WorktreeStore>) -> Self {
- Self { worktree_store }
+ pub fn new(worktree_store: Entity<WorktreeStore>, git_store: Entity<GitStore>) -> 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()) {
@@ -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<Inventory>,
buffer_store: WeakEntity<BufferStore>,
worktree_store: Entity<WorktreeStore>,
+ git_store: Entity<GitStore>,
toolchain_store: Arc<dyn LanguageToolchainStore>,
}
@@ -163,6 +164,7 @@ impl TaskStore {
worktree_store: Entity<WorktreeStore>,
toolchain_store: Arc<dyn LanguageToolchainStore>,
environment: Entity<ProjectEnvironment>,
+ git_store: Entity<GitStore>,
cx: &mut Context<Self>,
) -> 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<dyn LanguageToolchainStore>,
upstream_client: AnyProtoClient,
project_id: u64,
+ git_store: Entity<GitStore>,
cx: &mut Context<Self>,
) -> 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<WorktreeStore>,
+ git_store: Entity<GitStore>,
toolchain_store: Arc<dyn LanguageToolchainStore>,
environment: Entity<ProjectEnvironment>,
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<WorktreeStore>,
+ git_store: Entity<GitStore>,
captured_variables: TaskVariables,
location: Location,
toolchain_store: Arc<dyn LanguageToolchainStore>,
@@ -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,
@@ -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 {
@@ -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);
@@ -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}"),
}
}
}
@@ -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}"
@@ -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<TaskHook>,
}
#[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")]
@@ -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(),
)))),
);
@@ -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<Self>) {
+ let project = self.project().clone();
+ let hooks = HashSet::from_iter([TaskHook::CreateWorktree]);
+
+ let worktree_tasks: Vec<(WorktreeId, TaskContext, Vec<TaskTemplate>)> = {
+ 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<TaskTemplate> = 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)]