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