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