Move task centering code closer to user input (#22082)

Follow-up of https://github.com/zed-industries/zed/pull/22004 

* Reuse center terminals for tasks, when requested
* Extend task templates with `RevealTarget`, moving it from
`TaskSpawnTarget` into the core library
* Use `reveal_target` instead of `target` to avoid misinterpretations in
the task template context
* Do not expose `SpawnInTerminal` to user interface, avoid it
implementing `Serialize` and `Deserialize`
* Remove `NewCenterTask` action, extending `task::Spawn` interface
instead
* Do not require any extra unrelated parameters during task resolution,
instead, use task overrides on the resolved tasks on the modal side
* Add keybindings for opening the task modal in the
`RevealTarget::Center` mode

Release Notes:

- N/A
This commit is contained in:
Kirill Bulatov 2024-12-16 16:15:58 +02:00 committed by GitHub
parent ea012075fc
commit bc113e4b51
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 356 additions and 285 deletions

2
Cargo.lock generated
View file

@ -15603,7 +15603,6 @@ dependencies = [
"ui", "ui",
"util", "util",
"uuid", "uuid",
"zed_actions",
] ]
[[package]] [[package]]
@ -16108,6 +16107,7 @@ name = "zed_actions"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"gpui", "gpui",
"schemars",
"serde", "serde",
] ]

View file

@ -426,7 +426,10 @@
"ctrl-shift-r": "task::Rerun", "ctrl-shift-r": "task::Rerun",
"ctrl-alt-r": "task::Rerun", "ctrl-alt-r": "task::Rerun",
"alt-t": "task::Rerun", "alt-t": "task::Rerun",
"alt-shift-t": "task::Spawn" "alt-shift-t": "task::Spawn",
"alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
// also possible to spawn tasks by name:
// "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
} }
}, },
// Bindings from Sublime Text // Bindings from Sublime Text

View file

@ -495,8 +495,9 @@
"bindings": { "bindings": {
"cmd-shift-r": "task::Spawn", "cmd-shift-r": "task::Spawn",
"cmd-alt-r": "task::Rerun", "cmd-alt-r": "task::Rerun",
"alt-t": "task::Spawn", "ctrl-alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
"alt-shift-t": "task::Spawn" // also possible to spawn tasks by name:
// "foo-bar": ["task_name::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
} }
}, },
// Bindings from Sublime Text // Bindings from Sublime Text

View file

@ -15,10 +15,14 @@
// Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish, defaults to `false`. // Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish, defaults to `false`.
"allow_concurrent_runs": false, "allow_concurrent_runs": false,
// What to do with the terminal pane and tab, after the command was started: // What to do with the terminal pane and tab, after the command was started:
// * `always` always show the terminal pane, add and focus the corresponding task's tab in it (default) // * `always` always show the task's pane, and focus the corresponding tab in it (default)
// * `no_focus` always show the terminal pane, add/reuse the task's tab there, but don't focus it // * `no_focus` always show the task's pane, add the task's tab in it, but don't focus it
// * `never` avoid changing current terminal pane focus, but still add/reuse the task's tab there // * `never` do not alter focus, but still add/reuse the task's tab in its pane
"reveal": "always", "reveal": "always",
// Where to place the task's terminal item after starting the task:
// * `dock` in the terminal dock, "regular" terminal items' place (default)
// * `center` in the central pane group, "main" editor area
"reveal_target": "dock",
// What to do with the terminal pane and tab, after the command had finished: // What to do with the terminal pane and tab, after the command had finished:
// * `never` Do nothing when the command finishes (default) // * `never` Do nothing when the command finishes (default)
// * `always` always hide the terminal tab, hide the pane also if it was the last tab in it // * `always` always hide the terminal tab, hide the pane also if it was the last tab in it

View file

@ -522,7 +522,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(), Default::default(), cx) .resolve_task(&kind.to_id_base(), cx)
.map(|task| (kind.clone(), task)) .map(|task| (kind.clone(), task))
}) })
} }

View file

@ -184,7 +184,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, Default::default(), task_context)?, task.resolve_task(&id_base, task_context)?,
not_used_score, not_used_score,
)) ))
}) })
@ -378,7 +378,7 @@ mod test_inventory {
use crate::Inventory; use crate::Inventory;
use super::{task_source_kind_preference, TaskSourceKind}; use super::TaskSourceKind;
pub(super) fn task_template_names( pub(super) fn task_template_names(
inventory: &Model<Inventory>, inventory: &Model<Inventory>,
@ -409,7 +409,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, Default::default(), &TaskContext::default()) task.resolve_task(&id_base, &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}")),
); );
}); });
@ -427,31 +427,12 @@ mod test_inventory {
.into_iter() .into_iter()
.filter_map(|(source_kind, task)| { .filter_map(|(source_kind, task)| {
let id_base = source_kind.to_id_base(); let id_base = source_kind.to_id_base();
Some(( Some((source_kind, task.resolve_task(&id_base, task_context)?))
source_kind,
task.resolve_task(&id_base, Default::default(), task_context)?,
))
}) })
.map(|(source_kind, resolved_task)| (source_kind, resolved_task.resolved_label)) .map(|(source_kind, resolved_task)| (source_kind, resolved_task.resolved_label))
.collect() .collect()
}) })
} }
pub(super) async fn list_tasks_sorted_by_last_used(
inventory: &Model<Inventory>,
worktree: Option<WorktreeId>,
cx: &mut TestAppContext,
) -> Vec<(TaskSourceKind, String)> {
let (used, current) = inventory.update(cx, |inventory, cx| {
inventory.used_and_current_resolved_tasks(worktree, None, &TaskContext::default(), cx)
});
let mut all = used;
all.extend(current);
all.into_iter()
.map(|(source_kind, task)| (source_kind, task.resolved_label))
.sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
.collect()
}
} }
/// A context provided that tries to provide values for all non-custom [`VariableName`] variants for a currently opened file. /// A context provided that tries to provide values for all non-custom [`VariableName`] variants for a currently opened file.
@ -877,7 +858,7 @@ mod tests {
TaskStore::init(None); TaskStore::init(None);
} }
pub(super) async fn resolved_task_names( async fn resolved_task_names(
inventory: &Model<Inventory>, inventory: &Model<Inventory>,
worktree: Option<WorktreeId>, worktree: Option<WorktreeId>,
cx: &mut TestAppContext, cx: &mut TestAppContext,
@ -905,4 +886,20 @@ mod tests {
)) ))
.unwrap() .unwrap()
} }
async fn list_tasks_sorted_by_last_used(
inventory: &Model<Inventory>,
worktree: Option<WorktreeId>,
cx: &mut TestAppContext,
) -> Vec<(TaskSourceKind, String)> {
let (used, current) = inventory.update(cx, |inventory, cx| {
inventory.used_and_current_resolved_tasks(worktree, None, &TaskContext::default(), cx)
});
let mut all = used;
all.extend(current);
all.into_iter()
.map(|(source_kind, task)| (source_kind, task.resolved_label))
.sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
.collect()
}
} }

View file

@ -15,14 +15,15 @@ use std::str::FromStr;
pub use task_template::{HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates}; pub use task_template::{HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates};
pub use vscode_format::VsCodeTaskFile; pub use vscode_format::VsCodeTaskFile;
pub use zed_actions::RevealTarget;
/// 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, Serialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize)]
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, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq)]
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,
@ -47,6 +48,8 @@ pub struct SpawnInTerminal {
pub allow_concurrent_runs: bool, pub allow_concurrent_runs: bool,
/// What to do with the terminal pane and tab, after the command was started. /// What to do with the terminal pane and tab, after the command was started.
pub reveal: RevealStrategy, pub reveal: RevealStrategy,
/// Where to show tasks' terminal output.
pub reveal_target: RevealTarget,
/// What to do with the terminal pane and tab, after the command had finished. /// What to do with the terminal pane and tab, after the command had finished.
pub hide: HideStrategy, pub hide: HideStrategy,
/// Which shell to use when spawning the task. /// Which shell to use when spawning the task.
@ -57,15 +60,6 @@ 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 {
@ -84,9 +78,6 @@ 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

@ -9,7 +9,7 @@ use sha2::{Digest, Sha256};
use util::{truncate_and_remove_front, ResultExt}; use util::{truncate_and_remove_front, ResultExt};
use crate::{ use crate::{
ResolvedTask, Shell, SpawnInTerminal, TaskContext, TaskId, VariableName, ResolvedTask, RevealTarget, Shell, SpawnInTerminal, TaskContext, TaskId, VariableName,
ZED_VARIABLE_NAME_PREFIX, ZED_VARIABLE_NAME_PREFIX,
}; };
@ -42,10 +42,16 @@ pub struct TaskTemplate {
#[serde(default)] #[serde(default)]
pub allow_concurrent_runs: bool, pub allow_concurrent_runs: bool,
/// What to do with the terminal pane and tab, after the command was started: /// What to do with the terminal pane and tab, after the command was started:
/// * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default) /// * `always` — always show the task's pane, and focus the corresponding tab in it (default)
/// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there // * `no_focus` — always show the task's pane, add the task's tab in it, but don't focus it
// * `never` — do not alter focus, but still add/reuse the task's tab in its pane
#[serde(default)] #[serde(default)]
pub reveal: RevealStrategy, pub reveal: RevealStrategy,
/// Where to place the task's terminal item after starting the task.
/// * `dock` — in the terminal dock, "regular" terminal items' place (default).
/// * `center` — in the central pane group, "main" editor area.
#[serde(default)]
pub reveal_target: RevealTarget,
/// What to do with the terminal pane and tab, after the command had finished: /// What to do with the terminal pane and tab, after the command had finished:
/// * `never` — do nothing when the command finishes (default) /// * `never` — do nothing when the command finishes (default)
/// * `always` — always hide the terminal tab, hide the pane also if it was the last tab in it /// * `always` — always hide the terminal tab, hide the pane also if it was the last tab in it
@ -70,12 +76,12 @@ pub struct TaskTemplate {
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum RevealStrategy { pub enum RevealStrategy {
/// Always show the terminal pane, add and focus the corresponding task's tab in it. /// Always show the task's pane, and focus the corresponding tab in it.
#[default] #[default]
Always, Always,
/// Always show the terminal pane, add the task's tab in it, but don't focus it. /// Always show the task's pane, add the task's tab in it, but don't focus it.
NoFocus, NoFocus,
/// Do not change terminal pane focus, but still add/reuse the task's tab there. /// Do not alter focus, but still add/reuse the task's tab in its pane.
Never, Never,
} }
@ -115,12 +121,7 @@ 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( pub fn resolve_task(&self, id_base: &str, cx: &TaskContext) -> Option<ResolvedTask> {
&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;
} }
@ -219,7 +220,6 @@ 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 {
@ -241,6 +241,7 @@ impl TaskTemplate {
use_new_terminal: self.use_new_terminal, use_new_terminal: self.use_new_terminal,
allow_concurrent_runs: self.allow_concurrent_runs, allow_concurrent_runs: self.allow_concurrent_runs,
reveal: self.reveal, reveal: self.reveal,
reveal_target: self.reveal_target,
hide: self.hide, hide: self.hide,
shell: self.shell.clone(), shell: self.shell.clone(),
show_summary: self.show_summary, show_summary: self.show_summary,
@ -388,7 +389,7 @@ mod tests {
}, },
] { ] {
assert_eq!( assert_eq!(
task_with_blank_property.resolve_task(TEST_ID_BASE, Default::default(), &TaskContext::default()), task_with_blank_property.resolve_task(TEST_ID_BASE, &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:?}"
); );
@ -406,7 +407,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, Default::default(), task_cx) .resolve_task(TEST_ID_BASE, 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
@ -532,7 +533,6 @@ 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()),
@ -621,7 +621,6 @@ 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),
@ -638,10 +637,10 @@ mod tests {
label: "My task".into(), label: "My task".into(),
command: "echo".into(), command: "echo".into(),
args: vec!["$PATH".into()], args: vec!["$PATH".into()],
..Default::default() ..TaskTemplate::default()
}; };
let resolved_task = task let resolved_task = task
.resolve_task(TEST_ID_BASE, Default::default(), &TaskContext::default()) .resolve_task(TEST_ID_BASE, &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();
@ -656,10 +655,10 @@ mod tests {
label: "My task".into(), label: "My task".into(),
command: "echo".into(), command: "echo".into(),
args: vec!["$ZED_VARIABLE".into()], args: vec!["$ZED_VARIABLE".into()],
..Default::default() ..TaskTemplate::default()
}; };
assert!(task assert!(task
.resolve_task(TEST_ID_BASE, Default::default(), &TaskContext::default()) .resolve_task(TEST_ID_BASE, &TaskContext::default())
.is_none()); .is_none());
} }
@ -709,7 +708,7 @@ mod tests {
.enumerate() .enumerate()
{ {
let resolved = symbol_dependent_task let resolved = symbol_dependent_task
.resolve_task(TEST_ID_BASE, Default::default(), &cx) .resolve_task(TEST_ID_BASE, &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,
@ -751,9 +750,7 @@ 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 assert!(faulty_go_test.resolve_task("base", &context).is_some());
.resolve_task("base", Default::default(), &context)
.is_some());
} }
#[test] #[test]
@ -812,7 +809,7 @@ mod tests {
}; };
let resolved = template let resolved = template
.resolve_task(TEST_ID_BASE, Default::default(), &context) .resolve_task(TEST_ID_BASE, &context)
.unwrap() .unwrap()
.resolved .resolved
.unwrap(); .unwrap();

View file

@ -1,9 +1,9 @@
use ::settings::Settings; use ::settings::Settings;
use editor::{tasks::task_context, Editor}; use editor::{tasks::task_context, Editor};
use gpui::{AppContext, Task as AsyncTask, ViewContext, WindowContext}; use gpui::{AppContext, Task as AsyncTask, ViewContext, WindowContext};
use modal::TasksModal; use modal::{TaskOverrides, TasksModal};
use project::{Location, WorktreeId}; use project::{Location, WorktreeId};
use task::TaskId; use task::{RevealTarget, TaskId};
use workspace::tasks::schedule_task; use workspace::tasks::schedule_task;
use workspace::{tasks::schedule_resolved_task, Workspace}; use workspace::{tasks::schedule_resolved_task, Workspace};
@ -11,7 +11,6 @@ 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);
@ -54,7 +53,6 @@ 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,
) )
@ -81,7 +79,7 @@ pub fn init(cx: &mut AppContext) {
); );
} }
} else { } else {
toggle_modal(workspace, cx).detach(); toggle_modal(workspace, None, cx).detach();
}; };
}); });
}, },
@ -90,14 +88,25 @@ 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 {
Some(name) => spawn_task_with_name(name.clone(), action.target.unwrap_or_default(), cx) Spawn::ByName {
.detach_and_log_err(cx), task_name,
None => toggle_modal(workspace, cx).detach(), reveal_target,
} => {
let overrides = reveal_target.map(|reveal_target| TaskOverrides {
reveal_target: Some(reveal_target),
});
spawn_task_with_name(task_name.clone(), overrides, cx).detach_and_log_err(cx)
}
Spawn::ViaModal { reveal_target } => toggle_modal(workspace, *reveal_target, cx).detach(),
} }
} }
fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>) -> AsyncTask<()> { fn toggle_modal(
workspace: &mut Workspace,
reveal_target: Option<RevealTarget>,
cx: &mut ViewContext<'_, Workspace>,
) -> AsyncTask<()> {
let task_store = workspace.project().read(cx).task_store().clone(); let task_store = workspace.project().read(cx).task_store().clone();
let workspace_handle = workspace.weak_handle(); let workspace_handle = workspace.weak_handle();
let can_open_modal = workspace.project().update(cx, |project, cx| { let can_open_modal = workspace.project().update(cx, |project, cx| {
@ -110,7 +119,15 @@ fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>)
workspace workspace
.update(&mut cx, |workspace, cx| { .update(&mut cx, |workspace, cx| {
workspace.toggle_modal(cx, |cx| { workspace.toggle_modal(cx, |cx| {
TasksModal::new(task_store.clone(), task_context, workspace_handle, cx) TasksModal::new(
task_store.clone(),
task_context,
reveal_target.map(|target| TaskOverrides {
reveal_target: Some(target),
}),
workspace_handle,
cx,
)
}) })
}) })
.ok(); .ok();
@ -122,7 +139,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, overrides: Option<TaskOverrides>,
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 {
@ -157,14 +174,18 @@ fn spawn_task_with_name(
let did_spawn = workspace let did_spawn = workspace
.update(&mut cx, |workspace, cx| { .update(&mut cx, |workspace, cx| {
let (task_source_kind, target_task) = let (task_source_kind, mut target_task) =
tasks.into_iter().find(|(_, task)| task.label == name)?; tasks.into_iter().find(|(_, task)| task.label == name)?;
if let Some(overrides) = &overrides {
if let Some(target_override) = overrides.reveal_target {
target_task.reveal_target = target_override;
}
}
schedule_task( schedule_task(
workspace, workspace,
task_source_kind, task_source_kind,
&target_task, &target_task,
&task_context, &task_context,
task_target,
false, false,
cx, cx,
); );
@ -174,7 +195,13 @@ fn spawn_task_with_name(
if !did_spawn { if !did_spawn {
workspace workspace
.update(&mut cx, |workspace, cx| { .update(&mut cx, |workspace, cx| {
spawn_task_or_modal(workspace, &Spawn::default(), cx); spawn_task_or_modal(
workspace,
&Spawn::ViaModal {
reveal_target: overrides.and_then(|overrides| overrides.reveal_target),
},
cx,
);
}) })
.ok(); .ok();
} }

View file

@ -9,7 +9,7 @@ use gpui::{
}; };
use picker::{highlighted_match_with_paths::HighlightedText, Picker, PickerDelegate}; use picker::{highlighted_match_with_paths::HighlightedText, Picker, PickerDelegate};
use project::{task_store::TaskStore, TaskSourceKind}; use project::{task_store::TaskStore, TaskSourceKind};
use task::{ResolvedTask, TaskContext, TaskTemplate}; use task::{ResolvedTask, RevealTarget, TaskContext, TaskTemplate};
use ui::{ use ui::{
div, h_flex, v_flex, ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color, div, h_flex, v_flex, ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color,
FluentBuilder as _, Icon, IconButton, IconButtonShape, IconName, IconSize, IntoElement, FluentBuilder as _, Icon, IconButton, IconButtonShape, IconName, IconSize, IntoElement,
@ -24,6 +24,7 @@ pub use zed_actions::{Rerun, Spawn};
pub(crate) struct TasksModalDelegate { pub(crate) struct TasksModalDelegate {
task_store: Model<TaskStore>, task_store: Model<TaskStore>,
candidates: Option<Vec<(TaskSourceKind, ResolvedTask)>>, candidates: Option<Vec<(TaskSourceKind, ResolvedTask)>>,
task_overrides: Option<TaskOverrides>,
last_used_candidate_index: Option<usize>, last_used_candidate_index: Option<usize>,
divider_index: Option<usize>, divider_index: Option<usize>,
matches: Vec<StringMatch>, matches: Vec<StringMatch>,
@ -34,12 +35,28 @@ pub(crate) struct TasksModalDelegate {
placeholder_text: Arc<str>, placeholder_text: Arc<str>,
} }
/// Task template amendments to do before resolving the context.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct TaskOverrides {
/// See [`RevealTarget`].
pub(crate) reveal_target: Option<RevealTarget>,
}
impl TasksModalDelegate { impl TasksModalDelegate {
fn new( fn new(
task_store: Model<TaskStore>, task_store: Model<TaskStore>,
task_context: TaskContext, task_context: TaskContext,
task_overrides: Option<TaskOverrides>,
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
) -> Self { ) -> Self {
let placeholder_text = if let Some(TaskOverrides {
reveal_target: Some(RevealTarget::Center),
}) = &task_overrides
{
Arc::from("Find a task, or run a command in the central pane")
} else {
Arc::from("Find a task, or run a command")
};
Self { Self {
task_store, task_store,
workspace, workspace,
@ -50,7 +67,8 @@ impl TasksModalDelegate {
selected_index: 0, selected_index: 0,
prompt: String::default(), prompt: String::default(),
task_context, task_context,
placeholder_text: Arc::from("Find a task, or run a command"), task_overrides,
placeholder_text,
} }
} }
@ -61,14 +79,20 @@ impl TasksModalDelegate {
let source_kind = TaskSourceKind::UserInput; let source_kind = TaskSourceKind::UserInput;
let id_base = source_kind.to_id_base(); let id_base = source_kind.to_id_base();
let new_oneshot = TaskTemplate { let mut new_oneshot = TaskTemplate {
label: self.prompt.clone(), label: self.prompt.clone(),
command: self.prompt.clone(), command: self.prompt.clone(),
..TaskTemplate::default() ..TaskTemplate::default()
}; };
if let Some(TaskOverrides {
reveal_target: Some(reveal_target),
}) = &self.task_overrides
{
new_oneshot.reveal_target = *reveal_target;
}
Some(( Some((
source_kind, source_kind,
new_oneshot.resolve_task(&id_base, Default::default(), &self.task_context)?, new_oneshot.resolve_task(&id_base, &self.task_context)?,
)) ))
} }
@ -100,12 +124,13 @@ impl TasksModal {
pub(crate) fn new( pub(crate) fn new(
task_store: Model<TaskStore>, task_store: Model<TaskStore>,
task_context: TaskContext, task_context: TaskContext,
task_overrides: Option<TaskOverrides>,
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Self { ) -> Self {
let picker = cx.new_view(|cx| { let picker = cx.new_view(|cx| {
Picker::uniform_list( Picker::uniform_list(
TasksModalDelegate::new(task_store, task_context, workspace), TasksModalDelegate::new(task_store, task_context, task_overrides, workspace),
cx, cx,
) )
}); });
@ -257,9 +282,17 @@ impl PickerDelegate for TasksModalDelegate {
.as_ref() .as_ref()
.map(|candidates| candidates[ix].clone()) .map(|candidates| candidates[ix].clone())
}); });
let Some((task_source_kind, task)) = task else { let Some((task_source_kind, mut task)) = task else {
return; return;
}; };
if let Some(TaskOverrides {
reveal_target: Some(reveal_target),
}) = &self.task_overrides
{
if let Some(resolved_task) = &mut task.resolved {
resolved_task.reveal_target = *reveal_target;
}
}
self.workspace self.workspace
.update(cx, |workspace, cx| { .update(cx, |workspace, cx| {
@ -396,9 +429,18 @@ impl PickerDelegate for TasksModalDelegate {
} }
fn confirm_input(&mut self, omit_history_entry: bool, cx: &mut ViewContext<Picker<Self>>) { fn confirm_input(&mut self, omit_history_entry: bool, cx: &mut ViewContext<Picker<Self>>) {
let Some((task_source_kind, task)) = self.spawn_oneshot() else { let Some((task_source_kind, mut task)) = self.spawn_oneshot() else {
return; return;
}; };
if let Some(TaskOverrides {
reveal_target: Some(reveal_target),
}) = self.task_overrides
{
if let Some(resolved_task) = &mut task.resolved {
resolved_task.reveal_target = reveal_target;
}
}
self.workspace self.workspace
.update(cx, |workspace, cx| { .update(cx, |workspace, cx| {
schedule_resolved_task(workspace, task_source_kind, task, omit_history_entry, cx); schedule_resolved_task(workspace, task_source_kind, task, omit_history_entry, cx);
@ -682,9 +724,9 @@ mod tests {
"No query should be added to the list, as it was submitted with secondary action (that maps to omit_history = true)" "No query should be added to the list, as it was submitted with secondary action (that maps to omit_history = true)"
); );
cx.dispatch_action(Spawn { cx.dispatch_action(Spawn::ByName {
task_name: Some("example task".to_string()), task_name: "example task".to_string(),
target: None, reveal_target: None,
}); });
let tasks_picker = workspace.update(cx, |workspace, cx| { let tasks_picker = workspace.update(cx, |workspace, cx| {
workspace workspace
@ -995,7 +1037,7 @@ mod tests {
workspace: &View<Workspace>, workspace: &View<Workspace>,
cx: &mut VisualTestContext, cx: &mut VisualTestContext,
) -> View<Picker<TasksModalDelegate>> { ) -> View<Picker<TasksModalDelegate>> {
cx.dispatch_action(Spawn::default()); cx.dispatch_action(Spawn::modal());
workspace.update(cx, |workspace, cx| { workspace.update(cx, |workspace, cx| {
workspace workspace
.active_modal::<TasksModal>(cx) .active_modal::<TasksModal>(cx)

View file

@ -12,7 +12,7 @@ use collections::HashMap;
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use futures::future::join_all; use futures::future::join_all;
use gpui::{ use gpui::{
actions, Action, AnchorCorner, AnyView, AppContext, AsyncWindowContext, EventEmitter, actions, Action, AnchorCorner, AnyView, AppContext, AsyncWindowContext, Entity, EventEmitter,
ExternalPaths, FocusHandle, FocusableView, IntoElement, Model, ParentElement, Pixels, Render, ExternalPaths, FocusHandle, FocusableView, IntoElement, Model, ParentElement, Pixels, Render,
Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext, Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
}; };
@ -20,7 +20,7 @@ use itertools::Itertools;
use project::{terminals::TerminalKind, Fs, Project, ProjectEntryId}; use project::{terminals::TerminalKind, Fs, Project, ProjectEntryId};
use search::{buffer_search::DivRegistrar, BufferSearchBar}; use search::{buffer_search::DivRegistrar, BufferSearchBar};
use settings::Settings; use settings::Settings;
use task::{RevealStrategy, Shell, SpawnInTerminal, TaskId}; use task::{RevealStrategy, RevealTarget, Shell, SpawnInTerminal, TaskId};
use terminal::{ use terminal::{
terminal_settings::{TerminalDockPosition, TerminalSettings}, terminal_settings::{TerminalDockPosition, TerminalSettings},
Terminal, Terminal,
@ -40,7 +40,7 @@ use workspace::{
SplitUp, SwapPaneInDirection, ToggleZoom, Workspace, SplitUp, SwapPaneInDirection, ToggleZoom, Workspace,
}; };
use anyhow::Result; use anyhow::{anyhow, Context, Result};
use zed_actions::InlineAssist; use zed_actions::InlineAssist;
const TERMINAL_PANEL_KEY: &str = "TerminalPanel"; const TERMINAL_PANEL_KEY: &str = "TerminalPanel";
@ -53,11 +53,7 @@ pub fn init(cx: &mut AppContext) {
workspace.register_action(TerminalPanel::new_terminal); workspace.register_action(TerminalPanel::new_terminal);
workspace.register_action(TerminalPanel::open_terminal); workspace.register_action(TerminalPanel::open_terminal);
workspace.register_action(|workspace, _: &ToggleFocus, cx| { workspace.register_action(|workspace, _: &ToggleFocus, cx| {
if workspace if is_enabled_in_workspace(workspace, cx) {
.panel::<TerminalPanel>(cx)
.as_ref()
.is_some_and(|panel| panel.read(cx).enabled)
{
workspace.toggle_panel_focus::<TerminalPanel>(cx); workspace.toggle_panel_focus::<TerminalPanel>(cx);
} }
}); });
@ -76,7 +72,6 @@ pub struct TerminalPanel {
pending_serialization: Task<Option<()>>, pending_serialization: Task<Option<()>>,
pending_terminals_to_add: usize, pending_terminals_to_add: usize,
deferred_tasks: HashMap<TaskId, Task<()>>, deferred_tasks: HashMap<TaskId, Task<()>>,
enabled: bool,
assistant_enabled: bool, assistant_enabled: bool,
assistant_tab_bar_button: Option<AnyView>, assistant_tab_bar_button: Option<AnyView>,
} }
@ -86,7 +81,6 @@ impl TerminalPanel {
let project = workspace.project(); let project = workspace.project();
let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), false, cx); let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), false, cx);
let center = PaneGroup::new(pane.clone()); let center = PaneGroup::new(pane.clone());
let enabled = project.read(cx).supports_terminal(cx);
cx.focus_view(&pane); cx.focus_view(&pane);
let terminal_panel = Self { let terminal_panel = Self {
center, center,
@ -98,7 +92,6 @@ impl TerminalPanel {
height: None, height: None,
pending_terminals_to_add: 0, pending_terminals_to_add: 0,
deferred_tasks: HashMap::default(), deferred_tasks: HashMap::default(),
enabled,
assistant_enabled: false, assistant_enabled: false,
assistant_tab_bar_button: None, assistant_tab_bar_button: None,
}; };
@ -492,8 +485,8 @@ impl TerminalPanel {
!use_new_terminal, !use_new_terminal,
"Should have handled 'allow_concurrent_runs && use_new_terminal' case above" "Should have handled 'allow_concurrent_runs && use_new_terminal' case above"
); );
this.update(&mut cx, |this, cx| { this.update(&mut cx, |terminal_panel, cx| {
this.replace_terminal( terminal_panel.replace_terminal(
spawn_task, spawn_task,
task_pane, task_pane,
existing_item_index, existing_item_index,
@ -620,7 +613,17 @@ impl TerminalPanel {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<Result<Model<Terminal>>> { ) -> Task<Result<Model<Terminal>>> {
let reveal = spawn_task.reveal; let reveal = spawn_task.reveal;
self.add_terminal(TerminalKind::Task(spawn_task), reveal, cx) let reveal_target = spawn_task.reveal_target;
let kind = TerminalKind::Task(spawn_task);
match reveal_target {
RevealTarget::Center => self
.workspace
.update(cx, |workspace, cx| {
Self::add_center_terminal(workspace, kind, cx)
})
.unwrap_or_else(|e| Task::ready(Err(e))),
RevealTarget::Dock => self.add_terminal(kind, reveal, 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
@ -647,10 +650,11 @@ impl TerminalPanel {
label: &str, label: &str,
cx: &mut AppContext, cx: &mut AppContext,
) -> Vec<(usize, View<Pane>, View<TerminalView>)> { ) -> Vec<(usize, View<Pane>, View<TerminalView>)> {
self.center let Some(workspace) = self.workspace.upgrade() else {
.panes() return Vec::new();
.into_iter() };
.flat_map(|pane| {
let pane_terminal_views = |pane: View<Pane>| {
pane.read(cx) pane.read(cx)
.items() .items()
.enumerate() .enumerate()
@ -663,8 +667,23 @@ impl TerminalPanel {
None None
} }
}) })
.map(|(index, terminal_view)| (index, pane.clone(), terminal_view)) .map(move |(index, terminal_view)| (index, pane.clone(), terminal_view))
}) };
self.center
.panes()
.into_iter()
.cloned()
.flat_map(pane_terminal_views)
.chain(
workspace
.read(cx)
.panes()
.into_iter()
.cloned()
.flat_map(pane_terminal_views),
)
.sorted_by_key(|(_, _, terminal_view)| terminal_view.entity_id())
.collect() .collect()
} }
@ -680,14 +699,48 @@ impl TerminalPanel {
}) })
} }
pub fn add_center_terminal(
workspace: &mut Workspace,
kind: TerminalKind,
cx: &mut ViewContext<Workspace>,
) -> Task<Result<Model<Terminal>>> {
if !is_enabled_in_workspace(workspace, cx) {
return Task::ready(Err(anyhow!(
"terminal not yet supported for remote projects"
)));
}
let window = cx.window_handle();
let project = workspace.project().downgrade();
cx.spawn(move |workspace, mut cx| async move {
let terminal = project
.update(&mut cx, |project, cx| {
project.create_terminal(kind, window, cx)
})?
.await?;
workspace.update(&mut cx, |workspace, cx| {
let view = cx.new_view(|cx| {
TerminalView::new(
terminal.clone(),
workspace.weak_handle(),
workspace.database_id(),
cx,
)
});
workspace.add_item_to_active_pane(Box::new(view), None, true, cx);
})?;
Ok(terminal)
})
}
fn add_terminal( fn add_terminal(
&mut self, &mut self,
kind: TerminalKind, kind: TerminalKind,
reveal_strategy: RevealStrategy, reveal_strategy: RevealStrategy,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<Result<Model<Terminal>>> { ) -> Task<Result<Model<Terminal>>> {
if !self.enabled { if !self.is_enabled(cx) {
return Task::ready(Err(anyhow::anyhow!( return Task::ready(Err(anyhow!(
"terminal not yet supported for remote projects" "terminal not yet supported for remote projects"
))); )));
} }
@ -786,10 +839,11 @@ impl TerminalPanel {
cx: &mut ViewContext<'_, Self>, cx: &mut ViewContext<'_, Self>,
) -> Task<Option<()>> { ) -> Task<Option<()>> {
let reveal = spawn_task.reveal; let reveal = spawn_task.reveal;
let reveal_target = spawn_task.reveal_target;
let window = cx.window_handle(); let window = cx.window_handle();
let task_workspace = self.workspace.clone(); let task_workspace = self.workspace.clone();
cx.spawn(move |this, mut cx| async move { cx.spawn(move |terminal_panel, mut cx| async move {
let project = this let project = terminal_panel
.update(&mut cx, |this, cx| { .update(&mut cx, |this, cx| {
this.workspace this.workspace
.update(cx, |workspace, _| workspace.project().clone()) .update(cx, |workspace, _| workspace.project().clone())
@ -811,9 +865,29 @@ impl TerminalPanel {
.ok()?; .ok()?;
match reveal { match reveal {
RevealStrategy::Always => { RevealStrategy::Always => match reveal_target {
this.update(&mut cx, |this, cx| { RevealTarget::Center => {
this.activate_terminal_view(&task_pane, terminal_item_index, true, cx) task_workspace
.update(&mut cx, |workspace, cx| {
workspace
.active_item(cx)
.context("retrieving active terminal item in the workspace")
.log_err()?
.focus_handle(cx)
.focus(cx);
Some(())
})
.ok()??;
}
RevealTarget::Dock => {
terminal_panel
.update(&mut cx, |terminal_panel, cx| {
terminal_panel.activate_terminal_view(
&task_pane,
terminal_item_index,
true,
cx,
)
}) })
.ok()?; .ok()?;
@ -824,9 +898,24 @@ impl TerminalPanel {
}) })
.detach(); .detach();
} }
RevealStrategy::NoFocus => { },
this.update(&mut cx, |this, cx| { RevealStrategy::NoFocus => match reveal_target {
this.activate_terminal_view(&task_pane, terminal_item_index, false, cx) RevealTarget::Center => {
task_workspace
.update(&mut cx, |workspace, cx| {
workspace.active_pane().focus_handle(cx).focus(cx);
})
.ok()?;
}
RevealTarget::Dock => {
terminal_panel
.update(&mut cx, |terminal_panel, cx| {
terminal_panel.activate_terminal_view(
&task_pane,
terminal_item_index,
false,
cx,
)
}) })
.ok()?; .ok()?;
@ -837,6 +926,7 @@ impl TerminalPanel {
}) })
.detach(); .detach();
} }
},
RevealStrategy::Never => {} RevealStrategy::Never => {}
} }
@ -851,6 +941,16 @@ impl TerminalPanel {
pub fn assistant_enabled(&self) -> bool { pub fn assistant_enabled(&self) -> bool {
self.assistant_enabled self.assistant_enabled
} }
fn is_enabled(&self, cx: &WindowContext) -> bool {
self.workspace.upgrade().map_or(false, |workspace| {
is_enabled_in_workspace(workspace.read(cx), cx)
})
}
}
fn is_enabled_in_workspace(workspace: &Workspace, cx: &WindowContext) -> bool {
workspace.project().read(cx).supports_terminal(cx)
} }
pub fn new_terminal_pane( pub fn new_terminal_pane(
@ -1235,7 +1335,7 @@ impl Panel for TerminalPanel {
return; return;
}; };
this.add_terminal(kind, RevealStrategy::Never, cx) this.add_terminal(kind, RevealStrategy::Always, cx)
.detach_and_log_err(cx) .detach_and_log_err(cx)
}) })
} }
@ -1259,7 +1359,9 @@ impl Panel for TerminalPanel {
} }
fn icon(&self, cx: &WindowContext) -> Option<IconName> { fn icon(&self, cx: &WindowContext) -> Option<IconName> {
if (self.enabled || !self.has_no_terminals(cx)) && TerminalSettings::get_global(cx).button { if (self.is_enabled(cx) || !self.has_no_terminals(cx))
&& TerminalSettings::get_global(cx).button
{
Some(IconName::Terminal) Some(IconName::Terminal)
} else { } else {
None None

View file

@ -14,7 +14,6 @@ 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,
@ -31,7 +30,6 @@ use ui::{h_flex, prelude::*, ContextMenu, Icon, IconName, Label, Tooltip};
use util::{paths::PathWithPosition, ResultExt}; use util::{paths::PathWithPosition, ResultExt};
use workspace::{ use workspace::{
item::{BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams}, item::{BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams},
notifications::NotifyResultExt,
register_serializable_item, register_serializable_item,
searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}, searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
CloseActiveItem, NewCenterTerminal, NewTerminal, OpenVisible, ToolbarItemLocation, Workspace, CloseActiveItem, NewCenterTerminal, NewTerminal, OpenVisible, ToolbarItemLocation, Workspace,
@ -46,7 +44,7 @@ use zed_actions::InlineAssist;
use std::{ use std::{
cmp, cmp,
ops::{ControlFlow, RangeInclusive}, ops::RangeInclusive,
path::{Path, PathBuf}, path::{Path, PathBuf},
rc::Rc, rc::Rc,
sync::Arc, sync::Arc,
@ -81,7 +79,6 @@ pub fn init(cx: &mut AppContext) {
cx.observe_new_views(|workspace: &mut Workspace, _cx| { 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();
} }
@ -129,61 +126,6 @@ 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,
@ -191,38 +133,8 @@ impl TerminalView {
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,
) { ) {
let working_directory = default_working_directory(workspace, cx); let working_directory = default_working_directory(workspace, cx);
TerminalPanel::add_center_terminal(workspace, TerminalKind::Shell(working_directory), cx)
let window = cx.window_handle(); .detach_and_log_err(cx);
let project = workspace.project().downgrade();
cx.spawn(move |workspace, mut cx| async move {
let terminal = project
.update(&mut cx, |project, cx| {
project.create_terminal(TerminalKind::Shell(working_directory), window, cx)
})
.ok()?
.await;
let terminal = workspace
.update(&mut cx, |workspace, cx| terminal.notify_err(workspace, cx))
.ok()
.flatten()?;
workspace
.update(&mut cx, |workspace, cx| {
let view = cx.new_view(|cx| {
TerminalView::new(
terminal,
workspace.weak_handle(),
workspace.database_id(),
cx,
)
});
workspace.add_item_to_active_pane(Box::new(view), None, true, cx);
})
.ok();
Some(())
})
.detach()
} }
pub fn new( pub fn new(

View file

@ -61,7 +61,6 @@ 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,8 +1,7 @@
use project::TaskSourceKind; use project::TaskSourceKind;
use remote::ConnectionState; use remote::ConnectionState;
use task::{NewCenterTask, ResolvedTask, TaskContext, TaskTemplate}; use task::{ResolvedTask, TaskContext, TaskTemplate};
use ui::ViewContext; use ui::ViewContext;
use zed_actions::TaskSpawnTarget;
use crate::Workspace; use crate::Workspace;
@ -11,7 +10,6 @@ pub fn schedule_task(
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>,
) { ) {
@ -29,7 +27,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_target, task_cx) task_to_resolve.resolve_task(&task_source_kind.to_id_base(), task_cx)
{ {
schedule_resolved_task( schedule_resolved_task(
workspace, workspace,
@ -48,7 +46,6 @@ pub fn schedule_resolved_task(
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());
@ -63,17 +60,8 @@ pub fn schedule_resolved_task(
}); });
} }
match target {
TaskSpawnTarget::Center => {
cx.dispatch_action(Box::new(NewCenterTask {
action: spawn_in_terminal,
}));
}
TaskSpawnTarget::Dock => {
cx.emit(crate::Event::SpawnTask { cx.emit(crate::Event::SpawnTask {
action: Box::new(spawn_in_terminal), action: Box::new(spawn_in_terminal),
}); });
} }
} }
}
}

View file

@ -10,4 +10,5 @@ workspace = true
[dependencies] [dependencies]
gpui.workspace = true gpui.workspace = true
schemars.workspace = true
serde.workspace = true serde.workspace = true

View file

@ -1,5 +1,6 @@
use gpui::{actions, impl_actions}; use gpui::{actions, impl_actions};
use serde::Deserialize; use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
// If the zed binary doesn't use anything in this crate, it will be optimized away // If the zed binary doesn't use anything in this crate, it will be optimized away
// and the actions won't initialize. So we just provide an empty initialization function // and the actions won't initialize. So we just provide an empty initialization function
@ -90,33 +91,39 @@ 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)] /// Where to spawn the task in the UI.
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum TaskSpawnTarget { pub enum RevealTarget {
/// In the central pane group, "main" editor area.
Center, Center,
/// In the terminal dock, "regular" terminal items' place.
#[default] #[default]
Dock, 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(Debug, PartialEq, Clone, Deserialize)]
pub struct Spawn { #[serde(untagged)]
pub enum Spawn {
/// Spawns a task by the name given.
ByName {
task_name: String,
#[serde(default)] #[serde(default)]
/// Name of the task to spawn. reveal_target: Option<RevealTarget>,
/// If it is not set, a modal with a list of available tasks is opened instead. },
/// Defaults to None. /// Spawns a task via modal's selection.
pub task_name: Option<String>, ViaModal {
/// Which part of the UI the task should be spawned in. /// Selected task's `reveal_target` property override.
/// Defaults to Dock.
#[serde(default)] #[serde(default)]
pub target: Option<TaskSpawnTarget>, reveal_target: Option<RevealTarget>,
},
} }
impl Spawn { impl Spawn {
pub fn modal() -> Self { pub fn modal() -> Self {
Self { Self::ViaModal {
task_name: None, reveal_target: None,
target: None,
} }
} }
} }

View file

@ -17,9 +17,9 @@ Zed supports ways to spawn (and rerun) commands using its integrated terminal to
// Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish, defaults to `false`. // Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish, defaults to `false`.
"allow_concurrent_runs": false, "allow_concurrent_runs": false,
// What to do with the terminal pane and tab, after the command was started: // What to do with the terminal pane and tab, after the command was started:
// * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default) // * `always` — always show the task's pane, and focus the corresponding tab in it (default)
// * `no_focus` — always show the terminal pane, add/reuse the task's tab there, but don't focus it // * `no_focus` — always show the task's pane, add the task's tab in it, but don't focus it
// * `never`avoid changing current terminal pane focus, but still add/reuse the task's tab there // * `never`do not alter focus, but still add/reuse the task's tab in its pane
"reveal": "always", "reveal": "always",
// What to do with the terminal pane and tab, after the command had finished: // What to do with the terminal pane and tab, after the command had finished:
// * `never` — Do nothing when the command finishes (default) // * `never` — Do nothing when the command finishes (default)