mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-28 19:25:33 +03:00
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
@ -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>;
|
||||
}
|
||||
|
||||
|
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 {
|
||||
#[default]
|
||||
Unknown,
|
||||
#[allow(unused)]
|
||||
ConnectingToKernel,
|
||||
Queued,
|
||||
Executing,
|
||||
Finished,
|
||||
ShuttingDown,
|
||||
Shutdown,
|
||||
KernelErrored(String),
|
||||
}
|
||||
|
||||
pub struct ExecutionView {
|
||||
@ -317,10 +320,10 @@ pub struct ExecutionView {
|
||||
}
|
||||
|
||||
impl ExecutionView {
|
||||
pub fn new(_cx: &mut ViewContext<Self>) -> Self {
|
||||
pub fn new(status: ExecutionStatus, _cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
outputs: Default::default(),
|
||||
status: ExecutionStatus::Unknown,
|
||||
status,
|
||||
}
|
||||
}
|
||||
|
||||
@ -358,14 +361,16 @@ impl ExecutionView {
|
||||
self.outputs.push(output);
|
||||
}
|
||||
|
||||
// Comments from @rgbkrk, reach out with questions
|
||||
|
||||
// Set next input adds text to the next cell. Not required to support.
|
||||
// However, this could be implemented by
|
||||
// However, this could be implemented by adding text to the buffer.
|
||||
// runtimelib::Payload::SetNextInput { text, replace } => todo!(),
|
||||
|
||||
// Not likely to be used in the context of Zed, where someone could just open the buffer themselves
|
||||
// runtimelib::Payload::EditMagic { filename, line_number } => todo!(),
|
||||
|
||||
//
|
||||
// Ask the user if they want to exit the kernel. Not required to support.
|
||||
// runtimelib::Payload::AskExit { keepkernel } => todo!(),
|
||||
_ => {}
|
||||
}
|
||||
@ -431,28 +436,24 @@ impl ExecutionView {
|
||||
new_terminal.append_text(text);
|
||||
Some(OutputType::Stream(new_terminal))
|
||||
}
|
||||
|
||||
pub fn set_status(&mut self, status: ExecutionStatus, cx: &mut ViewContext<Self>) {
|
||||
self.status = status;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ExecutionView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
if self.outputs.len() == 0 {
|
||||
match self.status {
|
||||
ExecutionStatus::ConnectingToKernel => {
|
||||
return div().child("Connecting to kernel...").into_any_element()
|
||||
return match &self.status {
|
||||
ExecutionStatus::ConnectingToKernel => div().child("Connecting to kernel..."),
|
||||
ExecutionStatus::Executing => div().child("Executing..."),
|
||||
ExecutionStatus::Finished => div().child(Icon::new(IconName::Check)),
|
||||
ExecutionStatus::Unknown => div().child("..."),
|
||||
ExecutionStatus::ShuttingDown => div().child("Kernel shutting down..."),
|
||||
ExecutionStatus::Shutdown => div().child("Kernel shutdown"),
|
||||
ExecutionStatus::Queued => div().child("Queued"),
|
||||
ExecutionStatus::KernelErrored(error) => {
|
||||
div().child(format!("Kernel error: {}", error))
|
||||
}
|
||||
ExecutionStatus::Executing => {
|
||||
return div().child("Executing...").into_any_element()
|
||||
}
|
||||
ExecutionStatus::Finished => {
|
||||
return div().child(Icon::new(IconName::Check)).into_any_element()
|
||||
}
|
||||
ExecutionStatus::Unknown => return div().child("...").into_any_element(),
|
||||
}
|
||||
.into_any_element();
|
||||
}
|
||||
|
||||
div()
|
||||
|
@ -1,51 +1,19 @@
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use async_dispatcher::{set_dispatcher, timeout, Dispatcher, Runnable};
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
display_map::{
|
||||
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock,
|
||||
},
|
||||
Anchor, AnchorRangeExt, Editor,
|
||||
};
|
||||
use futures::{
|
||||
channel::mpsc::{self, UnboundedSender},
|
||||
future::Shared,
|
||||
Future, FutureExt, SinkExt as _, StreamExt,
|
||||
};
|
||||
use gpui::prelude::*;
|
||||
use gpui::{
|
||||
actions, AppContext, Context, EntityId, Global, Model, ModelContext, PlatformDispatcher, Task,
|
||||
WeakView,
|
||||
};
|
||||
use gpui::{Entity, View};
|
||||
use language::Point;
|
||||
use outputs::{ExecutionStatus, ExecutionView, LineHeight as _};
|
||||
use project::Fs;
|
||||
use runtime_settings::JupyterSettings;
|
||||
use runtimelib::JupyterMessageContent;
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use std::{ops::Range, time::Instant};
|
||||
use async_dispatcher::{set_dispatcher, Dispatcher, Runnable};
|
||||
use gpui::{AppContext, PlatformDispatcher};
|
||||
use settings::Settings as _;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use theme::{ActiveTheme, ThemeSettings};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
mod jupyter_settings;
|
||||
mod kernels;
|
||||
mod outputs;
|
||||
// mod runtime_panel;
|
||||
mod runtime_settings;
|
||||
mod runtimes;
|
||||
mod runtime_panel;
|
||||
mod session;
|
||||
mod stdio;
|
||||
|
||||
use runtimes::{get_runtime_specifications, Request, RunningKernel, RuntimeSpecification};
|
||||
pub use jupyter_settings::JupyterSettings;
|
||||
pub use runtime_panel::RuntimePanel;
|
||||
|
||||
actions!(repl, [Run]);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RuntimeManagerGlobal(Model<RuntimeManager>);
|
||||
|
||||
impl Global for RuntimeManagerGlobal {}
|
||||
|
||||
pub fn zed_dispatcher(cx: &mut AppContext) -> impl Dispatcher {
|
||||
fn zed_dispatcher(cx: &mut AppContext) -> impl Dispatcher {
|
||||
struct ZedDispatcher {
|
||||
dispatcher: Arc<dyn PlatformDispatcher>,
|
||||
}
|
||||
@ -69,503 +37,8 @@ pub fn zed_dispatcher(cx: &mut AppContext) -> impl Dispatcher {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
set_dispatcher(zed_dispatcher(cx));
|
||||
JupyterSettings::register(cx);
|
||||
|
||||
observe_jupyter_settings_changes(fs.clone(), cx);
|
||||
|
||||
cx.observe_new_views(
|
||||
|workspace: &mut Workspace, _: &mut ViewContext<Workspace>| {
|
||||
workspace.register_action(run);
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
let settings = JupyterSettings::get_global(cx);
|
||||
|
||||
if !settings.enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
initialize_runtime_manager(fs, cx);
|
||||
}
|
||||
|
||||
fn initialize_runtime_manager(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
let runtime_manager = cx.new_model(|cx| RuntimeManager::new(fs.clone(), cx));
|
||||
RuntimeManager::set_global(runtime_manager.clone(), cx);
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
let fs = fs.clone();
|
||||
|
||||
let runtime_specifications = get_runtime_specifications(fs).await?;
|
||||
|
||||
runtime_manager.update(&mut cx, |this, _cx| {
|
||||
this.runtime_specifications = runtime_specifications;
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn observe_jupyter_settings_changes(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
cx.observe_global::<SettingsStore>(move |cx| {
|
||||
let settings = JupyterSettings::get_global(cx);
|
||||
if settings.enabled && RuntimeManager::global(cx).is_none() {
|
||||
initialize_runtime_manager(fs.clone(), cx);
|
||||
} else {
|
||||
RuntimeManager::remove_global(cx);
|
||||
// todo!(): Remove action from workspace(s)
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Kernel {
|
||||
RunningKernel(RunningKernel),
|
||||
StartingKernel(Shared<Task<()>>),
|
||||
FailedLaunch,
|
||||
}
|
||||
|
||||
// Per workspace
|
||||
pub struct RuntimeManager {
|
||||
fs: Arc<dyn Fs>,
|
||||
runtime_specifications: Vec<RuntimeSpecification>,
|
||||
|
||||
instances: HashMap<EntityId, Kernel>,
|
||||
editors: HashMap<WeakView<Editor>, EditorRuntimeState>,
|
||||
// todo!(): Next
|
||||
// To reduce the number of open tasks and channels we have, let's feed the response
|
||||
// messages by ID over to the paired ExecutionView
|
||||
_execution_views_by_id: HashMap<String, View<ExecutionView>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct EditorRuntimeState {
|
||||
blocks: Vec<EditorRuntimeBlock>,
|
||||
// todo!(): Store a subscription to the editor so we can drop them when the editor is dropped
|
||||
// subscription: gpui::Subscription,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct EditorRuntimeBlock {
|
||||
code_range: Range<Anchor>,
|
||||
_execution_id: String,
|
||||
block_id: BlockId,
|
||||
_execution_view: View<ExecutionView>,
|
||||
}
|
||||
|
||||
impl RuntimeManager {
|
||||
pub fn new(fs: Arc<dyn Fs>, _cx: &mut AppContext) -> Self {
|
||||
Self {
|
||||
fs,
|
||||
runtime_specifications: Default::default(),
|
||||
instances: Default::default(),
|
||||
editors: Default::default(),
|
||||
_execution_views_by_id: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_or_launch_kernel(
|
||||
&mut self,
|
||||
entity_id: EntityId,
|
||||
language_name: Arc<str>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<UnboundedSender<Request>>> {
|
||||
let kernel = self.instances.get(&entity_id);
|
||||
let pending_kernel_start = match kernel {
|
||||
Some(Kernel::RunningKernel(running_kernel)) => {
|
||||
return Task::ready(anyhow::Ok(running_kernel.request_tx.clone()));
|
||||
}
|
||||
Some(Kernel::StartingKernel(task)) => task.clone(),
|
||||
Some(Kernel::FailedLaunch) | None => {
|
||||
self.instances.remove(&entity_id);
|
||||
|
||||
let kernel = self.launch_kernel(entity_id, language_name, cx);
|
||||
let pending_kernel = cx
|
||||
.spawn(|this, mut cx| async move {
|
||||
let running_kernel = kernel.await;
|
||||
|
||||
match running_kernel {
|
||||
Ok(running_kernel) => {
|
||||
let _ = this.update(&mut cx, |this, _cx| {
|
||||
this.instances
|
||||
.insert(entity_id, Kernel::RunningKernel(running_kernel));
|
||||
});
|
||||
}
|
||||
Err(_err) => {
|
||||
let _ = this.update(&mut cx, |this, _cx| {
|
||||
this.instances.insert(entity_id, Kernel::FailedLaunch);
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.shared();
|
||||
|
||||
self.instances
|
||||
.insert(entity_id, Kernel::StartingKernel(pending_kernel.clone()));
|
||||
|
||||
pending_kernel
|
||||
}
|
||||
};
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
pending_kernel_start.await;
|
||||
|
||||
this.update(&mut cx, |this, _cx| {
|
||||
let kernel = this
|
||||
.instances
|
||||
.get(&entity_id)
|
||||
.ok_or(anyhow!("unable to get a running kernel"))?;
|
||||
|
||||
match kernel {
|
||||
Kernel::RunningKernel(running_kernel) => Ok(running_kernel.request_tx.clone()),
|
||||
_ => Err(anyhow!("unable to get a running kernel")),
|
||||
}
|
||||
})?
|
||||
})
|
||||
}
|
||||
|
||||
fn launch_kernel(
|
||||
&mut self,
|
||||
entity_id: EntityId,
|
||||
language_name: Arc<str>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<RunningKernel>> {
|
||||
// Get first runtime that matches the language name (for now)
|
||||
let runtime_specification =
|
||||
self.runtime_specifications
|
||||
.iter()
|
||||
.find(|runtime_specification| {
|
||||
runtime_specification.kernelspec.language == language_name.to_string()
|
||||
});
|
||||
|
||||
let runtime_specification = match runtime_specification {
|
||||
Some(runtime_specification) => runtime_specification,
|
||||
None => {
|
||||
return Task::ready(Err(anyhow::anyhow!(
|
||||
"No runtime found for language {}",
|
||||
language_name
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let runtime_specification = runtime_specification.clone();
|
||||
|
||||
let fs = self.fs.clone();
|
||||
|
||||
cx.spawn(|_, cx| async move {
|
||||
let running_kernel =
|
||||
RunningKernel::new(runtime_specification, entity_id, fs.clone(), cx);
|
||||
|
||||
let running_kernel = running_kernel.await?;
|
||||
|
||||
let mut request_tx = running_kernel.request_tx.clone();
|
||||
|
||||
let overall_timeout_duration = Duration::from_secs(10);
|
||||
|
||||
let start_time = Instant::now();
|
||||
|
||||
loop {
|
||||
if start_time.elapsed() > overall_timeout_duration {
|
||||
// todo!(): Kill the kernel
|
||||
return Err(anyhow::anyhow!("Kernel did not respond in time"));
|
||||
}
|
||||
|
||||
let (tx, rx) = mpsc::unbounded();
|
||||
match request_tx
|
||||
.send(Request {
|
||||
request: runtimelib::KernelInfoRequest {}.into(),
|
||||
responses_rx: tx,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(_err) => {
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let mut rx = rx.fuse();
|
||||
|
||||
let kernel_info_timeout = Duration::from_secs(1);
|
||||
|
||||
let mut got_kernel_info = false;
|
||||
while let Ok(Some(message)) = timeout(kernel_info_timeout, rx.next()).await {
|
||||
match message {
|
||||
JupyterMessageContent::KernelInfoReply(_) => {
|
||||
got_kernel_info = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if got_kernel_info {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok(running_kernel)
|
||||
})
|
||||
}
|
||||
|
||||
fn execute_code(
|
||||
&mut self,
|
||||
entity_id: EntityId,
|
||||
language_name: Arc<str>,
|
||||
code: String,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> impl Future<Output = Result<mpsc::UnboundedReceiver<JupyterMessageContent>>> {
|
||||
let (tx, rx) = mpsc::unbounded();
|
||||
|
||||
let request_tx = self.get_or_launch_kernel(entity_id, language_name, cx);
|
||||
|
||||
async move {
|
||||
let request_tx = request_tx.await?;
|
||||
|
||||
request_tx
|
||||
.unbounded_send(Request {
|
||||
request: runtimelib::ExecuteRequest {
|
||||
code,
|
||||
allow_stdin: false,
|
||||
silent: false,
|
||||
store_history: true,
|
||||
stop_on_error: true,
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
responses_rx: tx,
|
||||
})
|
||||
.context("Failed to send execution request")?;
|
||||
|
||||
Ok(rx)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn global(cx: &AppContext) -> Option<Model<Self>> {
|
||||
cx.try_global::<RuntimeManagerGlobal>()
|
||||
.map(|runtime_manager| runtime_manager.0.clone())
|
||||
}
|
||||
|
||||
pub fn set_global(runtime_manager: Model<Self>, cx: &mut AppContext) {
|
||||
cx.set_global(RuntimeManagerGlobal(runtime_manager));
|
||||
}
|
||||
|
||||
pub fn remove_global(cx: &mut AppContext) {
|
||||
if RuntimeManager::global(cx).is_some() {
|
||||
cx.remove_global::<RuntimeManagerGlobal>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_active_editor(
|
||||
workspace: &mut Workspace,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> Option<View<Editor>> {
|
||||
workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.act_as::<Editor>(cx))
|
||||
}
|
||||
|
||||
// Gets the active selection in the editor or the current line
|
||||
pub fn selection(editor: View<Editor>, cx: &mut ViewContext<Workspace>) -> Range<Anchor> {
|
||||
let editor = editor.read(cx);
|
||||
let selection = editor.selections.newest::<usize>(cx);
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
|
||||
let range = if selection.is_empty() {
|
||||
let cursor = selection.head();
|
||||
|
||||
let line_start = buffer.offset_to_point(cursor).row;
|
||||
let mut start_offset = buffer.point_to_offset(Point::new(line_start, 0));
|
||||
|
||||
// Iterate backwards to find the start of the line
|
||||
while start_offset > 0 {
|
||||
let ch = buffer.chars_at(start_offset - 1).next().unwrap_or('\0');
|
||||
if ch == '\n' {
|
||||
break;
|
||||
}
|
||||
start_offset -= 1;
|
||||
}
|
||||
|
||||
let mut end_offset = cursor;
|
||||
|
||||
// Iterate forwards to find the end of the line
|
||||
while end_offset < buffer.len() {
|
||||
let ch = buffer.chars_at(end_offset).next().unwrap_or('\0');
|
||||
if ch == '\n' {
|
||||
break;
|
||||
}
|
||||
end_offset += 1;
|
||||
}
|
||||
|
||||
// Create a range from the start to the end of the line
|
||||
start_offset..end_offset
|
||||
} else {
|
||||
selection.range()
|
||||
};
|
||||
|
||||
let anchor_range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
|
||||
anchor_range
|
||||
}
|
||||
|
||||
pub fn run(workspace: &mut Workspace, _: &Run, cx: &mut ViewContext<Workspace>) {
|
||||
let (editor, runtime_manager) = if let (Some(editor), Some(runtime_manager)) =
|
||||
(get_active_editor(workspace, cx), RuntimeManager::global(cx))
|
||||
{
|
||||
(editor, runtime_manager)
|
||||
} else {
|
||||
log::warn!("No active editor or runtime manager found");
|
||||
return;
|
||||
};
|
||||
|
||||
let anchor_range = selection(editor.clone(), cx);
|
||||
|
||||
let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||
|
||||
let selected_text = buffer
|
||||
.text_for_range(anchor_range.clone())
|
||||
.collect::<String>();
|
||||
|
||||
let start_language = buffer.language_at(anchor_range.start);
|
||||
let end_language = buffer.language_at(anchor_range.end);
|
||||
|
||||
let language_name = if start_language == end_language {
|
||||
start_language
|
||||
.map(|language| language.code_fence_block_name())
|
||||
.filter(|lang| **lang != *"markdown")
|
||||
} else {
|
||||
// If the selection spans multiple languages, don't run it
|
||||
return;
|
||||
};
|
||||
|
||||
let language_name = if let Some(language_name) = language_name {
|
||||
language_name
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let entity_id = editor.entity_id();
|
||||
|
||||
let execution_view = cx.new_view(|cx| ExecutionView::new(cx));
|
||||
|
||||
// If any block overlaps with the new block, remove it
|
||||
// TODO: When inserting a new block, put it in order so that search is efficient
|
||||
let blocks_to_remove = runtime_manager.update(cx, |runtime_manager, _cx| {
|
||||
// Get the current `EditorRuntimeState` for this runtime_manager, inserting it if it doesn't exist
|
||||
let editor_runtime_state = runtime_manager
|
||||
.editors
|
||||
.entry(editor.downgrade())
|
||||
.or_insert_with(|| EditorRuntimeState { blocks: Vec::new() });
|
||||
|
||||
let mut blocks_to_remove: HashSet<BlockId> = HashSet::default();
|
||||
|
||||
editor_runtime_state.blocks.retain(|block| {
|
||||
if anchor_range.overlaps(&block.code_range, &buffer) {
|
||||
blocks_to_remove.insert(block.block_id);
|
||||
// Drop this block
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
blocks_to_remove
|
||||
});
|
||||
|
||||
let blocks_to_remove = blocks_to_remove.clone();
|
||||
|
||||
let block_id = editor.update(cx, |editor, cx| {
|
||||
editor.remove_blocks(blocks_to_remove, None, cx);
|
||||
let block = BlockProperties {
|
||||
position: anchor_range.end,
|
||||
height: execution_view.num_lines(cx).saturating_add(1),
|
||||
style: BlockStyle::Sticky,
|
||||
render: create_output_area_render(execution_view.clone()),
|
||||
disposition: BlockDisposition::Below,
|
||||
};
|
||||
|
||||
editor.insert_blocks([block], None, cx)[0]
|
||||
});
|
||||
|
||||
let receiver = runtime_manager.update(cx, |runtime_manager, cx| {
|
||||
let editor_runtime_state = runtime_manager
|
||||
.editors
|
||||
.entry(editor.downgrade())
|
||||
.or_insert_with(|| EditorRuntimeState { blocks: Vec::new() });
|
||||
|
||||
let editor_runtime_block = EditorRuntimeBlock {
|
||||
code_range: anchor_range.clone(),
|
||||
block_id,
|
||||
_execution_view: execution_view.clone(),
|
||||
_execution_id: Default::default(),
|
||||
};
|
||||
|
||||
editor_runtime_state
|
||||
.blocks
|
||||
.push(editor_runtime_block.clone());
|
||||
|
||||
runtime_manager.execute_code(entity_id, language_name, selected_text.clone(), cx)
|
||||
});
|
||||
|
||||
cx.spawn(|_this, mut cx| async move {
|
||||
execution_view.update(&mut cx, |execution_view, cx| {
|
||||
execution_view.set_status(ExecutionStatus::ConnectingToKernel, cx);
|
||||
})?;
|
||||
let mut receiver = receiver.await?;
|
||||
|
||||
let execution_view = execution_view.clone();
|
||||
while let Some(content) = receiver.next().await {
|
||||
execution_view.update(&mut cx, |execution_view, cx| {
|
||||
execution_view.push_message(&content, cx)
|
||||
})?;
|
||||
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
let mut replacements = HashMap::default();
|
||||
replacements.insert(
|
||||
block_id,
|
||||
(
|
||||
Some(execution_view.num_lines(cx).saturating_add(1)),
|
||||
create_output_area_render(execution_view.clone()),
|
||||
),
|
||||
);
|
||||
editor.replace_blocks(replacements, None, cx);
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn create_output_area_render(execution_view: View<ExecutionView>) -> RenderBlock {
|
||||
let render = move |cx: &mut BlockContext| {
|
||||
let execution_view = execution_view.clone();
|
||||
let text_font = ThemeSettings::get_global(cx).buffer_font.family.clone();
|
||||
// Note: we'll want to use `cx.anchor_x` when someone runs something with no output -- just show a checkmark and not make the full block below the line
|
||||
|
||||
let gutter_width = cx.gutter_dimensions.width;
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
.bg(cx.theme().colors().background)
|
||||
.border_y_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.pl(gutter_width)
|
||||
.child(
|
||||
div()
|
||||
.font_family(text_font)
|
||||
// .ml(gutter_width)
|
||||
.mx_1()
|
||||
.my_2()
|
||||
.h_full()
|
||||
.w_full()
|
||||
.mr(gutter_width)
|
||||
.child(execution_view),
|
||||
)
|
||||
.into_any_element()
|
||||
};
|
||||
|
||||
Box::new(render)
|
||||
runtime_panel::init(cx)
|
||||
}
|
||||
|
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 {
|
||||
let theme = cx.theme();
|
||||
let buffer_font = ThemeSettings::get_global(cx).buffer_font.family.clone();
|
||||
let mut text_runs = self.handler.text_runs.clone();
|
||||
text_runs.push(self.handler.current_text_run.clone());
|
||||
|
||||
let runs = text_runs
|
||||
let runs = self
|
||||
.handler
|
||||
.text_runs
|
||||
.iter()
|
||||
.chain(Some(&self.handler.current_text_run))
|
||||
.map(|ansi_run| {
|
||||
let color = terminal_view::terminal_element::convert_color(&ansi_run.fg, theme);
|
||||
let background_color = Some(terminal_view::terminal_element::convert_color(
|
||||
@ -88,16 +88,15 @@ impl TerminalOutput {
|
||||
|
||||
impl LineHeight for TerminalOutput {
|
||||
fn num_lines(&self, _cx: &mut WindowContext) -> u8 {
|
||||
// todo!(): Track this over time with our parser and just return it when needed
|
||||
self.handler.buffer.lines().count() as u8
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AnsiTextRun {
|
||||
pub len: usize,
|
||||
pub fg: alacritty_terminal::vte::ansi::Color,
|
||||
pub bg: alacritty_terminal::vte::ansi::Color,
|
||||
len: usize,
|
||||
fg: alacritty_terminal::vte::ansi::Color,
|
||||
bg: alacritty_terminal::vte::ansi::Color,
|
||||
}
|
||||
|
||||
impl AnsiTextRun {
|
||||
|
@ -221,7 +221,7 @@ fn init_ui(app_state: Arc<AppState>, cx: &mut AppContext) -> Result<()> {
|
||||
|
||||
assistant::init(app_state.fs.clone(), app_state.client.clone(), cx);
|
||||
|
||||
repl::init(app_state.fs.clone(), cx);
|
||||
repl::init(cx);
|
||||
|
||||
cx.observe_global::<SettingsStore>({
|
||||
let languages = app_state.languages.clone();
|
||||
|
@ -197,6 +197,10 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||
cx.spawn(|workspace_handle, mut cx| async move {
|
||||
let assistant_panel =
|
||||
assistant::AssistantPanel::load(workspace_handle.clone(), cx.clone());
|
||||
|
||||
// todo!(): enable/disable this based on settings
|
||||
let runtime_panel = repl::RuntimePanel::load(workspace_handle.clone(), cx.clone());
|
||||
|
||||
let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
|
||||
let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone());
|
||||
let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
|
||||
@ -214,6 +218,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||
outline_panel,
|
||||
terminal_panel,
|
||||
assistant_panel,
|
||||
runtime_panel,
|
||||
channels_panel,
|
||||
chat_panel,
|
||||
notification_panel,
|
||||
@ -222,6 +227,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||
outline_panel,
|
||||
terminal_panel,
|
||||
assistant_panel,
|
||||
runtime_panel,
|
||||
channels_panel,
|
||||
chat_panel,
|
||||
notification_panel,
|
||||
@ -229,6 +235,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||
|
||||
workspace_handle.update(&mut cx, |workspace, cx| {
|
||||
workspace.add_panel(assistant_panel, cx);
|
||||
workspace.add_panel(runtime_panel, cx);
|
||||
workspace.add_panel(project_panel, cx);
|
||||
workspace.add_panel(outline_panel, cx);
|
||||
workspace.add_panel(terminal_panel, cx);
|
||||
@ -3188,6 +3195,7 @@ mod tests {
|
||||
outline_panel::init((), cx);
|
||||
terminal_view::init(cx);
|
||||
assistant::init(app_state.fs.clone(), app_state.client.clone(), cx);
|
||||
repl::init(cx);
|
||||
tasks_ui::init(cx);
|
||||
initialize_workspace(app_state.clone(), cx);
|
||||
app_state
|
||||
|
Loading…
Reference in New Issue
Block a user