Runnables: Add oneshot runnables (#8061)
/cc @SomeoneToIgnore Fixes #7460 and partially addresses #7108 Release Notes: - N/A
This commit is contained in:
parent
8a73bc4c7d
commit
2ec910f772
10 changed files with 163 additions and 17 deletions
5
Cargo.lock
generated
5
Cargo.lock
generated
|
@ -7786,6 +7786,7 @@ dependencies = [
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui",
|
"gpui",
|
||||||
"log",
|
"log",
|
||||||
|
"menu",
|
||||||
"picker",
|
"picker",
|
||||||
"project",
|
"project",
|
||||||
"runnable",
|
"runnable",
|
||||||
|
@ -8561,9 +8562,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.2.0"
|
version = "1.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380"
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook"
|
name = "signal-hook"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//! Project-wide storage of the runnables available, capable of updating itself from the sources set.
|
//! 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 gpui::{AppContext, Context, Model, ModelContext, Subscription};
|
||||||
use runnable::{Runnable, RunnableId, Source};
|
use runnable::{Runnable, RunnableId, Source};
|
||||||
|
@ -14,6 +14,7 @@ pub struct Inventory {
|
||||||
struct SourceInInventory {
|
struct SourceInInventory {
|
||||||
source: Model<Box<dyn Source>>,
|
source: Model<Box<dyn Source>>,
|
||||||
_subscription: Subscription,
|
_subscription: Subscription,
|
||||||
|
type_id: TypeId,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Inventory {
|
impl Inventory {
|
||||||
|
@ -29,13 +30,29 @@ impl Inventory {
|
||||||
let _subscription = cx.observe(&source, |_, _, cx| {
|
let _subscription = cx.observe(&source, |_, _, cx| {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
let type_id = source.read(cx).type_id();
|
||||||
let source = SourceInInventory {
|
let source = SourceInInventory {
|
||||||
source,
|
source,
|
||||||
_subscription,
|
_subscription,
|
||||||
|
type_id,
|
||||||
};
|
};
|
||||||
self.sources.push(source);
|
self.sources.push(source);
|
||||||
cx.notify();
|
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).
|
/// 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(
|
pub fn list_runnables(
|
||||||
|
|
|
@ -15,7 +15,7 @@ use std::sync::Arc;
|
||||||
/// Runnable identifier, unique within the application.
|
/// Runnable identifier, unique within the application.
|
||||||
/// Based on it, runnable reruns and terminal tabs are managed.
|
/// Based on it, runnable reruns and terminal tabs are managed.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[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.
|
/// Contains all information needed by Zed to spawn a new terminal tab for the given runnable.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -36,6 +36,8 @@ pub struct SpawnInTerminal {
|
||||||
pub use_new_terminal: bool,
|
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.
|
/// 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,
|
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
|
/// Represents a short lived recipe of a runnable, whose main purpose
|
||||||
|
|
|
@ -31,6 +31,7 @@ impl Runnable for StaticRunnable {
|
||||||
command: self.definition.command.clone(),
|
command: self.definition.command.clone(),
|
||||||
args: self.definition.args.clone(),
|
args: self.definition.args.clone(),
|
||||||
env: self.definition.env.clone(),
|
env: self.definition.env.clone(),
|
||||||
|
separate_shell: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ futures.workspace = true
|
||||||
fuzzy.workspace = true
|
fuzzy.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
menu.workspace = true
|
||||||
picker.workspace = true
|
picker.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
runnable.workspace = true
|
runnable.workspace = true
|
||||||
|
|
|
@ -2,11 +2,13 @@ use std::path::PathBuf;
|
||||||
|
|
||||||
use gpui::{AppContext, ViewContext, WindowContext};
|
use gpui::{AppContext, ViewContext, WindowContext};
|
||||||
use modal::RunnablesModal;
|
use modal::RunnablesModal;
|
||||||
|
pub use oneshot_source::OneshotSource;
|
||||||
use runnable::Runnable;
|
use runnable::Runnable;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
mod modal;
|
mod modal;
|
||||||
|
mod oneshot_source;
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
cx.observe_new_views(
|
cx.observe_new_views(
|
||||||
|
|
|
@ -2,8 +2,8 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, rems, DismissEvent, EventEmitter, FocusableView, InteractiveElement, Model,
|
actions, rems, AppContext, DismissEvent, EventEmitter, FocusableView, InteractiveElement,
|
||||||
ParentElement, Render, SharedString, Styled, Subscription, Task, View, ViewContext,
|
Model, ParentElement, Render, SharedString, Styled, Subscription, Task, View, ViewContext,
|
||||||
VisualContext, WeakView,
|
VisualContext, WeakView,
|
||||||
};
|
};
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
|
@ -13,7 +13,7 @@ use ui::{v_flex, HighlightedLabel, ListItem, ListItemSpacing, Selectable};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::{ModalView, Workspace};
|
use workspace::{ModalView, Workspace};
|
||||||
|
|
||||||
use crate::schedule_runnable;
|
use crate::{schedule_runnable, OneshotSource};
|
||||||
|
|
||||||
actions!(runnables, [Spawn, Rerun]);
|
actions!(runnables, [Spawn, Rerun]);
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ pub(crate) struct RunnablesModalDelegate {
|
||||||
selected_index: usize,
|
selected_index: usize,
|
||||||
placeholder_text: Arc<str>,
|
placeholder_text: Arc<str>,
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
|
last_prompt: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RunnablesModalDelegate {
|
impl RunnablesModalDelegate {
|
||||||
|
@ -36,8 +37,21 @@ impl RunnablesModalDelegate {
|
||||||
matches: Vec::new(),
|
matches: Vec::new(),
|
||||||
selected_index: 0,
|
selected_index: 0,
|
||||||
placeholder_text: Arc::from("Select runnable..."),
|
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 {
|
pub(crate) struct RunnablesModal {
|
||||||
|
@ -149,6 +163,7 @@ impl PickerDelegate for RunnablesModalDelegate {
|
||||||
.update(&mut cx, |picker, _| {
|
.update(&mut cx, |picker, _| {
|
||||||
let delegate = &mut picker.delegate;
|
let delegate = &mut picker.delegate;
|
||||||
delegate.matches = matches;
|
delegate.matches = matches;
|
||||||
|
delegate.last_prompt = query;
|
||||||
|
|
||||||
if delegate.matches.is_empty() {
|
if delegate.matches.is_empty() {
|
||||||
delegate.selected_index = 0;
|
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 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;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let ix = current_match.candidate_id;
|
|
||||||
let runnable = &self.candidates[ix];
|
|
||||||
self.workspace
|
self.workspace
|
||||||
.update(cx, |workspace, cx| {
|
.update(cx, |workspace, cx| {
|
||||||
schedule_runnable(workspace, runnable.as_ref(), cx);
|
schedule_runnable(workspace, runnable.as_ref(), cx);
|
||||||
|
|
79
crates/runnables_ui/src/oneshot_source.rs
Normal file
79
crates/runnables_ui/src/oneshot_source.rs
Normal file
|
@ -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 serde::{Deserialize, Serialize};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use terminal::{
|
use terminal::{
|
||||||
terminal_settings::{TerminalDockPosition, TerminalSettings},
|
terminal_settings::{Shell, TerminalDockPosition, TerminalSettings},
|
||||||
SpawnRunnable,
|
SpawnRunnable,
|
||||||
};
|
};
|
||||||
use ui::{h_flex, ButtonCommon, Clickable, IconButton, IconSize, Selectable, Tooltip};
|
use ui::{h_flex, ButtonCommon, Clickable, IconButton, IconSize, Selectable, Tooltip};
|
||||||
|
@ -300,13 +300,30 @@ impl TerminalPanel {
|
||||||
spawn_in_terminal: &runnable::SpawnInTerminal,
|
spawn_in_terminal: &runnable::SpawnInTerminal,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
let spawn_runnable = SpawnRunnable {
|
let mut spawn_runnable = SpawnRunnable {
|
||||||
id: spawn_in_terminal.id.clone(),
|
id: spawn_in_terminal.id.clone(),
|
||||||
label: spawn_in_terminal.label.clone(),
|
label: spawn_in_terminal.label.clone(),
|
||||||
command: spawn_in_terminal.command.clone(),
|
command: spawn_in_terminal.command.clone(),
|
||||||
args: spawn_in_terminal.args.clone(),
|
args: spawn_in_terminal.args.clone(),
|
||||||
env: spawn_in_terminal.env.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 working_directory = spawn_in_terminal.cwd.clone();
|
||||||
let allow_concurrent_runs = spawn_in_terminal.allow_concurrent_runs;
|
let allow_concurrent_runs = spawn_in_terminal.allow_concurrent_runs;
|
||||||
let use_new_terminal = spawn_in_terminal.use_new_terminal;
|
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 release_channel::{AppCommitSha, ReleaseChannel};
|
||||||
use rope::Rope;
|
use rope::Rope;
|
||||||
use runnable::static_source::StaticSource;
|
use runnable::static_source::StaticSource;
|
||||||
|
use runnables_ui::OneshotSource;
|
||||||
use search::project_search::ProjectSearchBar;
|
use search::project_search::ProjectSearchBar;
|
||||||
use settings::{
|
use settings::{
|
||||||
initial_local_settings_content, watch_config_file, KeymapFile, Settings, SettingsStore,
|
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(),
|
app_state.fs.clone(),
|
||||||
paths::RUNNABLES.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.update(cx, |project, cx| {
|
||||||
project
|
project.runnable_inventory().update(cx, |inventory, cx| {
|
||||||
.runnable_inventory()
|
inventory.add_source(oneshot_source, cx);
|
||||||
.update(cx, |inventory, cx| inventory.add_source(source, cx))
|
inventory.add_source(static_source, cx);
|
||||||
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
cx.spawn(|workspace_handle, mut cx| async move {
|
cx.spawn(|workspace_handle, mut cx| async move {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue