Detailed changes
@@ -86,6 +86,30 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "alacritty_terminal"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6d1ea4484c8676f295307a4892d478c70ac8da1dbd8c7c10830a504b7f1022f"
+dependencies = [
+ "base64 0.22.0",
+ "bitflags 2.4.2",
+ "home",
+ "libc",
+ "log",
+ "miow",
+ "parking_lot",
+ "piper",
+ "polling 3.3.2",
+ "regex-automata 0.4.5",
+ "rustix-openpty",
+ "serde",
+ "signal-hook",
+ "unicode-width",
+ "vte",
+ "windows-sys 0.48.0",
+]
+
[[package]]
name = "alacritty_terminal"
version = "0.24.1-dev"
@@ -424,6 +448,16 @@ dependencies = [
"util",
]
+[[package]]
+name = "async-attributes"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5"
+dependencies = [
+ "quote",
+ "syn 1.0.109",
+]
+
[[package]]
name = "async-broadcast"
version = "0.7.0"
@@ -487,6 +521,16 @@ dependencies = [
"pin-project-lite",
]
+[[package]]
+name = "async-dispatcher"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c8bff43baa5b0ca8f8bcd7f9338f5d30fbd75236a2aa89130a7c5121a06d6ca"
+dependencies = [
+ "async-task",
+ "futures-lite 1.13.0",
+]
+
[[package]]
name = "async-executor"
version = "1.5.1"
@@ -736,6 +780,7 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d"
dependencies = [
+ "async-attributes",
"async-channel 1.9.0",
"async-global-executor",
"async-io 1.13.0",
@@ -838,6 +883,19 @@ dependencies = [
"thiserror",
]
+[[package]]
+name = "asynchronous-codec"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a860072022177f903e59730004fb5dc13db9275b79bb2aef7ba8ce831956c233"
+dependencies = [
+ "bytes 1.5.0",
+ "futures-sink",
+ "futures-util",
+ "memchr",
+ "pin-project-lite",
+]
+
[[package]]
name = "atoi"
version = "2.0.0"
@@ -1999,9 +2057,9 @@ dependencies = [
[[package]]
name = "chrono"
-version = "0.4.31"
+version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
+checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
@@ -2009,7 +2067,7 @@ dependencies = [
"num-traits",
"serde",
"wasm-bindgen",
- "windows-targets 0.48.5",
+ "windows-targets 0.52.5",
]
[[package]]
@@ -3340,7 +3398,7 @@ version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309"
dependencies = [
- "dirs-sys",
+ "dirs-sys 0.3.7",
]
[[package]]
@@ -3349,7 +3407,16 @@ version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
dependencies = [
- "dirs-sys",
+ "dirs-sys 0.3.7",
+]
+
+[[package]]
+name = "dirs"
+version = "5.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
+dependencies = [
+ "dirs-sys 0.4.1",
]
[[package]]
@@ -3373,6 +3440,18 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "dirs-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.48.0",
+]
+
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
@@ -4922,7 +5001,7 @@ dependencies = [
"project",
"rpc",
"settings",
- "shellexpand",
+ "shellexpand 2.1.2",
"signal-hook",
"util",
]
@@ -5620,7 +5699,7 @@ dependencies = [
"schemars",
"serde",
"settings",
- "shellexpand",
+ "shellexpand 2.1.2",
"workspace",
]
@@ -7071,6 +7150,12 @@ dependencies = [
"vcpkg",
]
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
[[package]]
name = "ordered-float"
version = "2.10.0"
@@ -8392,6 +8477,38 @@ dependencies = [
"thiserror",
]
+[[package]]
+name = "repl"
+version = "0.1.0"
+dependencies = [
+ "alacritty_terminal 0.23.0",
+ "anyhow",
+ "async-dispatcher",
+ "base64 0.13.1",
+ "collections",
+ "editor",
+ "env_logger",
+ "futures 0.3.28",
+ "gpui",
+ "http 0.1.0",
+ "image",
+ "language",
+ "log",
+ "project",
+ "runtimelib",
+ "schemars",
+ "serde",
+ "serde_json",
+ "settings",
+ "smol",
+ "terminal_view",
+ "theme",
+ "ui",
+ "util",
+ "uuid",
+ "workspace",
+]
+
[[package]]
name = "reqwest"
version = "0.11.20"
@@ -8637,6 +8754,32 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "runtimelib"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a4a788465cf51b7ac8f36e4e4ca3dd26013dcddd5ba8376f98752278244294"
+dependencies = [
+ "anyhow",
+ "async-dispatcher",
+ "async-std",
+ "base64 0.22.0",
+ "bytes 1.5.0",
+ "chrono",
+ "data-encoding",
+ "dirs 5.0.1",
+ "futures 0.3.28",
+ "glob",
+ "rand 0.8.5",
+ "ring",
+ "serde",
+ "serde_json",
+ "shellexpand 3.1.0",
+ "smol",
+ "uuid",
+ "zeromq",
+]
+
[[package]]
name = "rust-embed"
version = "8.4.0"
@@ -9380,6 +9523,15 @@ dependencies = [
"dirs 4.0.0",
]
+[[package]]
+name = "shellexpand"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b"
+dependencies = [
+ "dirs 5.0.1",
+]
+
[[package]]
name = "shlex"
version = "1.3.0"
@@ -9611,12 +9763,12 @@ dependencies = [
[[package]]
name = "socket2"
-version = "0.5.4"
+version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e"
+checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
dependencies = [
"libc",
- "windows-sys 0.48.0",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -10359,7 +10511,7 @@ dependencies = [
"serde",
"serde_json_lenient",
"sha2 0.10.7",
- "shellexpand",
+ "shellexpand 2.1.2",
"util",
]
@@ -10433,7 +10585,7 @@ dependencies = [
name = "terminal"
version = "0.1.0"
dependencies = [
- "alacritty_terminal",
+ "alacritty_terminal 0.24.1-dev",
"anyhow",
"collections",
"dirs 4.0.0",
@@ -10475,7 +10627,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
- "shellexpand",
+ "shellexpand 2.1.2",
"smol",
"task",
"tasks_ui",
@@ -10754,9 +10906,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
-version = "1.32.0"
+version = "1.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
+checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
dependencies = [
"backtrace",
"bytes 1.5.0",
@@ -10766,7 +10918,7 @@ dependencies = [
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
- "socket2 0.5.4",
+ "socket2 0.5.7",
"tokio-macros",
"windows-sys 0.48.0",
]
@@ -10784,9 +10936,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
-version = "2.1.0"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
+checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [
"proc-macro2",
"quote",
@@ -11576,9 +11728,9 @@ dependencies = [
[[package]]
name = "uuid"
-version = "1.4.1"
+version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
+checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
dependencies = [
"getrandom 0.2.10",
"serde",
@@ -12431,7 +12583,7 @@ dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
- "shellexpand",
+ "shellexpand 2.1.2",
"syn 2.0.59",
"witx",
]
@@ -13334,6 +13486,7 @@ dependencies = [
"quick_action_bar",
"recent_projects",
"release_channel",
+ "repl",
"rope",
"search",
"serde",
@@ -13620,6 +13773,33 @@ dependencies = [
"syn 2.0.59",
]
+[[package]]
+name = "zeromq"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb0560d00172817b7f7c2265060783519c475702ae290b154115ca75e976d4d0"
+dependencies = [
+ "async-dispatcher",
+ "async-std",
+ "async-trait",
+ "asynchronous-codec",
+ "bytes 1.5.0",
+ "crossbeam-queue",
+ "dashmap",
+ "futures-channel",
+ "futures-io",
+ "futures-task",
+ "futures-util",
+ "log",
+ "num-traits",
+ "once_cell",
+ "parking_lot",
+ "rand 0.8.5",
+ "regex",
+ "thiserror",
+ "uuid",
+]
+
[[package]]
name = "zstd"
version = "0.11.2+zstd.1.5.2"
@@ -77,6 +77,7 @@ members = [
"crates/refineable/derive_refineable",
"crates/release_channel",
"crates/dev_server_projects",
+ "crates/repl",
"crates/rich_text",
"crates/rope",
"crates/rpc",
@@ -227,6 +228,7 @@ quick_action_bar = { path = "crates/quick_action_bar" }
recent_projects = { path = "crates/recent_projects" }
release_channel = { path = "crates/release_channel" }
dev_server_projects = { path = "crates/dev_server_projects" }
+repl = { path = "crates/repl" }
rich_text = { path = "crates/rich_text" }
rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" }
@@ -264,10 +266,12 @@ workspace = { path = "crates/workspace" }
zed = { path = "crates/zed" }
zed_actions = { path = "crates/zed_actions" }
+alacritty_terminal = "0.23"
anyhow = "1.0.57"
any_vec = "0.13"
ashpd = "0.8.0"
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
+async-dispatcher = { version = "0.1"}
async-fs = "1.6"
async-recursion = "1.0.0"
async-tar = "0.4.2"
@@ -301,6 +305,7 @@ heed = { version = "0.20.1", features = ["read-txn-no-tls"] }
hex = "0.4.3"
html5ever = "0.27.0"
ignore = "0.4.22"
+image = "0.23"
indexmap = { version = "1.6.2", features = ["serde"] }
indoc = "1"
# We explicitly disable http2 support in isahc.
@@ -333,6 +338,7 @@ rand = "0.8.5"
refineable = { path = "./crates/refineable" }
regex = "1.5"
repair_json = "0.1.0"
+runtimelib = { version="0.12", default-features = false, features = ["async-dispatcher-runtime"] }
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
rust-embed = { version = "8.4", features = ["include-exclude"] }
schemars = "0.8"
@@ -379,6 +379,7 @@ fn metadata_from_extension_and_version(
pub fn convert_time_to_chrono(time: time::PrimitiveDateTime) -> chrono::DateTime<Utc> {
chrono::DateTime::from_naive_utc_and_offset(
+ #[allow(deprecated)]
chrono::NaiveDateTime::from_timestamp_opt(time.assume_utc().unix_timestamp(), 0).unwrap(),
Utc,
)
@@ -25,14 +25,16 @@ use rand::rngs::StdRng;
/// for spawning background tasks.
#[derive(Clone)]
pub struct BackgroundExecutor {
- dispatcher: Arc<dyn PlatformDispatcher>,
+ #[doc(hidden)]
+ pub dispatcher: Arc<dyn PlatformDispatcher>,
}
/// A pointer to the executor that is currently running,
/// for spawning tasks on the main thread.
#[derive(Clone)]
pub struct ForegroundExecutor {
- dispatcher: Arc<dyn PlatformDispatcher>,
+ #[doc(hidden)]
+ pub dispatcher: Arc<dyn PlatformDispatcher>,
not_send: PhantomData<Rc<()>>,
}
@@ -117,6 +117,7 @@ impl ToPoint for Anchor {
pub trait AnchorRangeExt {
fn cmp(&self, b: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> Ordering;
+ fn overlaps(&self, b: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> bool;
fn to_offset(&self, content: &MultiBufferSnapshot) -> Range<usize>;
fn to_point(&self, content: &MultiBufferSnapshot) -> Range<Point>;
}
@@ -129,6 +130,14 @@ impl AnchorRangeExt for Range<Anchor> {
}
}
+ fn overlaps(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> bool {
+ let start_cmp = self.start.cmp(&other.start, buffer);
+ let end_cmp = self.end.cmp(&other.end, buffer);
+
+ (start_cmp == Ordering::Less || start_cmp == Ordering::Equal)
+ && (end_cmp == Ordering::Greater || end_cmp == Ordering::Equal)
+ }
+
fn to_offset(&self, content: &MultiBufferSnapshot) -> Range<usize> {
self.start.to_offset(content)..self.end.to_offset(content)
}
@@ -0,0 +1,49 @@
+[package]
+name = "repl"
+version = "0.1.0"
+edition = "2021"
+publish = false
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/repl.rs"
+doctest = false
+
+[dependencies]
+anyhow.workspace = true
+alacritty_terminal.workspace = true
+async-dispatcher.workspace = true
+base64.workspace = true
+collections.workspace = true
+editor.workspace = true
+gpui.workspace = true
+futures.workspace = true
+image.workspace = true
+language.workspace = true
+log.workspace = true
+project.workspace = true
+runtimelib.workspace = true
+schemars.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+settings.workspace = true
+smol.workspace = true
+theme.workspace = true
+terminal_view.workspace = true
+ui.workspace = true
+uuid.workspace = true
+workspace.workspace = true
+
+[dev-dependencies]
+editor = { workspace = true, features = ["test-support"] }
+env_logger.workspace = true
+gpui = { workspace = true, features = ["test-support"] }
+language = { workspace = true, features = ["test-support"] }
+project = { workspace = true, features = ["test-support"] }
+settings = { workspace = true, features = ["test-support"] }
+theme = { workspace = true, features = ["test-support"] }
+util = { workspace = true, features = ["test-support"] }
+http = { workspace = true, features = ["test-support"] }
@@ -0,0 +1,479 @@
+use std::sync::Arc;
+
+use crate::stdio::TerminalOutput;
+use anyhow::Result;
+use gpui::{img, AnyElement, FontWeight, ImageData, Render, View};
+use runtimelib::datatable::TableSchema;
+use runtimelib::media::datatable::TabularDataResource;
+use runtimelib::{ExecutionState, JupyterMessageContent, MimeBundle, MimeType};
+use serde_json::Value;
+use ui::{div, prelude::*, v_flex, IntoElement, Styled, ViewContext};
+
+// Given these outputs are destined for the editor with the block decorations API, all of them must report
+// how many lines they will take up in the editor.
+pub trait LineHeight: Sized {
+ fn num_lines(&self, cx: &mut WindowContext) -> u8;
+}
+
+// When deciding what to render from a collection of mediatypes, we need to rank them in order of importance
+fn rank_mime_type(mimetype: &MimeType) -> usize {
+ match mimetype {
+ MimeType::DataTable(_) => 6,
+ MimeType::Png(_) => 4,
+ MimeType::Jpeg(_) => 3,
+ MimeType::Markdown(_) => 2,
+ MimeType::Plain(_) => 1,
+ // All other media types are not supported in Zed at this time
+ _ => 0,
+ }
+}
+
+/// ImageView renders an image inline in an editor, adapting to the line height to fit the image.
+pub struct ImageView {
+ height: u32,
+ width: u32,
+ image: Arc<ImageData>,
+}
+
+impl ImageView {
+ fn render(&self, cx: &ViewContext<ExecutionView>) -> AnyElement {
+ let line_height = cx.line_height();
+
+ let (height, width) = if self.height as f32 / line_height.0 == u8::MAX as f32 {
+ let height = u8::MAX as f32 * line_height.0;
+ let width = self.width as f32 * height / self.height as f32;
+ (height, width)
+ } else {
+ (self.height as f32, self.width as f32)
+ };
+
+ let image = self.image.clone();
+
+ div()
+ .h(Pixels(height))
+ .w(Pixels(width))
+ .child(img(image))
+ .into_any_element()
+ }
+
+ fn from(base64_encoded_data: &str) -> Result<Self> {
+ let bytes = base64::decode(base64_encoded_data)?;
+
+ let format = image::guess_format(&bytes)?;
+ let data = image::load_from_memory_with_format(&bytes, format)?.into_bgra8();
+
+ let height = data.height();
+ let width = data.width();
+
+ let gpui_image_data = ImageData::new(data);
+
+ return Ok(ImageView {
+ height,
+ width,
+ image: Arc::new(gpui_image_data),
+ });
+ }
+}
+
+impl LineHeight for ImageView {
+ fn num_lines(&self, cx: &mut WindowContext) -> u8 {
+ let line_height = cx.line_height();
+
+ let lines = self.height as f32 / line_height.0;
+
+ if lines > u8::MAX as f32 {
+ return u8::MAX;
+ }
+ lines as u8
+ }
+}
+
+/// TableView renders a static table inline in a buffer.
+/// It uses the https://specs.frictionlessdata.io/tabular-data-resource/ specification for data interchange.
+pub struct TableView {
+ pub table: TabularDataResource,
+}
+
+impl TableView {
+ pub fn render(&self, cx: &ViewContext<ExecutionView>) -> AnyElement {
+ let data = match &self.table.data {
+ Some(data) => data,
+ None => return div().into_any_element(),
+ };
+
+ // todo!(): compute the width of each column by finding the widest cell in each column
+
+ let mut headings = serde_json::Map::new();
+ for field in &self.table.schema.fields {
+ headings.insert(field.name.clone(), Value::String(field.name.clone()));
+ }
+ let header = self.render_row(&self.table.schema, true, &Value::Object(headings), cx);
+
+ let body = data
+ .iter()
+ .map(|row| self.render_row(&self.table.schema, false, &row, cx));
+
+ v_flex()
+ .w_full()
+ .child(header)
+ .children(body)
+ .into_any_element()
+ }
+
+ pub fn render_row(
+ &self,
+ schema: &TableSchema,
+ is_header: bool,
+ row: &Value,
+ cx: &ViewContext<ExecutionView>,
+ ) -> AnyElement {
+ let theme = cx.theme();
+
+ let row_cells = schema
+ .fields
+ .iter()
+ .map(|field| {
+ let container = match field.field_type {
+ runtimelib::datatable::FieldType::String => div(),
+
+ runtimelib::datatable::FieldType::Number
+ | runtimelib::datatable::FieldType::Integer
+ | runtimelib::datatable::FieldType::Date
+ | runtimelib::datatable::FieldType::Time
+ | runtimelib::datatable::FieldType::Datetime
+ | runtimelib::datatable::FieldType::Year
+ | runtimelib::datatable::FieldType::Duration
+ | runtimelib::datatable::FieldType::Yearmonth => v_flex().items_end(),
+
+ _ => div(),
+ };
+
+ let value = match row.get(&field.name) {
+ Some(Value::String(s)) => s.clone(),
+ Some(Value::Number(n)) => n.to_string(),
+ Some(Value::Bool(b)) => b.to_string(),
+ Some(Value::Array(arr)) => format!("{:?}", arr),
+ Some(Value::Object(obj)) => format!("{:?}", obj),
+ Some(Value::Null) | None => String::new(),
+ };
+
+ let mut cell = container
+ .w_full()
+ .child(value)
+ .px_2()
+ .py_1()
+ .border_color(theme.colors().border);
+
+ if is_header {
+ cell = cell.border_2().bg(theme.colors().border_focused)
+ } else {
+ cell = cell.border_1()
+ }
+ cell
+ })
+ .collect::<Vec<_>>();
+
+ h_flex().children(row_cells).into_any_element()
+ }
+}
+
+impl LineHeight for TableView {
+ fn num_lines(&self, _cx: &mut WindowContext) -> u8 {
+ let num_rows = match &self.table.data {
+ Some(data) => data.len(),
+ // We don't support Path based data sources
+ None => 0,
+ };
+
+ // Given that each cell has both `py_1` and a border, we have to estimate
+ // a reasonable size to add on, then round up.
+ let row_heights = (num_rows as f32 * 1.2) + 1.0;
+
+ (row_heights as u8).saturating_add(2) // Header + spacing
+ }
+}
+
+// Userspace error from the kernel
+pub struct ErrorView {
+ pub ename: String,
+ pub evalue: String,
+ pub traceback: TerminalOutput,
+}
+
+impl ErrorView {
+ fn render(&self, cx: &ViewContext<ExecutionView>) -> Option<AnyElement> {
+ let theme = cx.theme();
+
+ let colors = cx.theme().colors();
+
+ Some(
+ v_flex()
+ .w_full()
+ .bg(colors.background)
+ .p_4()
+ .border_l_1()
+ .border_color(theme.status().error_border)
+ .child(
+ h_flex()
+ .font_weight(FontWeight::BOLD)
+ .child(format!("{}: {}", self.ename, self.evalue)),
+ )
+ .child(self.traceback.render(cx))
+ .into_any_element(),
+ )
+ }
+}
+
+impl LineHeight for ErrorView {
+ fn num_lines(&self, cx: &mut WindowContext) -> u8 {
+ let mut height: u8 = 0;
+ height = height.saturating_add(self.ename.lines().count() as u8);
+ height = height.saturating_add(self.evalue.lines().count() as u8);
+ height = height.saturating_add(self.traceback.num_lines(cx));
+ height
+ }
+}
+
+pub enum OutputType {
+ Plain(TerminalOutput),
+ Stream(TerminalOutput),
+ Image(ImageView),
+ ErrorOutput(ErrorView),
+ Message(String),
+ Table(TableView),
+ ClearOutputWaitMarker,
+}
+
+impl OutputType {
+ fn render(&self, cx: &ViewContext<ExecutionView>) -> Option<AnyElement> {
+ let el = match self {
+ // Note: in typical frontends we would show the execute_result.execution_count
+ // Here we can just handle either
+ Self::Plain(stdio) => Some(stdio.render(cx)),
+ // Self::Markdown(markdown) => Some(markdown.render(theme)),
+ Self::Stream(stdio) => Some(stdio.render(cx)),
+ Self::Image(image) => Some(image.render(cx)),
+ Self::Message(message) => Some(div().child(message.clone()).into_any_element()),
+ Self::Table(table) => Some(table.render(cx)),
+ Self::ErrorOutput(error_view) => error_view.render(cx),
+ Self::ClearOutputWaitMarker => None,
+ };
+
+ el
+ }
+}
+
+impl LineHeight for OutputType {
+ /// Calculates the expected number of lines
+ fn num_lines(&self, cx: &mut WindowContext) -> u8 {
+ match self {
+ Self::Plain(stdio) => stdio.num_lines(cx),
+ Self::Stream(stdio) => stdio.num_lines(cx),
+ Self::Image(image) => image.num_lines(cx),
+ Self::Message(message) => message.lines().count() as u8,
+ Self::Table(table) => table.num_lines(cx),
+ Self::ErrorOutput(error_view) => error_view.num_lines(cx),
+ Self::ClearOutputWaitMarker => 0,
+ }
+ }
+}
+
+impl From<&MimeBundle> for OutputType {
+ fn from(data: &MimeBundle) -> Self {
+ match data.richest(rank_mime_type) {
+ Some(MimeType::Plain(text)) => OutputType::Plain(TerminalOutput::from(text)),
+ Some(MimeType::Markdown(text)) => OutputType::Plain(TerminalOutput::from(text)),
+ Some(MimeType::Png(data)) | Some(MimeType::Jpeg(data)) => match ImageView::from(data) {
+ Ok(view) => OutputType::Image(view),
+ Err(error) => OutputType::Message(format!("Failed to load image: {}", error)),
+ },
+ Some(MimeType::DataTable(data)) => OutputType::Table(TableView {
+ table: data.clone(),
+ }),
+ // Any other media types are not supported
+ _ => OutputType::Message("Unsupported media type".to_string()),
+ }
+ }
+}
+
+#[derive(Default)]
+pub enum ExecutionStatus {
+ #[default]
+ Unknown,
+ #[allow(unused)]
+ ConnectingToKernel,
+ Executing,
+ Finished,
+}
+
+pub struct ExecutionView {
+ pub outputs: Vec<OutputType>,
+ pub status: ExecutionStatus,
+}
+
+impl ExecutionView {
+ pub fn new(_cx: &mut ViewContext<Self>) -> Self {
+ Self {
+ outputs: Default::default(),
+ status: ExecutionStatus::Unknown,
+ }
+ }
+
+ /// Accept a Jupyter message belonging to this execution
+ pub fn push_message(&mut self, message: &JupyterMessageContent, cx: &mut ViewContext<Self>) {
+ let output: OutputType = match message {
+ JupyterMessageContent::ExecuteResult(result) => (&result.data).into(),
+ JupyterMessageContent::DisplayData(result) => (&result.data).into(),
+ JupyterMessageContent::StreamContent(result) => {
+ // Previous stream data will combine together, handling colors, carriage returns, etc
+ if let Some(new_terminal) = self.apply_terminal_text(&result.text) {
+ new_terminal
+ } else {
+ cx.notify();
+ return;
+ }
+ }
+ JupyterMessageContent::ErrorOutput(result) => {
+ let mut terminal = TerminalOutput::new();
+ terminal.append_text(&result.traceback.join("\n"));
+
+ OutputType::ErrorOutput(ErrorView {
+ ename: result.ename.clone(),
+ evalue: result.evalue.clone(),
+ traceback: terminal,
+ })
+ }
+ JupyterMessageContent::ExecuteReply(reply) => {
+ for payload in reply.payload.iter() {
+ match payload {
+ // Pager data comes in via `?` at the end of a statement in Python, used for showing documentation.
+ // Some UI will show this as a popup. For ease of implementation, it's included as an output here.
+ runtimelib::Payload::Page { data, .. } => {
+ let output: OutputType = (data).into();
+ self.outputs.push(output);
+ }
+
+ // Set next input adds text to the next cell. Not required to support.
+ // However, this could be implemented by
+ // 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!(),
+
+ //
+ // runtimelib::Payload::AskExit { keepkernel } => todo!(),
+ _ => {}
+ }
+ }
+ cx.notify();
+ return;
+ }
+ JupyterMessageContent::ClearOutput(options) => {
+ if !options.wait {
+ self.outputs.clear();
+ cx.notify();
+ return;
+ }
+
+ // Create a marker to clear the output after we get in a new output
+ OutputType::ClearOutputWaitMarker
+ }
+ JupyterMessageContent::Status(status) => {
+ match status.execution_state {
+ ExecutionState::Busy => {
+ self.status = ExecutionStatus::Executing;
+ }
+ ExecutionState::Idle => self.status = ExecutionStatus::Finished,
+ }
+ cx.notify();
+ return;
+ }
+ _msg => {
+ return;
+ }
+ };
+
+ // Check for a clear output marker as the previous output, so we can clear it out
+ if let Some(OutputType::ClearOutputWaitMarker) = self.outputs.last() {
+ self.outputs.clear();
+ }
+
+ self.outputs.push(output);
+
+ cx.notify();
+ }
+
+ fn apply_terminal_text(&mut self, text: &str) -> Option<OutputType> {
+ if let Some(last_output) = self.outputs.last_mut() {
+ match last_output {
+ OutputType::Stream(last_stream) => {
+ last_stream.append_text(text);
+ // Don't need to add a new output, we already have a terminal output
+ return None;
+ }
+ // Edge case note: a clear output marker
+ OutputType::ClearOutputWaitMarker => {
+ // Edge case note: a clear output marker is handled by the caller
+ // since we will return a new output at the end here as a new terminal output
+ }
+ // A different output type is "in the way", so we need to create a new output,
+ // which is the same as having no prior output
+ _ => {}
+ }
+ }
+
+ let mut new_terminal = TerminalOutput::new();
+ 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()
+ }
+ 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(),
+ }
+ }
+
+ div()
+ .w_full()
+ .children(self.outputs.iter().filter_map(|output| output.render(cx)))
+ .into_any_element()
+ }
+}
+
+impl LineHeight for ExecutionView {
+ fn num_lines(&self, cx: &mut WindowContext) -> u8 {
+ if self.outputs.is_empty() {
+ return 1; // For the status message if outputs are not there
+ }
+
+ self.outputs
+ .iter()
+ .map(|output| output.num_lines(cx))
+ .fold(0, |acc, additional_height| {
+ acc.saturating_add(additional_height)
+ })
+ }
+}
+
+impl LineHeight for View<ExecutionView> {
+ fn num_lines(&self, cx: &mut WindowContext) -> u8 {
+ self.update(cx, |execution_view, cx| execution_view.num_lines(cx))
+ }
+}
@@ -0,0 +1,571 @@
+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 std::{sync::Arc, time::Duration};
+use theme::{ActiveTheme, ThemeSettings};
+use ui::prelude::*;
+use workspace::Workspace;
+
+mod outputs;
+// mod runtime_panel;
+mod runtime_settings;
+mod runtimes;
+mod stdio;
+
+use runtimes::{get_runtime_specifications, Request, RunningKernel, RuntimeSpecification};
+
+actions!(repl, [Run]);
+
+#[derive(Clone)]
+pub struct RuntimeManagerGlobal(Model<RuntimeManager>);
+
+impl Global for RuntimeManagerGlobal {}
+
+pub fn zed_dispatcher(cx: &mut AppContext) -> impl Dispatcher {
+ struct ZedDispatcher {
+ dispatcher: Arc<dyn PlatformDispatcher>,
+ }
+
+ // PlatformDispatcher is _super_ close to the same interface we put in
+ // async-dispatcher, except for the task label in dispatch. Later we should
+ // just make that consistent so we have this dispatcher ready to go for
+ // other crates in Zed.
+ impl Dispatcher for ZedDispatcher {
+ fn dispatch(&self, runnable: Runnable) {
+ self.dispatcher.dispatch(runnable, None)
+ }
+
+ fn dispatch_after(&self, duration: Duration, runnable: Runnable) {
+ self.dispatcher.dispatch_after(duration, runnable);
+ }
+ }
+
+ ZedDispatcher {
+ dispatcher: cx.background_executor().dispatcher.clone(),
+ }
+}
+
+pub fn init(fs: Arc<dyn Fs>, 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)
+}
@@ -0,0 +1,66 @@
+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)
+ }
+}
@@ -0,0 +1,329 @@
+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"]
+ );
+ }
+}
@@ -0,0 +1,394 @@
+use crate::outputs::{ExecutionView, LineHeight};
+use alacritty_terminal::vte::{
+ ansi::{Attr, Color, NamedColor, Rgb},
+ Params, ParamsIter, Parser, Perform,
+};
+use core::iter;
+use gpui::{font, prelude::*, AnyElement, StyledText, TextRun};
+use settings::Settings as _;
+use theme::ThemeSettings;
+use ui::{div, prelude::*, IntoElement, ViewContext, WindowContext};
+
+/// Implements the most basic of terminal output for use by Jupyter outputs
+/// whether:
+///
+/// * stdout
+/// * stderr
+/// * text/plain
+/// * traceback from an error output
+///
+/// Ideally, we would instead use alacritty::vte::Processor to collect the
+/// output and then render up to u8::MAX lines of text. However, it's likely
+/// overkill for 95% of outputs.
+///
+/// Instead, this implementation handles:
+///
+/// * ANSI color codes (background, foreground), including 256 color
+/// * Carriage returns/line feeds
+///
+/// There is no support for cursor movement, clearing the screen, and other text styles
+pub struct TerminalOutput {
+ parser: Parser,
+ handler: TerminalHandler,
+}
+
+impl TerminalOutput {
+ pub fn new() -> Self {
+ Self {
+ parser: Parser::new(),
+ handler: TerminalHandler::new(),
+ }
+ }
+
+ pub fn from(text: &str) -> Self {
+ let mut output = Self::new();
+ output.append_text(text);
+ output
+ }
+
+ pub fn append_text(&mut self, text: &str) {
+ for byte in text.as_bytes() {
+ self.parser.advance(&mut self.handler, *byte);
+ }
+ }
+
+ 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
+ .iter()
+ .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(
+ &ansi_run.bg,
+ theme,
+ ));
+
+ TextRun {
+ len: ansi_run.len,
+ color,
+ background_color,
+ underline: Default::default(),
+ font: font(buffer_font.clone()),
+ strikethrough: None,
+ }
+ })
+ .collect::<Vec<TextRun>>();
+
+ let text = StyledText::new(self.handler.buffer.trim_end().to_string()).with_runs(runs);
+ div()
+ .font_family(buffer_font)
+ .child(text)
+ .into_any_element()
+ }
+}
+
+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,
+}
+
+impl AnsiTextRun {
+ fn default() -> Self {
+ Self {
+ len: 0,
+ fg: Color::Named(NamedColor::Foreground),
+ bg: Color::Named(NamedColor::Background),
+ }
+ }
+}
+
+struct TerminalHandler {
+ text_runs: Vec<AnsiTextRun>,
+ current_text_run: AnsiTextRun,
+ buffer: String,
+}
+
+impl TerminalHandler {
+ fn new() -> Self {
+ Self {
+ text_runs: Vec::new(),
+ current_text_run: AnsiTextRun {
+ len: 0,
+ fg: Color::Named(NamedColor::Foreground),
+ bg: Color::Named(NamedColor::Background),
+ },
+ buffer: String::new(),
+ }
+ }
+
+ fn add_text(&mut self, c: char) {
+ self.buffer.push(c);
+ self.current_text_run.len += 1;
+ }
+
+ fn reset(&mut self) {
+ if self.current_text_run.len > 0 {
+ self.text_runs.push(self.current_text_run.clone());
+ }
+
+ self.current_text_run = AnsiTextRun::default();
+ }
+
+ fn terminal_attribute(&mut self, attr: Attr) {
+ // println!("[terminal_attribute] attr={:?}", attr);
+ if Attr::Reset == attr {
+ self.reset();
+ return;
+ }
+
+ if self.current_text_run.len > 0 {
+ self.text_runs.push(self.current_text_run.clone());
+ }
+
+ let mut text_run = AnsiTextRun {
+ len: 0,
+ fg: self.current_text_run.fg,
+ bg: self.current_text_run.bg,
+ };
+
+ match attr {
+ Attr::Foreground(color) => text_run.fg = color,
+ Attr::Background(color) => text_run.bg = color,
+ _ => {}
+ }
+
+ self.current_text_run = text_run;
+ }
+
+ fn process_carriage_return(&mut self) {
+ // Find last carriage return's position
+ let last_cr = self.buffer.rfind('\r').unwrap_or(0);
+ self.buffer = self.buffer.chars().take(last_cr).collect();
+
+ // First work through our current text run
+ let mut total_len = self.current_text_run.len;
+ if total_len > last_cr {
+ // We are in the current text run
+ self.current_text_run.len = self.current_text_run.len - last_cr;
+ } else {
+ let mut last_cr_run = 0;
+ // Find the last run before the last carriage return
+ for (i, run) in self.text_runs.iter().enumerate() {
+ total_len += run.len;
+ if total_len > last_cr {
+ last_cr_run = i;
+ break;
+ }
+ }
+ self.text_runs = self.text_runs[..last_cr_run].to_vec();
+ self.current_text_run = self.text_runs.pop().unwrap_or(AnsiTextRun::default());
+ }
+
+ self.buffer.push('\r');
+ self.current_text_run.len += 1;
+ }
+}
+
+impl Perform for TerminalHandler {
+ fn print(&mut self, c: char) {
+ // println!("[print] c={:?}", c);
+ self.add_text(c);
+ }
+
+ fn execute(&mut self, byte: u8) {
+ match byte {
+ b'\n' => {
+ self.add_text('\n');
+ }
+ b'\r' => {
+ self.process_carriage_return();
+ }
+ _ => {
+ // Format as hex
+ println!("[execute] byte={:02x}", byte);
+ }
+ }
+ }
+
+ fn hook(&mut self, _params: &Params, _intermediates: &[u8], _ignore: bool, _c: char) {
+ // noop
+ // println!(
+ // "[hook] params={:?}, intermediates={:?}, c={:?}",
+ // _params, _intermediates, _c
+ // );
+ }
+
+ fn put(&mut self, _byte: u8) {
+ // noop
+ // println!("[put] byte={:02x}", _byte);
+ }
+
+ fn unhook(&mut self) {
+ // noop
+ }
+
+ fn osc_dispatch(&mut self, _params: &[&[u8]], _bell_terminated: bool) {
+ // noop
+ // println!("[osc_dispatch] params={:?}", _params);
+ }
+
+ fn csi_dispatch(
+ &mut self,
+ params: &alacritty_terminal::vte::Params,
+ intermediates: &[u8],
+ _ignore: bool,
+ action: char,
+ ) {
+ // println!(
+ // "[csi_dispatch] action={:?}, params={:?}, intermediates={:?}",
+ // action, params, intermediates
+ // );
+
+ let mut params_iter = params.iter();
+ // Collect colors
+ match (action, intermediates) {
+ ('m', []) => {
+ if params.is_empty() {
+ self.terminal_attribute(Attr::Reset);
+ } else {
+ for attr in attrs_from_sgr_parameters(&mut params_iter) {
+ match attr {
+ Some(attr) => self.terminal_attribute(attr),
+ None => return,
+ }
+ }
+ }
+ }
+ _ => {}
+ }
+ }
+
+ fn esc_dispatch(&mut self, _intermediates: &[u8], _ignore: bool, _byte: u8) {
+ // noop
+ // println!(
+ // "[esc_dispatch] intermediates={:?}, byte={:?}",
+ // _intermediates, _byte
+ // );
+ }
+}
+
+// The following was pulled from vte::ansi
+#[inline]
+fn attrs_from_sgr_parameters(params: &mut ParamsIter<'_>) -> Vec<Option<Attr>> {
+ let mut attrs = Vec::with_capacity(params.size_hint().0);
+
+ while let Some(param) = params.next() {
+ let attr = match param {
+ [0] => Some(Attr::Reset),
+ [1] => Some(Attr::Bold),
+ [2] => Some(Attr::Dim),
+ [3] => Some(Attr::Italic),
+ [4, 0] => Some(Attr::CancelUnderline),
+ [4, 2] => Some(Attr::DoubleUnderline),
+ [4, 3] => Some(Attr::Undercurl),
+ [4, 4] => Some(Attr::DottedUnderline),
+ [4, 5] => Some(Attr::DashedUnderline),
+ [4, ..] => Some(Attr::Underline),
+ [5] => Some(Attr::BlinkSlow),
+ [6] => Some(Attr::BlinkFast),
+ [7] => Some(Attr::Reverse),
+ [8] => Some(Attr::Hidden),
+ [9] => Some(Attr::Strike),
+ [21] => Some(Attr::CancelBold),
+ [22] => Some(Attr::CancelBoldDim),
+ [23] => Some(Attr::CancelItalic),
+ [24] => Some(Attr::CancelUnderline),
+ [25] => Some(Attr::CancelBlink),
+ [27] => Some(Attr::CancelReverse),
+ [28] => Some(Attr::CancelHidden),
+ [29] => Some(Attr::CancelStrike),
+ [30] => Some(Attr::Foreground(Color::Named(NamedColor::Black))),
+ [31] => Some(Attr::Foreground(Color::Named(NamedColor::Red))),
+ [32] => Some(Attr::Foreground(Color::Named(NamedColor::Green))),
+ [33] => Some(Attr::Foreground(Color::Named(NamedColor::Yellow))),
+ [34] => Some(Attr::Foreground(Color::Named(NamedColor::Blue))),
+ [35] => Some(Attr::Foreground(Color::Named(NamedColor::Magenta))),
+ [36] => Some(Attr::Foreground(Color::Named(NamedColor::Cyan))),
+ [37] => Some(Attr::Foreground(Color::Named(NamedColor::White))),
+ [38] => {
+ let mut iter = params.map(|param| param[0]);
+ parse_sgr_color(&mut iter).map(Attr::Foreground)
+ }
+ [38, params @ ..] => handle_colon_rgb(params).map(Attr::Foreground),
+ [39] => Some(Attr::Foreground(Color::Named(NamedColor::Foreground))),
+ [40] => Some(Attr::Background(Color::Named(NamedColor::Black))),
+ [41] => Some(Attr::Background(Color::Named(NamedColor::Red))),
+ [42] => Some(Attr::Background(Color::Named(NamedColor::Green))),
+ [43] => Some(Attr::Background(Color::Named(NamedColor::Yellow))),
+ [44] => Some(Attr::Background(Color::Named(NamedColor::Blue))),
+ [45] => Some(Attr::Background(Color::Named(NamedColor::Magenta))),
+ [46] => Some(Attr::Background(Color::Named(NamedColor::Cyan))),
+ [47] => Some(Attr::Background(Color::Named(NamedColor::White))),
+ [48] => {
+ let mut iter = params.map(|param| param[0]);
+ parse_sgr_color(&mut iter).map(Attr::Background)
+ }
+ [48, params @ ..] => handle_colon_rgb(params).map(Attr::Background),
+ [49] => Some(Attr::Background(Color::Named(NamedColor::Background))),
+ [58] => {
+ let mut iter = params.map(|param| param[0]);
+ parse_sgr_color(&mut iter).map(|color| Attr::UnderlineColor(Some(color)))
+ }
+ [58, params @ ..] => {
+ handle_colon_rgb(params).map(|color| Attr::UnderlineColor(Some(color)))
+ }
+ [59] => Some(Attr::UnderlineColor(None)),
+ [90] => Some(Attr::Foreground(Color::Named(NamedColor::BrightBlack))),
+ [91] => Some(Attr::Foreground(Color::Named(NamedColor::BrightRed))),
+ [92] => Some(Attr::Foreground(Color::Named(NamedColor::BrightGreen))),
+ [93] => Some(Attr::Foreground(Color::Named(NamedColor::BrightYellow))),
+ [94] => Some(Attr::Foreground(Color::Named(NamedColor::BrightBlue))),
+ [95] => Some(Attr::Foreground(Color::Named(NamedColor::BrightMagenta))),
+ [96] => Some(Attr::Foreground(Color::Named(NamedColor::BrightCyan))),
+ [97] => Some(Attr::Foreground(Color::Named(NamedColor::BrightWhite))),
+ [100] => Some(Attr::Background(Color::Named(NamedColor::BrightBlack))),
+ [101] => Some(Attr::Background(Color::Named(NamedColor::BrightRed))),
+ [102] => Some(Attr::Background(Color::Named(NamedColor::BrightGreen))),
+ [103] => Some(Attr::Background(Color::Named(NamedColor::BrightYellow))),
+ [104] => Some(Attr::Background(Color::Named(NamedColor::BrightBlue))),
+ [105] => Some(Attr::Background(Color::Named(NamedColor::BrightMagenta))),
+ [106] => Some(Attr::Background(Color::Named(NamedColor::BrightCyan))),
+ [107] => Some(Attr::Background(Color::Named(NamedColor::BrightWhite))),
+ _ => None,
+ };
+ attrs.push(attr);
+ }
+
+ attrs
+}
+
+/// Handle colon separated rgb color escape sequence.
+#[inline]
+fn handle_colon_rgb(params: &[u16]) -> Option<Color> {
+ let rgb_start = if params.len() > 4 { 2 } else { 1 };
+ let rgb_iter = params[rgb_start..].iter().copied();
+ let mut iter = iter::once(params[0]).chain(rgb_iter);
+
+ parse_sgr_color(&mut iter)
+}
+
+/// Parse a color specifier from list of attributes.
+fn parse_sgr_color(params: &mut dyn Iterator<Item = u16>) -> Option<Color> {
+ match params.next() {
+ Some(2) => Some(Color::Spec(Rgb {
+ r: u8::try_from(params.next()?).ok()?,
+ g: u8::try_from(params.next()?).ok()?,
+ b: u8::try_from(params.next()?).ok()?,
+ })),
+ Some(5) => Some(Color::Indexed(u8::try_from(params.next()?).ok()?)),
+ _ => None,
+ }
+}
@@ -1041,7 +1041,7 @@ fn to_highlighted_range_lines(
}
/// Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent.
-fn convert_color(fg: &terminal::alacritty_terminal::vte::ansi::Color, theme: &Theme) -> Hsla {
+pub fn convert_color(fg: &terminal::alacritty_terminal::vte::ansi::Color, theme: &Theme) -> Hsla {
let colors = theme.colors();
match fg {
// Named and theme defined colors
@@ -268,9 +268,11 @@ mod tests {
#[test]
fn test_format_distance() {
let date = DateTimeType::Naive(
+ #[allow(deprecated)]
NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date"),
);
let base_date = DateTimeType::Naive(
+ #[allow(deprecated)]
NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date"),
);
@@ -283,9 +285,11 @@ mod tests {
#[test]
fn test_format_distance_with_suffix() {
let date = DateTimeType::Naive(
+ #[allow(deprecated)]
NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date"),
);
let base_date = DateTimeType::Naive(
+ #[allow(deprecated)]
NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date"),
);
@@ -78,6 +78,7 @@ quick_action_bar.workspace = true
recent_projects.workspace = true
dev_server_projects.workspace = true
release_channel.workspace = true
+repl.workspace = true
rope.workspace = true
search.workspace = true
serde.workspace = true
@@ -221,6 +221,8 @@ fn init_ui(app_state: Arc<AppState>, cx: &mut AppContext) -> Result<()> {
assistant::init(app_state.client.clone(), cx);
+ repl::init(app_state.fs.clone(), cx);
+
cx.observe_global::<SettingsStore>({
let languages = app_state.languages.clone();
let http = app_state.client.http_client();