Add static Runnables (#8009)
Part of #7108 This PR includes just the static runnables part. We went with **not** having a dedicated panel for runnables. This is just a 1st PR out of N, as we want to start exploring the dynamic runnables front. Still, all that work is going to happen once this gets merged. Release Notes: - Added initial, static Runnables support to Zed. Such runnables are defined in `runnables.json` file (accessible via `zed: open runnables` action) and they can be spawned with `runnables: spawn` action. --------- Co-authored-by: Kirill Bulatov <kirill@zed.dev> Co-authored-by: Pitor <pitor@zed.dev> Co-authored-by: Beniamin <beniamin@zagan.be>
This commit is contained in:
co-authored by
Kirill Bulatov
Pitor
Beniamin
parent
ca251babcd
commit
f17d0b5729
@@ -4,6 +4,7 @@ pub mod lsp_command;
|
||||
pub mod lsp_ext_command;
|
||||
mod prettier_support;
|
||||
pub mod project_settings;
|
||||
mod runnable_inventory;
|
||||
pub mod search;
|
||||
pub mod terminals;
|
||||
pub mod worktree;
|
||||
@@ -57,7 +58,8 @@ use postage::watch;
|
||||
use prettier_support::{DefaultPrettier, PrettierInstance};
|
||||
use project_settings::{LspSettings, ProjectSettings};
|
||||
use rand::prelude::*;
|
||||
use rpc::{ErrorCode, ErrorExt};
|
||||
|
||||
use rpc::{ErrorCode, ErrorExt as _};
|
||||
use search::SearchQuery;
|
||||
use serde::Serialize;
|
||||
use settings::{Settings, SettingsStore};
|
||||
@@ -91,6 +93,7 @@ use util::{
|
||||
pub use fs::*;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX;
|
||||
pub use runnable_inventory::Inventory;
|
||||
pub use worktree::*;
|
||||
|
||||
const MAX_SERVER_REINSTALL_ATTEMPT_COUNT: u64 = 4;
|
||||
@@ -153,6 +156,7 @@ pub struct Project {
|
||||
default_prettier: DefaultPrettier,
|
||||
prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>,
|
||||
prettier_instances: HashMap<PathBuf, PrettierInstance>,
|
||||
runnables: Model<Inventory>,
|
||||
}
|
||||
|
||||
pub enum LanguageServerToQuery {
|
||||
@@ -615,6 +619,8 @@ impl Project {
|
||||
.detach();
|
||||
let copilot_lsp_subscription =
|
||||
Copilot::global(cx).map(|copilot| subscribe_for_copilot_events(&copilot, cx));
|
||||
let runnables = Inventory::new(cx);
|
||||
|
||||
Self {
|
||||
worktrees: Vec::new(),
|
||||
buffer_ordered_messages_tx: tx,
|
||||
@@ -665,6 +671,7 @@ impl Project {
|
||||
default_prettier: DefaultPrettier::default(),
|
||||
prettiers_per_worktree: HashMap::default(),
|
||||
prettier_instances: HashMap::default(),
|
||||
runnables,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -688,7 +695,10 @@ impl Project {
|
||||
.await?;
|
||||
let this = cx.new_model(|cx| {
|
||||
let replica_id = response.payload.replica_id as ReplicaId;
|
||||
|
||||
let runnables = Inventory::new(cx);
|
||||
// BIG CAUTION NOTE: The order in which we initialize fields here matters and it should match what's done in Self::local.
|
||||
// Otherwise, you might run into issues where worktree id on remote is different than what's on local host.
|
||||
// That's because Worktree's identifier is entity id, which should probably be changed.
|
||||
let mut worktrees = Vec::new();
|
||||
for worktree in response.payload.worktrees {
|
||||
let worktree =
|
||||
@@ -770,6 +780,7 @@ impl Project {
|
||||
default_prettier: DefaultPrettier::default(),
|
||||
prettiers_per_worktree: HashMap::default(),
|
||||
prettier_instances: HashMap::default(),
|
||||
runnables,
|
||||
};
|
||||
this.set_role(role, cx);
|
||||
for worktree in worktrees {
|
||||
@@ -1052,6 +1063,10 @@ impl Project {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn runnable_inventory(&self) -> &Model<Inventory> {
|
||||
&self.runnables
|
||||
}
|
||||
|
||||
pub fn collaborators(&self) -> &HashMap<proto::PeerId, Collaborator> {
|
||||
&self.collaborators
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
//! Project-wide storage of the runnables available, capable of updating itself from the sources set.
|
||||
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
use gpui::{AppContext, Context, Model, ModelContext, Subscription};
|
||||
use runnable::{Runnable, RunnableId, Source};
|
||||
|
||||
/// Inventory tracks available runnables for a given project.
|
||||
pub struct Inventory {
|
||||
sources: Vec<SourceInInventory>,
|
||||
pub last_scheduled_runnable: Option<RunnableId>,
|
||||
}
|
||||
|
||||
struct SourceInInventory {
|
||||
source: Model<Box<dyn Source>>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl Inventory {
|
||||
pub(crate) fn new(cx: &mut AppContext) -> Model<Self> {
|
||||
cx.new_model(|_| Self {
|
||||
sources: Vec::new(),
|
||||
last_scheduled_runnable: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Registers a new runnables source, that would be fetched for available runnables.
|
||||
pub fn add_source(&mut self, source: Model<Box<dyn Source>>, cx: &mut ModelContext<Self>) {
|
||||
let _subscription = cx.observe(&source, |_, _, cx| {
|
||||
cx.notify();
|
||||
});
|
||||
let source = SourceInInventory {
|
||||
source,
|
||||
_subscription,
|
||||
};
|
||||
self.sources.push(source);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// 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(
|
||||
&self,
|
||||
path: Option<&Path>,
|
||||
cx: &mut AppContext,
|
||||
) -> Vec<Arc<dyn Runnable>> {
|
||||
let mut runnables = Vec::new();
|
||||
for source in &self.sources {
|
||||
runnables.extend(
|
||||
source
|
||||
.source
|
||||
.update(cx, |source, cx| source.runnables_for_path(path, cx)),
|
||||
);
|
||||
}
|
||||
runnables
|
||||
}
|
||||
|
||||
/// Returns the last scheduled runnable, if any of the sources contains one with the matching id.
|
||||
pub fn last_scheduled_runnable(&self, cx: &mut AppContext) -> Option<Arc<dyn Runnable>> {
|
||||
self.last_scheduled_runnable.as_ref().and_then(|id| {
|
||||
// TODO straighten the `Path` story to understand what has to be passed here: or it will break in the future.
|
||||
self.list_runnables(None, cx)
|
||||
.into_iter()
|
||||
.find(|runnable| runnable.id() == id)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
use crate::Project;
|
||||
use gpui::{AnyWindowHandle, Context, Entity, Model, ModelContext, WeakModel};
|
||||
use settings::Settings;
|
||||
use smol::channel::bounded;
|
||||
use std::path::{Path, PathBuf};
|
||||
use terminal::{
|
||||
terminal_settings::{self, TerminalSettings, VenvSettingsContent},
|
||||
Terminal, TerminalBuilder,
|
||||
terminal_settings::{self, Shell, TerminalSettings, VenvSettingsContent},
|
||||
RunableState, SpawnRunnable, Terminal, TerminalBuilder,
|
||||
};
|
||||
|
||||
// #[cfg(target_os = "macos")]
|
||||
@@ -18,63 +19,83 @@ impl Project {
|
||||
pub fn create_terminal(
|
||||
&mut self,
|
||||
working_directory: Option<PathBuf>,
|
||||
spawn_runnable: Option<SpawnRunnable>,
|
||||
window: AnyWindowHandle,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> anyhow::Result<Model<Terminal>> {
|
||||
if self.is_remote() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"creating terminals as a guest is not supported yet"
|
||||
));
|
||||
} else {
|
||||
let settings = TerminalSettings::get_global(cx);
|
||||
let python_settings = settings.detect_venv.clone();
|
||||
let shell = settings.shell.clone();
|
||||
anyhow::ensure!(
|
||||
!self.is_remote(),
|
||||
"creating terminals as a guest is not supported yet"
|
||||
);
|
||||
|
||||
let terminal = TerminalBuilder::new(
|
||||
working_directory.clone(),
|
||||
shell.clone(),
|
||||
settings.env.clone(),
|
||||
Some(settings.blinking.clone()),
|
||||
settings.alternate_scroll,
|
||||
window,
|
||||
let settings = TerminalSettings::get_global(cx);
|
||||
let python_settings = settings.detect_venv.clone();
|
||||
let (completion_tx, completion_rx) = bounded(1);
|
||||
let mut env = settings.env.clone();
|
||||
let (spawn_runnable, shell) = if let Some(spawn_runnable) = spawn_runnable {
|
||||
env.extend(spawn_runnable.env);
|
||||
(
|
||||
Some(RunableState {
|
||||
id: spawn_runnable.id,
|
||||
label: spawn_runnable.label,
|
||||
completed: false,
|
||||
completion_rx,
|
||||
}),
|
||||
Shell::WithArguments {
|
||||
program: spawn_runnable.command,
|
||||
args: spawn_runnable.args,
|
||||
},
|
||||
)
|
||||
.map(|builder| {
|
||||
let terminal_handle = cx.new_model(|cx| builder.subscribe(cx));
|
||||
} else {
|
||||
(None, settings.shell.clone())
|
||||
};
|
||||
|
||||
self.terminals
|
||||
.local_handles
|
||||
.push(terminal_handle.downgrade());
|
||||
let terminal = TerminalBuilder::new(
|
||||
working_directory.clone(),
|
||||
spawn_runnable,
|
||||
shell,
|
||||
env,
|
||||
Some(settings.blinking.clone()),
|
||||
settings.alternate_scroll,
|
||||
window,
|
||||
completion_tx,
|
||||
)
|
||||
.map(|builder| {
|
||||
let terminal_handle = cx.new_model(|cx| builder.subscribe(cx));
|
||||
|
||||
let id = terminal_handle.entity_id();
|
||||
cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
|
||||
let handles = &mut project.terminals.local_handles;
|
||||
self.terminals
|
||||
.local_handles
|
||||
.push(terminal_handle.downgrade());
|
||||
|
||||
if let Some(index) = handles
|
||||
.iter()
|
||||
.position(|terminal| terminal.entity_id() == id)
|
||||
{
|
||||
handles.remove(index);
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
let id = terminal_handle.entity_id();
|
||||
cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
|
||||
let handles = &mut project.terminals.local_handles;
|
||||
|
||||
if let Some(python_settings) = &python_settings.as_option() {
|
||||
let activate_command = Project::get_activate_command(python_settings);
|
||||
let activate_script_path =
|
||||
self.find_activate_script_path(python_settings, working_directory);
|
||||
self.activate_python_virtual_environment(
|
||||
activate_command,
|
||||
activate_script_path,
|
||||
&terminal_handle,
|
||||
cx,
|
||||
);
|
||||
if let Some(index) = handles
|
||||
.iter()
|
||||
.position(|terminal| terminal.entity_id() == id)
|
||||
{
|
||||
handles.remove(index);
|
||||
cx.notify();
|
||||
}
|
||||
terminal_handle
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
|
||||
terminal
|
||||
}
|
||||
if let Some(python_settings) = &python_settings.as_option() {
|
||||
let activate_command = Project::get_activate_command(python_settings);
|
||||
let activate_script_path =
|
||||
self.find_activate_script_path(python_settings, working_directory);
|
||||
self.activate_python_virtual_environment(
|
||||
activate_command,
|
||||
activate_script_path,
|
||||
&terminal_handle,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
terminal_handle
|
||||
});
|
||||
|
||||
terminal
|
||||
}
|
||||
|
||||
pub fn find_activate_script_path(
|
||||
|
||||
Reference in New Issue
Block a user