Add the ability for tasks to target the center pane (#22004)

Closes #20060
Closes #20720
Closes #19873
Closes #9445

Release Notes:

- Fixed a bug where tasks would be spawned with their working directory
set to a file in some cases
- Added the ability to spawn tasks in the center pane, when spawning
from a keybinding:

```json5
[
  {
    // Assuming you have a task labeled "echo hello"
    "ctrl--": [
      "task::Spawn",
      { "task_name": "echo hello", "target": "center" }
    ]
  }
]
```
This commit is contained in:
Mikayla Maki 2024-12-13 19:39:46 -08:00 committed by GitHub
parent 85c3aec6e7
commit 4f96706161
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 263 additions and 106 deletions

2
Cargo.lock generated
View file

@ -12635,6 +12635,7 @@ dependencies = [
"sha2", "sha2",
"shellexpand 2.1.2", "shellexpand 2.1.2",
"util", "util",
"zed_actions",
] ]
[[package]] [[package]]
@ -15603,6 +15604,7 @@ dependencies = [
"ui", "ui",
"util", "util",
"uuid", "uuid",
"zed_actions",
] ]
[[package]] [[package]]

View file

@ -523,7 +523,7 @@ impl RunnableTasks {
) -> impl Iterator<Item = (TaskSourceKind, ResolvedTask)> + 'a { ) -> impl Iterator<Item = (TaskSourceKind, ResolvedTask)> + 'a {
self.templates.iter().filter_map(|(kind, template)| { self.templates.iter().filter_map(|(kind, template)| {
template template
.resolve_task(&kind.to_id_base(), cx) .resolve_task(&kind.to_id_base(), Default::default(), cx)
.map(|task| (kind.clone(), task)) .map(|task| (kind.clone(), task))
}) })
} }

View file

@ -177,7 +177,7 @@ impl Inventory {
let id_base = kind.to_id_base(); let id_base = kind.to_id_base();
Some(( Some((
kind, kind,
task.resolve_task(&id_base, task_context)?, task.resolve_task(&id_base, Default::default(), task_context)?,
not_used_score, not_used_score,
)) ))
}) })
@ -397,7 +397,7 @@ mod test_inventory {
let id_base = task_source_kind.to_id_base(); let id_base = task_source_kind.to_id_base();
inventory.task_scheduled( inventory.task_scheduled(
task_source_kind.clone(), 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}")), .unwrap_or_else(|| panic!("Failed to resolve task with name {task_name}")),
); );
}); });

View file

@ -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_id = location.buffer.read(cx).file().map(|f| f.worktree_id(cx));
let worktree_abs_path = worktree_id let worktree_abs_path = worktree_id
.and_then(|worktree_id| worktree_store.read(cx).worktree_for_id(worktree_id, cx)) .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 { cx.spawn(|mut cx| async move {
let worktree_abs_path = worktree_abs_path.clone(); let worktree_abs_path = worktree_abs_path.clone();

View file

@ -50,13 +50,7 @@ impl Project {
.and_then(|entry_id| self.worktree_for_entry(entry_id, cx)) .and_then(|entry_id| self.worktree_for_entry(entry_id, cx))
.into_iter() .into_iter()
.chain(self.worktrees(cx)) .chain(self.worktrees(cx))
.find_map(|tree| { .find_map(|tree| tree.read(cx).root_dir());
let worktree = tree.read(cx);
worktree
.root_entry()
.filter(|entry| entry.is_dir())
.map(|_| worktree.abs_path().clone())
});
worktree worktree
} }

View file

@ -21,6 +21,7 @@ serde_json_lenient.workspace = true
sha2.workspace = true sha2.workspace = true
shellexpand.workspace = true shellexpand.workspace = true
util.workspace = true util.workspace = true
zed_actions.workspace = true
[dev-dependencies] [dev-dependencies]
gpui = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] }

View file

