Runnables: Add oneshot runnables (#8061)

/cc @SomeoneToIgnore 
Fixes #7460 and partially addresses #7108 
Release Notes:

- N/A
This commit is contained in:
Piotr Osiewicz 2024-02-20 23:13:09 +01:00 committed by GitHub
parent 8a73bc4c7d
commit 2ec910f772
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 163 additions and 17 deletions

5
Cargo.lock generated
View file

@ -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"

View file

@ -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(

View file

@ -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

View file

@ -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,
})
}

View file

@ -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

View file

@ -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(

View file

@ -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);

View 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()
}
}

View file

@ -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;

View file

@ -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 {