Runtimes UI Starter (#13625)

Initial runtimes UI panel. The main draw here is that all message
subscription occurs with two background tasks that run for the life of
the kernel. Follow on to #12062

* [x] Disable previous cmd-enter behavior only if runtimes are enabled
in settings
* [x] Only show the runtimes panel if it is enabled via settings
* [x] Create clean UI for the current sessions

### Running Kernels UI

<img width="205" alt="image"
src="https://github.com/zed-industries/zed/assets/836375/814ae79b-0807-4e23-bc95-77ce64f9d732">

* [x] List running kernels
* [x] Implement shutdown
* [x] Delete connection file on `drop` of `RunningKernel`
* [x] Implement interrupt

#### Project-specific Kernel Settings

- [x] Modify JupyterSettings to include a `kernel_selections` field
(`HashMap<String, String>`).
- [x] Implement saving and loading of kernel selections to/from
`.zed/settings.json` (by default, rather than global settings?)

#### Kernel Selection Persistence

- [x] Save the selected kernel for each language when the user makes a
choice.
- [x] Load these selections when the RuntimePanel is initialized.

#### Use Selected Kernels

- [x] Modify kernel launch to use the selected kernel for the detected
language.
- [x] Fallback to default behavior if no selection is made.

### Empty states

- [x] Create helpful UI for when the user has 0 kernels they can launch
and/or 0 kernels running

<img width="694" alt="image"
src="https://github.com/zed-industries/zed/assets/836375/d6a75939-e4e4-40fb-80fe-014da041cc3c">

## Future work

### Kernel Discovery

- Improve the kernel discovery process to handle various installation
methods (system, virtualenv, poetry, etc.).
- Create a way to refresh the available kernels on demand

### Documentation:

- Update documentation to explain how users can configure kernels for
their projects.
- Provide examples of .zed/settings.json configurations for kernel
selection.

### Kernel Selection UI

- Implement a new section in the RuntimePanel to display available
kernels.
- Group on the language name from the kernel specification 
- Create a dropdown for each language group to select the default
kernel.


Release Notes:

- N/A

---------

Co-authored-by: Kirill <kirill@zed.dev>
This commit is contained in:
Kyle Kelley 2024-07-05 08:15:50 -07:00 committed by GitHub
parent 821aa0811d
commit c77ea47f43
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1438 additions and 965 deletions

View File

@ -12719,7 +12719,7 @@ pub(crate) fn split_words(text: &str) -> impl std::iter::Iterator<Item = &str> +
})
}
trait RangeToAnchorExt {
pub trait RangeToAnchorExt {
fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range<Anchor>;
}

View File

@ -0,0 +1,149 @@
use std::collections::HashMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use ui::Pixels;
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum JupyterDockPosition {
Left,
#[default]
Right,
Bottom,
}
#[derive(Debug, Default)]
pub struct JupyterSettings {
pub enabled: bool,
pub dock: JupyterDockPosition,
pub default_width: Pixels,
pub kernel_selections: HashMap<String, String>,
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
pub struct JupyterSettingsContent {
/// Whether the Jupyter feature is enabled.
///
/// Default: `false`
enabled: Option<bool>,
/// Where to dock the Jupyter panel.
///
/// Default: `right`
dock: Option<JupyterDockPosition>,
/// Default width in pixels when the jupyter panel is docked to the left or right.
///
/// Default: 640
pub default_width: Option<f32>,
/// Default kernels to select for each language.
///
/// Default: `{}`
pub kernel_selections: Option<HashMap<String, String>>,
}
impl JupyterSettingsContent {
pub fn set_dock(&mut self, dock: JupyterDockPosition) {
self.dock = Some(dock);
}
}
impl Default for JupyterSettingsContent {
fn default() -> Self {
JupyterSettingsContent {
enabled: Some(false),
dock: Some(JupyterDockPosition::Right),
default_width: Some(640.0),
kernel_selections: Some(HashMap::new()),
}
}
}
impl Settings for JupyterSettings {
const KEY: Option<&'static str> = Some("jupyter");
type FileContent = JupyterSettingsContent;
fn load(
sources: SettingsSources<Self::FileContent>,
_cx: &mut gpui::AppContext,
) -> anyhow::Result<Self>
where
Self: Sized,
{
let mut settings = JupyterSettings::default();
for value in sources.defaults_and_customizations() {
if let Some(enabled) = value.enabled {
settings.enabled = enabled;
}
if let Some(dock) = value.dock {
settings.dock = dock;
}
if let Some(default_width) = value.default_width {
settings.default_width = Pixels::from(default_width);
}
if let Some(source) = &value.kernel_selections {
for (k, v) in source {
settings.kernel_selections.insert(k.clone(), v.clone());
}
}
}
Ok(settings)
}
}
#[cfg(test)]
mod tests {
use gpui::{AppContext, UpdateGlobal};
use settings::SettingsStore;
use super::*;
#[gpui::test]
fn test_deserialize_jupyter_settings(cx: &mut AppContext) {
let store = settings::SettingsStore::test(cx);
cx.set_global(store);
JupyterSettings::register(cx);
assert_eq!(JupyterSettings::get_global(cx).enabled, false);
assert_eq!(
JupyterSettings::get_global(cx).dock,
JupyterDockPosition::Right
);
assert_eq!(
JupyterSettings::get_global(cx).default_width,
Pixels::from(640.0)
);
// Setting a custom setting through user settings
SettingsStore::update_global(cx, |store, cx| {
store
.set_user_settings(
r#"{
"jupyter": {
"enabled": true,
"dock": "left",
"default_width": 800.0
}
}"#,
cx,
)
.unwrap();
});
assert_eq!(JupyterSettings::get_global(cx).enabled, true);
assert_eq!(
JupyterSettings::get_global(cx).dock,
JupyterDockPosition::Left
);
assert_eq!(
JupyterSettings::get_global(cx).default_width,
Pixels::from(800.0)
);
}
}

395
crates/repl/src/kernels.rs Normal file
View File

@ -0,0 +1,395 @@
use anyhow::{Context as _, Result};
use futures::{
channel::mpsc::{self, Receiver},
future::Shared,
stream::{self, SelectAll, StreamExt},
SinkExt as _,
};
use gpui::{AppContext, EntityId, Task};
use project::Fs;
use runtimelib::{
dirs, ConnectionInfo, ExecutionState, JupyterKernelspec, JupyterMessage, JupyterMessageContent,
KernelInfoReply,
};
use smol::{net::TcpListener, process::Command};
use std::{
fmt::Debug,
net::{IpAddr, Ipv4Addr, SocketAddr},
path::PathBuf,
sync::Arc,
};
use ui::{Color, Indicator};
#[derive(Debug, Clone)]
pub struct KernelSpecification {
pub name: String,
pub path: PathBuf,
pub kernelspec: JupyterKernelspec,
}
impl KernelSpecification {
#[must_use]
fn command(&self, connection_path: &PathBuf) -> anyhow::Result<Command> {
let argv = &self.kernelspec.argv;
anyhow::ensure!(!argv.is_empty(), "Empty argv in kernelspec {}", self.name);
anyhow::ensure!(argv.len() >= 2, "Invalid argv in kernelspec {}", self.name);
anyhow::ensure!(
argv.iter().any(|arg| arg == "{connection_file}"),
"Missing 'connection_file' in argv in kernelspec {}",
self.name
);
let mut cmd = Command::new(&argv[0]);
for arg in &argv[1..] {
if arg == "{connection_file}" {
cmd.arg(connection_path);
} else {
cmd.arg(arg);
}
}
if let Some(env) = &self.kernelspec.env {
cmd.envs(env);
}
Ok(cmd)
}
}
// Find a set of open ports. This creates a listener with port set to 0. The listener will be closed at the end when it goes out of scope.
// There's a race condition between closing the ports and usage by a kernel, but it's inherent to the Jupyter protocol.
async fn peek_ports(ip: IpAddr) -> anyhow::Result<[u16; 5]> {
let mut addr_zeroport: SocketAddr = SocketAddr::new(ip, 0);
addr_zeroport.set_port(0);
let mut ports: [u16; 5] = [0; 5];
for i in 0..5 {
let listener = TcpListener::bind(addr_zeroport).await?;
let addr = listener.local_addr()?;
ports[i] = addr.port();
}
Ok(ports)
}
#[derive(Debug)]
pub enum Kernel {
RunningKernel(RunningKernel),
StartingKernel(Shared<Task<()>>),
ErroredLaunch(String),
ShuttingDown,
Shutdown,
}
impl Kernel {
pub fn dot(&mut self) -> Indicator {
match self {
Kernel::RunningKernel(kernel) => match kernel.execution_state {
ExecutionState::Idle => Indicator::dot().color(Color::Success),
ExecutionState::Busy => Indicator::dot().color(Color::Modified),
},
Kernel::StartingKernel(_) => Indicator::dot().color(Color::Modified),
Kernel::ErroredLaunch(_) => Indicator::dot().color(Color::Error),
Kernel::ShuttingDown => Indicator::dot().color(Color::Modified),
Kernel::Shutdown => Indicator::dot().color(Color::Disabled),
}
}
pub fn set_execution_state(&mut self, status: &ExecutionState) {
match self {
Kernel::RunningKernel(running_kernel) => {
running_kernel.execution_state = status.clone();
}
_ => {}
}
}
pub fn set_kernel_info(&mut self, kernel_info: &KernelInfoReply) {
match self {
Kernel::RunningKernel(running_kernel) => {
running_kernel.kernel_info = Some(kernel_info.clone());
}
_ => {}
}
}
}
pub struct RunningKernel {
pub process: smol::process::Child,
_shell_task: Task<anyhow::Result<()>>,
_iopub_task: Task<anyhow::Result<()>>,
_control_task: Task<anyhow::Result<()>>,
_routing_task: Task<anyhow::Result<()>>,
connection_path: PathBuf,
pub request_tx: mpsc::Sender<JupyterMessage>,
pub execution_state: ExecutionState,
pub kernel_info: Option<KernelInfoReply>,
}
type JupyterMessageChannel = stream::SelectAll<Receiver<JupyterMessage>>;
impl Debug for RunningKernel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RunningKernel")
.field("process", &self.process)
.finish()
}
}
impl RunningKernel {
pub fn new(
kernel_specification: KernelSpecification,
entity_id: EntityId,
fs: Arc<dyn Fs>,
cx: &mut AppContext,
) -> Task<anyhow::Result<(Self, JupyterMessageChannel)>> {
cx.spawn(|cx| async move {
let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
let ports = peek_ports(ip).await?;
let connection_info = ConnectionInfo {
transport: "tcp".to_string(),
ip: ip.to_string(),
stdin_port: ports[0],
control_port: ports[1],
hb_port: ports[2],
shell_port: ports[3],
iopub_port: ports[4],
signature_scheme: "hmac-sha256".to_string(),
key: uuid::Uuid::new_v4().to_string(),
kernel_name: Some(format!("zed-{}", kernel_specification.name)),
};
let connection_path = dirs::runtime_dir().join(format!("kernel-zed-{entity_id}.json"));
let content = serde_json::to_string(&connection_info)?;
// write out file to disk for kernel
fs.atomic_write(connection_path.clone(), content).await?;
let mut cmd = kernel_specification.command(&connection_path)?;
let process = cmd
// .stdout(Stdio::null())
// .stderr(Stdio::null())
.kill_on_drop(true)
.spawn()
.context("failed to start the kernel process")?;
let mut iopub_socket = connection_info.create_client_iopub_connection("").await?;
let mut shell_socket = connection_info.create_client_shell_connection().await?;
let mut control_socket = connection_info.create_client_control_connection().await?;
let (mut iopub, iosub) = futures::channel::mpsc::channel(100);
let (request_tx, mut request_rx) =
futures::channel::mpsc::channel::<JupyterMessage>(100);
let (mut control_reply_tx, control_reply_rx) = futures::channel::mpsc::channel(100);
let (mut shell_reply_tx, shell_reply_rx) = futures::channel::mpsc::channel(100);
let mut messages_rx = SelectAll::new();
messages_rx.push(iosub);
messages_rx.push(control_reply_rx);
messages_rx.push(shell_reply_rx);
let _iopub_task = cx.background_executor().spawn({
async move {
while let Ok(message) = iopub_socket.read().await {
iopub.send(message).await?;
}
anyhow::Ok(())
}
});
let (mut control_request_tx, mut control_request_rx) =
futures::channel::mpsc::channel(100);
let (mut shell_request_tx, mut shell_request_rx) = futures::channel::mpsc::channel(100);
let _routing_task = cx.background_executor().spawn({
async move {
while let Some(message) = request_rx.next().await {
match message.content {
JupyterMessageContent::DebugRequest(_)
| JupyterMessageContent::InterruptRequest(_)
| JupyterMessageContent::ShutdownRequest(_) => {
control_request_tx.send(message).await?;
}
_ => {
shell_request_tx.send(message).await?;
}
}
}
anyhow::Ok(())
}
});
let _shell_task = cx.background_executor().spawn({
async move {
while let Some(message) = shell_request_rx.next().await {
shell_socket.send(message).await.ok();
let reply = shell_socket.read().await?;
shell_reply_tx.send(reply).await?;
}
anyhow::Ok(())
}
});
let _control_task = cx.background_executor().spawn({
async move {
while let Some(message) = control_request_rx.next().await {
control_socket.send(message).await.ok();
let reply = control_socket.read().await?;
control_reply_tx.send(reply).await?;
}
anyhow::Ok(())
}
});
anyhow::Ok((
Self {
process,
request_tx,
_shell_task,
_iopub_task,
_control_task,
_routing_task,
connection_path,
execution_state: ExecutionState::Busy,
kernel_info: None,
},
messages_rx,
))
})
}
}
impl Drop for RunningKernel {
fn drop(&mut self) {
std::fs::remove_file(&self.connection_path).ok();
self.request_tx.close_channel();
}
}
async fn read_kernelspec_at(
// Path should be a directory to a jupyter kernelspec, as in
// /usr/local/share/jupyter/kernels/python3
kernel_dir: PathBuf,
fs: &dyn Fs,
) -> anyhow::Result<KernelSpecification> {
let path = kernel_dir;
let kernel_name = if let Some(kernel_name) = path.file_name() {
kernel_name.to_string_lossy().to_string()
} else {
anyhow::bail!("Invalid kernelspec directory: {path:?}");
};
if !fs.is_dir(path.as_path()).await {
anyhow::bail!("Not a directory: {path:?}");
}
let expected_kernel_json = path.join("kernel.json");
let spec = fs.load(expected_kernel_json.as_path()).await?;
let spec = serde_json::from_str::<JupyterKernelspec>(&spec)?;
Ok(KernelSpecification {
name: kernel_name,
path,
kernelspec: spec,
})
}
/// Read a directory of kernelspec directories
async fn read_kernels_dir(path: PathBuf, fs: &dyn Fs) -> anyhow::Result<Vec<KernelSpecification>> {
let mut kernelspec_dirs = fs.read_dir(&path).await?;
let mut valid_kernelspecs = Vec::new();
while let Some(path) = kernelspec_dirs.next().await {
match path {
Ok(path) => {
if fs.is_dir(path.as_path()).await {
if let Ok(kernelspec) = read_kernelspec_at(path, fs).await {
valid_kernelspecs.push(kernelspec);
}
}
}
Err(err) => log::warn!("Error reading kernelspec directory: {err:?}"),
}
}
Ok(valid_kernelspecs)
}
pub async fn kernel_specifications(fs: Arc<dyn Fs>) -> anyhow::Result<Vec<KernelSpecification>> {
let data_dirs = dirs::data_dirs();
let kernel_dirs = data_dirs
.iter()
.map(|dir| dir.join("kernels"))
.map(|path| read_kernels_dir(path, fs.as_ref()))
.collect::<Vec<_>>();
let kernel_dirs = futures::future::join_all(kernel_dirs).await;
let kernel_dirs = kernel_dirs
.into_iter()
.filter_map(Result::ok)
.flatten()
.collect::<Vec<_>>();
Ok(kernel_dirs)
}
#[cfg(test)]
mod test {
use super::*;
use std::path::PathBuf;
use gpui::TestAppContext;
use project::FakeFs;
use serde_json::json;
#[gpui::test]
async fn test_get_kernelspecs(cx: &mut TestAppContext) {
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/jupyter",
json!({
".zed": {
"settings.json": r#"{ "tab_size": 8 }"#,
"tasks.json": r#"[{
"label": "cargo check",
"command": "cargo",
"args": ["check", "--all"]
},]"#,
},
"kernels": {
"python": {
"kernel.json": r#"{
"display_name": "Python 3",
"language": "python",
"argv": ["python3", "-m", "ipykernel_launcher", "-f", "{connection_file}"],
"env": {}
}"#
},
"deno": {
"kernel.json": r#"{
"display_name": "Deno",
"language": "typescript",
"argv": ["deno", "run", "--unstable", "--allow-net", "--allow-read", "https://deno.land/std/http/file_server.ts", "{connection_file}"],
"env": {}
}"#
}
},
}),
)
.await;
let mut kernels = read_kernels_dir(PathBuf::from("/jupyter/kernels"), fs.as_ref())
.await
.unwrap();
kernels.sort_by(|a, b| a.name.cmp(&b.name));
assert_eq!(
kernels.iter().map(|c| c.name.clone()).collect::<Vec<_>>(),
vec!["deno", "python"]
);
}
}

View File

@ -301,14 +301,17 @@ impl From<&MimeBundle> for OutputType {
}
}
#[derive(Default)]
#[derive(Default, Clone)]
pub enum ExecutionStatus {
#[default]
Unknown,
#[allow(unused)]
ConnectingToKernel,
Queued,
Executing,
Finished,
ShuttingDown,
Shutdown,
KernelErrored(String),
}
pub struct ExecutionView {
@ -317,10 +320,10 @@ pub struct ExecutionView {
}
impl ExecutionView {
pub fn new(_cx: &mut ViewContext<Self>) -> Self {
pub fn new(status: ExecutionStatus, _cx: &mut ViewContext<Self>) -> Self {
Self {
outputs: Default::default(),
status: ExecutionStatus::Unknown,
status,
}
}
@ -358,14 +361,16 @@ impl ExecutionView {
self.outputs.push(output);
}
// Comments from @rgbkrk, reach out with questions
// Set next input adds text to the next cell. Not required to support.
// However, this could be implemented by
// However, this could be implemented by adding text to the buffer.
// runtimelib::Payload::SetNextInput { text, replace } => todo!(),
// Not likely to be used in the context of Zed, where someone could just open the buffer themselves
// runtimelib::Payload::EditMagic { filename, line_number } => todo!(),
//
// Ask the user if they want to exit the kernel. Not required to support.
// runtimelib::Payload::AskExit { keepkernel } => todo!(),
_ => {}
}
@ -431,28 +436,24 @@ impl ExecutionView {
new_terminal.append_text(text);
Some(OutputType::Stream(new_terminal))
}
pub fn set_status(&mut self, status: ExecutionStatus, cx: &mut ViewContext<Self>) {
self.status = status;
cx.notify();
}
}
impl Render for ExecutionView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
if self.outputs.len() == 0 {
match self.status {
ExecutionStatus::ConnectingToKernel => {
return div().child("Connecting to kernel...").into_any_element()
return match &self.status {
ExecutionStatus::ConnectingToKernel => div().child("Connecting to kernel..."),
ExecutionStatus::Executing => div().child("Executing..."),
ExecutionStatus::Finished => div().child(Icon::new(IconName::Check)),
ExecutionStatus::Unknown => div().child("..."),
ExecutionStatus::ShuttingDown => div().child("Kernel shutting down..."),
ExecutionStatus::Shutdown => div().child("Kernel shutdown"),
ExecutionStatus::Queued => div().child("Queued"),
ExecutionStatus::KernelErrored(error) => {
div().child(format!("Kernel error: {}", error))
}
ExecutionStatus::Executing => {
return div().child("Executing...").into_any_element()
}
ExecutionStatus::Finished => {
return div().child(Icon::new(IconName::Check)).into_any_element()
}
ExecutionStatus::Unknown => return div().child("...").into_any_element(),
}
.into_any_element();
}
div()

View File

@ -1,51 +1,19 @@
use anyhow::{anyhow, Context as _, Result};
use async_dispatcher::{set_dispatcher, timeout, Dispatcher, Runnable};
use collections::{HashMap, HashSet};
use editor::{
display_map::{
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock,
},
Anchor, AnchorRangeExt, Editor,
};
use futures::{
channel::mpsc::{self, UnboundedSender},
future::Shared,
Future, FutureExt, SinkExt as _, StreamExt,
};
use gpui::prelude::*;
use gpui::{
actions, AppContext, Context, EntityId, Global, Model, ModelContext, PlatformDispatcher, Task,
WeakView,
};
use gpui::{Entity, View};
use language::Point;
use outputs::{ExecutionStatus, ExecutionView, LineHeight as _};
use project::Fs;
use runtime_settings::JupyterSettings;
use runtimelib::JupyterMessageContent;
use settings::{Settings as _, SettingsStore};
use std::{ops::Range, time::Instant};
use async_dispatcher::{set_dispatcher, Dispatcher, Runnable};
use gpui::{AppContext, PlatformDispatcher};
use settings::Settings as _;
use std::{sync::Arc, time::Duration};
use theme::{ActiveTheme, ThemeSettings};
use ui::prelude::*;
use workspace::Workspace;
mod jupyter_settings;
mod kernels;
mod outputs;
// mod runtime_panel;
mod runtime_settings;
mod runtimes;
mod runtime_panel;
mod session;
mod stdio;
use runtimes::{get_runtime_specifications, Request, RunningKernel, RuntimeSpecification};
pub use jupyter_settings::JupyterSettings;
pub use runtime_panel::RuntimePanel;
actions!(repl, [Run]);
#[derive(Clone)]
pub struct RuntimeManagerGlobal(Model<RuntimeManager>);
impl Global for RuntimeManagerGlobal {}
pub fn zed_dispatcher(cx: &mut AppContext) -> impl Dispatcher {
fn zed_dispatcher(cx: &mut AppContext) -> impl Dispatcher {
struct ZedDispatcher {
dispatcher: Arc<dyn PlatformDispatcher>,
}
@ -69,503 +37,8 @@ pub fn zed_dispatcher(cx: &mut AppContext) -> impl Dispatcher {
}
}
pub fn init(fs: Arc<dyn Fs>, cx: &mut AppContext) {
pub fn init(cx: &mut AppContext) {
set_dispatcher(zed_dispatcher(cx));
JupyterSettings::register(cx);
observe_jupyter_settings_changes(fs.clone(), cx);
cx.observe_new_views(
|workspace: &mut Workspace, _: &mut ViewContext<Workspace>| {
workspace.register_action(run);
},
)
.detach();
let settings = JupyterSettings::get_global(cx);
if !settings.enabled {
return;
}
initialize_runtime_manager(fs, cx);
}
fn initialize_runtime_manager(fs: Arc<dyn Fs>, cx: &mut AppContext) {
let runtime_manager = cx.new_model(|cx| RuntimeManager::new(fs.clone(), cx));
RuntimeManager::set_global(runtime_manager.clone(), cx);
cx.spawn(|mut cx| async move {
let fs = fs.clone();
let runtime_specifications = get_runtime_specifications(fs).await?;
runtime_manager.update(&mut cx, |this, _cx| {
this.runtime_specifications = runtime_specifications;
})?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn observe_jupyter_settings_changes(fs: Arc<dyn Fs>, cx: &mut AppContext) {
cx.observe_global::<SettingsStore>(move |cx| {
let settings = JupyterSettings::get_global(cx);
if settings.enabled && RuntimeManager::global(cx).is_none() {
initialize_runtime_manager(fs.clone(), cx);
} else {
RuntimeManager::remove_global(cx);
// todo!(): Remove action from workspace(s)
}
})
.detach();
}
#[derive(Debug)]
pub enum Kernel {
RunningKernel(RunningKernel),
StartingKernel(Shared<Task<()>>),
FailedLaunch,
}
// Per workspace
pub struct RuntimeManager {
fs: Arc<dyn Fs>,
runtime_specifications: Vec<RuntimeSpecification>,
instances: HashMap<EntityId, Kernel>,
editors: HashMap<WeakView<Editor>, EditorRuntimeState>,
// todo!(): Next
// To reduce the number of open tasks and channels we have, let's feed the response
// messages by ID over to the paired ExecutionView
_execution_views_by_id: HashMap<String, View<ExecutionView>>,
}
#[derive(Debug, Clone)]
struct EditorRuntimeState {
blocks: Vec<EditorRuntimeBlock>,
// todo!(): Store a subscription to the editor so we can drop them when the editor is dropped
// subscription: gpui::Subscription,
}
#[derive(Debug, Clone)]
struct EditorRuntimeBlock {
code_range: Range<Anchor>,
_execution_id: String,
block_id: BlockId,
_execution_view: View<ExecutionView>,
}
impl RuntimeManager {
pub fn new(fs: Arc<dyn Fs>, _cx: &mut AppContext) -> Self {
Self {
fs,
runtime_specifications: Default::default(),
instances: Default::default(),
editors: Default::default(),
_execution_views_by_id: Default::default(),
}
}
fn get_or_launch_kernel(
&mut self,
entity_id: EntityId,
language_name: Arc<str>,
cx: &mut ModelContext<Self>,
) -> Task<Result<UnboundedSender<Request>>> {
let kernel = self.instances.get(&entity_id);
let pending_kernel_start = match kernel {
Some(Kernel::RunningKernel(running_kernel)) => {
return Task::ready(anyhow::Ok(running_kernel.request_tx.clone()));
}
Some(Kernel::StartingKernel(task)) => task.clone(),
Some(Kernel::FailedLaunch) | None => {
self.instances.remove(&entity_id);
let kernel = self.launch_kernel(entity_id, language_name, cx);
let pending_kernel = cx
.spawn(|this, mut cx| async move {
let running_kernel = kernel.await;
match running_kernel {
Ok(running_kernel) => {
let _ = this.update(&mut cx, |this, _cx| {
this.instances
.insert(entity_id, Kernel::RunningKernel(running_kernel));
});
}
Err(_err) => {
let _ = this.update(&mut cx, |this, _cx| {
this.instances.insert(entity_id, Kernel::FailedLaunch);
});
}
}
})
.shared();
self.instances
.insert(entity_id, Kernel::StartingKernel(pending_kernel.clone()));
pending_kernel
}
};
cx.spawn(|this, mut cx| async move {
pending_kernel_start.await;
this.update(&mut cx, |this, _cx| {
let kernel = this
.instances
.get(&entity_id)
.ok_or(anyhow!("unable to get a running kernel"))?;
match kernel {
Kernel::RunningKernel(running_kernel) => Ok(running_kernel.request_tx.clone()),
_ => Err(anyhow!("unable to get a running kernel")),
}
})?
})
}
fn launch_kernel(
&mut self,
entity_id: EntityId,
language_name: Arc<str>,
cx: &mut ModelContext<Self>,
) -> Task<Result<RunningKernel>> {
// Get first runtime that matches the language name (for now)
let runtime_specification =
self.runtime_specifications
.iter()
.find(|runtime_specification| {
runtime_specification.kernelspec.language == language_name.to_string()
});
let runtime_specification = match runtime_specification {
Some(runtime_specification) => runtime_specification,
None => {
return Task::ready(Err(anyhow::anyhow!(
"No runtime found for language {}",
language_name
)));
}
};
let runtime_specification = runtime_specification.clone();
let fs = self.fs.clone();
cx.spawn(|_, cx| async move {
let running_kernel =
RunningKernel::new(runtime_specification, entity_id, fs.clone(), cx);
let running_kernel = running_kernel.await?;
let mut request_tx = running_kernel.request_tx.clone();
let overall_timeout_duration = Duration::from_secs(10);
let start_time = Instant::now();
loop {
if start_time.elapsed() > overall_timeout_duration {
// todo!(): Kill the kernel
return Err(anyhow::anyhow!("Kernel did not respond in time"));
}
let (tx, rx) = mpsc::unbounded();
match request_tx
.send(Request {
request: runtimelib::KernelInfoRequest {}.into(),
responses_rx: tx,
})
.await
{
Ok(_) => {}
Err(_err) => {
break;
}
};
let mut rx = rx.fuse();
let kernel_info_timeout = Duration::from_secs(1);
let mut got_kernel_info = false;
while let Ok(Some(message)) = timeout(kernel_info_timeout, rx.next()).await {
match message {
JupyterMessageContent::KernelInfoReply(_) => {
got_kernel_info = true;
}
_ => {}
}
}
if got_kernel_info {
break;
}
}
anyhow::Ok(running_kernel)
})
}
fn execute_code(
&mut self,
entity_id: EntityId,
language_name: Arc<str>,
code: String,
cx: &mut ModelContext<Self>,
) -> impl Future<Output = Result<mpsc::UnboundedReceiver<JupyterMessageContent>>> {
let (tx, rx) = mpsc::unbounded();
let request_tx = self.get_or_launch_kernel(entity_id, language_name, cx);
async move {
let request_tx = request_tx.await?;
request_tx
.unbounded_send(Request {
request: runtimelib::ExecuteRequest {
code,
allow_stdin: false,
silent: false,
store_history: true,
stop_on_error: true,
..Default::default()
}
.into(),
responses_rx: tx,
})
.context("Failed to send execution request")?;
Ok(rx)
}
}
pub fn global(cx: &AppContext) -> Option<Model<Self>> {
cx.try_global::<RuntimeManagerGlobal>()
.map(|runtime_manager| runtime_manager.0.clone())
}
pub fn set_global(runtime_manager: Model<Self>, cx: &mut AppContext) {
cx.set_global(RuntimeManagerGlobal(runtime_manager));
}
pub fn remove_global(cx: &mut AppContext) {
if RuntimeManager::global(cx).is_some() {
cx.remove_global::<RuntimeManagerGlobal>();
}
}
}
pub fn get_active_editor(
workspace: &mut Workspace,
cx: &mut ViewContext<Workspace>,
) -> Option<View<Editor>> {
workspace
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx))
}
// Gets the active selection in the editor or the current line
pub fn selection(editor: View<Editor>, cx: &mut ViewContext<Workspace>) -> Range<Anchor> {
let editor = editor.read(cx);
let selection = editor.selections.newest::<usize>(cx);
let buffer = editor.buffer().read(cx).snapshot(cx);
let range = if selection.is_empty() {
let cursor = selection.head();
let line_start = buffer.offset_to_point(cursor).row;
let mut start_offset = buffer.point_to_offset(Point::new(line_start, 0));
// Iterate backwards to find the start of the line
while start_offset > 0 {
let ch = buffer.chars_at(start_offset - 1).next().unwrap_or('\0');
if ch == '\n' {
break;
}
start_offset -= 1;
}
let mut end_offset = cursor;
// Iterate forwards to find the end of the line
while end_offset < buffer.len() {
let ch = buffer.chars_at(end_offset).next().unwrap_or('\0');
if ch == '\n' {
break;
}
end_offset += 1;
}
// Create a range from the start to the end of the line
start_offset..end_offset
} else {
selection.range()
};
let anchor_range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
anchor_range
}
pub fn run(workspace: &mut Workspace, _: &Run, cx: &mut ViewContext<Workspace>) {
let (editor, runtime_manager) = if let (Some(editor), Some(runtime_manager)) =
(get_active_editor(workspace, cx), RuntimeManager::global(cx))
{
(editor, runtime_manager)
} else {
log::warn!("No active editor or runtime manager found");
return;
};
let anchor_range = selection(editor.clone(), cx);
let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
let selected_text = buffer
.text_for_range(anchor_range.clone())
.collect::<String>();
let start_language = buffer.language_at(anchor_range.start);
let end_language = buffer.language_at(anchor_range.end);
let language_name = if start_language == end_language {
start_language
.map(|language| language.code_fence_block_name())
.filter(|lang| **lang != *"markdown")
} else {
// If the selection spans multiple languages, don't run it
return;
};
let language_name = if let Some(language_name) = language_name {
language_name
} else {
return;
};
let entity_id = editor.entity_id();
let execution_view = cx.new_view(|cx| ExecutionView::new(cx));
// If any block overlaps with the new block, remove it
// TODO: When inserting a new block, put it in order so that search is efficient
let blocks_to_remove = runtime_manager.update(cx, |runtime_manager, _cx| {
// Get the current `EditorRuntimeState` for this runtime_manager, inserting it if it doesn't exist
let editor_runtime_state = runtime_manager
.editors
.entry(editor.downgrade())
.or_insert_with(|| EditorRuntimeState { blocks: Vec::new() });
let mut blocks_to_remove: HashSet<BlockId> = HashSet::default();
editor_runtime_state.blocks.retain(|block| {
if anchor_range.overlaps(&block.code_range, &buffer) {
blocks_to_remove.insert(block.block_id);
// Drop this block
false
} else {
true
}
});
blocks_to_remove
});
let blocks_to_remove = blocks_to_remove.clone();
let block_id = editor.update(cx, |editor, cx| {
editor.remove_blocks(blocks_to_remove, None, cx);
let block = BlockProperties {
position: anchor_range.end,
height: execution_view.num_lines(cx).saturating_add(1),
style: BlockStyle::Sticky,
render: create_output_area_render(execution_view.clone()),
disposition: BlockDisposition::Below,
};
editor.insert_blocks([block], None, cx)[0]
});
let receiver = runtime_manager.update(cx, |runtime_manager, cx| {
let editor_runtime_state = runtime_manager
.editors
.entry(editor.downgrade())
.or_insert_with(|| EditorRuntimeState { blocks: Vec::new() });
let editor_runtime_block = EditorRuntimeBlock {
code_range: anchor_range.clone(),
block_id,
_execution_view: execution_view.clone(),
_execution_id: Default::default(),
};
editor_runtime_state
.blocks
.push(editor_runtime_block.clone());
runtime_manager.execute_code(entity_id, language_name, selected_text.clone(), cx)
});
cx.spawn(|_this, mut cx| async move {
execution_view.update(&mut cx, |execution_view, cx| {
execution_view.set_status(ExecutionStatus::ConnectingToKernel, cx);
})?;
let mut receiver = receiver.await?;
let execution_view = execution_view.clone();
while let Some(content) = receiver.next().await {
execution_view.update(&mut cx, |execution_view, cx| {
execution_view.push_message(&content, cx)
})?;
editor.update(&mut cx, |editor, cx| {
let mut replacements = HashMap::default();
replacements.insert(
block_id,
(
Some(execution_view.num_lines(cx).saturating_add(1)),
create_output_area_render(execution_view.clone()),
),
);
editor.replace_blocks(replacements, None, cx);
})?;
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn create_output_area_render(execution_view: View<ExecutionView>) -> RenderBlock {
let render = move |cx: &mut BlockContext| {
let execution_view = execution_view.clone();
let text_font = ThemeSettings::get_global(cx).buffer_font.family.clone();
// Note: we'll want to use `cx.anchor_x` when someone runs something with no output -- just show a checkmark and not make the full block below the line
let gutter_width = cx.gutter_dimensions.width;
h_flex()
.w_full()
.bg(cx.theme().colors().background)
.border_y_1()
.border_color(cx.theme().colors().border)
.pl(gutter_width)
.child(
div()
.font_family(text_font)
// .ml(gutter_width)
.mx_1()
.my_2()
.h_full()
.w_full()
.mr(gutter_width)
.child(execution_view),
)
.into_any_element()
};
Box::new(render)
runtime_panel::init(cx)
}

View File

@ -0,0 +1,443 @@
use crate::{
jupyter_settings::{JupyterDockPosition, JupyterSettings},
kernels::{kernel_specifications, KernelSpecification},
session::{Session, SessionEvent},
};
use anyhow::{Context as _, Result};
use collections::HashMap;
use editor::{Anchor, Editor, RangeToAnchorExt};
use gpui::{
actions, prelude::*, AppContext, AsyncWindowContext, Entity, EntityId, EventEmitter,
FocusHandle, FocusOutEvent, FocusableView, Subscription, Task, View, WeakView,
};
use language::Point;
use project::Fs;
use settings::{Settings as _, SettingsStore};
use std::{ops::Range, sync::Arc};
use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding};
use workspace::{
dock::{Panel, PanelEvent},
Workspace,
};
actions!(repl, [Run, ToggleFocus, ClearOutputs]);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
workspace
.register_action(|workspace, _: &ToggleFocus, cx| {
workspace.toggle_panel_focus::<RuntimePanel>(cx);
})
.register_action(run)
.register_action(clear_outputs);
},
)
.detach();
}
pub struct RuntimePanel {
fs: Arc<dyn Fs>,
enabled: bool,
focus_handle: FocusHandle,
width: Option<Pixels>,
sessions: HashMap<EntityId, View<Session>>,
kernel_specifications: Vec<KernelSpecification>,
_subscriptions: Vec<Subscription>,
}
impl RuntimePanel {
pub fn load(
workspace: WeakView<Workspace>,
cx: AsyncWindowContext,
) -> Task<Result<View<Self>>> {
cx.spawn(|mut cx| async move {
let view = workspace.update(&mut cx, |workspace, cx| {
cx.new_view::<Self>(|cx| {
let focus_handle = cx.focus_handle();
let fs = workspace.app_state().fs.clone();
let subscriptions = vec![
cx.on_focus_in(&focus_handle, Self::focus_in),
cx.on_focus_out(&focus_handle, Self::focus_out),
cx.observe_global::<SettingsStore>(move |this, cx| {
let settings = JupyterSettings::get_global(cx);
this.set_enabled(settings.enabled, cx);
}),
];
let enabled = JupyterSettings::get_global(cx).enabled;
Self {
fs,
width: None,
focus_handle,
kernel_specifications: Vec::new(),
sessions: Default::default(),
_subscriptions: subscriptions,
enabled,
}
})
})?;
view.update(&mut cx, |this, cx| this.refresh_kernelspecs(cx))?
.await?;
Ok(view)
})
}
fn set_enabled(&mut self, enabled: bool, cx: &mut ViewContext<Self>) {
if self.enabled != enabled {
self.enabled = enabled;
cx.notify();
}
}
fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
cx.notify();
}
fn focus_out(&mut self, _event: FocusOutEvent, cx: &mut ViewContext<Self>) {
cx.notify();
}
// Gets the active selection in the editor or the current line
fn selection(&self, editor: View<Editor>, cx: &mut ViewContext<Self>) -> Range<Anchor> {
let editor = editor.read(cx);
let selection = editor.selections.newest::<usize>(cx);
let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let range = if selection.is_empty() {
let cursor = selection.head();
let line_start = multi_buffer_snapshot.offset_to_point(cursor).row;
let mut start_offset = multi_buffer_snapshot.point_to_offset(Point::new(line_start, 0));
// Iterate backwards to find the start of the line
while start_offset > 0 {
let ch = multi_buffer_snapshot
.chars_at(start_offset - 1)
.next()
.unwrap_or('\0');
if ch == '\n' {
break;
}
start_offset -= 1;
}
let mut end_offset = cursor;
// Iterate forwards to find the end of the line
while end_offset < multi_buffer_snapshot.len() {
let ch = multi_buffer_snapshot
.chars_at(end_offset)
.next()
.unwrap_or('\0');
if ch == '\n' {
break;
}
end_offset += 1;
}
// Create a range from the start to the end of the line
start_offset..end_offset
} else {
selection.range()
};
range.to_anchors(&multi_buffer_snapshot)
}
pub fn snippet(
&self,
editor: View<Editor>,
cx: &mut ViewContext<Self>,
) -> Option<(String, Arc<str>, Range<Anchor>)> {
let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
let anchor_range = self.selection(editor, cx);
let selected_text = buffer
.text_for_range(anchor_range.clone())
.collect::<String>();
let start_language = buffer.language_at(anchor_range.start);
let end_language = buffer.language_at(anchor_range.end);
let language_name = if start_language == end_language {
start_language
.map(|language| language.code_fence_block_name())
.filter(|lang| **lang != *"markdown")?
} else {
// If the selection spans multiple languages, don't run it
return None;
};
Some((selected_text, language_name, anchor_range))
}
pub fn refresh_kernelspecs(&mut self, cx: &mut ViewContext<Self>) -> Task<anyhow::Result<()>> {
let kernel_specifications = kernel_specifications(self.fs.clone());
cx.spawn(|this, mut cx| async move {
let kernel_specifications = kernel_specifications.await?;
this.update(&mut cx, |this, cx| {
this.kernel_specifications = kernel_specifications;
cx.notify();
})
})
}
pub fn kernelspec(
&self,
language_name: &str,
cx: &mut ViewContext<Self>,
) -> Option<KernelSpecification> {
let settings = JupyterSettings::get_global(cx);
let selected_kernel = settings.kernel_selections.get(language_name);
self.kernel_specifications
.iter()
.find(|runtime_specification| {
if let Some(selected) = selected_kernel {
// Top priority is the selected kernel
runtime_specification.name.to_lowercase() == selected.to_lowercase()
} else {
// Otherwise, we'll try to find a kernel that matches the language
runtime_specification.kernelspec.language.to_lowercase()
== language_name.to_lowercase()
}
})
.cloned()
}
pub fn run(
&mut self,
editor: View<Editor>,
fs: Arc<dyn Fs>,
cx: &mut ViewContext<Self>,
) -> anyhow::Result<()> {
if !self.enabled {
return Ok(());
}
let (selected_text, language_name, anchor_range) = match self.snippet(editor.clone(), cx) {
Some(snippet) => snippet,
None => return Ok(()),
};
let entity_id = editor.entity_id();
let kernel_specification = self
.kernelspec(&language_name, cx)
.with_context(|| format!("No kernel found for language: {language_name}"))?;
let session = self.sessions.entry(entity_id).or_insert_with(|| {
let view = cx.new_view(|cx| Session::new(editor, fs.clone(), kernel_specification, cx));
cx.notify();
let subscription = cx.subscribe(
&view,
|panel: &mut RuntimePanel, _session: View<Session>, event: &SessionEvent, _cx| {
match event {
SessionEvent::Shutdown(shutdown_event) => {
panel.sessions.remove(&shutdown_event.entity_id());
}
}
//
},
);
subscription.detach();
view
});
session.update(cx, |session, cx| {
session.execute(&selected_text, anchor_range, cx);
});
anyhow::Ok(())
}
pub fn clear_outputs(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
let entity_id = editor.entity_id();
if let Some(session) = self.sessions.get_mut(&entity_id) {
session.update(cx, |session, cx| {
session.clear_outputs(cx);
});
cx.notify();
}
}
}
pub fn run(workspace: &mut Workspace, _: &Run, cx: &mut ViewContext<Workspace>) {
let settings = JupyterSettings::get_global(cx);
if !settings.enabled {
return;
}
let editor = workspace
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx));
if let (Some(editor), Some(runtime_panel)) = (editor, workspace.panel::<RuntimePanel>(cx)) {
runtime_panel.update(cx, |runtime_panel, cx| {
runtime_panel
.run(editor, workspace.app_state().fs.clone(), cx)
.ok();
});
}
}
pub fn clear_outputs(workspace: &mut Workspace, _: &ClearOutputs, cx: &mut ViewContext<Workspace>) {
let settings = JupyterSettings::get_global(cx);
if !settings.enabled {
return;
}
let editor = workspace
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx));
if let (Some(editor), Some(runtime_panel)) = (editor, workspace.panel::<RuntimePanel>(cx)) {
runtime_panel.update(cx, |runtime_panel, cx| {
runtime_panel.clear_outputs(editor, cx);
});
}
}
impl Panel for RuntimePanel {
fn persistent_name() -> &'static str {
"RuntimePanel"
}
fn position(&self, cx: &ui::WindowContext) -> workspace::dock::DockPosition {
match JupyterSettings::get_global(cx).dock {
JupyterDockPosition::Left => workspace::dock::DockPosition::Left,
JupyterDockPosition::Right => workspace::dock::DockPosition::Right,
JupyterDockPosition::Bottom => workspace::dock::DockPosition::Bottom,
}
}
fn position_is_valid(&self, _position: workspace::dock::DockPosition) -> bool {
true
}
fn set_position(
&mut self,
position: workspace::dock::DockPosition,
cx: &mut ViewContext<Self>,
) {
settings::update_settings_file::<JupyterSettings>(self.fs.clone(), cx, move |settings| {
let dock = match position {
workspace::dock::DockPosition::Left => JupyterDockPosition::Left,
workspace::dock::DockPosition::Right => JupyterDockPosition::Right,
workspace::dock::DockPosition::Bottom => JupyterDockPosition::Bottom,
};
settings.set_dock(dock);
})
}
fn size(&self, cx: &ui::WindowContext) -> Pixels {
let settings = JupyterSettings::get_global(cx);
self.width.unwrap_or(settings.default_width)
}
fn set_size(&mut self, size: Option<ui::Pixels>, _cx: &mut ViewContext<Self>) {
self.width = size;
}
fn icon(&self, _cx: &ui::WindowContext) -> Option<ui::IconName> {
if !self.enabled {
return None;
}
Some(IconName::Code)
}
fn icon_tooltip(&self, _cx: &ui::WindowContext) -> Option<&'static str> {
Some("Runtime Panel")
}
fn toggle_action(&self) -> Box<dyn gpui::Action> {
Box::new(ToggleFocus)
}
}
impl EventEmitter<PanelEvent> for RuntimePanel {}
impl FocusableView for RuntimePanel {
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for RuntimePanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
// When there are no kernel specifications, show a link to the Zed docs explaining how to
// install kernels. It can be assumed they don't have a running kernel if we have no
// specifications.
if self.kernel_specifications.is_empty() {
return v_flex()
.p_4()
.size_full()
.gap_2()
.child(Label::new("No Jupyter Kernels Available").size(LabelSize::Large))
.child(
Label::new("To start interactively running code in your editor, you need to install and configure Jupyter kernels.")
.size(LabelSize::Default),
)
.child(
h_flex().w_full().p_4().justify_center().gap_2().child(
ButtonLike::new("install-kernels")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Install Kernels"))
.on_click(move |_, cx| {
cx.open_url(
"https://docs.jupyter.org/en/latest/install/kernels.html",
)
}),
),
)
.into_any_element();
}
// When there are no sessions, show the command to run code in an editor
if self.sessions.is_empty() {
return v_flex()
.p_4()
.size_full()
.gap_2()
.child(Label::new("No Jupyter Kernel Sessions").size(LabelSize::Large))
.child(
v_flex().child(
Label::new("To run code in a Jupyter kernel, select some code and use the 'repl::Run' command.")
.size(LabelSize::Default)
)
.children(
KeyBinding::for_action(&Run, cx)
.map(|binding|
binding.into_any_element()
)
)
)
.into_any_element();
}
v_flex()
.p_4()
.child(Label::new("Jupyter Kernel Sessions").size(LabelSize::Large))
.children(
self.sessions
.values()
.map(|session| session.clone().into_any_element()),
)
.into_any_element()
}
}

View File

@ -1,66 +0,0 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum RuntimesDockPosition {
Left,
#[default]
Right,
Bottom,
}
#[derive(Debug, Default)]
pub struct JupyterSettings {
pub enabled: bool,
pub dock: RuntimesDockPosition,
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
pub struct JupyterSettingsContent {
/// Whether the Runtimes feature is enabled.
///
/// Default: `false`
enabled: Option<bool>,
/// Where to dock the runtimes panel.
///
/// Default: `right`
dock: Option<RuntimesDockPosition>,
}
impl Default for JupyterSettingsContent {
fn default() -> Self {
JupyterSettingsContent {
enabled: Some(false),
dock: Some(RuntimesDockPosition::Right),
}
}
}
impl Settings for JupyterSettings {
const KEY: Option<&'static str> = Some("jupyter");
type FileContent = JupyterSettingsContent;
fn load(
sources: SettingsSources<Self::FileContent>,
_cx: &mut gpui::AppContext,
) -> anyhow::Result<Self>
where
Self: Sized,
{
let mut settings = JupyterSettings::default();
for value in sources.defaults_and_customizations() {
if let Some(enabled) = value.enabled {
settings.enabled = enabled;
}
if let Some(dock) = value.dock {
settings.dock = dock;
}
}
Ok(settings)
}
}

View File

@ -1,329 +0,0 @@
use anyhow::{Context as _, Result};
use collections::HashMap;
use futures::lock::Mutex;
use futures::{channel::mpsc, SinkExt as _, StreamExt as _};
use gpui::{AsyncAppContext, EntityId};
use project::Fs;
use runtimelib::{dirs, ConnectionInfo, JupyterKernelspec, JupyterMessage, JupyterMessageContent};
use smol::{net::TcpListener, process::Command};
use std::fmt::Debug;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::{path::PathBuf, sync::Arc};
#[derive(Debug)]
pub struct Request {
pub request: runtimelib::JupyterMessageContent,
pub responses_rx: mpsc::UnboundedSender<JupyterMessageContent>,
}
#[derive(Debug, Clone)]
pub struct RuntimeSpecification {
pub name: String,
pub path: PathBuf,
pub kernelspec: JupyterKernelspec,
}
impl RuntimeSpecification {
#[must_use]
fn command(&self, connection_path: &PathBuf) -> Result<Command> {
let argv = &self.kernelspec.argv;
if argv.is_empty() {
return Err(anyhow::anyhow!("Empty argv in kernelspec {}", self.name));
}
if argv.len() < 2 {
return Err(anyhow::anyhow!("Invalid argv in kernelspec {}", self.name));
}
if !argv.contains(&"{connection_file}".to_string()) {
return Err(anyhow::anyhow!(
"Missing 'connection_file' in argv in kernelspec {}",
self.name
));
}
let mut cmd = Command::new(&argv[0]);
for arg in &argv[1..] {
if arg == "{connection_file}" {
cmd.arg(connection_path);
} else {
cmd.arg(arg);
}
}
if let Some(env) = &self.kernelspec.env {
cmd.envs(env);
}
Ok(cmd)
}
}
// Find a set of open ports. This creates a listener with port set to 0. The listener will be closed at the end when it goes out of scope.
// There's a race condition between closing the ports and usage by a kernel, but it's inherent to the Jupyter protocol.
async fn peek_ports(ip: IpAddr) -> anyhow::Result<[u16; 5]> {
let mut addr_zeroport: SocketAddr = SocketAddr::new(ip, 0);
addr_zeroport.set_port(0);
let mut ports: [u16; 5] = [0; 5];
for i in 0..5 {
let listener = TcpListener::bind(addr_zeroport).await?;
let addr = listener.local_addr()?;
ports[i] = addr.port();
}
Ok(ports)
}
#[derive(Debug)]
pub struct RunningKernel {
#[allow(unused)]
runtime: RuntimeSpecification,
#[allow(unused)]
process: smol::process::Child,
pub request_tx: mpsc::UnboundedSender<Request>,
}
impl RunningKernel {
pub async fn new(
runtime: RuntimeSpecification,
entity_id: EntityId,
fs: Arc<dyn Fs>,
cx: AsyncAppContext,
) -> anyhow::Result<Self> {
let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
let ports = peek_ports(ip).await?;
let connection_info = ConnectionInfo {
transport: "tcp".to_string(),
ip: ip.to_string(),
stdin_port: ports[0],
control_port: ports[1],
hb_port: ports[2],
shell_port: ports[3],
iopub_port: ports[4],
signature_scheme: "hmac-sha256".to_string(),
key: uuid::Uuid::new_v4().to_string(),
kernel_name: Some(format!("zed-{}", runtime.name)),
};
let connection_path = dirs::runtime_dir().join(format!("kernel-zed-{}.json", entity_id));
let content = serde_json::to_string(&connection_info)?;
// write out file to disk for kernel
fs.atomic_write(connection_path.clone(), content).await?;
let mut cmd = runtime.command(&connection_path)?;
let process = cmd
// .stdout(Stdio::null())
// .stderr(Stdio::null())
.kill_on_drop(true)
.spawn()
.context("failed to start the kernel process")?;
let mut iopub = connection_info.create_client_iopub_connection("").await?;
let mut shell = connection_info.create_client_shell_connection().await?;
// Spawn a background task to handle incoming messages from the kernel as well
// as outgoing messages to the kernel
let child_messages: Arc<
Mutex<HashMap<String, mpsc::UnboundedSender<JupyterMessageContent>>>,
> = Default::default();
let (request_tx, mut request_rx) = mpsc::unbounded::<Request>();
cx.background_executor()
.spawn({
let child_messages = child_messages.clone();
async move {
let child_messages = child_messages.clone();
while let Ok(message) = iopub.read().await {
if let Some(parent_header) = message.parent_header {
let child_messages = child_messages.lock().await;
let sender = child_messages.get(&parent_header.msg_id);
match sender {
Some(mut sender) => {
sender.send(message.content).await?;
}
None => {}
}
}
}
anyhow::Ok(())
}
})
.detach();
cx.background_executor()
.spawn({
let child_messages = child_messages.clone();
async move {
while let Some(request) = request_rx.next().await {
let rx = request.responses_rx.clone();
let request: JupyterMessage = request.request.into();
let msg_id = request.header.msg_id.clone();
let mut sender = rx.clone();
child_messages
.lock()
.await
.insert(msg_id.clone(), sender.clone());
shell.send(request).await?;
let response = shell.read().await?;
sender.send(response.content).await?;
}
anyhow::Ok(())
}
})
.detach();
Ok(Self {
runtime,
process,
request_tx,
})
}
}
async fn read_kernelspec_at(
// Path should be a directory to a jupyter kernelspec, as in
// /usr/local/share/jupyter/kernels/python3
kernel_dir: PathBuf,
fs: Arc<dyn Fs>,
) -> anyhow::Result<RuntimeSpecification> {
let path = kernel_dir;
let kernel_name = if let Some(kernel_name) = path.file_name() {
kernel_name.to_string_lossy().to_string()
} else {
return Err(anyhow::anyhow!("Invalid kernelspec directory: {:?}", path));
};
if !fs.is_dir(path.as_path()).await {
return Err(anyhow::anyhow!("Not a directory: {:?}", path));
}
let expected_kernel_json = path.join("kernel.json");
let spec = fs.load(expected_kernel_json.as_path()).await?;
let spec = serde_json::from_str::<JupyterKernelspec>(&spec)?;
Ok(RuntimeSpecification {
name: kernel_name,
path,
kernelspec: spec,
})
}
/// Read a directory of kernelspec directories
async fn read_kernels_dir(
path: PathBuf,
fs: Arc<dyn Fs>,
) -> anyhow::Result<Vec<RuntimeSpecification>> {
let mut kernelspec_dirs = fs.read_dir(&path).await?;
let mut valid_kernelspecs = Vec::new();
while let Some(path) = kernelspec_dirs.next().await {
match path {
Ok(path) => {
if fs.is_dir(path.as_path()).await {
let fs = fs.clone();
if let Ok(kernelspec) = read_kernelspec_at(path, fs).await {
valid_kernelspecs.push(kernelspec);
}
}
}
Err(err) => {
log::warn!("Error reading kernelspec directory: {:?}", err);
}
}
}
Ok(valid_kernelspecs)
}
pub async fn get_runtime_specifications(
fs: Arc<dyn Fs>,
) -> anyhow::Result<Vec<RuntimeSpecification>> {
let data_dirs = dirs::data_dirs();
let kernel_dirs = data_dirs
.iter()
.map(|dir| dir.join("kernels"))
.map(|path| read_kernels_dir(path, fs.clone()))
.collect::<Vec<_>>();
let kernel_dirs = futures::future::join_all(kernel_dirs).await;
let kernel_dirs = kernel_dirs
.into_iter()
.filter_map(Result::ok)
.flatten()
.collect::<Vec<_>>();
Ok(kernel_dirs)
}
#[cfg(test)]
mod test {
use super::*;
use std::path::PathBuf;
use gpui::TestAppContext;
use project::FakeFs;
use serde_json::json;
#[gpui::test]
async fn test_get_kernelspecs(cx: &mut TestAppContext) {
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/jupyter",
json!({
".zed": {
"settings.json": r#"{ "tab_size": 8 }"#,
"tasks.json": r#"[{
"label": "cargo check",
"command": "cargo",
"args": ["check", "--all"]
},]"#,
},
"kernels": {
"python": {
"kernel.json": r#"{
"display_name": "Python 3",
"language": "python",
"argv": ["python3", "-m", "ipykernel_launcher", "-f", "{connection_file}"],
"env": {}
}"#
},
"deno": {
"kernel.json": r#"{
"display_name": "Deno",
"language": "typescript",
"argv": ["deno", "run", "--unstable", "--allow-net", "--allow-read", "https://deno.land/std/http/file_server.ts", "{connection_file}"],
"env": {}
}"#
}
},
}),
)
.await;
let mut kernels = read_kernels_dir(PathBuf::from("/jupyter/kernels"), fs)
.await
.unwrap();
kernels.sort_by(|a, b| a.name.cmp(&b.name));
assert_eq!(
kernels.iter().map(|c| c.name.clone()).collect::<Vec<_>>(),
vec!["deno", "python"]
);
}
}

