diff --git a/Cargo.lock b/Cargo.lock index 80b4203938..01b2f33866 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15603,7 +15603,6 @@ dependencies = [ "ui", "util", "uuid", - "zed_actions", ] [[package]] @@ -16108,6 +16107,7 @@ name = "zed_actions" version = "0.1.0" dependencies = [ "gpui", + "schemars", "serde", ] diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 5c300e8288..3de58a5d9d 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -426,7 +426,10 @@ "ctrl-shift-r": "task::Rerun", "ctrl-alt-r": "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 diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index a3f35dccdd..321aa28369 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -495,8 +495,9 @@ "bindings": { "cmd-shift-r": "task::Spawn", "cmd-alt-r": "task::Rerun", - "alt-t": "task::Spawn", - "alt-shift-t": "task::Spawn" + "ctrl-alt-shift-r": ["task::Spawn", { "reveal_target": "center" }] + // also possible to spawn tasks by name: + // "foo-bar": ["task_name::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }] } }, // Bindings from Sublime Text diff --git a/assets/settings/initial_tasks.json b/assets/settings/initial_tasks.json index 31808ac632..a49b834020 100644 --- a/assets/settings/initial_tasks.json +++ b/assets/settings/initial_tasks.json @@ -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`. "allow_concurrent_runs": false, // 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) - // * `no_focus` — always show the terminal pane, add/reuse the task's tab there, but don't focus it - // * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there + // * `always` — always show the task's pane, and focus the corresponding tab in it (default) + // * `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 "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: // * `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 diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0979f1cc70..2bda887875 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -522,7 +522,7 @@ impl RunnableTasks { ) -> impl Iterator + 'a { self.templates.iter().filter_map(|(kind, template)| { template - .resolve_task(&kind.to_id_base(), Default::default(), cx) + .resolve_task(&kind.to_id_base(), cx) .map(|task| (kind.clone(), task)) }) } diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index c70a72f0e1..2a31710df6 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -184,7 +184,7 @@ impl Inventory { let id_base = kind.to_id_base(); Some(( kind, - task.resolve_task(&id_base, Default::default(), task_context)?, + task.resolve_task(&id_base, task_context)?, not_used_score, )) }) @@ -378,7 +378,7 @@ mod test_inventory { use crate::Inventory; - use super::{task_source_kind_preference, TaskSourceKind}; + use super::TaskSourceKind; pub(super) fn task_template_names( inventory: &Model, @@ -409,7 +409,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, Default::default(), &TaskContext::default()) + task.resolve_task(&id_base, &TaskContext::default()) .unwrap_or_else(|| panic!("Failed to resolve task with name {task_name}")), ); }); @@ -427,31 +427,12 @@ mod test_inventory { .into_iter() .filter_map(|(source_kind, task)| { let id_base = source_kind.to_id_base(); - Some(( - source_kind, - task.resolve_task(&id_base, Default::default(), task_context)?, - )) + Some((source_kind, task.resolve_task(&id_base, task_context)?)) }) .map(|(source_kind, resolved_task)| (source_kind, resolved_task.resolved_label)) .collect() }) } - - pub(super) async fn list_tasks_sorted_by_last_used( - inventory: &Model, - worktree: Option, - 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. @@ -877,7 +858,7 @@ mod tests { TaskStore::init(None); } - pub(super) async fn resolved_task_names( + async fn resolved_task_names( inventory: &Model, worktree: Option, cx: &mut TestAppContext, @@ -905,4 +886,20 @@ mod tests { )) .unwrap() } + + async fn list_tasks_sorted_by_last_used( + inventory: &Model, + worktree: Option, + 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() + } } diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index c5ad843679..7b81ae078c 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -15,14 +15,15 @@ use std::str::FromStr; pub use task_template::{HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates}; pub use vscode_format::VsCodeTaskFile; +pub use zed_actions::RevealTarget; /// 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, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize)] 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, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct SpawnInTerminal { /// Id of the task to use when determining task tab affinity. pub id: TaskId, @@ -47,6 +48,8 @@ pub struct SpawnInTerminal { pub allow_concurrent_runs: bool, /// What to do with the terminal pane and tab, after the command was started. 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. pub hide: HideStrategy, /// Which shell to use when spawning the task. @@ -57,15 +60,6 @@ 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 { @@ -84,9 +78,6 @@ 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 c2a4e1878b..a4a02494e5 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -9,7 +9,7 @@ use sha2::{Digest, Sha256}; use util::{truncate_and_remove_front, ResultExt}; use crate::{ - ResolvedTask, Shell, SpawnInTerminal, TaskContext, TaskId, VariableName, + ResolvedTask, RevealTarget, Shell, SpawnInTerminal, TaskContext, TaskId, VariableName, ZED_VARIABLE_NAME_PREFIX, }; @@ -42,10 +42,16 @@ pub struct TaskTemplate { #[serde(default)] pub allow_concurrent_runs: bool, /// 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) - /// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there + /// * `always` — always show the task's pane, and focus the corresponding tab in it (default) + // * `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)] 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: /// * `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 @@ -70,12 +76,12 @@ pub struct TaskTemplate { #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] 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] 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, - /// 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, } @@ -115,12 +121,7 @@ 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, - target: zed_actions::TaskSpawnTarget, - cx: &TaskContext, - ) -> Option { + pub fn resolve_task(&self, id_base: &str, cx: &TaskContext) -> Option { if self.label.trim().is_empty() || self.command.trim().is_empty() { return None; } @@ -219,7 +220,6 @@ impl TaskTemplate { Some(ResolvedTask { id: id.clone(), substituted_variables, - target, original_task: self.clone(), resolved_label: full_label.clone(), resolved: Some(SpawnInTerminal { @@ -241,6 +241,7 @@ impl TaskTemplate { use_new_terminal: self.use_new_terminal, allow_concurrent_runs: self.allow_concurrent_runs, reveal: self.reveal, + reveal_target: self.reveal_target, hide: self.hide, shell: self.shell.clone(), show_summary: self.show_summary, @@ -388,7 +389,7 @@ mod tests { }, ] { 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, "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 - .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:?}")); assert_substituted_variables(&resolved_task, Vec::new()); resolved_task @@ -532,7 +533,6 @@ 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()), @@ -621,7 +621,6 @@ 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), @@ -638,10 +637,10 @@ mod tests { label: "My task".into(), command: "echo".into(), args: vec!["$PATH".into()], - ..Default::default() + ..TaskTemplate::default() }; let resolved_task = task - .resolve_task(TEST_ID_BASE, Default::default(), &TaskContext::default()) + .resolve_task(TEST_ID_BASE, &TaskContext::default()) .unwrap(); assert_substituted_variables(&resolved_task, Vec::new()); let resolved = resolved_task.resolved.unwrap(); @@ -656,10 +655,10 @@ mod tests { label: "My task".into(), command: "echo".into(), args: vec!["$ZED_VARIABLE".into()], - ..Default::default() + ..TaskTemplate::default() }; assert!(task - .resolve_task(TEST_ID_BASE, Default::default(), &TaskContext::default()) + .resolve_task(TEST_ID_BASE, &TaskContext::default()) .is_none()); } @@ -709,7 +708,7 @@ mod tests { .enumerate() { 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:?}")); assert_eq!( resolved.substituted_variables, @@ -751,9 +750,7 @@ mod tests { context .task_variables .insert(VariableName::Symbol, "my-symbol".to_string()); - assert!(faulty_go_test - .resolve_task("base", Default::default(), &context) - .is_some()); + assert!(faulty_go_test.resolve_task("base", &context).is_some()); } #[test] @@ -812,7 +809,7 @@ mod tests { }; let resolved = template - .resolve_task(TEST_ID_BASE, Default::default(), &context) + .resolve_task(TEST_ID_BASE, &context) .unwrap() .resolved .unwrap(); diff --git a/crates/tasks_ui/src/lib.rs b/crates/tasks_ui/src/lib.rs index 0d278bc2f4..8616b4266a 100644 --- a/crates/tasks_ui/src/lib.rs +++ b/crates/tasks_ui/src/lib.rs @@ -1,9 +1,9 @@ use ::settings::Settings; use editor::{tasks::task_context, Editor}; use gpui::{AppContext, Task as AsyncTask, ViewContext, WindowContext}; -use modal::TasksModal; +use modal::{TaskOverrides, TasksModal}; use project::{Location, WorktreeId}; -use task::TaskId; +use task::{RevealTarget, TaskId}; use workspace::tasks::schedule_task; use workspace::{tasks::schedule_resolved_task, Workspace}; @@ -11,7 +11,6 @@ mod modal; mod settings; pub use modal::{Rerun, Spawn}; -use zed_actions::TaskSpawnTarget; pub fn init(cx: &mut AppContext) { settings::TaskSettings::register(cx); @@ -54,7 +53,6 @@ pub fn init(cx: &mut AppContext) { task_source_kind, &original_task, &task_context, - Default::default(), false, cx, ) @@ -81,7 +79,7 @@ pub fn init(cx: &mut AppContext) { ); } } 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) { - match &action.task_name { - 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(), + match action { + Spawn::ByName { + task_name, + 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, + cx: &mut ViewContext<'_, Workspace>, +) -> AsyncTask<()> { let task_store = workspace.project().read(cx).task_store().clone(); let workspace_handle = workspace.weak_handle(); 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 .update(&mut cx, |workspace, 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(); @@ -122,7 +139,7 @@ fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>) fn spawn_task_with_name( name: String, - task_target: TaskSpawnTarget, + overrides: Option, cx: &mut ViewContext, ) -> AsyncTask> { cx.spawn(|workspace, mut cx| async move { @@ -157,14 +174,18 @@ fn spawn_task_with_name( let did_spawn = workspace .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)?; + if let Some(overrides) = &overrides { + if let Some(target_override) = overrides.reveal_target { + target_task.reveal_target = target_override; + } + } schedule_task( workspace, task_source_kind, &target_task, &task_context, - task_target, false, cx, ); @@ -174,7 +195,13 @@ fn spawn_task_with_name( if !did_spawn { workspace .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(); } diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 521115cf5f..5595feaca7 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -9,7 +9,7 @@ use gpui::{ }; use picker::{highlighted_match_with_paths::HighlightedText, Picker, PickerDelegate}; use project::{task_store::TaskStore, TaskSourceKind}; -use task::{ResolvedTask, TaskContext, TaskTemplate}; +use task::{ResolvedTask, RevealTarget, TaskContext, TaskTemplate}; use ui::{ div, h_flex, v_flex, ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color, FluentBuilder as _, Icon, IconButton, IconButtonShape, IconName, IconSize, IntoElement, @@ -24,6 +24,7 @@ pub use zed_actions::{Rerun, Spawn}; pub(crate) struct TasksModalDelegate { task_store: Model, candidates: Option>, + task_overrides: Option, last_used_candidate_index: Option, divider_index: Option, matches: Vec, @@ -34,12 +35,28 @@ pub(crate) struct TasksModalDelegate { placeholder_text: Arc, } +/// 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, +} + impl TasksModalDelegate { fn new( task_store: Model, task_context: TaskContext, + task_overrides: Option, workspace: WeakView, ) -> 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 { task_store, workspace, @@ -50,7 +67,8 @@ impl TasksModalDelegate { selected_index: 0, prompt: String::default(), 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 id_base = source_kind.to_id_base(); - let new_oneshot = TaskTemplate { + let mut new_oneshot = TaskTemplate { label: self.prompt.clone(), command: self.prompt.clone(), ..TaskTemplate::default() }; + if let Some(TaskOverrides { + reveal_target: Some(reveal_target), + }) = &self.task_overrides + { + new_oneshot.reveal_target = *reveal_target; + } Some(( 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( task_store: Model, task_context: TaskContext, + task_overrides: Option, workspace: WeakView, cx: &mut ViewContext, ) -> Self { let picker = cx.new_view(|cx| { Picker::uniform_list( - TasksModalDelegate::new(task_store, task_context, workspace), + TasksModalDelegate::new(task_store, task_context, task_overrides, workspace), cx, ) }); @@ -257,9 +282,17 @@ impl PickerDelegate for TasksModalDelegate { .as_ref() .map(|candidates| candidates[ix].clone()) }); - let Some((task_source_kind, task)) = task else { + let Some((task_source_kind, mut task)) = task else { 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 .update(cx, |workspace, cx| { @@ -396,9 +429,18 @@ impl PickerDelegate for TasksModalDelegate { } fn confirm_input(&mut self, omit_history_entry: bool, cx: &mut ViewContext>) { - let Some((task_source_kind, task)) = self.spawn_oneshot() else { + let Some((task_source_kind, mut task)) = self.spawn_oneshot() else { 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 .update(cx, |workspace, 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)" ); - cx.dispatch_action(Spawn { - task_name: Some("example task".to_string()), - target: None, + cx.dispatch_action(Spawn::ByName { + task_name: "example task".to_string(), + reveal_target: None, }); let tasks_picker = workspace.update(cx, |workspace, cx| { workspace @@ -995,7 +1037,7 @@ mod tests { workspace: &View, cx: &mut VisualTestContext, ) -> View> { - cx.dispatch_action(Spawn::default()); + cx.dispatch_action(Spawn::modal()); workspace.update(cx, |workspace, cx| { workspace .active_modal::(cx) diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 2bcd8feebc..a4f5e7df4f 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -12,7 +12,7 @@ use collections::HashMap; use db::kvp::KEY_VALUE_STORE; use futures::future::join_all; 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, Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; @@ -20,7 +20,7 @@ use itertools::Itertools; use project::{terminals::TerminalKind, Fs, Project, ProjectEntryId}; use search::{buffer_search::DivRegistrar, BufferSearchBar}; use settings::Settings; -use task::{RevealStrategy, Shell, SpawnInTerminal, TaskId}; +use task::{RevealStrategy, RevealTarget, Shell, SpawnInTerminal, TaskId}; use terminal::{ terminal_settings::{TerminalDockPosition, TerminalSettings}, Terminal, @@ -40,7 +40,7 @@ use workspace::{ SplitUp, SwapPaneInDirection, ToggleZoom, Workspace, }; -use anyhow::Result; +use anyhow::{anyhow, Context, Result}; use zed_actions::InlineAssist; 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::open_terminal); workspace.register_action(|workspace, _: &ToggleFocus, cx| { - if workspace - .panel::(cx) - .as_ref() - .is_some_and(|panel| panel.read(cx).enabled) - { + if is_enabled_in_workspace(workspace, cx) { workspace.toggle_panel_focus::(cx); } }); @@ -76,7 +72,6 @@ pub struct TerminalPanel { pending_serialization: Task>, pending_terminals_to_add: usize, deferred_tasks: HashMap>, - enabled: bool, assistant_enabled: bool, assistant_tab_bar_button: Option, } @@ -86,7 +81,6 @@ impl TerminalPanel { let project = workspace.project(); let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), false, cx); let center = PaneGroup::new(pane.clone()); - let enabled = project.read(cx).supports_terminal(cx); cx.focus_view(&pane); let terminal_panel = Self { center, @@ -98,7 +92,6 @@ impl TerminalPanel { height: None, pending_terminals_to_add: 0, deferred_tasks: HashMap::default(), - enabled, assistant_enabled: false, assistant_tab_bar_button: None, }; @@ -492,8 +485,8 @@ impl TerminalPanel { !use_new_terminal, "Should have handled 'allow_concurrent_runs && use_new_terminal' case above" ); - this.update(&mut cx, |this, cx| { - this.replace_terminal( + this.update(&mut cx, |terminal_panel, cx| { + terminal_panel.replace_terminal( spawn_task, task_pane, existing_item_index, @@ -620,7 +613,17 @@ impl TerminalPanel { cx: &mut ViewContext, ) -> Task>> { 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 @@ -647,24 +650,40 @@ impl TerminalPanel { label: &str, cx: &mut AppContext, ) -> Vec<(usize, View, View)> { + let Some(workspace) = self.workspace.upgrade() else { + return Vec::new(); + }; + + let pane_terminal_views = |pane: View| { + pane.read(cx) + .items() + .enumerate() + .filter_map(|(index, item)| Some((index, item.act_as::(cx)?))) + .filter_map(|(index, terminal_view)| { + let task_state = terminal_view.read(cx).terminal().read(cx).task()?; + if &task_state.full_label == label { + Some((index, terminal_view)) + } else { + None + } + }) + .map(move |(index, terminal_view)| (index, pane.clone(), terminal_view)) + }; + self.center .panes() .into_iter() - .flat_map(|pane| { - pane.read(cx) - .items() - .enumerate() - .filter_map(|(index, item)| Some((index, item.act_as::(cx)?))) - .filter_map(|(index, terminal_view)| { - let task_state = terminal_view.read(cx).terminal().read(cx).task()?; - if &task_state.full_label == label { - Some((index, terminal_view)) - } else { - None - } - }) - .map(|(index, terminal_view)| (index, pane.clone(), terminal_view)) - }) + .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() } @@ -680,14 +699,48 @@ impl TerminalPanel { }) } + pub fn add_center_terminal( + workspace: &mut Workspace, + kind: TerminalKind, + cx: &mut ViewContext, + ) -> Task>> { + 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( &mut self, kind: TerminalKind, reveal_strategy: RevealStrategy, cx: &mut ViewContext, ) -> Task>> { - if !self.enabled { - return Task::ready(Err(anyhow::anyhow!( + if !self.is_enabled(cx) { + return Task::ready(Err(anyhow!( "terminal not yet supported for remote projects" ))); } @@ -786,10 +839,11 @@ impl TerminalPanel { cx: &mut ViewContext<'_, Self>, ) -> Task> { let reveal = spawn_task.reveal; + let reveal_target = spawn_task.reveal_target; let window = cx.window_handle(); let task_workspace = self.workspace.clone(); - cx.spawn(move |this, mut cx| async move { - let project = this + cx.spawn(move |terminal_panel, mut cx| async move { + let project = terminal_panel .update(&mut cx, |this, cx| { this.workspace .update(cx, |workspace, _| workspace.project().clone()) @@ -811,32 +865,68 @@ impl TerminalPanel { .ok()?; match reveal { - RevealStrategy::Always => { - this.update(&mut cx, |this, cx| { - this.activate_terminal_view(&task_pane, terminal_item_index, true, cx) - }) - .ok()?; - - cx.spawn(|mut cx| async move { + RevealStrategy::Always => match reveal_target { + RevealTarget::Center => { task_workspace - .update(&mut cx, |workspace, cx| workspace.focus_panel::(cx)) - .ok() - }) - .detach(); - } - RevealStrategy::NoFocus => { - this.update(&mut cx, |this, cx| { - this.activate_terminal_view(&task_pane, terminal_item_index, false, cx) - }) - .ok()?; + .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()?; - cx.spawn(|mut cx| async move { + cx.spawn(|mut cx| async move { + task_workspace + .update(&mut cx, |workspace, cx| workspace.focus_panel::(cx)) + .ok() + }) + .detach(); + } + }, + RevealStrategy::NoFocus => match reveal_target { + RevealTarget::Center => { task_workspace - .update(&mut cx, |workspace, cx| workspace.open_panel::(cx)) - .ok() - }) - .detach(); - } + .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()?; + + cx.spawn(|mut cx| async move { + task_workspace + .update(&mut cx, |workspace, cx| workspace.open_panel::(cx)) + .ok() + }) + .detach(); + } + }, RevealStrategy::Never => {} } @@ -851,6 +941,16 @@ impl TerminalPanel { pub fn assistant_enabled(&self) -> bool { 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( @@ -1235,7 +1335,7 @@ impl Panel for TerminalPanel { return; }; - this.add_terminal(kind, RevealStrategy::Never, cx) + this.add_terminal(kind, RevealStrategy::Always, cx) .detach_and_log_err(cx) }) } @@ -1259,7 +1359,9 @@ impl Panel for TerminalPanel { } fn icon(&self, cx: &WindowContext) -> Option { - 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) } else { None diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index fb46c5ae95..9cc7b3ccec 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -14,7 +14,6 @@ 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, @@ -31,7 +30,6 @@ use ui::{h_flex, prelude::*, ContextMenu, Icon, IconName, Label, Tooltip}; use util::{paths::PathWithPosition, ResultExt}; use workspace::{ item::{BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams}, - notifications::NotifyResultExt, register_serializable_item, searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}, CloseActiveItem, NewCenterTerminal, NewTerminal, OpenVisible, ToolbarItemLocation, Workspace, @@ -46,7 +44,7 @@ use zed_actions::InlineAssist; use std::{ cmp, - ops::{ControlFlow, RangeInclusive}, + ops::RangeInclusive, path::{Path, PathBuf}, rc::Rc, sync::Arc, @@ -81,7 +79,6 @@ pub fn init(cx: &mut AppContext) { cx.observe_new_views(|workspace: &mut Workspace, _cx| { workspace.register_action(TerminalView::deploy); - workspace.register_action(TerminalView::deploy_center_task); }) .detach(); } @@ -129,61 +126,6 @@ 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, @@ -191,38 +133,8 @@ impl TerminalView { cx: &mut ViewContext, ) { let working_directory = default_working_directory(workspace, cx); - - 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(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() + TerminalPanel::add_center_terminal(workspace, TerminalKind::Shell(working_directory), cx) + .detach_and_log_err(cx); } pub fn new( diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 6bd2382f35..3b17ed8dab 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -61,7 +61,6 @@ 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 7cf6ebcae8..33b3c1fa80 100644 --- a/crates/workspace/src/tasks.rs +++ b/crates/workspace/src/tasks.rs @@ -1,8 +1,7 @@ use project::TaskSourceKind; use remote::ConnectionState; -use task::{NewCenterTask, ResolvedTask, TaskContext, TaskTemplate}; +use task::{ResolvedTask, TaskContext, TaskTemplate}; use ui::ViewContext; -use zed_actions::TaskSpawnTarget; use crate::Workspace; @@ -11,7 +10,6 @@ pub fn schedule_task( task_source_kind: TaskSourceKind, task_to_resolve: &TaskTemplate, task_cx: &TaskContext, - task_target: zed_actions::TaskSpawnTarget, omit_history: bool, cx: &mut ViewContext<'_, Workspace>, ) { @@ -29,7 +27,7 @@ pub fn schedule_task( } 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( workspace, @@ -48,7 +46,6 @@ pub fn schedule_resolved_task( 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()); @@ -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 { - action: Box::new(spawn_in_terminal), - }); - } - } + cx.emit(crate::Event::SpawnTask { + action: Box::new(spawn_in_terminal), + }); } } diff --git a/crates/zed_actions/Cargo.toml b/crates/zed_actions/Cargo.toml index ee279cde65..1bf26dc4f0 100644 --- a/crates/zed_actions/Cargo.toml +++ b/crates/zed_actions/Cargo.toml @@ -10,4 +10,5 @@ workspace = true [dependencies] gpui.workspace = true +schemars.workspace = true serde.workspace = true diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index b823b38bbc..3a9d5d7221 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -1,5 +1,6 @@ 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 // 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::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")] -pub enum TaskSpawnTarget { +pub enum RevealTarget { + /// In the central pane group, "main" editor area. Center, + /// In the terminal dock, "regular" terminal items' place. #[default] Dock, } /// Spawn a task with name or open tasks modal -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct Spawn { - #[serde(default)] - /// Name of the task to 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, +#[derive(Debug, PartialEq, Clone, Deserialize)] +#[serde(untagged)] +pub enum Spawn { + /// Spawns a task by the name given. + ByName { + task_name: String, + #[serde(default)] + reveal_target: Option, + }, + /// Spawns a task via modal's selection. + ViaModal { + /// Selected task's `reveal_target` property override. + #[serde(default)] + reveal_target: Option, + }, } impl Spawn { pub fn modal() -> Self { - Self { - task_name: None, - target: None, + Self::ViaModal { + reveal_target: None, } } } diff --git a/docs/src/tasks.md b/docs/src/tasks.md index 6399c6f5e0..45f5a4e094 100644 --- a/docs/src/tasks.md +++ b/docs/src/tasks.md @@ -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`. "allow_concurrent_runs": false, // 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) - // * `no_focus` — always show the terminal pane, add/reuse the task's tab there, but don't focus it - // * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there + // * `always` — always show the task's pane, and focus the corresponding tab in it (default) + // * `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 "reveal": "always", // What to do with the terminal pane and tab, after the command had finished: // * `never` — Do nothing when the command finishes (default)