diff --git a/Cargo.lock b/Cargo.lock index e7e9449bdf..af7fd610de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12635,6 +12635,7 @@ dependencies = [ "sha2", "shellexpand 2.1.2", "util", + "zed_actions", ] [[package]] @@ -15603,6 +15604,7 @@ dependencies = [ "ui", "util", "uuid", + "zed_actions", ] [[package]] diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 7e50b0e97d..01c41bb84c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -523,7 +523,7 @@ impl RunnableTasks { ) -> impl Iterator + 'a { self.templates.iter().filter_map(|(kind, template)| { template - .resolve_task(&kind.to_id_base(), cx) + .resolve_task(&kind.to_id_base(), Default::default(), cx) .map(|task| (kind.clone(), task)) }) } diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index 52583ea8fb..dd6b201687 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -177,7 +177,7 @@ impl Inventory { let id_base = kind.to_id_base(); Some(( kind, - task.resolve_task(&id_base, task_context)?, + task.resolve_task(&id_base, Default::default(), task_context)?, not_used_score, )) }) @@ -397,7 +397,7 @@ mod test_inventory { let id_base = task_source_kind.to_id_base(); inventory.task_scheduled( task_source_kind.clone(), - task.resolve_task(&id_base, &TaskContext::default()) + task.resolve_task(&id_base, Default::default(), &TaskContext::default()) .unwrap_or_else(|| panic!("Failed to resolve task with name {task_name}")), ); }); diff --git a/crates/project/src/task_store.rs b/crates/project/src/task_store.rs index 662a5a4d47..33916cfd7b 100644 --- a/crates/project/src/task_store.rs +++ b/crates/project/src/task_store.rs @@ -331,7 +331,7 @@ fn local_task_context_for_location( let worktree_id = location.buffer.read(cx).file().map(|f| f.worktree_id(cx)); let worktree_abs_path = worktree_id .and_then(|worktree_id| worktree_store.read(cx).worktree_for_id(worktree_id, cx)) - .map(|worktree| worktree.read(cx).abs_path()); + .and_then(|worktree| worktree.read(cx).root_dir()); cx.spawn(|mut cx| async move { let worktree_abs_path = worktree_abs_path.clone(); diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 34ef4d8a82..6bee662a75 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -50,13 +50,7 @@ impl Project { .and_then(|entry_id| self.worktree_for_entry(entry_id, cx)) .into_iter() .chain(self.worktrees(cx)) - .find_map(|tree| { - let worktree = tree.read(cx); - worktree - .root_entry() - .filter(|entry| entry.is_dir()) - .map(|_| worktree.abs_path().clone()) - }); + .find_map(|tree| tree.read(cx).root_dir()); worktree } diff --git a/crates/task/Cargo.toml b/crates/task/Cargo.toml index 43e3060a4e..6bc7489d86 100644 --- a/crates/task/Cargo.toml +++ b/crates/task/Cargo.toml @@ -21,6 +21,7 @@ serde_json_lenient.workspace = true sha2.workspace = true shellexpand.workspace = true util.workspace = true +zed_actions.workspace = true [dev-dependencies] gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index d9f72b4258..c5ad843679 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -18,11 +18,11 @@ pub use vscode_format::VsCodeTaskFile; /// Task identifier, unique within the application. /// Based on it, task reruns and terminal tabs are managed. -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, Serialize)] pub struct TaskId(pub String); /// Contains all information needed by Zed to spawn a new terminal tab for the given task. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SpawnInTerminal { /// Id of the task to use when determining task tab affinity. pub id: TaskId, @@ -57,6 +57,15 @@ pub struct SpawnInTerminal { pub show_command: bool, } +/// An action for spawning a specific task +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct NewCenterTask { + /// The specification of the task to spawn. + pub action: SpawnInTerminal, +} + +gpui::impl_actions!(tasks, [NewCenterTask]); + /// A final form of the [`TaskTemplate`], that got resolved with a particualar [`TaskContext`] and now is ready to spawn the actual task. #[derive(Clone, Debug, PartialEq, Eq)] pub struct ResolvedTask { @@ -75,6 +84,9 @@ pub struct ResolvedTask { /// Further actions that need to take place after the resolved task is spawned, /// with all task variables resolved. pub resolved: Option, + + /// where to sawn the task in the UI, either in the terminal panel or in the center pane + pub target: zed_actions::TaskSpawnTarget, } impl ResolvedTask { diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index 23a2bc8ca7..c2a4e1878b 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -115,7 +115,12 @@ impl TaskTemplate { /// /// Every [`ResolvedTask`] gets a [`TaskId`], based on the `id_base` (to avoid collision with various task sources), /// and hashes of its template and [`TaskContext`], see [`ResolvedTask`] fields' documentation for more details. - pub fn resolve_task(&self, id_base: &str, cx: &TaskContext) -> Option { + pub fn resolve_task( + &self, + id_base: &str, + target: zed_actions::TaskSpawnTarget, + cx: &TaskContext, + ) -> Option { if self.label.trim().is_empty() || self.command.trim().is_empty() { return None; } @@ -214,6 +219,7 @@ impl TaskTemplate { Some(ResolvedTask { id: id.clone(), substituted_variables, + target, original_task: self.clone(), resolved_label: full_label.clone(), resolved: Some(SpawnInTerminal { @@ -382,7 +388,7 @@ mod tests { }, ] { assert_eq!( - task_with_blank_property.resolve_task(TEST_ID_BASE, &TaskContext::default()), + task_with_blank_property.resolve_task(TEST_ID_BASE, Default::default(), &TaskContext::default()), None, "should not resolve task with blank label and/or command: {task_with_blank_property:?}" ); @@ -400,7 +406,7 @@ mod tests { let resolved_task = |task_template: &TaskTemplate, task_cx| { let resolved_task = task_template - .resolve_task(TEST_ID_BASE, task_cx) + .resolve_task(TEST_ID_BASE, Default::default(), task_cx) .unwrap_or_else(|| panic!("failed to resolve task {task_without_cwd:?}")); assert_substituted_variables(&resolved_task, Vec::new()); resolved_task @@ -526,6 +532,7 @@ mod tests { for i in 0..15 { let resolved_task = task_with_all_variables.resolve_task( TEST_ID_BASE, + Default::default(), &TaskContext { cwd: None, task_variables: TaskVariables::from_iter(all_variables.clone()), @@ -614,6 +621,7 @@ mod tests { let removed_variable = not_all_variables.remove(i); let resolved_task_attempt = task_with_all_variables.resolve_task( TEST_ID_BASE, + Default::default(), &TaskContext { cwd: None, task_variables: TaskVariables::from_iter(not_all_variables), @@ -633,7 +641,7 @@ mod tests { ..Default::default() }; let resolved_task = task - .resolve_task(TEST_ID_BASE, &TaskContext::default()) + .resolve_task(TEST_ID_BASE, Default::default(), &TaskContext::default()) .unwrap(); assert_substituted_variables(&resolved_task, Vec::new()); let resolved = resolved_task.resolved.unwrap(); @@ -651,7 +659,7 @@ mod tests { ..Default::default() }; assert!(task - .resolve_task(TEST_ID_BASE, &TaskContext::default()) + .resolve_task(TEST_ID_BASE, Default::default(), &TaskContext::default()) .is_none()); } @@ -701,7 +709,7 @@ mod tests { .enumerate() { let resolved = symbol_dependent_task - .resolve_task(TEST_ID_BASE, &cx) + .resolve_task(TEST_ID_BASE, Default::default(), &cx) .unwrap_or_else(|| panic!("Failed to resolve task {symbol_dependent_task:?}")); assert_eq!( resolved.substituted_variables, @@ -743,7 +751,9 @@ mod tests { context .task_variables .insert(VariableName::Symbol, "my-symbol".to_string()); - assert!(faulty_go_test.resolve_task("base", &context).is_some()); + assert!(faulty_go_test + .resolve_task("base", Default::default(), &context) + .is_some()); } #[test] @@ -802,7 +812,7 @@ mod tests { }; let resolved = template - .resolve_task(TEST_ID_BASE, &context) + .resolve_task(TEST_ID_BASE, Default::default(), &context) .unwrap() .resolved .unwrap(); diff --git a/crates/tasks_ui/src/lib.rs b/crates/tasks_ui/src/lib.rs index 02ced4f479..0d278bc2f4 100644 --- a/crates/tasks_ui/src/lib.rs +++ b/crates/tasks_ui/src/lib.rs @@ -11,6 +11,7 @@ mod modal; mod settings; pub use modal::{Rerun, Spawn}; +use zed_actions::TaskSpawnTarget; pub fn init(cx: &mut AppContext) { settings::TaskSettings::register(cx); @@ -53,6 +54,7 @@ pub fn init(cx: &mut AppContext) { task_source_kind, &original_task, &task_context, + Default::default(), false, cx, ) @@ -89,7 +91,8 @@ pub fn init(cx: &mut AppContext) { fn spawn_task_or_modal(workspace: &mut Workspace, action: &Spawn, cx: &mut ViewContext) { match &action.task_name { - Some(name) => spawn_task_with_name(name.clone(), cx).detach_and_log_err(cx), + Some(name) => spawn_task_with_name(name.clone(), action.target.unwrap_or_default(), cx) + .detach_and_log_err(cx), None => toggle_modal(workspace, cx).detach(), } } @@ -119,6 +122,7 @@ fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>) fn spawn_task_with_name( name: String, + task_target: TaskSpawnTarget, cx: &mut ViewContext, ) -> AsyncTask> { cx.spawn(|workspace, mut cx| async move { @@ -160,6 +164,7 @@ fn spawn_task_with_name( task_source_kind, &target_task, &task_context, + task_target, false, cx, ); diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 9cabf555cd..1e074a65a6 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -68,7 +68,7 @@ impl TasksModalDelegate { }; Some(( source_kind, - new_oneshot.resolve_task(&id_base, &self.task_context)?, + new_oneshot.resolve_task(&id_base, Default::default(), &self.task_context)?, )) } @@ -684,6 +684,7 @@ mod tests { cx.dispatch_action(Spawn { task_name: Some("example task".to_string()), + target: None, }); let tasks_picker = workspace.update(cx, |workspace, cx| { workspace diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index a703de467e..fbed29f3be 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -255,7 +255,10 @@ impl TerminalPanel { terminal_panel .update(&mut cx, |_, cx| { cx.subscribe(&workspace, |terminal_panel, _, e, cx| { - if let workspace::Event::SpawnTask(spawn_in_terminal) = e { + if let workspace::Event::SpawnTask { + action: spawn_in_terminal, + } = e + { terminal_panel.spawn_task(spawn_in_terminal, cx); }; }) @@ -450,83 +453,17 @@ impl TerminalPanel { fn spawn_task(&mut self, spawn_in_terminal: &SpawnInTerminal, cx: &mut ViewContext) { let mut spawn_task = spawn_in_terminal.clone(); - // Set up shell args unconditionally, as tasks are always spawned inside of a shell. - let Some((shell, mut user_args)) = (match spawn_in_terminal.shell.clone() { - Shell::System => { - match self - .workspace - .update(cx, |workspace, cx| workspace.project().read(cx).is_local()) - { - Ok(local) => { - if local { - retrieve_system_shell().map(|shell| (shell, Vec::new())) - } else { - Some(("\"${SHELL:-sh}\"".to_string(), Vec::new())) - } - } - Err(_no_window_e) => return, - } - } - Shell::Program(shell) => Some((shell, Vec::new())), - Shell::WithArguments { program, args, .. } => Some((program, args)), - }) else { + let Ok(is_local) = self + .workspace + .update(cx, |workspace, cx| workspace.project().read(cx).is_local()) + else { return; }; - #[cfg(target_os = "windows")] - let windows_shell_type = to_windows_shell_type(&shell); - - #[cfg(not(target_os = "windows"))] + if let ControlFlow::Break(_) = + Self::fill_command(is_local, spawn_in_terminal, &mut spawn_task) { - spawn_task.command_label = format!("{shell} -i -c '{}'", spawn_task.command_label); + return; } - #[cfg(target_os = "windows")] - { - use crate::terminal_panel::WindowsShellType; - - match windows_shell_type { - WindowsShellType::Powershell => { - spawn_task.command_label = format!("{shell} -C '{}'", spawn_task.command_label) - } - WindowsShellType::Cmd => { - spawn_task.command_label = format!("{shell} /C '{}'", spawn_task.command_label) - } - WindowsShellType::Other => { - spawn_task.command_label = - format!("{shell} -i -c '{}'", spawn_task.command_label) - } - } - } - - let task_command = std::mem::replace(&mut spawn_task.command, shell); - let task_args = std::mem::take(&mut spawn_task.args); - let combined_command = task_args - .into_iter() - .fold(task_command, |mut command, arg| { - command.push(' '); - #[cfg(not(target_os = "windows"))] - command.push_str(&arg); - #[cfg(target_os = "windows")] - command.push_str(&to_windows_shell_variable(windows_shell_type, arg)); - command - }); - - #[cfg(not(target_os = "windows"))] - user_args.extend(["-i".to_owned(), "-c".to_owned(), combined_command]); - #[cfg(target_os = "windows")] - { - use crate::terminal_panel::WindowsShellType; - - match windows_shell_type { - WindowsShellType::Powershell => { - user_args.extend(["-C".to_owned(), combined_command]) - } - WindowsShellType::Cmd => user_args.extend(["/C".to_owned(), combined_command]), - WindowsShellType::Other => { - user_args.extend(["-i".to_owned(), "-c".to_owned(), combined_command]) - } - } - } - spawn_task.args = user_args; let spawn_task = spawn_task; let allow_concurrent_runs = spawn_in_terminal.allow_concurrent_runs; @@ -602,6 +539,81 @@ impl TerminalPanel { .detach() } + pub fn fill_command( + is_local: bool, + spawn_in_terminal: &SpawnInTerminal, + spawn_task: &mut SpawnInTerminal, + ) -> ControlFlow<()> { + let Some((shell, mut user_args)) = (match spawn_in_terminal.shell.clone() { + Shell::System => { + if is_local { + retrieve_system_shell().map(|shell| (shell, Vec::new())) + } else { + Some(("\"${SHELL:-sh}\"".to_string(), Vec::new())) + } + } + Shell::Program(shell) => Some((shell, Vec::new())), + Shell::WithArguments { program, args, .. } => Some((program, args)), + }) else { + return ControlFlow::Break(()); + }; + #[cfg(target_os = "windows")] + let windows_shell_type = to_windows_shell_type(&shell); + #[cfg(not(target_os = "windows"))] + { + spawn_task.command_label = format!("{shell} -i -c '{}'", spawn_task.command_label); + } + #[cfg(target_os = "windows")] + { + use crate::terminal_panel::WindowsShellType; + + match windows_shell_type { + WindowsShellType::Powershell => { + spawn_task.command_label = format!("{shell} -C '{}'", spawn_task.command_label) + } + WindowsShellType::Cmd => { + spawn_task.command_label = format!("{shell} /C '{}'", spawn_task.command_label) + } + WindowsShellType::Other => { + spawn_task.command_label = + format!("{shell} -i -c '{}'", spawn_task.command_label) + } + } + } + let task_command = std::mem::replace(&mut spawn_task.command, shell); + let task_args = std::mem::take(&mut spawn_task.args); + let combined_command = task_args + .into_iter() + .fold(task_command, |mut command, arg| { + command.push(' '); + #[cfg(not(target_os = "windows"))] + command.push_str(&arg); + #[cfg(target_os = "windows")] + command.push_str(&to_windows_shell_variable(windows_shell_type, arg)); + command + }); + #[cfg(not(target_os = "windows"))] + user_args.extend(["-i".to_owned(), "-c".to_owned(), combined_command]); + #[cfg(target_os = "windows")] + { + use crate::terminal_panel::WindowsShellType; + + match windows_shell_type { + WindowsShellType::Powershell => { + user_args.extend(["-C".to_owned(), combined_command]) + } + WindowsShellType::Cmd => user_args.extend(["/C".to_owned(), combined_command]), + WindowsShellType::Other => { + user_args.extend(["-i".to_owned(), "-c".to_owned(), combined_command]) + } + } + } + spawn_task.args = user_args; + // Set up shell args unconditionally, as tasks are always spawned inside of a shell. + + ControlFlow::Continue(()) + } + pub fn spawn_in_new_terminal( &mut self, spawn_task: SpawnInTerminal, diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 7a83e530fe..fb46c5ae95 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -14,6 +14,7 @@ use gpui::{ use language::Bias; use persistence::TERMINAL_DB; use project::{search::SearchQuery, terminals::TerminalKind, Fs, Metadata, Project}; +use task::{NewCenterTask, RevealStrategy}; use terminal::{ alacritty_terminal::{ index::Point, @@ -45,7 +46,7 @@ use zed_actions::InlineAssist; use std::{ cmp, - ops::RangeInclusive, + ops::{ControlFlow, RangeInclusive}, path::{Path, PathBuf}, rc::Rc, sync::Arc, @@ -78,8 +79,9 @@ pub fn init(cx: &mut AppContext) { register_serializable_item::(cx); - cx.observe_new_views(|workspace: &mut Workspace, _| { + cx.observe_new_views(|workspace: &mut Workspace, _cx| { workspace.register_action(TerminalView::deploy); + workspace.register_action(TerminalView::deploy_center_task); }) .detach(); } @@ -127,6 +129,61 @@ impl FocusableView for TerminalView { } impl TerminalView { + pub fn deploy_center_task( + workspace: &mut Workspace, + task: &NewCenterTask, + cx: &mut ViewContext, + ) { + let reveal_strategy: RevealStrategy = task.action.reveal; + let mut spawn_task = task.action.clone(); + + let is_local = workspace.project().read(cx).is_local(); + + if let ControlFlow::Break(_) = + TerminalPanel::fill_command(is_local, &task.action, &mut spawn_task) + { + return; + } + + let kind = TerminalKind::Task(spawn_task); + + let project = workspace.project().clone(); + let database_id = workspace.database_id(); + cx.spawn(|workspace, mut cx| async move { + let terminal = cx + .update(|cx| { + let window = cx.window_handle(); + project.update(cx, |project, cx| project.create_terminal(kind, window, cx)) + })? + .await?; + + let terminal_view = cx.new_view(|cx| { + TerminalView::new(terminal.clone(), workspace.clone(), database_id, cx) + })?; + + cx.update(|cx| { + let focus_item = match reveal_strategy { + RevealStrategy::Always => true, + RevealStrategy::Never | RevealStrategy::NoFocus => false, + }; + + workspace.update(cx, |workspace, cx| { + workspace.add_item_to_active_pane( + Box::new(terminal_view), + None, + focus_item, + cx, + ); + })?; + + anyhow::Ok(()) + })??; + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + ///Create a new Terminal in the current working directory or the user's home directory pub fn deploy( workspace: &mut Workspace, diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 3b17ed8dab..6bd2382f35 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -61,6 +61,7 @@ ui.workspace = true util.workspace = true uuid.workspace = true strum.workspace = true +zed_actions.workspace = true [dev-dependencies] call = { workspace = true, features = ["test-support"] } diff --git a/crates/workspace/src/tasks.rs b/crates/workspace/src/tasks.rs index c2b6e51acd..7cf6ebcae8 100644 --- a/crates/workspace/src/tasks.rs +++ b/crates/workspace/src/tasks.rs @@ -1,15 +1,17 @@ use project::TaskSourceKind; use remote::ConnectionState; -use task::{ResolvedTask, TaskContext, TaskTemplate}; +use task::{NewCenterTask, ResolvedTask, TaskContext, TaskTemplate}; use ui::ViewContext; +use zed_actions::TaskSpawnTarget; use crate::Workspace; pub fn schedule_task( - workspace: &Workspace, + workspace: &mut Workspace, task_source_kind: TaskSourceKind, task_to_resolve: &TaskTemplate, task_cx: &TaskContext, + task_target: zed_actions::TaskSpawnTarget, omit_history: bool, cx: &mut ViewContext<'_, Workspace>, ) { @@ -27,7 +29,7 @@ pub fn schedule_task( } if let Some(spawn_in_terminal) = - task_to_resolve.resolve_task(&task_source_kind.to_id_base(), task_cx) + task_to_resolve.resolve_task(&task_source_kind.to_id_base(), task_target, task_cx) { schedule_resolved_task( workspace, @@ -40,12 +42,13 @@ pub fn schedule_task( } pub fn schedule_resolved_task( - workspace: &Workspace, + workspace: &mut Workspace, task_source_kind: TaskSourceKind, mut resolved_task: ResolvedTask, omit_history: bool, cx: &mut ViewContext<'_, Workspace>, ) { + let target = resolved_task.target; if let Some(spawn_in_terminal) = resolved_task.resolved.take() { if !omit_history { resolved_task.resolved = Some(spawn_in_terminal.clone()); @@ -59,6 +62,18 @@ pub fn schedule_resolved_task( } }); } - cx.emit(crate::Event::SpawnTask(Box::new(spawn_in_terminal))); + + match target { + TaskSpawnTarget::Center => { + cx.dispatch_action(Box::new(NewCenterTask { + action: spawn_in_terminal, + })); + } + TaskSpawnTarget::Dock => { + cx.emit(crate::Event::SpawnTask { + action: Box::new(spawn_in_terminal), + }); + } + } } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 1569e93094..398b0ea07c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -686,7 +686,9 @@ pub enum Event { }, ContactRequestedJoin(u64), WorkspaceCreated(WeakView), - SpawnTask(Box), + SpawnTask { + action: Box, + }, OpenBundledFile { text: Cow<'static, str>, title: &'static str, diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 249d04cb7b..7b118c1c57 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -2533,6 +2533,12 @@ impl Snapshot { self.entry_for_path("") } + pub fn root_dir(&self) -> Option> { + self.root_entry() + .filter(|entry| entry.is_dir()) + .map(|_| self.abs_path().clone()) + } + pub fn root_name(&self) -> &str { &self.root_name } diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index b4bb6d2152..b823b38bbc 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -90,6 +90,14 @@ pub struct OpenRecent { gpui::impl_actions!(projects, [OpenRecent]); gpui::actions!(projects, [OpenRemote]); +#[derive(PartialEq, Eq, Clone, Copy, Deserialize, Default, Debug)] +#[serde(rename_all = "snake_case")] +pub enum TaskSpawnTarget { + Center, + #[default] + Dock, +} + /// Spawn a task with name or open tasks modal #[derive(PartialEq, Clone, Deserialize, Default)] pub struct Spawn { @@ -98,11 +106,18 @@ pub struct Spawn { /// If it is not set, a modal with a list of available tasks is opened instead. /// Defaults to None. pub task_name: Option, + /// Which part of the UI the task should be spawned in. + /// Defaults to Dock. + #[serde(default)] + pub target: Option, } impl Spawn { pub fn modal() -> Self { - Self { task_name: None } + Self { + task_name: None, + target: None, + } } } diff --git a/docs/src/tasks.md b/docs/src/tasks.md index f32e5778fc..6399c6f5e0 100644 --- a/docs/src/tasks.md +++ b/docs/src/tasks.md @@ -155,6 +155,30 @@ You can define your own keybindings for your tasks via additional argument to `t } ``` +Note that these tasks can also have a 'target' specified to control where the spawned task should show up. +This could be useful for launching a terminal application that you want to use in the center area: + +```json +// In tasks.json +{ + "label": "start lazygit", + "command": "lazygit -p $ZED_WORKTREE_ROOT" +} +``` + +```json +// In keymap.json +{ + "context": "Workspace", + "bindings": { + "alt-g": [ + "task::Spawn", + { "task_name": "start lazygit", "target": "center" } + ] + } +} +``` + ## Binding runnable tags to task templates Zed supports overriding default action for inline runnable indicators via workspace-local and global `tasks.json` file with the following precedence hierarchy: