diff --git a/Cargo.lock b/Cargo.lock index 43efeab533..3e4daf6d12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7786,6 +7786,7 @@ dependencies = [ "fuzzy", "gpui", "log", + "menu", "picker", "project", "runnable", @@ -8561,9 +8562,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook" diff --git a/crates/project/src/runnable_inventory.rs b/crates/project/src/runnable_inventory.rs index 52867acc49..54dc4d77bd 100644 --- a/crates/project/src/runnable_inventory.rs +++ b/crates/project/src/runnable_inventory.rs @@ -1,6 +1,6 @@ //! Project-wide storage of the runnables available, capable of updating itself from the sources set. -use std::{path::Path, sync::Arc}; +use std::{any::TypeId, path::Path, sync::Arc}; use gpui::{AppContext, Context, Model, ModelContext, Subscription}; use runnable::{Runnable, RunnableId, Source}; @@ -14,6 +14,7 @@ pub struct Inventory { struct SourceInInventory { source: Model>, _subscription: Subscription, + type_id: TypeId, } impl Inventory { @@ -29,13 +30,29 @@ impl Inventory { let _subscription = cx.observe(&source, |_, _, cx| { cx.notify(); }); + let type_id = source.read(cx).type_id(); let source = SourceInInventory { source, _subscription, + type_id, }; self.sources.push(source); cx.notify(); } + pub fn source(&self) -> Option>> { + let target_type_id = std::any::TypeId::of::(); + self.sources.iter().find_map( + |SourceInInventory { + type_id, source, .. + }| { + if &target_type_id == type_id { + Some(source.clone()) + } else { + None + } + }, + ) + } /// Pulls its sources to list runanbles for the path given (up to the source to decide what to return for no path). pub fn list_runnables( diff --git a/crates/runnable/src/lib.rs b/crates/runnable/src/lib.rs index 99c485c1a4..d10b70f9c5 100644 --- a/crates/runnable/src/lib.rs +++ b/crates/runnable/src/lib.rs @@ -15,7 +15,7 @@ use std::sync::Arc; /// Runnable identifier, unique within the application. /// Based on it, runnable reruns and terminal tabs are managed. #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct RunnableId(String); +pub struct RunnableId(pub String); /// Contains all information needed by Zed to spawn a new terminal tab for the given runnable. #[derive(Debug, Clone)] @@ -36,6 +36,8 @@ pub struct SpawnInTerminal { pub use_new_terminal: bool, /// Whether to allow multiple instances of the same runnable to be run, or rather wait for the existing ones to finish. pub allow_concurrent_runs: bool, + /// Whether the command should be spawned in a separate shell instance. + pub separate_shell: bool, } /// Represents a short lived recipe of a runnable, whose main purpose diff --git a/crates/runnable/src/static_runnable.rs b/crates/runnable/src/static_runnable.rs index 7138dc91c2..f7bc6cf9d1 100644 --- a/crates/runnable/src/static_runnable.rs +++ b/crates/runnable/src/static_runnable.rs @@ -31,6 +31,7 @@ impl Runnable for StaticRunnable { command: self.definition.command.clone(), args: self.definition.args.clone(), env: self.definition.env.clone(), + separate_shell: false, }) } diff --git a/crates/runnables_ui/Cargo.toml b/crates/runnables_ui/Cargo.toml index f78edd1d70..31faf69891 100644 --- a/crates/runnables_ui/Cargo.toml +++ b/crates/runnables_ui/Cargo.toml @@ -14,6 +14,7 @@ futures.workspace = true fuzzy.workspace = true gpui.workspace = true log.workspace = true +menu.workspace = true picker.workspace = true project.workspace = true runnable.workspace = true diff --git a/crates/runnables_ui/src/lib.rs b/crates/runnables_ui/src/lib.rs index 90a642bb58..532454f33c 100644 --- a/crates/runnables_ui/src/lib.rs +++ b/crates/runnables_ui/src/lib.rs @@ -2,11 +2,13 @@ use std::path::PathBuf; use gpui::{AppContext, ViewContext, WindowContext}; use modal::RunnablesModal; +pub use oneshot_source::OneshotSource; use runnable::Runnable; use util::ResultExt; use workspace::Workspace; mod modal; +mod oneshot_source; pub fn init(cx: &mut AppContext) { cx.observe_new_views( diff --git a/crates/runnables_ui/src/modal.rs b/crates/runnables_ui/src/modal.rs index f15bf86a2f..b6cde7c6b1 100644 --- a/crates/runnables_ui/src/modal.rs +++ b/crates/runnables_ui/src/modal.rs @@ -2,8 +2,8 @@ use std::sync::Arc; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - actions, rems, DismissEvent, EventEmitter, FocusableView, InteractiveElement, Model, - ParentElement, Render, SharedString, Styled, Subscription, Task, View, ViewContext, + actions, rems, AppContext, DismissEvent, EventEmitter, FocusableView, InteractiveElement, + Model, ParentElement, Render, SharedString, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; @@ -13,7 +13,7 @@ use ui::{v_flex, HighlightedLabel, ListItem, ListItemSpacing, Selectable}; use util::ResultExt; use workspace::{ModalView, Workspace}; -use crate::schedule_runnable; +use crate::{schedule_runnable, OneshotSource}; actions!(runnables, [Spawn, Rerun]); @@ -25,6 +25,7 @@ pub(crate) struct RunnablesModalDelegate { selected_index: usize, placeholder_text: Arc, workspace: WeakView, + last_prompt: String, } impl RunnablesModalDelegate { @@ -36,8 +37,21 @@ impl RunnablesModalDelegate { matches: Vec::new(), selected_index: 0, placeholder_text: Arc::from("Select runnable..."), + last_prompt: String::default(), } } + + fn spawn_oneshot(&mut self, cx: &mut AppContext) -> Option> { + let oneshot_source = self + .inventory + .update(cx, |this, _| this.source::())?; + oneshot_source.update(cx, |this, _| { + let Some(this) = this.as_any().downcast_mut::() else { + return None; + }; + Some(this.spawn(self.last_prompt.clone())) + }) + } } pub(crate) struct RunnablesModal { @@ -149,6 +163,7 @@ impl PickerDelegate for RunnablesModalDelegate { .update(&mut cx, |picker, _| { let delegate = &mut picker.delegate; delegate.matches = matches; + delegate.last_prompt = query; if delegate.matches.is_empty() { delegate.selected_index = 0; @@ -161,14 +176,21 @@ impl PickerDelegate for RunnablesModalDelegate { }) } - fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext>) { + fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>) { let current_match_index = self.selected_index(); - let Some(current_match) = self.matches.get(current_match_index) else { + let Some(runnable) = secondary + .then(|| self.spawn_oneshot(cx)) + .flatten() + .or_else(|| { + self.matches.get(current_match_index).map(|current_match| { + let ix = current_match.candidate_id; + self.candidates[ix].clone() + }) + }) + else { return; }; - let ix = current_match.candidate_id; - let runnable = &self.candidates[ix]; self.workspace .update(cx, |workspace, cx| { schedule_runnable(workspace, runnable.as_ref(), cx); diff --git a/crates/runnables_ui/src/oneshot_source.rs b/crates/runnables_ui/src/oneshot_source.rs new file mode 100644 index 0000000000..60c9178304 --- /dev/null +++ b/crates/runnables_ui/src/oneshot_source.rs @@ -0,0 +1,79 @@ +use std::sync::Arc; + +use gpui::{AppContext, Model}; +use runnable::{Runnable, RunnableId, Source}; +use ui::Context; + +pub struct OneshotSource { + runnables: Vec>, +} + +#[derive(Clone)] +struct OneshotRunnable { + id: RunnableId, +} + +impl OneshotRunnable { + fn new(prompt: String) -> Self { + Self { + id: RunnableId(prompt), + } + } +} + +impl Runnable for OneshotRunnable { + fn id(&self) -> &runnable::RunnableId { + &self.id + } + + fn name(&self) -> &str { + &self.id.0 + } + + fn cwd(&self) -> Option<&std::path::Path> { + None + } + + fn exec(&self, cwd: Option) -> Option { + if self.id().0.is_empty() { + return None; + } + Some(runnable::SpawnInTerminal { + id: self.id().clone(), + label: self.name().to_owned(), + command: self.id().0.clone(), + args: vec![], + cwd, + env: Default::default(), + use_new_terminal: Default::default(), + allow_concurrent_runs: Default::default(), + separate_shell: true, + }) + } +} + +impl OneshotSource { + pub fn new(cx: &mut AppContext) -> Model> { + cx.new_model(|_| Box::new(Self { runnables: vec![] }) as Box) + } + + pub fn spawn(&mut self, prompt: String) -> Arc { + let ret = Arc::new(OneshotRunnable::new(prompt)); + self.runnables.push(ret.clone()); + ret + } +} + +impl Source for OneshotSource { + fn as_any(&mut self) -> &mut dyn std::any::Any { + self + } + + fn runnables_for_path( + &mut self, + _path: Option<&std::path::Path>, + _cx: &mut gpui::ModelContext>, + ) -> Vec> { + self.runnables.clone() + } +} diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 832132edcc..428468cdeb 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -16,7 +16,7 @@ use search::{buffer_search::DivRegistrar, BufferSearchBar}; use serde::{Deserialize, Serialize}; use settings::Settings; use terminal::{ - terminal_settings::{TerminalDockPosition, TerminalSettings}, + terminal_settings::{Shell, TerminalDockPosition, TerminalSettings}, SpawnRunnable, }; use ui::{h_flex, ButtonCommon, Clickable, IconButton, IconSize, Selectable, Tooltip}; @@ -300,13 +300,30 @@ impl TerminalPanel { spawn_in_terminal: &runnable::SpawnInTerminal, cx: &mut ViewContext, ) { - let spawn_runnable = SpawnRunnable { + let mut spawn_runnable = SpawnRunnable { id: spawn_in_terminal.id.clone(), label: spawn_in_terminal.label.clone(), command: spawn_in_terminal.command.clone(), args: spawn_in_terminal.args.clone(), env: spawn_in_terminal.env.clone(), }; + if spawn_in_terminal.separate_shell { + let Some((shell, mut user_args)) = (match TerminalSettings::get_global(cx).shell.clone() + { + Shell::System => std::env::var("SHELL").ok().map(|shell| (shell, vec![])), + Shell::Program(shell) => Some((shell, vec![])), + Shell::WithArguments { program, args } => Some((program, args)), + }) else { + return; + }; + + let command = std::mem::take(&mut spawn_runnable.command); + let args = std::mem::take(&mut spawn_runnable.args); + spawn_runnable.command = shell; + user_args.extend(["-c".to_owned(), command]); + user_args.extend(args); + spawn_runnable.args = user_args; + } let working_directory = spawn_in_terminal.cwd.clone(); let allow_concurrent_runs = spawn_in_terminal.allow_concurrent_runs; let use_new_terminal = spawn_in_terminal.use_new_terminal; diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 4ccff7ab39..a1103d83ad 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -23,6 +23,7 @@ use quick_action_bar::QuickActionBar; use release_channel::{AppCommitSha, ReleaseChannel}; use rope::Rope; use runnable::static_source::StaticSource; +use runnables_ui::OneshotSource; use search::project_search::ProjectSearchBar; use settings::{ initial_local_settings_content, watch_config_file, KeymapFile, Settings, SettingsStore, @@ -163,11 +164,14 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { app_state.fs.clone(), paths::RUNNABLES.clone(), ); - let source = StaticSource::new(runnables_file_rx, cx); + let static_source = StaticSource::new(runnables_file_rx, cx); + let oneshot_source = OneshotSource::new(cx); + project.update(cx, |project, cx| { - project - .runnable_inventory() - .update(cx, |inventory, cx| inventory.add_source(source, cx)) + project.runnable_inventory().update(cx, |inventory, cx| { + inventory.add_source(oneshot_source, cx); + inventory.add_source(static_source, cx); + }) }); } cx.spawn(|workspace_handle, mut cx| async move {