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:
parent
821aa0811d
commit
c77ea47f43
12 changed files with 1438 additions and 965 deletions
|
@ -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>;
|
fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range<Anchor>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
149
crates/repl/src/jupyter_settings.rs
Normal file
149
crates/repl/src/jupyter_settings.rs
Normal 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
395
crates/repl/src/kernels.rs
Normal 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"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -301,14 +301,17 @@ impl From<&MimeBundle> for OutputType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default, Clone)]
|
||||||
pub enum ExecutionStatus {
|
pub enum ExecutionStatus {
|
||||||
#[default]
|
#[default]
|
||||||
Unknown,
|
Unknown,
|
||||||
#[allow(unused)]
|
|
||||||
ConnectingToKernel,
|
ConnectingToKernel,
|
||||||
|
Queued,
|
||||||
Executing,
|
Executing,
|
||||||
Finished,
|
Finished,
|
||||||
|
ShuttingDown,
|
||||||
|
Shutdown,
|
||||||
|
KernelErrored(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ExecutionView {
|
pub struct ExecutionView {
|
||||||
|
@ -317,10 +320,10 @@ pub struct ExecutionView {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExecutionView {
|
impl ExecutionView {
|
||||||
pub fn new(_cx: &mut ViewContext<Self>) -> Self {
|
pub fn new(status: ExecutionStatus, _cx: &mut ViewContext<Self>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
outputs: Default::default(),
|
outputs: Default::default(),
|
||||||
status: ExecutionStatus::Unknown,
|
status,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -358,14 +361,16 @@ impl ExecutionView {
|
||||||
self.outputs.push(output);
|
self.outputs.push(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Comments from @rgbkrk, reach out with questions
|
||||||
|
|
||||||
// Set next input adds text to the next cell. Not required to support.
|
// 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!(),
|
// runtimelib::Payload::SetNextInput { text, replace } => todo!(),
|
||||||
|
|
||||||
// Not likely to be used in the context of Zed, where someone could just open the buffer themselves
|
// 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!(),
|
// 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!(),
|
// runtimelib::Payload::AskExit { keepkernel } => todo!(),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
@ -431,28 +436,24 @@ impl ExecutionView {
|
||||||
new_terminal.append_text(text);
|
new_terminal.append_text(text);
|
||||||
Some(OutputType::Stream(new_terminal))
|
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 {
|
impl Render for ExecutionView {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
if self.outputs.len() == 0 {
|
if self.outputs.len() == 0 {
|
||||||
match self.status {
|
return match &self.status {
|
||||||
ExecutionStatus::ConnectingToKernel => {
|
ExecutionStatus::ConnectingToKernel => div().child("Connecting to kernel..."),
|
||||||
return div().child("Connecting to kernel...").into_any_element()
|
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()
|
div()
|
||||||
|
|
|
@ -1,51 +1,19 @@
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use async_dispatcher::{set_dispatcher, Dispatcher, Runnable};
|
||||||
use async_dispatcher::{set_dispatcher, timeout, Dispatcher, Runnable};
|
use gpui::{AppContext, PlatformDispatcher};
|
||||||
use collections::{HashMap, HashSet};
|
use settings::Settings as _;
|
||||||
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 std::{sync::Arc, time::Duration};
|
use std::{sync::Arc, time::Duration};
|
||||||
use theme::{ActiveTheme, ThemeSettings};
|
|
||||||
use ui::prelude::*;
|
|
||||||
use workspace::Workspace;
|
|
||||||
|
|
||||||
|
mod jupyter_settings;
|
||||||
|
mod kernels;
|
||||||
mod outputs;
|
mod outputs;
|
||||||
// mod runtime_panel;
|
mod runtime_panel;
|
||||||
mod runtime_settings;
|
mod session;
|
||||||
mod runtimes;
|
|
||||||
mod stdio;
|
mod stdio;
|
||||||
|
|
||||||
use runtimes::{get_runtime_specifications, Request, RunningKernel, RuntimeSpecification};
|
pub use jupyter_settings::JupyterSettings;
|
||||||
|
pub use runtime_panel::RuntimePanel;
|
||||||
|
|
||||||
actions!(repl, [Run]);
|
fn zed_dispatcher(cx: &mut AppContext) -> impl Dispatcher {
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct RuntimeManagerGlobal(Model<RuntimeManager>);
|
|
||||||
|
|
||||||
impl Global for RuntimeManagerGlobal {}
|
|
||||||
|
|
||||||
pub fn zed_dispatcher(cx: &mut AppContext) -> impl Dispatcher {
|
|
||||||
struct ZedDispatcher {
|
struct ZedDispatcher {
|
||||||
dispatcher: Arc<dyn PlatformDispatcher>,
|
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));
|
set_dispatcher(zed_dispatcher(cx));
|
||||||
JupyterSettings::register(cx);
|
JupyterSettings::register(cx);
|
||||||
|
runtime_panel::init(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)
|
|
||||||
}
|
}
|
||||||
|
|
443
crates/repl/src/runtime_panel.rs
Normal file
443
crates/repl/src/runtime_panel.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
400
crates/repl/src/session.rs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -55,11 +55,11 @@ impl TerminalOutput {
|
||||||
pub fn render(&self, cx: &ViewContext<ExecutionView>) -> AnyElement {
|
pub fn render(&self, cx: &ViewContext<ExecutionView>) -> AnyElement {
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let buffer_font = ThemeSettings::get_global(cx).buffer_font.family.clone();
|
let buffer_font = ThemeSettings::get_global(cx).buffer_font.family.clone();
|
||||||
let mut text_runs = self.handler.text_runs.clone();
|
let runs = self
|
||||||
text_runs.push(self.handler.current_text_run.clone());
|
.handler
|
||||||
|
.text_runs
|
||||||
let runs = text_runs
|
|
||||||
.iter()
|
.iter()
|
||||||
|
.chain(Some(&self.handler.current_text_run))
|
||||||
.map(|ansi_run| {
|
.map(|ansi_run| {
|
||||||
let color = terminal_view::terminal_element::convert_color(&ansi_run.fg, theme);
|
let color = terminal_view::terminal_element::convert_color(&ansi_run.fg, theme);
|
||||||
let background_color = Some(terminal_view::terminal_element::convert_color(
|
let background_color = Some(terminal_view::terminal_element::convert_color(
|
||||||
|
@ -88,16 +88,15 @@ impl TerminalOutput {
|
||||||
|
|
||||||
impl LineHeight for TerminalOutput {
|
impl LineHeight for TerminalOutput {
|
||||||
fn num_lines(&self, _cx: &mut WindowContext) -> u8 {
|
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
|
self.handler.buffer.lines().count() as u8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct AnsiTextRun {
|
struct AnsiTextRun {
|
||||||
pub len: usize,
|
len: usize,
|
||||||
pub fg: alacritty_terminal::vte::ansi::Color,
|
fg: alacritty_terminal::vte::ansi::Color,
|
||||||
pub bg: alacritty_terminal::vte::ansi::Color,
|
bg: alacritty_terminal::vte::ansi::Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AnsiTextRun {
|
impl AnsiTextRun {
|
||||||
|
|
|
@ -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);
|
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>({
|
cx.observe_global::<SettingsStore>({
|
||||||
let languages = app_state.languages.clone();
|
let languages = app_state.languages.clone();
|
||||||
|
|
|
@ -197,6 +197,10 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||||
cx.spawn(|workspace_handle, mut cx| async move {
|
cx.spawn(|workspace_handle, mut cx| async move {
|
||||||
let assistant_panel =
|
let assistant_panel =
|
||||||
assistant::AssistantPanel::load(workspace_handle.clone(), cx.clone());
|
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 project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
|
||||||
let outline_panel = OutlinePanel::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());
|
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,
|
outline_panel,
|
||||||
terminal_panel,
|
terminal_panel,
|
||||||
assistant_panel,
|
assistant_panel,
|
||||||
|
runtime_panel,
|
||||||
channels_panel,
|
channels_panel,
|
||||||
chat_panel,
|
chat_panel,
|
||||||
notification_panel,
|
notification_panel,
|
||||||
|
@ -222,6 +227,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||||
outline_panel,
|
outline_panel,
|
||||||
terminal_panel,
|
terminal_panel,
|
||||||
assistant_panel,
|
assistant_panel,
|
||||||
|
runtime_panel,
|
||||||
channels_panel,
|
channels_panel,
|
||||||
chat_panel,
|
chat_panel,
|
||||||
notification_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_handle.update(&mut cx, |workspace, cx| {
|
||||||
workspace.add_panel(assistant_panel, cx);
|
workspace.add_panel(assistant_panel, cx);
|
||||||
|
workspace.add_panel(runtime_panel, cx);
|
||||||
workspace.add_panel(project_panel, cx);
|
workspace.add_panel(project_panel, cx);
|
||||||
workspace.add_panel(outline_panel, cx);
|
workspace.add_panel(outline_panel, cx);
|
||||||
workspace.add_panel(terminal_panel, cx);
|
workspace.add_panel(terminal_panel, cx);
|
||||||
|
@ -3188,6 +3195,7 @@ mod tests {
|
||||||
outline_panel::init((), cx);
|
outline_panel::init((), cx);
|
||||||
terminal_view::init(cx);
|
terminal_view::init(cx);
|
||||||
assistant::init(app_state.fs.clone(), app_state.client.clone(), cx);
|
assistant::init(app_state.fs.clone(), app_state.client.clone(), cx);
|
||||||
|
repl::init(cx);
|
||||||
tasks_ui::init(cx);
|
tasks_ui::init(cx);
|
||||||
initialize_workspace(app_state.clone(), cx);
|
initialize_workspace(app_state.clone(), cx);
|
||||||
app_state
|
app_state
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue