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