Detailed changes
@@ -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"
@@ -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<Box<dyn Source>>,
_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<T: Source>(&self) -> Option<Model<Box<dyn Source>>> {
+ let target_type_id = std::any::TypeId::of::<T>();
+ 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(
@@ -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
@@ -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,
})
}
@@ -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
@@ -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(
@@ -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<str>,
workspace: WeakView<Workspace>,
+ 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<Arc<dyn Runnable>> {
+ let oneshot_source = self
+ .inventory
+ .update(cx, |this, _| this.source::<OneshotSource>())?;
+ oneshot_source.update(cx, |this, _| {
+ let Some(this) = this.as_any().downcast_mut::<OneshotSource>() 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<picker::Picker<Self>>) {
+ fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<picker::Picker<Self>>) {
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);
@@ -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<Arc<dyn runnable::Runnable>>,
+}
+
+#[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<std::path::PathBuf>) -> Option<runnable::SpawnInTerminal> {
+ 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<Box<dyn Source>> {
+ cx.new_model(|_| Box::new(Self { runnables: vec![] }) as Box<dyn Source>)
+ }
+
+ pub fn spawn(&mut self, prompt: String) -> Arc<dyn runnable::Runnable> {
+ 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<Box<dyn Source>>,
+ ) -> Vec<Arc<dyn runnable::Runnable>> {
+ self.runnables.clone()
+ }
+}
@@ -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<Self>,
) {
- 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;
@@ -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<AppState>, 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 {