400
crates/repl/src/session.rs Normal file
View File

@ -0,0 +1,400 @@
use crate::{
kernels::{Kernel, KernelSpecification, RunningKernel},
outputs::{ExecutionStatus, ExecutionView, LineHeight as _},
};
use collections::{HashMap, HashSet};
use editor::{
display_map::{
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock,
},
Anchor, AnchorRangeExt as _, Editor,
};
use futures::{FutureExt as _, StreamExt as _};
use gpui::{div, prelude::*, Entity, EventEmitter, Render, Task, View, ViewContext};
use project::Fs;
use runtimelib::{
ExecuteRequest, InterruptRequest, JupyterMessage, JupyterMessageContent, KernelInfoRequest,
ShutdownRequest,
};
use settings::Settings as _;
use std::{ops::Range, sync::Arc, time::Duration};
use theme::{ActiveTheme, ThemeSettings};
use ui::{h_flex, prelude::*, v_flex, ButtonLike, ButtonStyle, Label};
pub struct Session {
editor: View<Editor>,
kernel: Kernel,
blocks: HashMap<String, EditorBlock>,
messaging_task: Task<()>,
kernel_specification: KernelSpecification,
}
#[derive(Debug)]
struct EditorBlock {
editor: View<Editor>,
code_range: Range<Anchor>,
block_id: BlockId,
execution_view: View<ExecutionView>,
}
impl EditorBlock {
fn new(
editor: View<Editor>,
code_range: Range<Anchor>,
status: ExecutionStatus,
cx: &mut ViewContext<Session>,
) -> Self {
let execution_view = cx.new_view(|cx| ExecutionView::new(status, cx));
let block_id = editor.update(cx, |editor, cx| {
let block = BlockProperties {
position: code_range.end,
height: execution_view.num_lines(cx).saturating_add(1),
style: BlockStyle::Sticky,
render: Self::create_output_area_render(execution_view.clone()),
disposition: BlockDisposition::Below,
};
editor.insert_blocks([block], None, cx)[0]
});
Self {
editor,
code_range,
block_id,
execution_view,
}
}
fn handle_message(&mut self, message: &JupyterMessage, cx: &mut ViewContext<Session>) {
self.execution_view.update(cx, |execution_view, cx| {
execution_view.push_message(&message.content, cx);
});
self.editor.update(cx, |editor, cx| {
let mut replacements = HashMap::default();
replacements.insert(
self.block_id,
(
Some(self.execution_view.num_lines(cx).saturating_add(1)),
Self::create_output_area_render(self.execution_view.clone()),
),
);
editor.replace_blocks(replacements, None, cx);
})
}
fn create_output_area_render(execution_view: View<ExecutionView>) -> RenderBlock {
let render = move |cx: &mut BlockContext| {
let execution_view = execution_view.clone();
let text_font = ThemeSettings::get_global(cx).buffer_font.family.clone();
// Note: we'll want to use `cx.anchor_x` when someone runs something with no output -- just show a checkmark and not make the full block below the line
let gutter_width = cx.gutter_dimensions.width;
h_flex()
.w_full()
.bg(cx.theme().colors().background)
.border_y_1()
.border_color(cx.theme().colors().border)
.pl(gutter_width)
.child(
div()
.font_family(text_font)
// .ml(gutter_width)
.mx_1()
.my_2()
.h_full()
.w_full()
.mr(gutter_width)
.child(execution_view),
)
.into_any_element()
};
Box::new(render)
}
}
impl Session {
pub fn new(
editor: View<Editor>,
fs: Arc<dyn Fs>,
kernel_specification: KernelSpecification,
cx: &mut ViewContext<Self>,
) -> Self {
let entity_id = editor.entity_id();
let kernel = RunningKernel::new(kernel_specification.clone(), entity_id, fs.clone(), cx);
let pending_kernel = cx
.spawn(|this, mut cx| async move {
let kernel = kernel.await;
match kernel {
Ok((kernel, mut messages_rx)) => {
this.update(&mut cx, |this, cx| {
// At this point we can create a new kind of kernel that has the process and our long running background tasks
this.kernel = Kernel::RunningKernel(kernel);
this.messaging_task = cx.spawn(|session, mut cx| async move {
while let Some(message) = messages_rx.next().await {
session
.update(&mut cx, |session, cx| {
session.route(&message, cx);
})
.ok();
}
});
// For some reason sending a kernel info request will brick the ark (R) kernel.
// Note that Deno and Python do not have this issue.
if this.kernel_specification.name == "ark" {
return;
}
// Get kernel info after (possibly) letting the kernel start
cx.spawn(|this, mut cx| async move {
cx.background_executor()
.timer(Duration::from_millis(120))
.await;
this.update(&mut cx, |this, _cx| {
this.send(KernelInfoRequest {}.into(), _cx).ok();
})
.ok();
})
.detach();
})
.ok();
}
Err(err) => {
this.update(&mut cx, |this, _cx| {
this.kernel = Kernel::ErroredLaunch(err.to_string());
})
.ok();
}
}
})
.shared();
return Self {
editor,
kernel: Kernel::StartingKernel(pending_kernel),
messaging_task: Task::ready(()),
blocks: HashMap::default(),
kernel_specification,
};
}
fn send(&mut self, message: JupyterMessage, _cx: &mut ViewContext<Self>) -> anyhow::Result<()> {
match &mut self.kernel {
Kernel::RunningKernel(kernel) => {
kernel.request_tx.try_send(message).ok();
}
_ => {}
}
anyhow::Ok(())
}
pub fn clear_outputs(&mut self, cx: &mut ViewContext<Self>) {
let blocks_to_remove: HashSet<BlockId> =
self.blocks.values().map(|block| block.block_id).collect();
self.editor.update(cx, |editor, cx| {
editor.remove_blocks(blocks_to_remove, None, cx);
});
self.blocks.clear();
}
pub fn execute(&mut self, code: &str, anchor_range: Range<Anchor>, cx: &mut ViewContext<Self>) {
let execute_request = ExecuteRequest {
code: code.to_string(),
..ExecuteRequest::default()
};
let message: JupyterMessage = execute_request.into();
let mut blocks_to_remove: HashSet<BlockId> = HashSet::default();
let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
self.blocks.retain(|_key, block| {
if anchor_range.overlaps(&block.code_range, &buffer) {
blocks_to_remove.insert(block.block_id);
false
} else {
true
}
});
self.editor.update(cx, |editor, cx| {
editor.remove_blocks(blocks_to_remove, None, cx);
});
let status = match &self.kernel {
Kernel::RunningKernel(_) => ExecutionStatus::Queued,
Kernel::StartingKernel(_) => ExecutionStatus::ConnectingToKernel,
Kernel::ErroredLaunch(error) => ExecutionStatus::KernelErrored(error.clone()),
Kernel::ShuttingDown => ExecutionStatus::ShuttingDown,
Kernel::Shutdown => ExecutionStatus::Shutdown,
};
let editor_block = EditorBlock::new(self.editor.clone(), anchor_range, status, cx);
self.blocks
.insert(message.header.msg_id.clone(), editor_block);
match &self.kernel {
Kernel::RunningKernel(_) => {
self.send(message, cx).ok();
}
Kernel::StartingKernel(task) => {
// Queue up the execution as a task to run after the kernel starts
let task = task.clone();
let message = message.clone();
cx.spawn(|this, mut cx| async move {
task.await;
this.update(&mut cx, |this, cx| {
this.send(message, cx).ok();
})
.ok();
})
.detach();
}
_ => {}
}
}
fn route(&mut self, message: &JupyterMessage, cx: &mut ViewContext<Self>) {
let parent_message_id = match message.parent_header.as_ref() {
Some(header) => &header.msg_id,
None => return,
};
match &message.content {
JupyterMessageContent::Status(status) => {
self.kernel.set_execution_state(&status.execution_state);
cx.notify();
}
JupyterMessageContent::KernelInfoReply(reply) => {
self.kernel.set_kernel_info(&reply);
cx.notify();
}
_ => {}
}
if let Some(block) = self.blocks.get_mut(parent_message_id) {
block.handle_message(&message, cx);
return;
}
}
fn interrupt(&mut self, cx: &mut ViewContext<Self>) {
match &mut self.kernel {
Kernel::RunningKernel(_kernel) => {
self.send(InterruptRequest {}.into(), cx).ok();
}
Kernel::StartingKernel(_task) => {
// NOTE: If we switch to a literal queue instead of chaining on to the task, clear all queued executions
}
_ => {}
}
}
fn shutdown(&mut self, cx: &mut ViewContext<Self>) {
let kernel = std::mem::replace(&mut self.kernel, Kernel::ShuttingDown);
// todo!(): emit event for the runtime panel to remove this session once in shutdown state
match kernel {
Kernel::RunningKernel(mut kernel) => {
let mut request_tx = kernel.request_tx.clone();
cx.spawn(|this, mut cx| async move {
let message: JupyterMessage = ShutdownRequest { restart: false }.into();
request_tx.try_send(message).ok();
// Give the kernel a bit of time to clean up
cx.background_executor().timer(Duration::from_secs(3)).await;
kernel.process.kill().ok();
this.update(&mut cx, |this, cx| {
cx.emit(SessionEvent::Shutdown(this.editor.clone()));
this.clear_outputs(cx);
this.kernel = Kernel::Shutdown;
cx.notify();
})
.ok();
})
.detach();
}
Kernel::StartingKernel(_kernel) => {
self.kernel = Kernel::Shutdown;
}
_ => {
self.kernel = Kernel::Shutdown;
}
}
cx.notify();
}
}
pub enum SessionEvent {
Shutdown(View<Editor>),
}
impl EventEmitter<SessionEvent> for Session {}
impl Render for Session {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let mut buttons = vec![];
buttons.push(
ButtonLike::new("shutdown")
.child(Label::new("Shutdown"))
.style(ButtonStyle::Subtle)
.on_click(cx.listener(move |session, _, cx| {
session.shutdown(cx);
})),
);
let status_text = match &self.kernel {
Kernel::RunningKernel(kernel) => {
buttons.push(
ButtonLike::new("interrupt")
.child(Label::new("Interrupt"))
.style(ButtonStyle::Subtle)
.on_click(cx.listener(move |session, _, cx| {
session.interrupt(cx);
})),
);
let mut name = self.kernel_specification.name.clone();
if let Some(info) = &kernel.kernel_info {
name.push_str(" (");
name.push_str(&info.language_info.name);
name.push_str(")");
}
name
}
Kernel::StartingKernel(_) => format!("{} (Starting)", self.kernel_specification.name),
Kernel::ErroredLaunch(err) => {
format!("{} (Error: {})", self.kernel_specification.name, err)
}
Kernel::ShuttingDown => format!("{} (Shutting Down)", self.kernel_specification.name),
Kernel::Shutdown => format!("{} (Shutdown)", self.kernel_specification.name),
};
return v_flex()
.gap_1()
.child(
h_flex()
.gap_2()
.child(self.kernel.dot())
.child(Label::new(status_text)),
)
.child(h_flex().gap_2().children(buttons));
}
}

View File

@ -55,11 +55,11 @@ impl TerminalOutput {
pub fn render(&self, cx: &ViewContext<ExecutionView>) -> AnyElement {
let theme = cx.theme();
let buffer_font = ThemeSettings::get_global(cx).buffer_font.family.clone();
let mut text_runs = self.handler.text_runs.clone();
text_runs.push(self.handler.current_text_run.clone());
let runs = text_runs
let runs = self
.handler
.text_runs
.iter()
.chain(Some(&self.handler.current_text_run))
.map(|ansi_run| {
let color = terminal_view::terminal_element::convert_color(&ansi_run.fg, theme);
let background_color = Some(terminal_view::terminal_element::convert_color(
@ -88,16 +88,15 @@ impl TerminalOutput {
impl LineHeight for TerminalOutput {
fn num_lines(&self, _cx: &mut WindowContext) -> u8 {
// todo!(): Track this over time with our parser and just return it when needed
self.handler.buffer.lines().count() as u8
}
}
#[derive(Clone)]
struct AnsiTextRun {
pub len: usize,
pub fg: alacritty_terminal::vte::ansi::Color,
pub bg: alacritty_terminal::vte::ansi::Color,
len: usize,
fg: alacritty_terminal::vte::ansi::Color,
bg: alacritty_terminal::vte::ansi::Color,
}
impl AnsiTextRun {

View File

@ -221,7 +221,7 @@ fn init_ui(app_state: Arc<AppState>, cx: &mut AppContext) -> Result<()> {
assistant::init(app_state.fs.clone(), app_state.client.clone(), cx);
repl::init(app_state.fs.clone(), cx);
repl::init(cx);
cx.observe_global::<SettingsStore>({
let languages = app_state.languages.clone();

View File

@ -197,6 +197,10 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
cx.spawn(|workspace_handle, mut cx| async move {
let assistant_panel =
assistant::AssistantPanel::load(workspace_handle.clone(), cx.clone());
// todo!(): enable/disable this based on settings
let runtime_panel = repl::RuntimePanel::load(workspace_handle.clone(), cx.clone());
let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone());
let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
@ -214,6 +218,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
outline_panel,
terminal_panel,
assistant_panel,
runtime_panel,
channels_panel,
chat_panel,
notification_panel,
@ -222,6 +227,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
outline_panel,
terminal_panel,
assistant_panel,
runtime_panel,
channels_panel,
chat_panel,
notification_panel,
@ -229,6 +235,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
workspace_handle.update(&mut cx, |workspace, cx| {
workspace.add_panel(assistant_panel, cx);
workspace.add_panel(runtime_panel, cx);
workspace.add_panel(project_panel, cx);
workspace.add_panel(outline_panel, cx);
workspace.add_panel(terminal_panel, cx);
@ -3188,6 +3195,7 @@ mod tests {
outline_panel::init((), cx);
terminal_view::init(cx);
assistant::init(app_state.fs.clone(), app_state.client.clone(), cx);
repl::init(cx);
tasks_ui::init(cx);
initialize_workspace(app_state.clone(), cx);
app_state