1use collections::HashMap;
2use editor::Editor;
3use gpui::{
4 actions, prelude::*, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView,
5 FontWeight, Subscription, View,
6};
7use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding, ListItem, Tooltip};
8use util::ResultExt as _;
9use workspace::item::ItemEvent;
10use workspace::WorkspaceId;
11use workspace::{item::Item, Workspace};
12
13use crate::jupyter_settings::JupyterSettings;
14use crate::repl_store::ReplStore;
15use crate::KernelSpecification;
16
17actions!(
18 repl,
19 [
20 Run,
21 RunInPlace,
22 ClearOutputs,
23 Sessions,
24 Interrupt,
25 Shutdown,
26 Restart,
27 RefreshKernelspecs
28 ]
29);
30
31pub fn init(cx: &mut AppContext) {
32 cx.observe_new_views(
33 |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
34 workspace.register_action(|workspace, _: &Sessions, cx| {
35 let existing = workspace
36 .active_pane()
37 .read(cx)
38 .items()
39 .find_map(|item| item.downcast::<ReplSessionsPage>());
40
41 if let Some(existing) = existing {
42 workspace.activate_item(&existing, true, true, cx);
43 } else {
44 let repl_sessions_page = ReplSessionsPage::new(cx);
45 workspace.add_item_to_active_pane(Box::new(repl_sessions_page), None, true, cx)
46 }
47 });
48
49 workspace.register_action(|_workspace, _: &RefreshKernelspecs, cx| {
50 let store = ReplStore::global(cx);
51 store.update(cx, |store, cx| {
52 store.refresh_kernelspecs(cx).detach();
53 });
54 });
55 },
56 )
57 .detach();
58
59 cx.observe_new_views(move |editor: &mut Editor, cx: &mut ViewContext<Editor>| {
60 if !editor.use_modal_editing() || !editor.buffer().read(cx).is_singleton() {
61 return;
62 }
63
64 let editor_handle = cx.view().downgrade();
65
66 editor
67 .register_action({
68 let editor_handle = editor_handle.clone();
69 move |_: &Run, cx| {
70 if !JupyterSettings::enabled(cx) {
71 return;
72 }
73
74 crate::run(editor_handle.clone(), true, cx).log_err();
75 }
76 })
77 .detach();
78
79 editor
80 .register_action({
81 let editor_handle = editor_handle.clone();
82 move |_: &RunInPlace, cx| {
83 if !JupyterSettings::enabled(cx) {
84 return;
85 }
86
87 crate::run(editor_handle.clone(), false, cx).log_err();
88 }
89 })
90 .detach();
91
92 editor
93 .register_action({
94 let editor_handle = editor_handle.clone();
95 move |_: &ClearOutputs, cx| {
96 if !JupyterSettings::enabled(cx) {
97 return;
98 }
99
100 crate::clear_outputs(editor_handle.clone(), cx);
101 }
102 })
103 .detach();
104
105 editor
106 .register_action({
107 let editor_handle = editor_handle.clone();
108 move |_: &Interrupt, cx| {
109 if !JupyterSettings::enabled(cx) {
110 return;
111 }
112
113 crate::interrupt(editor_handle.clone(), cx);
114 }
115 })
116 .detach();
117
118 editor
119 .register_action({
120 let editor_handle = editor_handle.clone();
121 move |_: &Shutdown, cx| {
122 if !JupyterSettings::enabled(cx) {
123 return;
124 }
125
126 crate::shutdown(editor_handle.clone(), cx);
127 }
128 })
129 .detach();
130
131 editor
132 .register_action({
133 let editor_handle = editor_handle.clone();
134 move |_: &Restart, cx| {
135 if !JupyterSettings::enabled(cx) {
136 return;
137 }
138
139 crate::restart(editor_handle.clone(), cx);
140 }
141 })
142 .detach();
143 })
144 .detach();
145}
146
147pub struct ReplSessionsPage {
148 focus_handle: FocusHandle,
149 _subscriptions: Vec<Subscription>,
150}
151
152impl ReplSessionsPage {
153 pub fn new(cx: &mut ViewContext<Workspace>) -> View<Self> {
154 cx.new_view(|cx: &mut ViewContext<Self>| {
155 let focus_handle = cx.focus_handle();
156
157 let subscriptions = vec![
158 cx.on_focus_in(&focus_handle, |_this, cx| cx.notify()),
159 cx.on_focus_out(&focus_handle, |_this, _event, cx| cx.notify()),
160 ];
161
162 Self {
163 focus_handle,
164 _subscriptions: subscriptions,
165 }
166 })
167 }
168}
169
170impl EventEmitter<ItemEvent> for ReplSessionsPage {}
171
172impl FocusableView for ReplSessionsPage {
173 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
174 self.focus_handle.clone()
175 }
176}
177
178impl Item for ReplSessionsPage {
179 type Event = ItemEvent;
180
181 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
182 Some("REPL Sessions".into())
183 }
184
185 fn telemetry_event_text(&self) -> Option<&'static str> {
186 Some("repl sessions")
187 }
188
189 fn show_toolbar(&self) -> bool {
190 false
191 }
192
193 fn clone_on_split(
194 &self,
195 _workspace_id: Option<WorkspaceId>,
196 _: &mut ViewContext<Self>,
197 ) -> Option<View<Self>> {
198 None
199 }
200
201 fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
202 f(*event)
203 }
204}
205
206impl Render for ReplSessionsPage {
207 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
208 let store = ReplStore::global(cx);
209
210 let (kernel_specifications, sessions) = store.update(cx, |store, _cx| {
211 (
212 store.kernel_specifications().cloned().collect::<Vec<_>>(),
213 store.sessions().cloned().collect::<Vec<_>>(),
214 )
215 });
216
217 // When there are no kernel specifications, show a link to the Zed docs explaining how to
218 // install kernels. It can be assumed they don't have a running kernel if we have no
219 // specifications.
220 if kernel_specifications.is_empty() {
221 let instructions = "To start interactively running code in your editor, you need to install and configure Jupyter kernels.";
222
223 return ReplSessionsContainer::new("No Jupyter Kernels Available")
224 .child(Label::new(instructions))
225 .child(
226 h_flex().w_full().p_4().justify_center().gap_2().child(
227 ButtonLike::new("install-kernels")
228 .style(ButtonStyle::Filled)
229 .size(ButtonSize::Large)
230 .layer(ElevationIndex::ModalSurface)
231 .child(Label::new("Install Kernels"))
232 .on_click(move |_, cx| {
233 cx.open_url(
234 "https://zed.dev/docs/repl#language-specific-instructions",
235 )
236 }),
237 ),
238 );
239 }
240
241 let mut kernels_by_language: HashMap<String, Vec<KernelSpecification>> = HashMap::default();
242 for spec in kernel_specifications {
243 kernels_by_language
244 .entry(spec.kernelspec.language.clone())
245 .or_default()
246 .push(spec);
247 }
248
249 let kernels_available = v_flex()
250 .child(Label::new("Kernels available").size(LabelSize::Large))
251 .gap_2()
252 .child(
253 h_flex()
254 .child(Label::new(
255 "Defaults indicated with a checkmark. Learn how to change your default kernel in the ",
256 ))
257 .child(
258 ButtonLike::new("configure-kernels")
259 .style(ButtonStyle::Filled)
260 // .size(ButtonSize::Compact)
261 .layer(ElevationIndex::Surface)
262 .child(Label::new("REPL documentation"))
263 .child(Icon::new(IconName::Link))
264 .on_click(move |_, cx| {
265 cx.open_url("https://zed.dev/docs/repl#changing-kernels")
266 }),
267 ),
268 )
269 .children(kernels_by_language.into_iter().map(|(language, specs)| {
270 let chosen_kernel = store.read(cx).kernelspec(&language, cx);
271
272 v_flex()
273 .gap_1()
274 .child(Label::new(language.clone()).weight(FontWeight::BOLD))
275 .children(specs.into_iter().map(|spec| {
276 let is_choice = if let Some(chosen_kernel) = &chosen_kernel {
277 chosen_kernel.name.to_lowercase() == spec.name.to_lowercase()
278 && chosen_kernel.path == spec.path
279 } else {
280 false
281 };
282
283 let path = SharedString::from(spec.path.to_string_lossy().to_string());
284
285 ListItem::new(path.clone())
286 .selectable(false)
287 .tooltip({
288 let path = path.clone();
289 move |cx| Tooltip::text(path.clone(), cx)})
290 .child(
291 h_flex()
292 .gap_1()
293 .child(div().id(path.clone()).child(Label::new(spec.name.clone())))
294 .when(is_choice, |el| {
295
296 let language = language.clone();
297
298 el.child(
299
300 div().id("check").tooltip(move |cx| Tooltip::text(format!("Default Kernel for {language}"), cx))
301 .child(Icon::new(IconName::Check)))}),
302 )
303
304 }))
305 }));
306
307 // When there are no sessions, show the command to run code in an editor
308 if sessions.is_empty() {
309 let instructions = "To run code in a Jupyter kernel, select some code and use the 'repl::Run' command.";
310
311 return ReplSessionsContainer::new("No Jupyter Kernel Sessions")
312 .child(
313 v_flex()
314 .child(Label::new(instructions))
315 .children(KeyBinding::for_action(&Run, cx)),
316 )
317 .child(div().pt_3().child(kernels_available));
318 }
319
320 ReplSessionsContainer::new("Jupyter Kernel Sessions")
321 .children(sessions)
322 .child(kernels_available)
323 }
324}
325
326#[derive(IntoElement)]
327struct ReplSessionsContainer {
328 title: SharedString,
329 children: Vec<AnyElement>,
330}
331
332impl ReplSessionsContainer {
333 pub fn new(title: impl Into<SharedString>) -> Self {
334 Self {
335 title: title.into(),
336 children: Vec::new(),
337 }
338 }
339}
340
341impl ParentElement for ReplSessionsContainer {
342 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
343 self.children.extend(elements)
344 }
345}
346
347impl RenderOnce for ReplSessionsContainer {
348 fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
349 v_flex()
350 .p_4()
351 .gap_2()
352 .size_full()
353 .child(Label::new(self.title).size(LabelSize::Large))
354 .children(self.children)
355 }
356}