repl: Replace REPL panel with sessions view (#14981)

This PR removes the REPL panel and replaces it with a new sessions view
that gets displayed in its own pane.

The sessions view can be opened with the `repl: sessions` command (we
can adjust the name, as needed).

There was a rather in-depth refactoring needed to extricate the various
REPL functionality on the editor from the `RuntimePanel`.

<img width="1136" alt="Screenshot 2024-07-22 at 4 12 12 PM"
src="https://github.com/user-attachments/assets/ac0da351-778e-4200-b08c-39f9e77d78bf">

<img width="1136" alt="Screenshot 2024-07-22 at 4 12 17 PM"
src="https://github.com/user-attachments/assets/6ca53476-6ac4-4f8b-afc8-f7863f7065c7">

Release Notes:

- N/A
This commit is contained in:
Marshall Bowers 2024-07-22 16:22:50 -04:00 committed by GitHub
parent 8f20ea1093
commit d8a42bbf63
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 474 additions and 637 deletions

View file

@ -2,8 +2,8 @@ use std::time::Duration;
use gpui::{percentage, Animation, AnimationExt, AnyElement, Transformation, View};
use repl::{
ExecutionState, JupyterSettings, Kernel, KernelSpecification, KernelStatus, RuntimePanel,
Session, SessionSupport,
ExecutionState, JupyterSettings, Kernel, KernelSpecification, KernelStatus, Session,
SessionSupport,
};
use ui::{
prelude::*, ButtonLike, ContextMenu, IconWithIndicator, Indicator, IntoElement, PopoverMenu,
@ -39,15 +39,7 @@ impl QuickActionBar {
return None;
}
let workspace = self.workspace.upgrade()?.read(cx);
let (editor, repl_panel) = if let (Some(editor), Some(repl_panel)) =
(self.active_editor(), workspace.panel::<RuntimePanel>(cx))
{
(editor, repl_panel)
} else {
return None;
};
let editor = self.active_editor()?;
let has_nonempty_selection = {
editor.update(cx, |this, cx| {
@ -62,10 +54,7 @@ impl QuickActionBar {
})
};
let session = repl_panel.update(cx, |repl_panel, cx| {
repl_panel.session(editor.downgrade(), cx)
});
let session = repl::session(editor.downgrade(), cx);
let session = match session {
SessionSupport::ActiveSession(session) => session,
SessionSupport::Inactive(spec) => {
@ -84,18 +73,15 @@ impl QuickActionBar {
let element_id = |suffix| ElementId::Name(format!("{}-{}", id, suffix).into());
let panel_clone = repl_panel.clone();
let editor_clone = editor.downgrade();
let editor = editor.downgrade();
let dropdown_menu = PopoverMenu::new(element_id("menu"))
.menu(move |cx| {
let panel_clone = panel_clone.clone();
let editor_clone = editor_clone.clone();
let editor = editor.clone();
let session = session.clone();
ContextMenu::build(cx, move |menu, cx| {
let menu_state = session_state(session, cx);
let status = menu_state.status;
let editor_clone = editor_clone.clone();
let panel_clone = panel_clone.clone();
let editor = editor.clone();
menu.when_else(
status.is_connected(),
@ -139,7 +125,6 @@ impl QuickActionBar {
},
)
.separator()
// Run
.custom_entry(
move |_cx| {
Label::new(if has_nonempty_selection {
@ -150,17 +135,12 @@ impl QuickActionBar {
.into_any_element()
},
{
let panel_clone = panel_clone.clone();
let editor_clone = editor_clone.clone();
let editor = editor.clone();
move |cx| {
let editor_clone = editor_clone.clone();
panel_clone.update(cx, |this, cx| {
this.run(editor_clone.clone(), cx).log_err();
});
repl::run(editor.clone(), cx).log_err();
}
},
)
// Interrupt
.custom_entry(
move |_cx| {
Label::new("Interrupt")
@ -169,17 +149,12 @@ impl QuickActionBar {
.into_any_element()
},
{
let panel_clone = panel_clone.clone();
let editor_clone = editor_clone.clone();
let editor = editor.clone();
move |cx| {
let editor_clone = editor_clone.clone();
panel_clone.update(cx, |this, cx| {
this.interrupt(editor_clone, cx);
});
repl::interrupt(editor.clone(), cx);
}
},
)
// Clear Outputs
.custom_entry(
move |_cx| {
Label::new("Clear Outputs")
@ -188,13 +163,9 @@ impl QuickActionBar {
.into_any_element()
},
{
let panel_clone = panel_clone.clone();
let editor_clone = editor_clone.clone();
let editor = editor.clone();
move |cx| {
let editor_clone = editor_clone.clone();
panel_clone.update(cx, |this, cx| {
this.clear_outputs(editor_clone, cx);
});
repl::clear_outputs(editor.clone(), cx);
}
},
)
@ -207,7 +178,6 @@ impl QuickActionBar {
)
// TODO: Add Restart action
// .action("Restart", Box::new(gpui::NoAction))
// Shut down kernel
.custom_entry(
move |_cx| {
Label::new("Shut Down Kernel")
@ -216,13 +186,9 @@ impl QuickActionBar {
.into_any_element()
},
{
let panel_clone = panel_clone.clone();
let editor_clone = editor_clone.clone();
let editor = editor.clone();
move |cx| {
let editor_clone = editor_clone.clone();
panel_clone.update(cx, |this, cx| {
this.shutdown(editor_clone, cx);
});
repl::shutdown(editor.clone(), cx);
}
},
)

View file

@ -5,21 +5,9 @@ use gpui::AppContext;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use ui::Pixels;
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum JupyterDockPosition {
Left,
#[default]
Right,
Bottom,
}
#[derive(Debug, Default)]
pub struct JupyterSettings {
pub dock: JupyterDockPosition,
pub default_width: Pixels,
pub kernel_selections: HashMap<String, String>,
}
@ -34,31 +22,15 @@ impl JupyterSettings {
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
pub struct JupyterSettingsContent {
/// Where to dock the Jupyter panel.
///
/// Default: `right`
dock: Option<JupyterDockPosition>,
/// Default width in pixels when the jupyter panel is docked to the left or right.
///
/// Default: 640
pub default_width: Option<f32>,
/// Default kernels to select for each language.
///
/// Default: `{}`
pub kernel_selections: Option<HashMap<String, String>>,
}
impl JupyterSettingsContent {
pub fn set_dock(&mut self, dock: JupyterDockPosition) {
self.dock = Some(dock);
}
}
impl Default for JupyterSettingsContent {
fn default() -> Self {
JupyterSettingsContent {
dock: Some(JupyterDockPosition::Right),
default_width: Some(640.0),
kernel_selections: Some(HashMap::new()),
}
}
@ -79,14 +51,6 @@ impl Settings for JupyterSettings {
let mut settings = JupyterSettings::default();
for value in sources.defaults_and_customizations() {
if let Some(dock) = value.dock {
settings.dock = dock;
}
if let Some(default_width) = value.default_width {
settings.default_width = Pixels::from(default_width);
}
if let Some(source) = &value.kernel_selections {
for (k, v) in source {
settings.kernel_selections.insert(k.clone(), v.clone());
@ -114,14 +78,6 @@ mod tests {
JupyterSettings::register(cx);
assert_eq!(JupyterSettings::enabled(cx), false);
assert_eq!(
JupyterSettings::get_global(cx).dock,
JupyterDockPosition::Right
);
assert_eq!(
JupyterSettings::get_global(cx).default_width,
Pixels::from(640.0)
);
// Setting a custom setting through user settings
SettingsStore::update_global(cx, |store, cx| {
@ -140,13 +96,5 @@ mod tests {
});
assert_eq!(JupyterSettings::enabled(cx), true);
assert_eq!(
JupyterSettings::get_global(cx).dock,
JupyterDockPosition::Left
);
assert_eq!(
JupyterSettings::get_global(cx).default_width,
Pixels::from(800.0)
);
}
}

View file

@ -7,15 +7,16 @@ use std::{sync::Arc, time::Duration};
mod jupyter_settings;
mod kernels;
mod outputs;
mod repl_editor;
mod repl_sessions_ui;
mod repl_store;
mod runtime_panel;
mod session;
mod stdio;
pub use jupyter_settings::JupyterSettings;
pub use kernels::{Kernel, KernelSpecification, KernelStatus};
pub use runtime_panel::{ClearOutputs, Interrupt, Run, Shutdown};
pub use runtime_panel::{RuntimePanel, SessionSupport};
pub use repl_editor::*;
pub use repl_sessions_ui::{ClearOutputs, Interrupt, ReplSessionsPage, Run, Shutdown};
pub use runtimelib::ExecutionState;
pub use session::Session;
@ -48,7 +49,7 @@ fn zed_dispatcher(cx: &mut AppContext) -> impl Dispatcher {
pub fn init(fs: Arc<dyn Fs>, cx: &mut AppContext) {
set_dispatcher(zed_dispatcher(cx));
JupyterSettings::register(cx);
editor::init_settings(cx);
runtime_panel::init(cx);
::editor::init_settings(cx);
repl_sessions_ui::init(cx);
ReplStore::init(fs, cx);
}

View file

@ -0,0 +1,185 @@
//! REPL operations on an [`Editor`].
use std::ops::Range;
use std::sync::Arc;
use anyhow::{Context, Result};
use editor::{Anchor, Editor, RangeToAnchorExt};
use gpui::{prelude::*, AppContext, View, WeakView, WindowContext};
use language::{Language, Point};
use multi_buffer::MultiBufferRow;
use crate::repl_store::ReplStore;
use crate::session::SessionEvent;
use crate::{KernelSpecification, Session};
pub fn run(editor: WeakView<Editor>, cx: &mut WindowContext) -> Result<()> {
let store = ReplStore::global(cx);
if !store.read(cx).is_enabled() {
return Ok(());
}
let (selected_text, language, anchor_range) = match snippet(editor.clone(), cx) {
Some(snippet) => snippet,
None => return Ok(()),
};
let entity_id = editor.entity_id();
let kernel_specification = store.update(cx, |store, cx| {
store
.kernelspec(&language, cx)
.with_context(|| format!("No kernel found for language: {}", language.name()))
})?;
let fs = store.read(cx).fs().clone();
let session = if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
session
} else {
let session = cx.new_view(|cx| Session::new(editor.clone(), fs, kernel_specification, cx));
editor.update(cx, |_editor, cx| {
cx.notify();
cx.subscribe(&session, {
let store = store.clone();
move |_this, _session, event, cx| match event {
SessionEvent::Shutdown(shutdown_event) => {
store.update(cx, |store, _cx| {
store.remove_session(shutdown_event.entity_id());
});
}
}
})
.detach();
})?;
store.update(cx, |store, _cx| {
store.insert_session(entity_id, session.clone());
});
session
};
session.update(cx, |session, cx| {
session.execute(&selected_text, anchor_range, cx);
});
anyhow::Ok(())
}
pub enum SessionSupport {
ActiveSession(View<Session>),
Inactive(Box<KernelSpecification>),
RequiresSetup(Arc<str>),
Unsupported,
}
pub fn session(editor: WeakView<Editor>, cx: &mut AppContext) -> SessionSupport {
let store = ReplStore::global(cx);
let entity_id = editor.entity_id();
if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
return SessionSupport::ActiveSession(session);
};
let language = get_language(editor, cx);
let language = match language {
Some(language) => language,
None => return SessionSupport::Unsupported,
};
let kernelspec = store.update(cx, |store, cx| store.kernelspec(&language, cx));
match kernelspec {
Some(kernelspec) => SessionSupport::Inactive(Box::new(kernelspec)),
None => match language.name().as_ref() {
"TypeScript" | "Python" => SessionSupport::RequiresSetup(language.name()),
_ => SessionSupport::Unsupported,
},
}
}
pub fn clear_outputs(editor: WeakView<Editor>, cx: &mut WindowContext) {
let store = ReplStore::global(cx);
let entity_id = editor.entity_id();
if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
session.update(cx, |session, cx| {
session.clear_outputs(cx);
cx.notify();
});
}
}
pub fn interrupt(editor: WeakView<Editor>, cx: &mut WindowContext) {
let store = ReplStore::global(cx);
let entity_id = editor.entity_id();
if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
session.update(cx, |session, cx| {
session.interrupt(cx);
cx.notify();
});
}
}
pub fn shutdown(editor: WeakView<Editor>, cx: &mut WindowContext) {
let store = ReplStore::global(cx);
let entity_id = editor.entity_id();
if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
session.update(cx, |session, cx| {
session.shutdown(cx);
cx.notify();
});
}
}
fn snippet(
editor: WeakView<Editor>,
cx: &mut WindowContext,
) -> Option<(String, Arc<Language>, Range<Anchor>)> {
let editor = editor.upgrade()?;
let editor = editor.read(cx);
let buffer = editor.buffer().read(cx).snapshot(cx);
let selection = editor.selections.newest::<usize>(cx);
let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let range = if selection.is_empty() {
let cursor = selection.head();
let cursor_row = multi_buffer_snapshot.offset_to_point(cursor).row;
let start_offset = multi_buffer_snapshot.point_to_offset(Point::new(cursor_row, 0));
let end_point = Point::new(
cursor_row,
multi_buffer_snapshot.line_len(MultiBufferRow(cursor_row)),
);
let end_offset = start_offset.saturating_add(end_point.column as usize);
// Create a range from the start to the end of the line
start_offset..end_offset
} else {
selection.range()
};
let anchor_range = range.to_anchors(&multi_buffer_snapshot);
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)?;
if start_language != end_language {
return None;
}
Some((selected_text, start_language.clone(), anchor_range))
}
fn get_language(editor: WeakView<Editor>, cx: &mut AppContext) -> Option<Arc<Language>> {
let editor = editor.upgrade()?;
let selection = editor.read(cx).selections.newest::<usize>(cx);
let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
buffer.language_at(selection.head()).cloned()
}

View file

@ -0,0 +1,257 @@
use editor::Editor;
use gpui::{
actions, prelude::*, AppContext, EventEmitter, FocusHandle, FocusableView, Subscription, View,
};
use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding};
use util::ResultExt as _;
use workspace::item::ItemEvent;
use workspace::WorkspaceId;
use workspace::{item::Item, Workspace};
use crate::jupyter_settings::JupyterSettings;
use crate::repl_store::ReplStore;
actions!(
repl,
[
Run,
ClearOutputs,
Sessions,
Interrupt,
Shutdown,
RefreshKernelspecs
]
);
actions!(repl_panel, [ToggleFocus]);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
workspace.register_action(|workspace, _: &Sessions, cx| {
let existing = workspace
.active_pane()
.read(cx)
.items()
.find_map(|item| item.downcast::<ReplSessionsPage>());
if let Some(existing) = existing {
workspace.activate_item(&existing, true, true, cx);
} else {
let extensions_page = ReplSessionsPage::new(cx);
workspace.add_item_to_active_pane(Box::new(extensions_page), None, true, cx)
}
});
workspace.register_action(|_workspace, _: &RefreshKernelspecs, cx| {
let store = ReplStore::global(cx);
store.update(cx, |store, cx| {
store.refresh_kernelspecs(cx).detach();
});
});
},
)
.detach();
cx.observe_new_views(move |editor: &mut Editor, cx: &mut ViewContext<Editor>| {
if !editor.use_modal_editing() || !editor.buffer().read(cx).is_singleton() {
return;
}
let editor_handle = cx.view().downgrade();
editor
.register_action({
let editor_handle = editor_handle.clone();
move |_: &Run, cx| {
if !JupyterSettings::enabled(cx) {
return;
}
crate::run(editor_handle.clone(), cx).log_err();
}
})
.detach();
editor
.register_action({
let editor_handle = editor_handle.clone();
move |_: &ClearOutputs, cx| {
if !JupyterSettings::enabled(cx) {
return;
}
crate::clear_outputs(editor_handle.clone(), cx);
}
})
.detach();
editor
.register_action({
let editor_handle = editor_handle.clone();
move |_: &Interrupt, cx| {
if !JupyterSettings::enabled(cx) {
return;
}
crate::interrupt(editor_handle.clone(), cx);
}
})
.detach();
editor
.register_action({
let editor_handle = editor_handle.clone();
move |_: &Shutdown, cx| {
if !JupyterSettings::enabled(cx) {
return;
}
crate::shutdown(editor_handle.clone(), cx);
}
})
.detach();
})
.detach();
}
pub struct ReplSessionsPage {
focus_handle: FocusHandle,
_subscriptions: Vec<Subscription>,
}
impl ReplSessionsPage {
pub fn new(cx: &mut ViewContext<Workspace>) -> View<Self> {
cx.new_view(|cx: &mut ViewContext<Self>| {
let focus_handle = cx.focus_handle();
let subscriptions = vec![
cx.on_focus_in(&focus_handle, |_this, cx| cx.notify()),
cx.on_focus_out(&focus_handle, |_this, _event, cx| cx.notify()),
];
Self {
focus_handle,
_subscriptions: subscriptions,
}
})
}
}
impl EventEmitter<ItemEvent> for ReplSessionsPage {}
impl FocusableView for ReplSessionsPage {
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Item for ReplSessionsPage {
type Event = ItemEvent;
fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
Some("REPL Sessions".into())
}
fn telemetry_event_text(&self) -> Option<&'static str> {
Some("repl sessions")
}
fn show_toolbar(&self) -> bool {
false
}
fn clone_on_split(
&self,
_workspace_id: Option<WorkspaceId>,
_: &mut ViewContext<Self>,
) -> Option<View<Self>> {
None
}
fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
f(*event)
}
}
impl Render for ReplSessionsPage {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let store = ReplStore::global(cx);
let (kernel_specifications, sessions) = store.update(cx, |store, _cx| {
(
store.kernel_specifications().cloned().collect::<Vec<_>>(),
store.sessions().cloned().collect::<Vec<_>>(),
)
});
// When there are no kernel specifications, show a link to the Zed docs explaining how to
// install kernels. It can be assumed they don't have a running kernel if we have no
// specifications.
if kernel_specifications.is_empty() {
return v_flex()
.p_4()
.size_full()
.gap_2()
.child(Label::new("No Jupyter Kernels Available").size(LabelSize::Large))
.child(
Label::new("To start interactively running code in your editor, you need to install and configure Jupyter kernels.")
.size(LabelSize::Default),
)
.child(
h_flex().w_full().p_4().justify_center().gap_2().child(
ButtonLike::new("install-kernels")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Install Kernels"))
.on_click(move |_, cx| {
cx.open_url(
"https://docs.jupyter.org/en/latest/install/kernels.html",
)
}),
),
)
.into_any_element();
}
// When there are no sessions, show the command to run code in an editor
if sessions.is_empty() {
return v_flex()
.p_4()
.size_full()
.gap_2()
.child(Label::new("No Jupyter Kernel Sessions").size(LabelSize::Large))
.child(
v_flex().child(
Label::new("To run code in a Jupyter kernel, select some code and use the 'repl::Run' command.")
.size(LabelSize::Default)
)
.children(
KeyBinding::for_action(&Run, cx)
.map(|binding|
binding.into_any_element()
)
)
)
.child(Label::new("Kernels available").size(LabelSize::Large))
.children(
kernel_specifications.into_iter().map(|spec| {
h_flex().gap_2().child(Label::new(spec.name.clone()))
.child(Label::new(spec.kernelspec.language.clone()).color(Color::Muted))
})
)
.into_any_element();
}
v_flex()
.p_4()
.child(Label::new("Jupyter Kernel Sessions").size(LabelSize::Large))
.children(
sessions
.into_iter()
.map(|session| session.clone().into_any_element()),
)
.into_any_element()
}
}

View file

@ -28,6 +28,10 @@ impl ReplStore {
pub(crate) fn init(fs: Arc<dyn Fs>, cx: &mut AppContext) {
let store = cx.new_model(move |cx| Self::new(fs, cx));
store
.update(cx, |store, cx| store.refresh_kernelspecs(cx))
.detach_and_log_err(cx);
cx.set_global(GlobalReplStore(store))
}
@ -49,6 +53,10 @@ impl ReplStore {
}
}
pub fn fs(&self) -> &Arc<dyn Fs> {
&self.fs
}
pub fn is_enabled(&self) -> bool {
self.enabled
}

View file

@ -1,523 +0,0 @@
use crate::repl_store::ReplStore;
use crate::{
jupyter_settings::{JupyterDockPosition, JupyterSettings},
kernels::KernelSpecification,
session::{Session, SessionEvent},
};
use anyhow::{Context as _, Result};
use editor::{Anchor, Editor, RangeToAnchorExt};
use gpui::{
actions, prelude::*, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusOutEvent,
FocusableView, Subscription, Task, View, WeakView,
};
use language::{Language, Point};
use multi_buffer::MultiBufferRow;
use project::Fs;
use settings::Settings as _;
use std::{ops::Range, sync::Arc};
use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding};
use util::ResultExt as _;
use workspace::{
dock::{Panel, PanelEvent},
Workspace,
};
actions!(
repl,
[Run, ClearOutputs, Interrupt, Shutdown, RefreshKernelspecs]
);
actions!(repl_panel, [ToggleFocus]);
pub enum SessionSupport {
ActiveSession(View<Session>),
Inactive(Box<KernelSpecification>),
RequiresSetup(Arc<str>),
Unsupported,
}
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
workspace.toggle_panel_focus::<RuntimePanel>(cx);
});
workspace.register_action(|_workspace, _: &RefreshKernelspecs, cx| {
let store = ReplStore::global(cx);
store.update(cx, |store, cx| {
store.refresh_kernelspecs(cx).detach();
});
});
},
)
.detach();
cx.observe_new_views(move |editor: &mut Editor, cx: &mut ViewContext<Editor>| {
// Only allow editors that support vim mode and are singleton buffers
if !editor.use_modal_editing() || !editor.buffer().read(cx).is_singleton() {
return;
}
editor
.register_action(cx.listener(
move |editor: &mut Editor, _: &Run, cx: &mut ViewContext<Editor>| {
if !JupyterSettings::enabled(cx) {
return;
}
let Some(workspace) = editor.workspace() else {
return;
};
let Some(panel) = workspace.read(cx).panel::<RuntimePanel>(cx) else {
return;
};
let weak_editor = cx.view().downgrade();
panel.update(cx, |_, cx| {
cx.defer(|panel, cx| {
panel.run(weak_editor, cx).log_err();
});
})
},
))
.detach();
editor
.register_action(cx.listener(
move |editor: &mut Editor, _: &ClearOutputs, cx: &mut ViewContext<Editor>| {
if !JupyterSettings::enabled(cx) {
return;
}
let Some(workspace) = editor.workspace() else {
return;
};
let Some(panel) = workspace.read(cx).panel::<RuntimePanel>(cx) else {
return;
};
let weak_editor = cx.view().downgrade();
panel.update(cx, |_, cx| {
cx.defer(|panel, cx| {
panel.clear_outputs(weak_editor, cx);
});
})
},
))
.detach();
editor
.register_action(cx.listener(
move |editor: &mut Editor, _: &Interrupt, cx: &mut ViewContext<Editor>| {
if !JupyterSettings::enabled(cx) {
return;
}
let Some(workspace) = editor.workspace() else {
return;
};
let Some(panel) = workspace.read(cx).panel::<RuntimePanel>(cx) else {
return;
};
let weak_editor = cx.view().downgrade();
panel.update(cx, |_, cx| {
cx.defer(|panel, cx| {
panel.interrupt(weak_editor, cx);
});
})
},
))
.detach();
editor
.register_action(cx.listener(
move |editor: &mut Editor, _: &Shutdown, cx: &mut ViewContext<Editor>| {
if !JupyterSettings::enabled(cx) {
return;
}
let Some(workspace) = editor.workspace() else {
return;
};
let Some(panel) = workspace.read(cx).panel::<RuntimePanel>(cx) else {
return;
};
let weak_editor = cx.view().downgrade();
panel.update(cx, |_, cx| {
cx.defer(|panel, cx| {
panel.shutdown(weak_editor, cx);
});
})
},
))
.detach();
})
.detach();
}
pub struct RuntimePanel {
fs: Arc<dyn Fs>,
focus_handle: FocusHandle,
width: Option<Pixels>,
_subscriptions: Vec<Subscription>,
}
impl RuntimePanel {
pub fn load(
workspace: WeakView<Workspace>,
cx: AsyncWindowContext,
) -> Task<Result<View<Self>>> {
cx.spawn(|mut cx| async move {
let view = workspace.update(&mut cx, |workspace, cx| {
cx.new_view::<Self>(|cx| {
let focus_handle = cx.focus_handle();
let fs = workspace.app_state().fs.clone();
let subscriptions = vec![
cx.on_focus_in(&focus_handle, Self::focus_in),
cx.on_focus_out(&focus_handle, Self::focus_out),
];
let runtime_panel = Self {
fs,
width: None,
focus_handle,
_subscriptions: subscriptions,
};
runtime_panel
})
})?;
view.update(&mut cx, |_panel, cx| {
let store = ReplStore::global(cx);
store.update(cx, |store, cx| store.refresh_kernelspecs(cx))
})?
.await?;
Ok(view)
})
}
fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
cx.notify();
}
fn focus_out(&mut self, _event: FocusOutEvent, cx: &mut ViewContext<Self>) {
cx.notify();
}
fn snippet(
editor: WeakView<Editor>,
cx: &mut ViewContext<Self>,
) -> Option<(String, Arc<Language>, Range<Anchor>)> {
let editor = editor.upgrade()?;
let editor = editor.read(cx);
let buffer = editor.buffer().read(cx).snapshot(cx);
let selection = editor.selections.newest::<usize>(cx);
let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let range = if selection.is_empty() {
let cursor = selection.head();
let cursor_row = multi_buffer_snapshot.offset_to_point(cursor).row;
let start_offset = multi_buffer_snapshot.point_to_offset(Point::new(cursor_row, 0));
let end_point = Point::new(
cursor_row,
multi_buffer_snapshot.line_len(MultiBufferRow(cursor_row)),
);
let end_offset = start_offset.saturating_add(end_point.column as usize);
// Create a range from the start to the end of the line
start_offset..end_offset
} else {
selection.range()
};
let anchor_range = range.to_anchors(&multi_buffer_snapshot);
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)?;
if start_language != end_language {
return None;
}
Some((selected_text, start_language.clone(), anchor_range))
}
fn language(editor: WeakView<Editor>, cx: &mut ViewContext<Self>) -> Option<Arc<Language>> {
let editor = editor.upgrade()?;
let selection = editor.read(cx).selections.newest::<usize>(cx);
let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
buffer.language_at(selection.head()).cloned()
}
pub fn run(&mut self, editor: WeakView<Editor>, cx: &mut ViewContext<Self>) -> Result<()> {
let store = ReplStore::global(cx);
if !store.read(cx).is_enabled() {
return Ok(());
}
let (selected_text, language, anchor_range) = match Self::snippet(editor.clone(), cx) {
Some(snippet) => snippet,
None => return Ok(()),
};
let entity_id = editor.entity_id();
let kernel_specification = store.update(cx, |store, cx| {
store
.kernelspec(&language, cx)
.with_context(|| format!("No kernel found for language: {}", language.name()))
})?;
let session = if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
session
} else {
let session =
cx.new_view(|cx| Session::new(editor, self.fs.clone(), kernel_specification, cx));
cx.notify();
let subscription = cx.subscribe(&session, {
let store = store.clone();
move |_this, _session, event, cx| match event {
SessionEvent::Shutdown(shutdown_event) => {
store.update(cx, |store, _cx| {
store.remove_session(shutdown_event.entity_id());
});
}
}
});
subscription.detach();
store.update(cx, |store, _cx| {
store.insert_session(entity_id, session.clone());
});
session
};
session.update(cx, |session, cx| {
session.execute(&selected_text, anchor_range, cx);
});
anyhow::Ok(())
}
pub fn session(
&mut self,
editor: WeakView<Editor>,
cx: &mut ViewContext<Self>,
) -> SessionSupport {
let store = ReplStore::global(cx);
let entity_id = editor.entity_id();
if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
return SessionSupport::ActiveSession(session);
};
let language = Self::language(editor, cx);
let language = match language {
Some(language) => language,
None => return SessionSupport::Unsupported,
};
let kernelspec = store.update(cx, |store, cx| store.kernelspec(&language, cx));
match kernelspec {
Some(kernelspec) => SessionSupport::Inactive(Box::new(kernelspec)),
None => match language.name().as_ref() {
"TypeScript" | "Python" => SessionSupport::RequiresSetup(language.name()),
_ => SessionSupport::Unsupported,
},
}
}
pub fn clear_outputs(&mut self, editor: WeakView<Editor>, cx: &mut ViewContext<Self>) {
let store = ReplStore::global(cx);
let entity_id = editor.entity_id();
if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
session.update(cx, |session, cx| {
session.clear_outputs(cx);
});
cx.notify();
}
}
pub fn interrupt(&mut self, editor: WeakView<Editor>, cx: &mut ViewContext<Self>) {
let store = ReplStore::global(cx);
let entity_id = editor.entity_id();
if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
session.update(cx, |session, cx| {
session.interrupt(cx);
});
cx.notify();
}
}
pub fn shutdown(&self, editor: WeakView<Editor>, cx: &mut ViewContext<Self>) {
let store = ReplStore::global(cx);
let entity_id = editor.entity_id();
if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
session.update(cx, |session, cx| {
session.shutdown(cx);
});
cx.notify();
}
}
}
impl Panel for RuntimePanel {
fn persistent_name() -> &'static str {
"RuntimePanel"
}
fn position(&self, cx: &ui::WindowContext) -> workspace::dock::DockPosition {
match JupyterSettings::get_global(cx).dock {
JupyterDockPosition::Left => workspace::dock::DockPosition::Left,
JupyterDockPosition::Right => workspace::dock::DockPosition::Right,
JupyterDockPosition::Bottom => workspace::dock::DockPosition::Bottom,
}
}
fn position_is_valid(&self, _position: workspace::dock::DockPosition) -> bool {
true
}
fn set_position(
&mut self,
position: workspace::dock::DockPosition,
cx: &mut ViewContext<Self>,
) {
settings::update_settings_file::<JupyterSettings>(self.fs.clone(), cx, move |settings| {
let dock = match position {
workspace::dock::DockPosition::Left => JupyterDockPosition::Left,
workspace::dock::DockPosition::Right => JupyterDockPosition::Right,
workspace::dock::DockPosition::Bottom => JupyterDockPosition::Bottom,
};
settings.set_dock(dock);
})
}
fn size(&self, cx: &ui::WindowContext) -> Pixels {
let settings = JupyterSettings::get_global(cx);
self.width.unwrap_or(settings.default_width)
}
fn set_size(&mut self, size: Option<ui::Pixels>, _cx: &mut ViewContext<Self>) {
self.width = size;
}
fn icon(&self, cx: &ui::WindowContext) -> Option<ui::IconName> {
let store = ReplStore::global(cx);
if !store.read(cx).is_enabled() {
return None;
}
Some(IconName::Code)
}
fn icon_tooltip(&self, _cx: &ui::WindowContext) -> Option<&'static str> {
Some("Runtime Panel")
}
fn toggle_action(&self) -> Box<dyn gpui::Action> {
Box::new(ToggleFocus)
}
}
impl EventEmitter<PanelEvent> for RuntimePanel {}
impl FocusableView for RuntimePanel {
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for RuntimePanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let store = ReplStore::global(cx);
let (kernel_specifications, sessions) = store.update(cx, |store, _cx| {
(
store.kernel_specifications().cloned().collect::<Vec<_>>(),
store.sessions().cloned().collect::<Vec<_>>(),
)
});
// When there are no kernel specifications, show a link to the Zed docs explaining how to
// install kernels. It can be assumed they don't have a running kernel if we have no
// specifications.
if kernel_specifications.is_empty() {
return v_flex()
.p_4()
.size_full()
.gap_2()
.child(Label::new("No Jupyter Kernels Available").size(LabelSize::Large))
.child(
Label::new("To start interactively running code in your editor, you need to install and configure Jupyter kernels.")
.size(LabelSize::Default),
)
.child(
h_flex().w_full().p_4().justify_center().gap_2().child(
ButtonLike::new("install-kernels")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Install Kernels"))
.on_click(move |_, cx| {
cx.open_url(
"https://docs.jupyter.org/en/latest/install/kernels.html",
)
}),
),
)
.into_any_element();
}
// When there are no sessions, show the command to run code in an editor
if sessions.is_empty() {
return v_flex()
.p_4()
.size_full()
.gap_2()
.child(Label::new("No Jupyter Kernel Sessions").size(LabelSize::Large))
.child(
v_flex().child(
Label::new("To run code in a Jupyter kernel, select some code and use the 'repl::Run' command.")
.size(LabelSize::Default)
)
.children(
KeyBinding::for_action(&Run, cx)
.map(|binding|
binding.into_any_element()
)
)
)
.child(Label::new("Kernels available").size(LabelSize::Large))
.children(
kernel_specifications.into_iter().map(|spec| {
h_flex().gap_2().child(Label::new(spec.name.clone()))
.child(Label::new(spec.kernelspec.language.clone()).color(Color::Muted))
})
)
.into_any_element();
}
v_flex()
.p_4()
.child(Label::new("Jupyter Kernel Sessions").size(LabelSize::Large))
.children(
sessions
.into_iter()
.map(|session| session.clone().into_any_element()),
)
.into_any_element()
}
}

View file

@ -80,7 +80,7 @@ impl EditorBlock {
position: code_range.end,
height: execution_view.num_lines(cx).saturating_add(1),
style: BlockStyle::Sticky,
render: Self::create_output_area_render(execution_view.clone(), on_close.clone()),
render: Self::create_output_area_renderer(execution_view.clone(), on_close.clone()),
disposition: BlockDisposition::Below,
};
@ -111,7 +111,7 @@ impl EditorBlock {
self.block_id,
(
Some(self.execution_view.num_lines(cx).saturating_add(1)),
Self::create_output_area_render(
Self::create_output_area_renderer(
self.execution_view.clone(),
self.on_close.clone(),
),
@ -122,7 +122,7 @@ impl EditorBlock {
.ok();
}
fn create_output_area_render(
fn create_output_area_renderer(
execution_view: View<ExecutionView>,
on_close: CloseBlockFn,
) -> RenderBlock {

View file

@ -199,8 +199,6 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
let assistant_panel =
assistant::AssistantPanel::load(workspace_handle.clone(), cx.clone());
let runtime_panel = repl::RuntimePanel::load(workspace_handle.clone(), cx.clone());
let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone());
let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
@ -218,7 +216,6 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
outline_panel,
terminal_panel,
assistant_panel,
runtime_panel,
channels_panel,
chat_panel,
notification_panel,
@ -227,7 +224,6 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
outline_panel,
terminal_panel,
assistant_panel,
runtime_panel,
channels_panel,
chat_panel,
notification_panel,
@ -235,7 +231,6 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
workspace_handle.update(&mut cx, |workspace, cx| {
workspace.add_panel(assistant_panel, cx);
workspace.add_panel(runtime_panel, cx);
workspace.add_panel(project_panel, cx);
workspace.add_panel(outline_panel, cx);
workspace.add_panel(terminal_panel, cx);