mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-26 07:12:03 +03:00
Bring Jupyter to Zed Editing (#12062)
Run any Jupyter kernel in Zed on any buffer (editor): <img width="1074" alt="image" src="https://github.com/zed-industries/zed/assets/836375/eac8ed69-d02b-4d46-b379-6186d8f59470"> ## 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
This commit is contained in:
parent
d95c424d18
commit
221edfc267
222
Cargo.lock
generated
222
Cargo.lock
generated
@ -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)
|
||||
}
|
||||
|
49
crates/repl/Cargo.toml
Normal file
49
crates/repl/Cargo.toml
Normal file
@ -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"] }
|
479
crates/repl/src/outputs.rs
Normal file
479
crates/repl/src/outputs.rs
Normal file
@ -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))
|
||||
}
|
||||
}
|
571
crates/repl/src/repl.rs
Normal file
571
crates/repl/src/repl.rs
Normal file
@ -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)
|
||||
}
|
66
crates/repl/src/runtime_settings.rs
Normal file
66
crates/repl/src/runtime_settings.rs
Normal file
@ -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)
|
||||
}
|
||||
}
|
329
crates/repl/src/runtimes.rs
Normal file
329
crates/repl/src/runtimes.rs
Normal file
@ -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"]
|
||||
);
|
||||
}
|
||||
}
|
394
crates/repl/src/stdio.rs
Normal file
394
crates/repl/src/stdio.rs
Normal file
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user