use editor::Editor; use gpui::{ actions, prelude::*, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, FontWeight, Subscription, View, }; use std::collections::HashMap; use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding, ListItem, Tooltip}; 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; use crate::{KernelSpecification, KERNEL_DOCS_URL}; actions!( repl, [ Run, RunInPlace, ClearOutputs, Sessions, Interrupt, Shutdown, Restart, RefreshKernelspecs ] ); pub fn init(cx: &mut AppContext) { cx.observe_new_views( |workspace: &mut Workspace, _cx: &mut ViewContext| { workspace.register_action(|workspace, _: &Sessions, cx| { let existing = workspace .active_pane() .read(cx) .items() .find_map(|item| item.downcast::()); if let Some(existing) = existing { workspace.activate_item(&existing, true, true, cx); } else { let repl_sessions_page = ReplSessionsPage::new(cx); workspace.add_item_to_active_pane(Box::new(repl_sessions_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| { if !editor.use_modal_editing() || !editor.buffer().read(cx).is_singleton() { return; } cx.defer(|editor, cx| { let workspace = Workspace::for_window(cx); let is_local_project = workspace .map(|workspace| workspace.read(cx).project().read(cx).is_local()) .unwrap_or(false); if !is_local_project { 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(), true, cx).log_err(); } }) .detach(); editor .register_action({ let editor_handle = editor_handle.clone(); move |_: &RunInPlace, cx| { if !JupyterSettings::enabled(cx) { return; } crate::run(editor_handle.clone(), false, cx).log_err(); } }) .detach(); }); }) .detach(); } pub struct ReplSessionsPage { focus_handle: FocusHandle, _subscriptions: Vec, } impl ReplSessionsPage { pub fn new(cx: &mut ViewContext) -> View { cx.new_view(|cx: &mut ViewContext| { 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 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 { 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, _: &mut ViewContext, ) -> Option> { 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) -> impl IntoElement { let store = ReplStore::global(cx); let (kernel_specifications, sessions) = store.update(cx, |store, _cx| { ( store.kernel_specifications().cloned().collect::>(), store.sessions().cloned().collect::>(), ) }); // 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() { let instructions = "To start interactively running code in your editor, you need to install and configure Jupyter kernels."; return ReplSessionsContainer::new("No Jupyter Kernels Available") .child(Label::new(instructions)) .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://zed.dev/docs/repl#language-specific-instructions", ) }), ), ); } let mut kernels_by_language: HashMap> = kernel_specifications .iter() .map(|spec| (spec.language(), spec)) .fold(HashMap::new(), |mut acc, (language, spec)| { acc.entry(language).or_default().push(spec); acc }); for kernels in kernels_by_language.values_mut() { kernels.sort_by_key(|a| a.name()) } // Convert to a sorted Vec of tuples let mut sorted_kernels: Vec<(SharedString, Vec<&KernelSpecification>)> = kernels_by_language.into_iter().collect(); sorted_kernels.sort_by(|a, b| a.0.cmp(&b.0)); let kernels_available = v_flex() .child(Label::new("Kernels available").size(LabelSize::Large)) .gap_2() .child( h_flex() .child(Label::new( "Defaults indicated with a checkmark. Learn how to change your default kernel in the ", )) .child( ButtonLike::new("configure-kernels") .style(ButtonStyle::Filled) // .size(ButtonSize::Compact) .layer(ElevationIndex::Surface) .child(Label::new("REPL documentation")) .child(Icon::new(IconName::Link)) .on_click(move |_, cx| { cx.open_url(KERNEL_DOCS_URL) }), ), ) .children(sorted_kernels.into_iter().map(|(language, specs)| { let chosen_kernel = store.read(cx).kernelspec(&language, cx); v_flex() .gap_1() .child(Label::new(language.clone()).weight(FontWeight::BOLD)) .children(specs.into_iter().map(|spec| { let is_choice = if let Some(chosen_kernel) = &chosen_kernel { chosen_kernel == spec } else { false }; let path = spec.path(); ListItem::new(path.clone()) .selectable(false) .tooltip({ let path = path.clone(); move |cx| Tooltip::text(path.clone(), cx)}) .child( h_flex() .gap_1() .child(div().id(path.clone()).child(Label::new(spec.name()))) .when(is_choice, |el| { let language = language.clone(); el.child( div().id("check").tooltip(move |cx| Tooltip::text(format!("Default Kernel for {language}"), cx)) .child(Icon::new(IconName::Check)))}), ) })) })); // When there are no sessions, show the command to run code in an editor if sessions.is_empty() { let instructions = "To run code in a Jupyter kernel, select some code and use the 'repl::Run' command."; return ReplSessionsContainer::new("No Jupyter Kernel Sessions") .child( v_flex() .child(Label::new(instructions)) .children(KeyBinding::for_action(&Run, cx)), ) .child(div().pt_3().child(kernels_available)); } ReplSessionsContainer::new("Jupyter Kernel Sessions") .children(sessions) .child(kernels_available) } } #[derive(IntoElement)] struct ReplSessionsContainer { title: SharedString, children: Vec, } impl ReplSessionsContainer { pub fn new(title: impl Into) -> Self { Self { title: title.into(), children: Vec::new(), } } } impl ParentElement for ReplSessionsContainer { fn extend(&mut self, elements: impl IntoIterator) { self.children.extend(elements) } } impl RenderOnce for ReplSessionsContainer { fn render(self, _cx: &mut WindowContext) -> impl IntoElement { v_flex() .p_4() .gap_2() .size_full() .child(Label::new(self.title).size(LabelSize::Large)) .children(self.children) } }