Detailed changes
@@ -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);
}
},
)
@@ -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)
- );
}
}
@@ -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);
}
@@ -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()
+}
@@ -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()
+ }
+}
@@ -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
}
@@ -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()
- }
-}
@@ -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 {
@@ -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);