1use crate::{
2 jupyter_settings::{JupyterDockPosition, JupyterSettings},
3 kernels::{kernel_specifications, KernelSpecification},
4 session::{Session, SessionEvent},
5};
6use anyhow::{Context as _, Result};
7use collections::HashMap;
8use editor::{Anchor, Editor, RangeToAnchorExt};
9use gpui::{
10 actions, prelude::*, AppContext, AsyncWindowContext, EntityId, EventEmitter, FocusHandle,
11 FocusOutEvent, FocusableView, Subscription, Task, View, WeakView,
12};
13use language::{Language, Point};
14use multi_buffer::MultiBufferRow;
15use project::Fs;
16use settings::{Settings as _, SettingsStore};
17use std::{ops::Range, sync::Arc};
18use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding};
19use util::ResultExt as _;
20use workspace::{
21 dock::{Panel, PanelEvent},
22 Workspace,
23};
24
25actions!(
26 repl,
27 [Run, ClearOutputs, Interrupt, Shutdown, RefreshKernelspecs]
28);
29actions!(repl_panel, [ToggleFocus]);
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, _: &ToggleFocus, cx| {
35 workspace.toggle_panel_focus::<RuntimePanel>(cx);
36 });
37
38 workspace.register_action(|workspace, _: &RefreshKernelspecs, cx| {
39 if let Some(panel) = workspace.panel::<RuntimePanel>(cx) {
40 panel.update(cx, |panel, cx| {
41 panel.refresh_kernelspecs(cx).detach();
42 });
43 }
44 });
45 },
46 )
47 .detach();
48
49 cx.observe_new_views(move |editor: &mut Editor, cx: &mut ViewContext<Editor>| {
50 // Only allow editors that support vim mode and are singleton buffers
51 if !editor.use_modal_editing() || !editor.buffer().read(cx).is_singleton() {
52 return;
53 }
54
55 editor
56 .register_action(cx.listener(
57 move |editor: &mut Editor, _: &Run, cx: &mut ViewContext<Editor>| {
58 if !JupyterSettings::enabled(cx) {
59 return;
60 }
61 let Some(workspace) = editor.workspace() else {
62 return;
63 };
64 let Some(panel) = workspace.read(cx).panel::<RuntimePanel>(cx) else {
65 return;
66 };
67 let weak_editor = cx.view().downgrade();
68 panel.update(cx, |_, cx| {
69 cx.defer(|panel, cx| {
70 panel.run(weak_editor, cx).log_err();
71 });
72 })
73 },
74 ))
75 .detach();
76
77 editor
78 .register_action(cx.listener(
79 move |editor: &mut Editor, _: &ClearOutputs, cx: &mut ViewContext<Editor>| {
80 if !JupyterSettings::enabled(cx) {
81 return;
82 }
83 let Some(workspace) = editor.workspace() else {
84 return;
85 };
86 let Some(panel) = workspace.read(cx).panel::<RuntimePanel>(cx) else {
87 return;
88 };
89 let weak_editor = cx.view().downgrade();
90 panel.update(cx, |_, cx| {
91 cx.defer(|panel, cx| {
92 panel.clear_outputs(weak_editor, cx);
93 });
94 })
95 },
96 ))
97 .detach();
98
99 editor
100 .register_action(cx.listener(
101 move |editor: &mut Editor, _: &Interrupt, cx: &mut ViewContext<Editor>| {
102 if !JupyterSettings::enabled(cx) {
103 return;
104 }
105 let Some(workspace) = editor.workspace() else {
106 return;
107 };
108 let Some(panel) = workspace.read(cx).panel::<RuntimePanel>(cx) else {
109 return;
110 };
111 let weak_editor = cx.view().downgrade();
112 panel.update(cx, |_, cx| {
113 cx.defer(|panel, cx| {
114 panel.interrupt(weak_editor, cx);
115 });
116 })
117 },
118 ))
119 .detach();
120
121 editor
122 .register_action(cx.listener(
123 move |editor: &mut Editor, _: &Shutdown, cx: &mut ViewContext<Editor>| {
124 if !JupyterSettings::enabled(cx) {
125 return;
126 }
127 let Some(workspace) = editor.workspace() else {
128 return;
129 };
130 let Some(panel) = workspace.read(cx).panel::<RuntimePanel>(cx) else {
131 return;
132 };
133 let weak_editor = cx.view().downgrade();
134 panel.update(cx, |_, cx| {
135 cx.defer(|panel, cx| {
136 panel.shutdown(weak_editor, cx);
137 });
138 })
139 },
140 ))
141 .detach();
142 })
143 .detach();
144}
145
146pub struct RuntimePanel {
147 fs: Arc<dyn Fs>,
148 enabled: bool,
149 focus_handle: FocusHandle,
150 width: Option<Pixels>,
151 sessions: HashMap<EntityId, View<Session>>,
152 kernel_specifications: Vec<KernelSpecification>,
153 _subscriptions: Vec<Subscription>,
154}
155
156impl RuntimePanel {
157 pub fn load(
158 workspace: WeakView<Workspace>,
159 cx: AsyncWindowContext,
160 ) -> Task<Result<View<Self>>> {
161 cx.spawn(|mut cx| async move {
162 let view = workspace.update(&mut cx, |workspace, cx| {
163 cx.new_view::<Self>(|cx| {
164 let focus_handle = cx.focus_handle();
165
166 let fs = workspace.app_state().fs.clone();
167
168 let subscriptions = vec![
169 cx.on_focus_in(&focus_handle, Self::focus_in),
170 cx.on_focus_out(&focus_handle, Self::focus_out),
171 cx.observe_global::<SettingsStore>(move |this, cx| {
172 this.set_enabled(JupyterSettings::enabled(cx), cx);
173 }),
174 ];
175
176 let runtime_panel = Self {
177 fs: fs.clone(),
178 width: None,
179 focus_handle,
180 kernel_specifications: Vec::new(),
181 sessions: Default::default(),
182 _subscriptions: subscriptions,
183 enabled: JupyterSettings::enabled(cx),
184 };
185
186 runtime_panel
187 })
188 })?;
189
190 view.update(&mut cx, |this, cx| this.refresh_kernelspecs(cx))?
191 .await?;
192
193 Ok(view)
194 })
195 }
196
197 fn set_enabled(&mut self, enabled: bool, cx: &mut ViewContext<Self>) {
198 if self.enabled != enabled {
199 self.enabled = enabled;
200 cx.notify();
201 }
202 }
203
204 fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
205 cx.notify();
206 }
207
208 fn focus_out(&mut self, _event: FocusOutEvent, cx: &mut ViewContext<Self>) {
209 cx.notify();
210 }
211
212 pub fn snippet(
213 &self,
214 editor: WeakView<Editor>,
215 cx: &mut ViewContext<Self>,
216 ) -> Option<(String, Arc<Language>, Range<Anchor>)> {
217 let editor = editor.upgrade()?;
218 let editor = editor.read(cx);
219
220 let buffer = editor.buffer().read(cx).snapshot(cx);
221
222 let selection = editor.selections.newest::<usize>(cx);
223 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
224
225 let range = if selection.is_empty() {
226 let cursor = selection.head();
227
228 let cursor_row = multi_buffer_snapshot.offset_to_point(cursor).row;
229 let start_offset = multi_buffer_snapshot.point_to_offset(Point::new(cursor_row, 0));
230
231 let end_point = Point::new(
232 cursor_row,
233 multi_buffer_snapshot.line_len(MultiBufferRow(cursor_row)),
234 );
235 let end_offset = start_offset.saturating_add(end_point.column as usize);
236
237 // Create a range from the start to the end of the line
238 start_offset..end_offset
239 } else {
240 selection.range()
241 };
242
243 let anchor_range = range.to_anchors(&multi_buffer_snapshot);
244
245 let selected_text = buffer
246 .text_for_range(anchor_range.clone())
247 .collect::<String>();
248
249 let start_language = buffer.language_at(anchor_range.start)?;
250 let end_language = buffer.language_at(anchor_range.end)?;
251 if start_language != end_language {
252 return None;
253 }
254
255 Some((selected_text, start_language.clone(), anchor_range))
256 }
257
258 pub fn language(
259 &self,
260 editor: WeakView<Editor>,
261 cx: &mut ViewContext<Self>,
262 ) -> Option<Arc<Language>> {
263 let editor = editor.upgrade()?;
264 let selection = editor.read(cx).selections.newest::<usize>(cx);
265 let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
266 buffer.language_at(selection.head()).cloned()
267 }
268
269 pub fn refresh_kernelspecs(&mut self, cx: &mut ViewContext<Self>) -> Task<anyhow::Result<()>> {
270 let kernel_specifications = kernel_specifications(self.fs.clone());
271 cx.spawn(|this, mut cx| async move {
272 let kernel_specifications = kernel_specifications.await?;
273
274 this.update(&mut cx, |this, cx| {
275 this.kernel_specifications = kernel_specifications;
276 cx.notify();
277 })
278 })
279 }
280
281 pub fn kernelspec(
282 &self,
283 language: &Language,
284 cx: &mut ViewContext<Self>,
285 ) -> Option<KernelSpecification> {
286 let settings = JupyterSettings::get_global(cx);
287 let language_name = language.code_fence_block_name();
288 let selected_kernel = settings.kernel_selections.get(language_name.as_ref());
289
290 self.kernel_specifications
291 .iter()
292 .find(|runtime_specification| {
293 if let Some(selected) = selected_kernel {
294 // Top priority is the selected kernel
295 runtime_specification.name.to_lowercase() == selected.to_lowercase()
296 } else {
297 // Otherwise, we'll try to find a kernel that matches the language
298 runtime_specification.kernelspec.language.to_lowercase()
299 == language_name.to_lowercase()
300 }
301 })
302 .cloned()
303 }
304
305 pub fn run(
306 &mut self,
307 editor: WeakView<Editor>,
308 cx: &mut ViewContext<Self>,
309 ) -> anyhow::Result<()> {
310 if !self.enabled {
311 return Ok(());
312 }
313
314 let (selected_text, language, anchor_range) = match self.snippet(editor.clone(), cx) {
315 Some(snippet) => snippet,
316 None => return Ok(()),
317 };
318
319 let entity_id = editor.entity_id();
320
321 let kernel_specification = self
322 .kernelspec(&language, cx)
323 .with_context(|| format!("No kernel found for language: {}", language.name()))?;
324
325 let session = self.sessions.entry(entity_id).or_insert_with(|| {
326 let view =
327 cx.new_view(|cx| Session::new(editor, self.fs.clone(), kernel_specification, cx));
328 cx.notify();
329
330 let subscription = cx.subscribe(
331 &view,
332 |panel: &mut RuntimePanel, _session: View<Session>, event: &SessionEvent, _cx| {
333 match event {
334 SessionEvent::Shutdown(shutdown_event) => {
335 panel.sessions.remove(&shutdown_event.entity_id());
336 }
337 }
338 },
339 );
340
341 subscription.detach();
342
343 view
344 });
345
346 session.update(cx, |session, cx| {
347 session.execute(&selected_text, anchor_range, cx);
348 });
349
350 anyhow::Ok(())
351 }
352
353 pub fn clear_outputs(&mut self, editor: WeakView<Editor>, cx: &mut ViewContext<Self>) {
354 let entity_id = editor.entity_id();
355 if let Some(session) = self.sessions.get_mut(&entity_id) {
356 session.update(cx, |session, cx| {
357 session.clear_outputs(cx);
358 });
359 cx.notify();
360 }
361 }
362
363 pub fn interrupt(&mut self, editor: WeakView<Editor>, cx: &mut ViewContext<Self>) {
364 let entity_id = editor.entity_id();
365 if let Some(session) = self.sessions.get_mut(&entity_id) {
366 session.update(cx, |session, cx| {
367 session.interrupt(cx);
368 });
369 cx.notify();
370 }
371 }
372
373 pub fn shutdown(&self, editor: WeakView<Editor>, cx: &mut ViewContext<RuntimePanel>) {
374 let entity_id = editor.entity_id();
375 if let Some(session) = self.sessions.get(&entity_id) {
376 session.update(cx, |session, cx| {
377 session.shutdown(cx);
378 });
379 cx.notify();
380 }
381 }
382}
383
384pub enum SessionSupport {
385 ActiveSession(View<Session>),
386 Inactive(Box<KernelSpecification>),
387 RequiresSetup(Arc<str>),
388 Unsupported,
389}
390
391impl RuntimePanel {
392 pub fn session(
393 &mut self,
394 editor: WeakView<Editor>,
395 cx: &mut ViewContext<Self>,
396 ) -> SessionSupport {
397 let entity_id = editor.entity_id();
398 let session = self.sessions.get(&entity_id).cloned();
399
400 match session {
401 Some(session) => SessionSupport::ActiveSession(session),
402 None => {
403 let language = self.language(editor, cx);
404 let language = match language {
405 Some(language) => language,
406 None => return SessionSupport::Unsupported,
407 };
408 // Check for kernelspec
409 let kernelspec = self.kernelspec(&language, cx);
410
411 match kernelspec {
412 Some(kernelspec) => SessionSupport::Inactive(Box::new(kernelspec)),
413 None => {
414 // If no kernelspec but language is one of typescript or python
415 // then we return RequiresSetup
416 match language.name().as_ref() {
417 "TypeScript" | "Python" => {
418 SessionSupport::RequiresSetup(language.name())
419 }
420 _ => SessionSupport::Unsupported,
421 }
422 }
423 }
424 }
425 }
426 }
427}
428
429impl Panel for RuntimePanel {
430 fn persistent_name() -> &'static str {
431 "RuntimePanel"
432 }
433
434 fn position(&self, cx: &ui::WindowContext) -> workspace::dock::DockPosition {
435 match JupyterSettings::get_global(cx).dock {
436 JupyterDockPosition::Left => workspace::dock::DockPosition::Left,
437 JupyterDockPosition::Right => workspace::dock::DockPosition::Right,
438 JupyterDockPosition::Bottom => workspace::dock::DockPosition::Bottom,
439 }
440 }
441
442 fn position_is_valid(&self, _position: workspace::dock::DockPosition) -> bool {
443 true
444 }
445
446 fn set_position(
447 &mut self,
448 position: workspace::dock::DockPosition,
449 cx: &mut ViewContext<Self>,
450 ) {
451 settings::update_settings_file::<JupyterSettings>(self.fs.clone(), cx, move |settings| {
452 let dock = match position {
453 workspace::dock::DockPosition::Left => JupyterDockPosition::Left,
454 workspace::dock::DockPosition::Right => JupyterDockPosition::Right,
455 workspace::dock::DockPosition::Bottom => JupyterDockPosition::Bottom,
456 };
457 settings.set_dock(dock);
458 })
459 }
460
461 fn size(&self, cx: &ui::WindowContext) -> Pixels {
462 let settings = JupyterSettings::get_global(cx);
463
464 self.width.unwrap_or(settings.default_width)
465 }
466
467 fn set_size(&mut self, size: Option<ui::Pixels>, _cx: &mut ViewContext<Self>) {
468 self.width = size;
469 }
470
471 fn icon(&self, _cx: &ui::WindowContext) -> Option<ui::IconName> {
472 if !self.enabled {
473 return None;
474 }
475
476 Some(IconName::Code)
477 }
478
479 fn icon_tooltip(&self, _cx: &ui::WindowContext) -> Option<&'static str> {
480 Some("Runtime Panel")
481 }
482
483 fn toggle_action(&self) -> Box<dyn gpui::Action> {
484 Box::new(ToggleFocus)
485 }
486}
487
488impl EventEmitter<PanelEvent> for RuntimePanel {}
489
490impl FocusableView for RuntimePanel {
491 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
492 self.focus_handle.clone()
493 }
494}
495
496impl Render for RuntimePanel {
497 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
498 // When there are no kernel specifications, show a link to the Zed docs explaining how to
499 // install kernels. It can be assumed they don't have a running kernel if we have no
500 // specifications.
501 if self.kernel_specifications.is_empty() {
502 return v_flex()
503 .p_4()
504 .size_full()
505 .gap_2()
506 .child(Label::new("No Jupyter Kernels Available").size(LabelSize::Large))
507 .child(
508 Label::new("To start interactively running code in your editor, you need to install and configure Jupyter kernels.")
509 .size(LabelSize::Default),
510 )
511 .child(
512 h_flex().w_full().p_4().justify_center().gap_2().child(
513 ButtonLike::new("install-kernels")
514 .style(ButtonStyle::Filled)
515 .size(ButtonSize::Large)
516 .layer(ElevationIndex::ModalSurface)
517 .child(Label::new("Install Kernels"))
518 .on_click(move |_, cx| {
519 cx.open_url(
520 "https://docs.jupyter.org/en/latest/install/kernels.html",
521 )
522 }),
523 ),
524 )
525 .into_any_element();
526 }
527
528 // When there are no sessions, show the command to run code in an editor
529 if self.sessions.is_empty() {
530 return v_flex()
531 .p_4()
532 .size_full()
533 .gap_2()
534 .child(Label::new("No Jupyter Kernel Sessions").size(LabelSize::Large))
535 .child(
536 v_flex().child(
537 Label::new("To run code in a Jupyter kernel, select some code and use the 'repl::Run' command.")
538 .size(LabelSize::Default)
539 )
540 .children(
541 KeyBinding::for_action(&Run, cx)
542 .map(|binding|
543 binding.into_any_element()
544 )
545 )
546 )
547 .child(Label::new("Kernels available").size(LabelSize::Large))
548 .children(
549 self.kernel_specifications.iter().map(|spec| {
550 h_flex().gap_2().child(Label::new(spec.name.clone()))
551 .child(Label::new(spec.kernelspec.language.clone()).color(Color::Muted))
552 })
553 )
554
555 .into_any_element();
556 }
557
558 v_flex()
559 .p_4()
560 .child(Label::new("Jupyter Kernel Sessions").size(LabelSize::Large))
561 .children(
562 self.sessions
563 .values()
564 .map(|session| session.clone().into_any_element()),
565 )
566 .into_any_element()
567 }
568}