@ -18,11 +18,11 @@ pub use vscode_format::VsCodeTaskFile;
/// Task identifier, unique within the application. /// Task identifier, unique within the application.
/// Based on it, task reruns and terminal tabs are managed. /// 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); pub struct TaskId(pub String);
/// Contains all information needed by Zed to spawn a new terminal tab for the given task. /// 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 { pub struct SpawnInTerminal {
/// Id of the task to use when determining task tab affinity. /// Id of the task to use when determining task tab affinity.
pub id: TaskId, pub id: TaskId,
@ -57,6 +57,15 @@ pub struct SpawnInTerminal {
pub show_command: bool, 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. /// 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)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResolvedTask { pub struct ResolvedTask {
@ -75,6 +84,9 @@ pub struct ResolvedTask {
/// Further actions that need to take place after the resolved task is spawned, /// Further actions that need to take place after the resolved task is spawned,
/// with all task variables resolved. /// with all task variables resolved.
pub resolved: Option<SpawnInTerminal>, pub resolved: Option<SpawnInTerminal>,
/// 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 { impl ResolvedTask {

View file

@ -115,7 +115,12 @@ impl TaskTemplate {
/// ///
/// Every [`ResolvedTask`] gets a [`TaskId`], based on the `id_base` (to avoid collision with various task sources), /// 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. /// 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<ResolvedTask> { pub fn resolve_task(
&self,
id_base: &str,
target: zed_actions::TaskSpawnTarget,
cx: &TaskContext,
) -> Option<ResolvedTask> {
if self.label.trim().is_empty() || self.command.trim().is_empty() { if self.label.trim().is_empty() || self.command.trim().is_empty() {
return None; return None;
} }
@ -214,6 +219,7 @@ impl TaskTemplate {
Some(ResolvedTask { Some(ResolvedTask {
id: id.clone(), id: id.clone(),
substituted_variables, substituted_variables,
target,
original_task: self.clone(), original_task: self.clone(),
resolved_label: full_label.clone(), resolved_label: full_label.clone(),
resolved: Some(SpawnInTerminal { resolved: Some(SpawnInTerminal {
@ -382,7 +388,7 @@ mod tests {
}, },
] { ] {
assert_eq!( 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, None,
"should not resolve task with blank label and/or command: {task_with_blank_property:?}" "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: &TaskTemplate, task_cx| {
let resolved_task = task_template 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:?}")); .unwrap_or_else(|| panic!("failed to resolve task {task_without_cwd:?}"));
assert_substituted_variables(&resolved_task, Vec::new()); assert_substituted_variables(&resolved_task, Vec::new());
resolved_task resolved_task
@ -526,6 +532,7 @@ mod tests {
for i in 0..15 { for i in 0..15 {
let resolved_task = task_with_all_variables.resolve_task( let resolved_task = task_with_all_variables.resolve_task(
TEST_ID_BASE, TEST_ID_BASE,
Default::default(),
&TaskContext { &TaskContext {
cwd: None, cwd: None,
task_variables: TaskVariables::from_iter(all_variables.clone()), task_variables: TaskVariables::from_iter(all_variables.clone()),
@ -614,6 +621,7 @@ mod tests {
let removed_variable = not_all_variables.remove(i); let removed_variable = not_all_variables.remove(i);
let resolved_task_attempt = task_with_all_variables.resolve_task( let resolved_task_attempt = task_with_all_variables.resolve_task(
TEST_ID_BASE, TEST_ID_BASE,
Default::default(),
&TaskContext { &TaskContext {
cwd: None, cwd: None,
task_variables: TaskVariables::from_iter(not_all_variables), task_variables: TaskVariables::from_iter(not_all_variables),
@ -633,7 +641,7 @@ mod tests {
..Default::default() ..Default::default()
}; };
let resolved_task = task let resolved_task = task
.resolve_task(TEST_ID_BASE, &TaskContext::default()) .resolve_task(TEST_ID_BASE, Default::default(), &TaskContext::default())
.unwrap(); .unwrap();
assert_substituted_variables(&resolved_task, Vec::new()); assert_substituted_variables(&resolved_task, Vec::new());
let resolved = resolved_task.resolved.unwrap(); let resolved = resolved_task.resolved.unwrap();
@ -651,7 +659,7 @@ mod tests {
..Default::default() ..Default::default()
}; };
assert!(task assert!(task
.resolve_task(TEST_ID_BASE, &TaskContext::default()) .resolve_task(TEST_ID_BASE, Default::default(), &TaskContext::default())
.is_none()); .is_none());
} }
@ -701,7 +709,7 @@ mod tests {
.enumerate() .enumerate()
{ {
let resolved = symbol_dependent_task 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:?}")); .unwrap_or_else(|| panic!("Failed to resolve task {symbol_dependent_task:?}"));
assert_eq!( assert_eq!(
resolved.substituted_variables, resolved.substituted_variables,
@ -743,7 +751,9 @@ mod tests {
context context
.task_variables .task_variables
.insert(VariableName::Symbol, "my-symbol".to_string()); .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] #[test]
@ -802,7 +812,7 @@ mod tests {
}; };
let resolved = template let resolved = template
.resolve_task(TEST_ID_BASE, &context) .resolve_task(TEST_ID_BASE, Default::default(), &context)
.unwrap() .unwrap()
.resolved .resolved
.unwrap(); .unwrap();

View file

@ -11,6 +11,7 @@ mod modal;
mod settings; mod settings;
pub use modal::{Rerun, Spawn}; pub use modal::{Rerun, Spawn};
use zed_actions::TaskSpawnTarget;
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
settings::TaskSettings::register(cx); settings::TaskSettings::register(cx);
@ -53,6 +54,7 @@ pub fn init(cx: &mut AppContext) {
task_source_kind, task_source_kind,
&original_task, &original_task,
&task_context, &task_context,
Default::default(),
false, false,
cx, cx,
) )
@ -89,7 +91,8 @@ pub fn init(cx: &mut AppContext) {
fn spawn_task_or_modal(workspace: &mut Workspace, action: &Spawn, cx: &mut ViewContext<Workspace>) { fn spawn_task_or_modal(workspace: &mut Workspace, action: &Spawn, cx: &mut ViewContext<Workspace>) {
match &action.task_name { 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(), 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( fn spawn_task_with_name(
name: String, name: String,
task_target: TaskSpawnTarget,
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,
) -> AsyncTask<anyhow::Result<()>> { ) -> AsyncTask<anyhow::Result<()>> {
cx.spawn(|workspace, mut cx| async move { cx.spawn(|workspace, mut cx| async move {
@ -160,6 +164,7 @@ fn spawn_task_with_name(
task_source_kind, task_source_kind,
&target_task, &target_task,
&task_context, &task_context,
task_target,
false, false,
cx, cx,
); );

View file

@ -68,7 +68,7 @@ impl TasksModalDelegate {
}; };
Some(( Some((
source_kind, 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 { cx.dispatch_action(Spawn {
task_name: Some("example task".to_string()), task_name: Some("example task".to_string()),
target: None,
}); });
let tasks_picker = workspace.update(cx, |workspace, cx| { let tasks_picker = workspace.update(cx, |workspace, cx| {
workspace workspace

View file

@ -255,7 +255,10 @@ impl TerminalPanel {
terminal_panel terminal_panel
.update(&mut cx, |_, cx| { .update(&mut cx, |_, cx| {
cx.subscribe(&workspace, |terminal_panel, _, e, 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); 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<Self>) { fn spawn_task(&mut self, spawn_in_terminal: &SpawnInTerminal, cx: &mut ViewContext<Self>) {
let mut spawn_task = spawn_in_terminal.clone(); let mut spawn_task = spawn_in_terminal.clone();
// Set up shell args unconditionally, as tasks are always spawned inside of a shell. let Ok(is_local) = self
let Some((shell, mut user_args)) = (match spawn_in_terminal.shell.clone() { .workspace
Shell::System => { .update(cx, |workspace, cx| workspace.project().read(cx).is_local())
match self else {
.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 {
return; return;
}; };
#[cfg(target_os = "windows")] if let ControlFlow::Break(_) =
let windows_shell_type = to_windows_shell_type(&shell); Self::fill_command(is_local, spawn_in_terminal, &mut spawn_task)
#[cfg(not(target_os = "windows"))]
{ {
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 spawn_task = spawn_task;
let allow_concurrent_runs = spawn_in_terminal.allow_concurrent_runs; let allow_concurrent_runs = spawn_in_terminal.allow_concurrent_runs;
@ -602,6 +539,81 @@ impl TerminalPanel {
.detach() .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( pub fn spawn_in_new_terminal(
&mut self, &mut self,
spawn_task: SpawnInTerminal, spawn_task: SpawnInTerminal,

View file

@ -14,6 +14,7 @@ use gpui::{
use language::Bias; use language::Bias;
use persistence::TERMINAL_DB; use persistence::TERMINAL_DB;
use project::{search::SearchQuery, terminals::TerminalKind, Fs, Metadata, Project}; use project::{search::SearchQuery, terminals::TerminalKind, Fs, Metadata, Project};
use task::{NewCenterTask, RevealStrategy};
use terminal::{ use terminal::{
alacritty_terminal::{ alacritty_terminal::{
index::Point, index::Point,
@ -45,7 +46,7 @@ use zed_actions::InlineAssist;
use std::{ use std::{
cmp, cmp,
ops::RangeInclusive, ops::{ControlFlow, RangeInclusive},
path::{Path, PathBuf}, path::{Path, PathBuf},
rc::Rc, rc::Rc,
sync::Arc, sync::Arc,
@ -78,8 +79,9 @@ pub fn init(cx: &mut AppContext) {
register_serializable_item::<TerminalView>(cx); register_serializable_item::<TerminalView>(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);
workspace.register_action(TerminalView::deploy_center_task);
}) })
.detach(); .detach();
} }
@ -127,6 +129,61 @@ impl FocusableView for TerminalView {
} }
impl TerminalView { impl TerminalView {
pub fn deploy_center_task(
workspace: &mut Workspace,
task: &NewCenterTask,
cx: &mut ViewContext<Workspace>,
) {
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 ///Create a new Terminal in the current working directory or the user's home directory
pub fn deploy( pub fn deploy(
workspace: &mut Workspace, workspace: &mut Workspace,

View file

@ -61,6 +61,7 @@ ui.workspace = true
util.workspace = true util.workspace = true
uuid.workspace = true uuid.workspace = true
strum.workspace = true strum.workspace = true
zed_actions.workspace = true
[dev-dependencies] [dev-dependencies]
call = { workspace = true, features = ["test-support"] } call = { workspace = true, features = ["test-support"] }

View file

@ -1,15 +1,17 @@
use project::TaskSourceKind; use project::TaskSourceKind;
use remote::ConnectionState; use remote::ConnectionState;
use task::{ResolvedTask, TaskContext, TaskTemplate}; use task::{NewCenterTask, ResolvedTask, TaskContext, TaskTemplate};
use ui::ViewContext; use ui::ViewContext;
use zed_actions::TaskSpawnTarget;
use crate::Workspace; use crate::Workspace;
pub fn schedule_task( pub fn schedule_task(
workspace: &Workspace, workspace: &mut Workspace,
task_source_kind: TaskSourceKind, task_source_kind: TaskSourceKind,
task_to_resolve: &TaskTemplate, task_to_resolve: &TaskTemplate,
task_cx: &TaskContext, task_cx: &TaskContext,
task_target: zed_actions::TaskSpawnTarget,
omit_history: bool, omit_history: bool,
cx: &mut ViewContext<'_, Workspace>, cx: &mut ViewContext<'_, Workspace>,
) { ) {
@ -27,7 +29,7 @@ pub fn schedule_task(
} }
if let Some(spawn_in_terminal) = 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( schedule_resolved_task(
workspace, workspace,
@ -40,12 +42,13 @@ pub fn schedule_task(
} }
pub fn schedule_resolved_task( pub fn schedule_resolved_task(
workspace: &Workspace, workspace: &mut Workspace,
task_source_kind: TaskSourceKind, task_source_kind: TaskSourceKind,
mut resolved_task: ResolvedTask, mut resolved_task: ResolvedTask,
omit_history: bool, omit_history: bool,
cx: &mut ViewContext<'_, Workspace>, cx: &mut ViewContext<'_, Workspace>,
) { ) {
let target = resolved_task.target;
if let Some(spawn_in_terminal) = resolved_task.resolved.take() { if let Some(spawn_in_terminal) = resolved_task.resolved.take() {
if !omit_history { if !omit_history {
resolved_task.resolved = Some(spawn_in_terminal.clone()); 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),
});
}
}
} }
} }

View file

@ -686,7 +686,9 @@ pub enum Event {
}, },
ContactRequestedJoin(u64), ContactRequestedJoin(u64),
WorkspaceCreated(WeakView<Workspace>), WorkspaceCreated(WeakView<Workspace>),
SpawnTask(Box<SpawnInTerminal>), SpawnTask {
action: Box<SpawnInTerminal>,
},
OpenBundledFile { OpenBundledFile {
text: Cow<'static, str>, text: Cow<'static, str>,
title: &'static str, title: &'static str,

View file

@ -2533,6 +2533,12 @@ impl Snapshot {
self.entry_for_path("") self.entry_for_path("")
} }
pub fn root_dir(&self) -> Option<Arc<Path>> {
self.root_entry()
.filter(|entry| entry.is_dir())
.map(|_| self.abs_path().clone())
}
pub fn root_name(&self) -> &str { pub fn root_name(&self) -> &str {
&self.root_name &self.root_name
} }

View file

@ -90,6 +90,14 @@ pub struct OpenRecent {
gpui::impl_actions!(projects, [OpenRecent]); gpui::impl_actions!(projects, [OpenRecent]);
gpui::actions!(projects, [OpenRemote]); 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 /// Spawn a task with name or open tasks modal
#[derive(PartialEq, Clone, Deserialize, Default)] #[derive(PartialEq, Clone, Deserialize, Default)]
pub struct Spawn { 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. /// If it is not set, a modal with a list of available tasks is opened instead.
/// Defaults to None. /// Defaults to None.
pub task_name: Option<String>, pub task_name: Option<String>,
/// Which part of the UI the task should be spawned in.
/// Defaults to Dock.
#[serde(default)]
pub target: Option<TaskSpawnTarget>,
} }
impl Spawn { impl Spawn {
pub fn modal() -> Self { pub fn modal() -> Self {
Self { task_name: None } Self {
task_name: None,
target: None,
}
} }
} }

View file

@ -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 ## 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: Zed supports overriding default action for inline runnable indicators via workspace-local and global `tasks.json` file with the following precedence hierarchy: