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:
Piotr Osiewicz
2024-02-19 18:41:43 +02:00
committed by GitHub
co-authored by Kirill Bulatov Pitor Beniamin
parent ca251babcd
commit f17d0b5729
30 changed files with 1394 additions and 275 deletions
+17 -2
View File
@@ -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
}
+66
View File
@@ -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)
})
}
}
+69 -48
View File
@@ -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(