From 221edfc2671c1a4e96d12a57de9952698f30d237 Mon Sep 17 00:00:00 2001 From: Kyle Kelley Date: Mon, 17 Jun 2024 10:02:31 -0700 Subject: [PATCH] Bring Jupyter to Zed Editing (#12062) Run any Jupyter kernel in Zed on any buffer (editor): image ## TODO ### Lifecycle * [x] Launch kernels on demand * [x] Wait for kernel to be started * [x] Request Kernel info on start * [x] Show in progress indicator * [ ] Allow picking kernel (it defaults to first matching language name) * [ ] Menu for interrupting and shutting down the kernel * [ ] Drop running kernels once editor is dropped ### Media Outputs * [x] Render text and tracebacks with ANSI color handling * [x] Render markdown as text * [x] Render PNG and JPEG images using an explicit height based on line-height * ~~Render SVG~~ -- not happening for this PR due to lack of text in SVG support * [ ] Process `update_display_data` message and related `display_id` * [x] Process `page` data from payloads as outputs * [ ] Render markdown as, well, rendered markdown -- Note: unsure if we can get line heights here ### Document * [x] Select code and run * [x] Run current line * [x] Clear previous overlapping runs * [ ] Support running markdown code blocks * [ ] Action to export session as notebook or output files * [ ] Action to clear all outputs * [ ] Delete outputs when lines are deleted ## Other missing features The following is a list of missing functionality or expectations that are out of scope for this PR. ### Python Environments Detecting python environments should probably be done in a separate PR in tandem with how they're used with LSP. Users likely want to pick an environment for their project, whether a virtualenv, conda env, pyenv, poetry backed virtualenv, or the system. Related issues: * https://github.com/zed-industries/zed/issues/7646 * https://github.com/zed-industries/zed/issues/7808 * https://github.com/zed-industries/zed/issues/7296 ### LSP Integration * Submit `complete_request` messages for completions to interleave interactive variables with LSP * LSP for IPython semantics (`%%timeit`, `!ls`, `get_ipython`, etc.) ## Future release notes - Run code in any editor, whether it's a script or a markdown document Release Notes: - N/A --- Cargo.lock | 222 ++++++- Cargo.toml | 6 + crates/collab/src/db/queries/extensions.rs | 1 + crates/gpui/src/executor.rs | 6 +- crates/multi_buffer/src/anchor.rs | 9 + crates/repl/Cargo.toml | 49 ++ crates/repl/src/outputs.rs | 479 ++++++++++++++++ crates/repl/src/repl.rs | 571 +++++++++++++++++++ crates/repl/src/runtime_settings.rs | 66 +++ crates/repl/src/runtimes.rs | 329 +++++++++++ crates/repl/src/stdio.rs | 394 +++++++++++++ crates/terminal_view/src/terminal_element.rs | 2 +- crates/ui/src/utils/format_distance.rs | 4 + crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 2 + 15 files changed, 2117 insertions(+), 24 deletions(-) create mode 100644 crates/repl/Cargo.toml create mode 100644 crates/repl/src/outputs.rs create mode 100644 crates/repl/src/repl.rs create mode 100644 crates/repl/src/runtime_settings.rs create mode 100644 crates/repl/src/runtimes.rs create mode 100644 crates/repl/src/stdio.rs diff --git a/Cargo.lock b/Cargo.lock index c2142511ab..c741502f93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 4d61614f33..79e90509cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/collab/src/db/queries/extensions.rs b/crates/collab/src/db/queries/extensions.rs index d6938fd776..93604868fa 100644 --- a/crates/collab/src/db/queries/extensions.rs +++ b/crates/collab/src/db/queries/extensions.rs @@ -379,6 +379,7 @@ fn metadata_from_extension_and_version( pub fn convert_time_to_chrono(time: time::PrimitiveDateTime) -> chrono::DateTime { chrono::DateTime::from_naive_utc_and_offset( + #[allow(deprecated)] chrono::NaiveDateTime::from_timestamp_opt(time.assume_utc().unix_timestamp(), 0).unwrap(), Utc, ) diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index b92ce67aaf..2e1bd1bda3 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -25,14 +25,16 @@ use rand::rngs::StdRng; /// for spawning background tasks. #[derive(Clone)] pub struct BackgroundExecutor { - dispatcher: Arc, + #[doc(hidden)] + pub dispatcher: Arc, } /// A pointer to the executor that is currently running, /// for spawning tasks on the main thread. #[derive(Clone)] pub struct ForegroundExecutor { - dispatcher: Arc, + #[doc(hidden)] + pub dispatcher: Arc, not_send: PhantomData>, } diff --git a/crates/multi_buffer/src/anchor.rs b/crates/multi_buffer/src/anchor.rs index b13a1fe2e6..ff900ca066 100644 --- a/crates/multi_buffer/src/anchor.rs +++ b/crates/multi_buffer/src/anchor.rs @@ -117,6 +117,7 @@ impl ToPoint for Anchor { pub trait AnchorRangeExt { fn cmp(&self, b: &Range, buffer: &MultiBufferSnapshot) -> Ordering; + fn overlaps(&self, b: &Range, buffer: &MultiBufferSnapshot) -> bool; fn to_offset(&self, content: &MultiBufferSnapshot) -> Range; fn to_point(&self, content: &MultiBufferSnapshot) -> Range; } @@ -129,6 +130,14 @@ impl AnchorRangeExt for Range { } } + fn overlaps(&self, other: &Range, 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 { self.start.to_offset(content)..self.end.to_offset(content) } diff --git a/crates/repl/Cargo.toml b/crates/repl/Cargo.toml new file mode 100644 index 0000000000..443cf69055 --- /dev/null +++ b/crates/repl/Cargo.toml @@ -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"] } diff --git a/crates/repl/src/outputs.rs b/crates/repl/src/outputs.rs new file mode 100644 index 0000000000..0c81ca972f --- /dev/null +++ b/crates/repl/src/outputs.rs @@ -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, +} + +impl ImageView { + fn render(&self, cx: &ViewContext) -> 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 { + 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) -> 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, + ) -> 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::>(); + + 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) -> Option { + 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) -> Option { + 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, + pub status: ExecutionStatus, +} + +impl ExecutionView { + pub fn new(_cx: &mut ViewContext) -> 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) { + 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 { + 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.status = status; + cx.notify(); + } +} + +impl Render for ExecutionView { + fn render(&mut self, cx: &mut ViewContext) -> 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 { + fn num_lines(&self, cx: &mut WindowContext) -> u8 { + self.update(cx, |execution_view, cx| execution_view.num_lines(cx)) + } +} diff --git a/crates/repl/src/repl.rs b/crates/repl/src/repl.rs new file mode 100644 index 0000000000..2ae9809d33 --- /dev/null +++ b/crates/repl/src/repl.rs @@ -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); + +impl Global for RuntimeManagerGlobal {} + +pub fn zed_dispatcher(cx: &mut AppContext) -> impl Dispatcher { + struct ZedDispatcher { + dispatcher: Arc, + } + + // 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, 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.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, 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, cx: &mut AppContext) { + cx.observe_global::(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>), + FailedLaunch, +} + +// Per workspace +pub struct RuntimeManager { + fs: Arc, + runtime_specifications: Vec, + + instances: HashMap, + editors: HashMap, 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>, +} + +#[derive(Debug, Clone)] +struct EditorRuntimeState { + blocks: Vec, + // 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, + _execution_id: String, + block_id: BlockId, + _execution_view: View, +} + +impl RuntimeManager { + pub fn new(fs: Arc, _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, + cx: &mut ModelContext, + ) -> Task>> { + 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, + cx: &mut ModelContext, + ) -> Task> { + // 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, + code: String, + cx: &mut ModelContext, + ) -> impl Future>> { + 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> { + cx.try_global::() + .map(|runtime_manager| runtime_manager.0.clone()) + } + + pub fn set_global(runtime_manager: Model, 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::(); + } + } +} + +pub fn get_active_editor( + workspace: &mut Workspace, + cx: &mut ViewContext, +) -> Option> { + workspace + .active_item(cx) + .and_then(|item| item.act_as::(cx)) +} + +// Gets the active selection in the editor or the current line +pub fn selection(editor: View, cx: &mut ViewContext) -> Range { + let editor = editor.read(cx); + let selection = editor.selections.newest::(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) { + 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::(); + + 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 = 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) -> 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) +} diff --git a/crates/repl/src/runtime_settings.rs b/crates/repl/src/runtime_settings.rs new file mode 100644 index 0000000000..64fe8cb3a3 --- /dev/null +++ b/crates/repl/src/runtime_settings.rs @@ -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, + /// Where to dock the runtimes panel. + /// + /// Default: `right` + dock: Option, +} + +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, + _cx: &mut gpui::AppContext, + ) -> anyhow::Result + 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) + } +} diff --git a/crates/repl/src/runtimes.rs b/crates/repl/src/runtimes.rs new file mode 100644 index 0000000000..ec4150d545 --- /dev/null +++ b/crates/repl/src/runtimes.rs @@ -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, +} + +#[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 { + 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, +} + +impl RunningKernel { + pub async fn new( + runtime: RuntimeSpecification, + entity_id: EntityId, + fs: Arc, + cx: AsyncAppContext, + ) -> anyhow::Result { + 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>>, + > = Default::default(); + + let (request_tx, mut request_rx) = mpsc::unbounded::(); + + 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, +) -> anyhow::Result { + 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::(&spec)?; + + Ok(RuntimeSpecification { + name: kernel_name, + path, + kernelspec: spec, + }) +} + +/// Read a directory of kernelspec directories +async fn read_kernels_dir( + path: PathBuf, + fs: Arc, +) -> anyhow::Result> { + 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, +) -> anyhow::Result> { + 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::>(); + + let kernel_dirs = futures::future::join_all(kernel_dirs).await; + let kernel_dirs = kernel_dirs + .into_iter() + .filter_map(Result::ok) + .flatten() + .collect::>(); + + 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!["deno", "python"] + ); + } +} diff --git a/crates/repl/src/stdio.rs b/crates/repl/src/stdio.rs new file mode 100644 index 0000000000..e2c9bbc745 --- /dev/null +++ b/crates/repl/src/stdio.rs @@ -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) -> 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::>(); + + 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, + 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> { + 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 { + 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) -> Option { + 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, + } +} diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 9c0cfbd846..75d14efe06 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -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 diff --git a/crates/ui/src/utils/format_distance.rs b/crates/ui/src/utils/format_distance.rs index 14f3237e56..f40a4d5da0 100644 --- a/crates/ui/src/utils/format_distance.rs +++ b/crates/ui/src/utils/format_distance.rs @@ -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"), ); diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 26c4145388..196a04540c 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -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 diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 2faa55221c..f18d29f584 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -221,6 +221,8 @@ fn init_ui(app_state: Arc, cx: &mut AppContext) -> Result<()> { assistant::init(app_state.client.clone(), cx); + repl::init(app_state.fs.clone(), cx); + cx.observe_global::({ let languages = app_state.languages.clone(); let http = app_state.client.http_client();