1use editor::Editor;
2use gpui::{
3 actions, prelude::*, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView,
4 Subscription, View,
5};
6use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding};
7use util::ResultExt as _;
8use workspace::item::ItemEvent;
9use workspace::WorkspaceId;
10use workspace::{item::Item, Workspace};
11
12use crate::components::KernelListItem;
13use crate::jupyter_settings::JupyterSettings;
14use crate::repl_store::ReplStore;
15
16actions!(
17 repl,
18 [
19 Run,
20 ClearOutputs,
21 Sessions,
22 Interrupt,
23 Shutdown,
24 RefreshKernelspecs
25 ]
26);
27
28pub fn init(cx: &mut AppContext) {
29 cx.observe_new_views(
30 |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
31 workspace.register_action(|workspace, _: &Sessions, cx| {
32 let existing = workspace
33 .active_pane()
34 .read(cx)
35 .items()
36 .find_map(|item| item.downcast::<ReplSessionsPage>());
37
38 if let Some(existing) = existing {
39 workspace.activate_item(&existing, true, true, cx);
40 } else {
41 let repl_sessions_page = ReplSessionsPage::new(cx);
42 workspace.add_item_to_active_pane(Box::new(repl_sessions_page), None, true, cx)
43 }
44 });
45
46 workspace.register_action(|_workspace, _: &RefreshKernelspecs, cx| {
47 let store = ReplStore::global(cx);
48 store.update(cx, |store, cx| {
49 store.refresh_kernelspecs(cx).detach();
50 });
51 });
52 },
53 )
54 .detach();
55
56 cx.observe_new_views(move |editor: &mut Editor, cx: &mut ViewContext<Editor>| {
57 if !editor.use_modal_editing() || !editor.buffer().read(cx).is_singleton() {
58 return;
59 }
60
61 let editor_handle = cx.view().downgrade();
62
63 editor
64 .register_action({
65 let editor_handle = editor_handle.clone();
66 move |_: &Run, cx| {
67 if !JupyterSettings::enabled(cx) {
68 return;
69 }
70
71 crate::run(editor_handle.clone(), cx).log_err();
72 }
73 })
74 .detach();
75
76 editor
77 .register_action({
78 let editor_handle = editor_handle.clone();
79 move |_: &ClearOutputs, cx| {
80 if !JupyterSettings::enabled(cx) {
81 return;
82 }
83
84 crate::clear_outputs(editor_handle.clone(), cx);
85 }
86 })
87 .detach();
88
89 editor
90 .register_action({
91 let editor_handle = editor_handle.clone();
92 move |_: &Interrupt, cx| {
93 if !JupyterSettings::enabled(cx) {
94 return;
95 }
96
97 crate::interrupt(editor_handle.clone(), cx);
98 }
99 })
100 .detach();
101
102 editor
103 .register_action({
104 let editor_handle = editor_handle.clone();
105 move |_: &Shutdown, cx| {
106 if !JupyterSettings::enabled(cx) {
107 return;
108 }
109
110 crate::shutdown(editor_handle.clone(), cx);
111 }
112 })
113 .detach();
114 })
115 .detach();
116}
117
118pub struct ReplSessionsPage {
119 focus_handle: FocusHandle,
120 _subscriptions: Vec<Subscription>,
121}
122
123impl ReplSessionsPage {
124 pub fn new(cx: &mut ViewContext<Workspace>) -> View<Self> {
125 cx.new_view(|cx: &mut ViewContext<Self>| {
126 let focus_handle = cx.focus_handle();
127
128 let subscriptions = vec![
129 cx.on_focus_in(&focus_handle, |_this, cx| cx.notify()),
130 cx.on_focus_out(&focus_handle, |_this, _event, cx| cx.notify()),
131 ];
132
133 Self {
134 focus_handle,
135 _subscriptions: subscriptions,
136 }
137 })
138 }
139}
140
141impl EventEmitter<ItemEvent> for ReplSessionsPage {}
142
143impl FocusableView for ReplSessionsPage {
144 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
145 self.focus_handle.clone()
146 }
147}
148
149impl Item for ReplSessionsPage {
150 type Event = ItemEvent;
151
152 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
153 Some("REPL Sessions".into())
154 }
155
156 fn telemetry_event_text(&self) -> Option<&'static str> {
157 Some("repl sessions")
158 }
159
160 fn show_toolbar(&self) -> bool {
161 false
162 }
163
164 fn clone_on_split(
165 &self,
166 _workspace_id: Option<WorkspaceId>,
167 _: &mut ViewContext<Self>,
168 ) -> Option<View<Self>> {
169 None
170 }
171
172 fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
173 f(*event)
174 }
175}
176
177impl Render for ReplSessionsPage {
178 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
179 let store = ReplStore::global(cx);
180
181 let (kernel_specifications, sessions) = store.update(cx, |store, _cx| {
182 (
183 store.kernel_specifications().cloned().collect::<Vec<_>>(),
184 store.sessions().cloned().collect::<Vec<_>>(),
185 )
186 });
187
188 // When there are no kernel specifications, show a link to the Zed docs explaining how to
189 // install kernels. It can be assumed they don't have a running kernel if we have no
190 // specifications.
191 if kernel_specifications.is_empty() {
192 let instructions = "To start interactively running code in your editor, you need to install and configure Jupyter kernels.";
193
194 return ReplSessionsContainer::new("No Jupyter Kernels Available")
195 .child(Label::new(instructions))
196 .child(
197 h_flex().w_full().p_4().justify_center().gap_2().child(
198 ButtonLike::new("install-kernels")
199 .style(ButtonStyle::Filled)
200 .size(ButtonSize::Large)
201 .layer(ElevationIndex::ModalSurface)
202 .child(Label::new("Install Kernels"))
203 .on_click(move |_, cx| {
204 cx.open_url(
205 "https://docs.jupyter.org/en/latest/install/kernels.html",
206 )
207 }),
208 ),
209 );
210 }
211
212 // When there are no sessions, show the command to run code in an editor
213 if sessions.is_empty() {
214 let instructions = "To run code in a Jupyter kernel, select some code and use the 'repl::Run' command.";
215
216 return ReplSessionsContainer::new("No Jupyter Kernel Sessions")
217 .child(
218 v_flex()
219 .child(Label::new(instructions))
220 .children(KeyBinding::for_action(&Run, cx)),
221 )
222 .child(Label::new("Kernels available").size(LabelSize::Large))
223 .children(kernel_specifications.into_iter().map(|spec| {
224 KernelListItem::new(spec.clone()).child(
225 h_flex()
226 .gap_2()
227 .child(Label::new(spec.name))
228 .child(Label::new(spec.kernelspec.language).color(Color::Muted)),
229 )
230 }));
231 }
232
233 ReplSessionsContainer::new("Jupyter Kernel Sessions").children(sessions)
234 }
235}
236
237#[derive(IntoElement)]
238struct ReplSessionsContainer {
239 title: SharedString,
240 children: Vec<AnyElement>,
241}
242
243impl ReplSessionsContainer {
244 pub fn new(title: impl Into<SharedString>) -> Self {
245 Self {
246 title: title.into(),
247 children: Vec::new(),
248 }
249 }
250}
251
252impl ParentElement for ReplSessionsContainer {
253 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
254 self.children.extend(elements)
255 }
256}
257
258impl RenderOnce for ReplSessionsContainer {
259 fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
260 v_flex()
261 .p_4()
262 .gap_2()
263 .size_full()
264 .child(Label::new(self.title).size(LabelSize::Large))
265 .children(self.children)
266 }
267}