use editor::Editor; use gpui::{ actions, prelude::*, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, Subscription, View, }; use project::ProjectItem as _; 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, 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 project = workspace.map(|workspace| workspace.read(cx).project().clone()); let is_local_project = project .as_ref() .map(|project| project.read(cx).is_local()) .unwrap_or(false); if !is_local_project { return; } let buffer = editor.buffer().read(cx).as_singleton(); let language = buffer .as_ref() .and_then(|buffer| buffer.read(cx).language()); let project_path = buffer.and_then(|buffer| buffer.read(cx).project_path(cx)); let editor_handle = cx.view().downgrade(); if let Some(language) = language { if language.name() == "Python".into() { if let (Some(project_path), Some(project)) = (project_path, project) { let store = ReplStore::global(cx); store.update(cx, |store, cx| { store .refresh_python_kernelspecs(project_path.worktree_id, &project, cx) .detach_and_log_err(cx); }); } } } 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 .pure_jupyter_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", ) }), ), ); } // 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)), ); } ReplSessionsContainer::new("Jupyter Kernel Sessions").children(sessions) } } #[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) } }