1use crate::components::KernelListItem;
2use crate::kernels::RemoteRunningKernel;
3use crate::setup_editor_session_actions;
4use crate::{
5 KernelStatus,
6 kernels::{Kernel, KernelSession, KernelSpecification, NativeRunningKernel},
7 outputs::{
8 ExecutionStatus, ExecutionView, ExecutionViewFinishedEmpty, ExecutionViewFinishedSmall,
9 InputReplyEvent,
10 },
11 repl_settings::ReplSettings,
12};
13use anyhow::Context as _;
14use collections::{HashMap, HashSet};
15use editor::SelectionEffects;
16use editor::{
17 Anchor, AnchorRangeExt as _, Editor, Inlay, MultiBuffer, ToOffset, ToPoint,
18 display_map::{
19 BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId,
20 RenderBlock,
21 },
22 scroll::Autoscroll,
23};
24use project::InlayId;
25
26/// Marker types
27enum ReplExecutedRange {}
28
29use futures::FutureExt as _;
30use gpui::{
31 Context, Entity, EventEmitter, Render, Subscription, Task, WeakEntity, Window, div, prelude::*,
32};
33use language::Point;
34use project::Fs;
35use runtimelib::{
36 ExecuteRequest, ExecutionState, InputReply, InterruptRequest, JupyterMessage,
37 JupyterMessageContent, ReplyStatus, ShutdownRequest,
38};
39use settings::Settings as _;
40use std::{env::temp_dir, ops::Range, sync::Arc, time::Duration};
41use theme::ActiveTheme;
42use ui::{IconButtonShape, Tooltip, prelude::*};
43use util::ResultExt as _;
44
45pub struct Session {
46 fs: Arc<dyn Fs>,
47 editor: WeakEntity<Editor>,
48 pub kernel: Kernel,
49 pub kernel_specification: KernelSpecification,
50
51 blocks: HashMap<String, EditorBlock>,
52 result_inlays: HashMap<String, (InlayId, Range<Anchor>, usize)>,
53 next_inlay_id: usize,
54
55 _subscriptions: Vec<Subscription>,
56}
57
58struct EditorBlock {
59 code_range: Range<Anchor>,
60 invalidation_anchor: Anchor,
61 block_id: CustomBlockId,
62 execution_view: Entity<ExecutionView>,
63}
64
65type CloseBlockFn =
66 Arc<dyn for<'a> Fn(CustomBlockId, &'a mut Window, &mut App) + Send + Sync + 'static>;
67
68impl EditorBlock {
69 fn new(
70 editor: WeakEntity<Editor>,
71 code_range: Range<Anchor>,
72 status: ExecutionStatus,
73 on_close: CloseBlockFn,
74 cx: &mut Context<Session>,
75 ) -> anyhow::Result<Self> {
76 let editor = editor.upgrade().context("editor is not open")?;
77 let workspace = editor.read(cx).workspace().context("workspace dropped")?;
78
79 let execution_view = cx.new(|cx| ExecutionView::new(status, workspace.downgrade(), cx));
80
81 let (block_id, invalidation_anchor) = editor.update(cx, |editor, cx| {
82 let buffer = editor.buffer().clone();
83 let buffer_snapshot = buffer.read(cx).snapshot(cx);
84 let end_point = code_range.end.to_point(&buffer_snapshot);
85 let next_row_start = end_point + Point::new(1, 0);
86 if next_row_start > buffer_snapshot.max_point() {
87 buffer.update(cx, |buffer, cx| {
88 buffer.edit(
89 [(
90 buffer_snapshot.max_point()..buffer_snapshot.max_point(),
91 "\n",
92 )],
93 None,
94 cx,
95 )
96 });
97 }
98
99 // Re-read snapshot after potential buffer edit and create a fresh anchor for
100 // block placement. Using anchor_before (Bias::Left) ensures the anchor stays
101 // at the end of the code line regardless of whether we inserted a newline.
102 let buffer_snapshot = buffer.read(cx).snapshot(cx);
103 let block_placement_anchor = buffer_snapshot.anchor_before(end_point);
104 let invalidation_anchor = buffer_snapshot.anchor_before(next_row_start);
105 let block = BlockProperties {
106 placement: BlockPlacement::Below(block_placement_anchor),
107 // Take up at least one height for status, allow the editor to determine the real height based on the content from render
108 height: Some(1),
109 style: BlockStyle::Sticky,
110 render: Self::create_output_area_renderer(execution_view.clone(), on_close.clone()),
111 priority: 0,
112 };
113
114 let block_id = editor.insert_blocks([block], None, cx)[0];
115 (block_id, invalidation_anchor)
116 });
117
118 anyhow::Ok(Self {
119 code_range,
120 invalidation_anchor,
121 block_id,
122 execution_view,
123 })
124 }
125
126 fn handle_message(
127 &mut self,
128 message: &JupyterMessage,
129 window: &mut Window,
130 cx: &mut Context<Session>,
131 ) {
132 self.execution_view.update(cx, |execution_view, cx| {
133 if matches!(&message.content, JupyterMessageContent::InputRequest(_)) {
134 execution_view.handle_input_request(message, window, cx);
135 } else {
136 execution_view.push_message(&message.content, window, cx);
137 }
138 });
139 }
140
141 fn create_output_area_renderer(
142 execution_view: Entity<ExecutionView>,
143 on_close: CloseBlockFn,
144 ) -> RenderBlock {
145 Arc::new(move |cx: &mut BlockContext| {
146 let execution_view = execution_view.clone();
147 let text_style = crate::outputs::plain::text_style(cx.window, cx.app);
148
149 let editor_margins = cx.margins;
150 let gutter = editor_margins.gutter;
151
152 let block_id = cx.block_id;
153 let on_close = on_close.clone();
154
155 let rem_size = cx.window.rem_size();
156
157 let text_line_height = text_style.line_height_in_pixels(rem_size);
158 let output_settings = ReplSettings::get_global(cx.app);
159 let output_max_height = if output_settings.output_max_height_lines > 0 {
160 Some(text_line_height * output_settings.output_max_height_lines as f32)
161 } else {
162 None
163 };
164
165 let close_button = h_flex()
166 .flex_none()
167 .items_center()
168 .justify_center()
169 .absolute()
170 .top(text_line_height / 2.)
171 .right(
172 // 2px is a magic number to nudge the button just a bit closer to
173 // the line number start
174 gutter.full_width() / 2.0 - text_line_height / 2.0 - px(2.),
175 )
176 .w(text_line_height)
177 .h(text_line_height)
178 .child(
179 IconButton::new("close_output_area", IconName::Close)
180 .icon_size(IconSize::Small)
181 .icon_color(Color::Muted)
182 .size(ButtonSize::Compact)
183 .shape(IconButtonShape::Square)
184 .tooltip(Tooltip::text("Close output area"))
185 .on_click(move |_, window, cx| {
186 if let BlockId::Custom(block_id) = block_id {
187 (on_close)(block_id, window, cx)
188 }
189 }),
190 );
191
192 div()
193 .id(cx.block_id)
194 .block_mouse_except_scroll()
195 .flex()
196 .items_start()
197 .min_h(text_line_height)
198 .w_full()
199 .border_y_1()
200 .border_color(cx.theme().colors().border)
201 .bg(cx.theme().colors().background)
202 .child(
203 div()
204 .relative()
205 .w(gutter.full_width())
206 .h(text_line_height * 2)
207 .child(close_button),
208 )
209 .child(
210 div()
211 .id((ElementId::from(cx.block_id), "output-scroll"))
212 .flex_1()
213 .overflow_x_hidden()
214 .py(text_line_height / 2.)
215 .mr(editor_margins.right)
216 .pr_2()
217 .when_some(output_max_height, |div, max_h| {
218 div.max_h(max_h).overflow_y_scroll()
219 })
220 .child(execution_view),
221 )
222 .into_any_element()
223 })
224 }
225}
226
227impl Session {
228 pub fn new(
229 editor: WeakEntity<Editor>,
230 fs: Arc<dyn Fs>,
231 kernel_specification: KernelSpecification,
232 window: &mut Window,
233 cx: &mut Context<Self>,
234 ) -> Self {
235 let subscription = match editor.upgrade() {
236 Some(editor) => {
237 let buffer = editor.read(cx).buffer().clone();
238 cx.subscribe(&buffer, Self::on_buffer_event)
239 }
240 None => Subscription::new(|| {}),
241 };
242
243 let editor_handle = editor.clone();
244
245 editor
246 .update(cx, |editor, _cx| {
247 setup_editor_session_actions(editor, editor_handle);
248 })
249 .ok();
250
251 let mut session = Self {
252 fs,
253 editor,
254 kernel: Kernel::StartingKernel(Task::ready(()).shared()),
255 blocks: HashMap::default(),
256 result_inlays: HashMap::default(),
257 next_inlay_id: 0,
258 kernel_specification,
259 _subscriptions: vec![subscription],
260 };
261
262 session.start_kernel(window, cx);
263 session
264 }
265
266 fn start_kernel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
267 let kernel_language = self.kernel_specification.language();
268 let entity_id = self.editor.entity_id();
269 let working_directory = self
270 .editor
271 .upgrade()
272 .and_then(|editor| editor.read(cx).working_directory(cx))
273 .unwrap_or_else(temp_dir);
274
275 telemetry::event!(
276 "Kernel Status Changed",
277 kernel_language,
278 kernel_status = KernelStatus::Starting.to_string(),
279 repl_session_id = cx.entity_id().to_string(),
280 );
281
282 let session_view = cx.entity();
283
284 let kernel = match self.kernel_specification.clone() {
285 KernelSpecification::Jupyter(kernel_specification) => NativeRunningKernel::new(
286 kernel_specification,
287 entity_id,
288 working_directory,
289 self.fs.clone(),
290 session_view,
291 window,
292 cx,
293 ),
294 KernelSpecification::PythonEnv(env_specification) => NativeRunningKernel::new(
295 env_specification.as_local_spec(),
296 entity_id,
297 working_directory,
298 self.fs.clone(),
299 session_view,
300 window,
301 cx,
302 ),
303 KernelSpecification::Remote(remote_kernel_specification) => RemoteRunningKernel::new(
304 remote_kernel_specification,
305 working_directory,
306 session_view,
307 window,
308 cx,
309 ),
310 };
311
312 let pending_kernel = cx
313 .spawn(async move |this, cx| {
314 let kernel = kernel.await;
315
316 match kernel {
317 Ok(kernel) => {
318 this.update(cx, |session, cx| {
319 session.kernel(Kernel::RunningKernel(kernel), cx);
320 })
321 .ok();
322 }
323 Err(err) => {
324 this.update(cx, |session, cx| {
325 session.kernel_errored(err.to_string(), cx);
326 })
327 .ok();
328 }
329 }
330 })
331 .shared();
332
333 self.kernel(Kernel::StartingKernel(pending_kernel), cx);
334 cx.notify();
335 }
336
337 pub fn kernel_errored(&mut self, error_message: String, cx: &mut Context<Self>) {
338 self.kernel(Kernel::ErroredLaunch(error_message.clone()), cx);
339
340 self.blocks.values().for_each(|block| {
341 block.execution_view.update(cx, |execution_view, cx| {
342 match execution_view.status {
343 ExecutionStatus::Finished => {
344 // Do nothing when the output was good
345 }
346 _ => {
347 // All other cases, set the status to errored
348 execution_view.status =
349 ExecutionStatus::KernelErrored(error_message.clone())
350 }
351 }
352 cx.notify();
353 });
354 });
355 }
356
357 fn on_buffer_event(
358 &mut self,
359 buffer: Entity<MultiBuffer>,
360 event: &multi_buffer::Event,
361 cx: &mut Context<Self>,
362 ) {
363 if let multi_buffer::Event::Edited { .. } = event {
364 let snapshot = buffer.read(cx).snapshot(cx);
365
366 let mut blocks_to_remove: HashSet<CustomBlockId> = HashSet::default();
367 let mut gutter_ranges_to_remove: Vec<Range<Anchor>> = Vec::new();
368 let mut keys_to_remove: Vec<String> = Vec::new();
369
370 self.blocks.retain(|id, block| {
371 if block.invalidation_anchor.is_valid(&snapshot) {
372 true
373 } else {
374 blocks_to_remove.insert(block.block_id);
375 gutter_ranges_to_remove.push(block.code_range.clone());
376 keys_to_remove.push(id.clone());
377 false
378 }
379 });
380
381 let mut inlays_to_remove: Vec<InlayId> = Vec::new();
382
383 self.result_inlays
384 .retain(|id, (inlay_id, code_range, original_len)| {
385 let start_offset = code_range.start.to_offset(&snapshot);
386 let end_offset = code_range.end.to_offset(&snapshot);
387 let current_len = end_offset.saturating_sub(start_offset);
388
389 if current_len != *original_len {
390 inlays_to_remove.push(*inlay_id);
391 gutter_ranges_to_remove.push(code_range.clone());
392 keys_to_remove.push(id.clone());
393 false
394 } else {
395 true
396 }
397 });
398
399 if !blocks_to_remove.is_empty()
400 || !inlays_to_remove.is_empty()
401 || !gutter_ranges_to_remove.is_empty()
402 {
403 self.editor
404 .update(cx, |editor, cx| {
405 if !blocks_to_remove.is_empty() {
406 editor.remove_blocks(blocks_to_remove, None, cx);
407 }
408 if !inlays_to_remove.is_empty() {
409 editor.splice_inlays(&inlays_to_remove, vec![], cx);
410 }
411 if !gutter_ranges_to_remove.is_empty() {
412 editor.remove_gutter_highlights::<ReplExecutedRange>(
413 gutter_ranges_to_remove,
414 cx,
415 );
416 }
417 })
418 .ok();
419 cx.notify();
420 }
421 }
422 }
423
424 fn send(&mut self, message: JupyterMessage, _cx: &mut Context<Self>) -> anyhow::Result<()> {
425 if let Kernel::RunningKernel(kernel) = &mut self.kernel {
426 kernel.request_tx().try_send(message).ok();
427 }
428
429 anyhow::Ok(())
430 }
431
432 fn send_stdin_reply(
433 &mut self,
434 value: String,
435 parent_message: &JupyterMessage,
436 _cx: &mut Context<Self>,
437 ) {
438 if let Kernel::RunningKernel(kernel) = &mut self.kernel {
439 let reply = InputReply {
440 value,
441 status: ReplyStatus::Ok,
442 error: None,
443 };
444 let message = reply.as_child_of(parent_message);
445 kernel.stdin_tx().try_send(message).log_err();
446 }
447 }
448
449 fn replace_block_with_inlay(&mut self, message_id: &str, text: &str, cx: &mut Context<Self>) {
450 let Some(block) = self.blocks.remove(message_id) else {
451 return;
452 };
453
454 let Some(editor) = self.editor.upgrade() else {
455 return;
456 };
457
458 let code_range = block.code_range.clone();
459
460 editor.update(cx, |editor, cx| {
461 let mut block_ids = HashSet::default();
462 block_ids.insert(block.block_id);
463 editor.remove_blocks(block_ids, None, cx);
464
465 let buffer = editor.buffer().read(cx).snapshot(cx);
466 let start_offset = code_range.start.to_offset(&buffer);
467 let end_offset = code_range.end.to_offset(&buffer);
468 let original_len = end_offset.saturating_sub(start_offset);
469
470 let end_point = code_range.end.to_point(&buffer);
471 let inlay_position = buffer.anchor_after(end_point);
472
473 let inlay_id = self.next_inlay_id;
474 self.next_inlay_id += 1;
475
476 let inlay = Inlay::repl_result(inlay_id, inlay_position, format!(" {}", text));
477
478 editor.splice_inlays(&[], vec![inlay], cx);
479 self.result_inlays.insert(
480 message_id.to_string(),
481 (
482 InlayId::ReplResult(inlay_id),
483 code_range.clone(),
484 original_len,
485 ),
486 );
487
488 editor.insert_gutter_highlight::<ReplExecutedRange>(
489 code_range,
490 |cx| cx.theme().status().success,
491 cx,
492 );
493 });
494
495 cx.notify();
496 }
497
498 pub fn clear_outputs(&mut self, cx: &mut Context<Self>) {
499 let blocks_to_remove: HashSet<CustomBlockId> =
500 self.blocks.values().map(|block| block.block_id).collect();
501
502 let inlays_to_remove: Vec<InlayId> =
503 self.result_inlays.values().map(|(id, _, _)| *id).collect();
504
505 self.editor
506 .update(cx, |editor, cx| {
507 editor.remove_blocks(blocks_to_remove, None, cx);
508 editor.splice_inlays(&inlays_to_remove, vec![], cx);
509 editor.clear_gutter_highlights::<ReplExecutedRange>(cx);
510 })
511 .ok();
512
513 self.blocks.clear();
514 self.result_inlays.clear();
515 }
516
517 pub fn execute(
518 &mut self,
519 code: String,
520 anchor_range: Range<Anchor>,
521 next_cell: Option<Anchor>,
522 move_down: bool,
523 window: &mut Window,
524 cx: &mut Context<Self>,
525 ) {
526 let Some(editor) = self.editor.upgrade() else {
527 return;
528 };
529
530 if code.is_empty() {
531 return;
532 }
533
534 let execute_request = ExecuteRequest {
535 code,
536 allow_stdin: true,
537 ..ExecuteRequest::default()
538 };
539
540 let message: JupyterMessage = execute_request.into();
541
542 let mut blocks_to_remove: HashSet<CustomBlockId> = HashSet::default();
543 let mut inlays_to_remove: Vec<InlayId> = Vec::new();
544 let mut gutter_ranges_to_remove: Vec<Range<Anchor>> = Vec::new();
545
546 let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
547
548 self.blocks.retain(|_key, block| {
549 if anchor_range.overlaps(&block.code_range, &buffer) {
550 blocks_to_remove.insert(block.block_id);
551 false
552 } else {
553 true
554 }
555 });
556
557 self.result_inlays
558 .retain(|_key, (inlay_id, inlay_range, _)| {
559 if anchor_range.overlaps(inlay_range, &buffer) {
560 inlays_to_remove.push(*inlay_id);
561 gutter_ranges_to_remove.push(inlay_range.clone());
562 false
563 } else {
564 true
565 }
566 });
567
568 self.editor
569 .update(cx, |editor, cx| {
570 editor.remove_blocks(blocks_to_remove, None, cx);
571 if !inlays_to_remove.is_empty() {
572 editor.splice_inlays(&inlays_to_remove, vec![], cx);
573 }
574 if !gutter_ranges_to_remove.is_empty() {
575 editor
576 .remove_gutter_highlights::<ReplExecutedRange>(gutter_ranges_to_remove, cx);
577 }
578 })
579 .ok();
580
581 let status = match &self.kernel {
582 Kernel::Restarting => ExecutionStatus::Restarting,
583 Kernel::RunningKernel(_) => ExecutionStatus::Queued,
584 Kernel::StartingKernel(_) => ExecutionStatus::ConnectingToKernel,
585 Kernel::ErroredLaunch(error) => ExecutionStatus::KernelErrored(error.clone()),
586 Kernel::ShuttingDown => ExecutionStatus::ShuttingDown,
587 Kernel::Shutdown => ExecutionStatus::Shutdown,
588 };
589
590 let parent_message_id = message.header.msg_id.clone();
591 let session_view = cx.entity().downgrade();
592 let weak_editor = self.editor.clone();
593 let code_range_for_close = anchor_range.clone();
594
595 let on_close: CloseBlockFn = Arc::new(
596 move |block_id: CustomBlockId, _: &mut Window, cx: &mut App| {
597 if let Some(session) = session_view.upgrade() {
598 session.update(cx, |session, cx| {
599 session.blocks.remove(&parent_message_id);
600 cx.notify();
601 });
602 }
603
604 if let Some(editor) = weak_editor.upgrade() {
605 editor.update(cx, |editor, cx| {
606 let mut block_ids = HashSet::default();
607 block_ids.insert(block_id);
608 editor.remove_blocks(block_ids, None, cx);
609 editor.remove_gutter_highlights::<ReplExecutedRange>(
610 vec![code_range_for_close.clone()],
611 cx,
612 );
613 });
614 }
615 },
616 );
617
618 let Ok(editor_block) = EditorBlock::new(
619 self.editor.clone(),
620 anchor_range.clone(),
621 status,
622 on_close,
623 cx,
624 ) else {
625 return;
626 };
627
628 self.editor
629 .update(cx, |editor, cx| {
630 editor.insert_gutter_highlight::<ReplExecutedRange>(
631 anchor_range.clone(),
632 |cx| cx.theme().status().success,
633 cx,
634 );
635 })
636 .ok();
637
638 let new_cursor_pos = if let Some(next_cursor) = next_cell {
639 next_cursor
640 } else {
641 editor_block.invalidation_anchor
642 };
643
644 let msg_id = message.header.msg_id.clone();
645 let subscription = cx.subscribe(
646 &editor_block.execution_view,
647 move |session, _execution_view, _event: &ExecutionViewFinishedEmpty, cx| {
648 session.replace_block_with_inlay(&msg_id, "✓", cx);
649 },
650 );
651 self._subscriptions.push(subscription);
652
653 let msg_id = message.header.msg_id.clone();
654 let subscription = cx.subscribe(
655 &editor_block.execution_view,
656 move |session, _execution_view, event: &ExecutionViewFinishedSmall, cx| {
657 session.replace_block_with_inlay(&msg_id, &event.0, cx);
658 },
659 );
660 self._subscriptions.push(subscription);
661
662 let subscription = cx.subscribe(
663 &editor_block.execution_view,
664 |session, _execution_view, event: &InputReplyEvent, cx| {
665 session.send_stdin_reply(event.value.clone(), &event.parent_message, cx);
666 },
667 );
668 self._subscriptions.push(subscription);
669
670 self.blocks
671 .insert(message.header.msg_id.clone(), editor_block);
672
673 match &self.kernel {
674 Kernel::RunningKernel(_) => {
675 self.send(message, cx).ok();
676 }
677 Kernel::StartingKernel(task) => {
678 // Queue up the execution as a task to run after the kernel starts
679 let task = task.clone();
680
681 cx.spawn(async move |this, cx| {
682 task.await;
683 this.update(cx, |session, cx| {
684 session.send(message, cx).ok();
685 })
686 .ok();
687 })
688 .detach();
689 }
690 _ => {}
691 }
692
693 if move_down {
694 editor.update(cx, move |editor, cx| {
695 editor.change_selections(
696 SelectionEffects::scroll(Autoscroll::top_relative(8)),
697 window,
698 cx,
699 |selections| {
700 selections.select_ranges([new_cursor_pos..new_cursor_pos]);
701 },
702 );
703 });
704 }
705 }
706
707 pub fn interrupt(&mut self, cx: &mut Context<Self>) {
708 match &mut self.kernel {
709 Kernel::RunningKernel(_kernel) => {
710 self.send(InterruptRequest {}.into(), cx).ok();
711 }
712 Kernel::StartingKernel(_task) => {
713 // NOTE: If we switch to a literal queue instead of chaining on to the task, clear all queued executions
714 }
715 _ => {}
716 }
717 }
718
719 pub fn kernel(&mut self, kernel: Kernel, cx: &mut Context<Self>) {
720 if let Kernel::Shutdown = kernel {
721 cx.emit(SessionEvent::Shutdown(self.editor.clone()));
722 }
723
724 let kernel_status = KernelStatus::from(&kernel).to_string();
725 let kernel_language = self.kernel_specification.language();
726
727 telemetry::event!(
728 "Kernel Status Changed",
729 kernel_language,
730 kernel_status,
731 repl_session_id = cx.entity_id().to_string(),
732 );
733
734 self.kernel = kernel;
735 }
736
737 pub fn shutdown(&mut self, window: &mut Window, cx: &mut Context<Self>) {
738 let kernel = std::mem::replace(&mut self.kernel, Kernel::ShuttingDown);
739
740 match kernel {
741 Kernel::RunningKernel(mut kernel) => {
742 let mut request_tx = kernel.request_tx();
743
744 let forced = kernel.force_shutdown(window, cx);
745
746 cx.spawn(async move |this, cx| {
747 let message: JupyterMessage = ShutdownRequest { restart: false }.into();
748 request_tx.try_send(message).ok();
749
750 forced.await.log_err();
751
752 // Give the kernel a bit of time to clean up
753 cx.background_executor().timer(Duration::from_secs(3)).await;
754
755 this.update(cx, |session, cx| {
756 session.clear_outputs(cx);
757 session.kernel(Kernel::Shutdown, cx);
758 cx.notify();
759 })
760 .ok();
761 })
762 .detach();
763 }
764 _ => {
765 self.kernel(Kernel::Shutdown, cx);
766 }
767 }
768 cx.notify();
769 }
770
771 pub fn restart(&mut self, window: &mut Window, cx: &mut Context<Self>) {
772 let kernel = std::mem::replace(&mut self.kernel, Kernel::Restarting);
773
774 match kernel {
775 Kernel::Restarting => {
776 // Do nothing if already restarting
777 }
778 Kernel::RunningKernel(mut kernel) => {
779 let mut request_tx = kernel.request_tx();
780
781 let forced = kernel.force_shutdown(window, cx);
782
783 cx.spawn_in(window, async move |this, cx| {
784 // Send shutdown request with restart flag
785 log::debug!("restarting kernel");
786 let message: JupyterMessage = ShutdownRequest { restart: true }.into();
787 request_tx.try_send(message).ok();
788
789 // Wait for kernel to shutdown
790 cx.background_executor().timer(Duration::from_secs(1)).await;
791
792 // Force kill the kernel if it hasn't shut down
793 forced.await.log_err();
794
795 // Start a new kernel
796 this.update_in(cx, |session, window, cx| {
797 // TODO: Differentiate between restart and restart+clear-outputs
798 session.clear_outputs(cx);
799 session.start_kernel(window, cx);
800 })
801 .ok();
802 })
803 .detach();
804 }
805 _ => {
806 self.clear_outputs(cx);
807 self.start_kernel(window, cx);
808 }
809 }
810 cx.notify();
811 }
812}
813
814pub enum SessionEvent {
815 Shutdown(WeakEntity<Editor>),
816}
817
818impl EventEmitter<SessionEvent> for Session {}
819
820impl Render for Session {
821 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
822 let (status_text, interrupt_button) = match &self.kernel {
823 Kernel::RunningKernel(kernel) => (
824 kernel
825 .kernel_info()
826 .as_ref()
827 .map(|info| info.language_info.name.clone()),
828 Some(
829 Button::new("interrupt", "Interrupt")
830 .style(ButtonStyle::Subtle)
831 .on_click(cx.listener(move |session, _, _, cx| {
832 session.interrupt(cx);
833 })),
834 ),
835 ),
836 Kernel::StartingKernel(_) => (Some("Starting".into()), None),
837 Kernel::ErroredLaunch(err) => (Some(format!("Error: {err}")), None),
838 Kernel::ShuttingDown => (Some("Shutting Down".into()), None),
839 Kernel::Shutdown => (Some("Shutdown".into()), None),
840 Kernel::Restarting => (Some("Restarting".into()), None),
841 };
842
843 KernelListItem::new(self.kernel_specification.clone())
844 .status_color(match &self.kernel {
845 Kernel::RunningKernel(kernel) => match kernel.execution_state() {
846 ExecutionState::Idle => Color::Success,
847 ExecutionState::Busy => Color::Modified,
848 ExecutionState::Unknown => Color::Modified,
849 ExecutionState::Starting => Color::Modified,
850 ExecutionState::Restarting => Color::Modified,
851 ExecutionState::Terminating => Color::Disabled,
852 ExecutionState::AutoRestarting => Color::Modified,
853 ExecutionState::Dead => Color::Disabled,
854 ExecutionState::Other(_) => Color::Modified,
855 },
856 Kernel::StartingKernel(_) => Color::Modified,
857 Kernel::ErroredLaunch(_) => Color::Error,
858 Kernel::ShuttingDown => Color::Modified,
859 Kernel::Shutdown => Color::Disabled,
860 Kernel::Restarting => Color::Modified,
861 })
862 .child(Label::new(self.kernel_specification.name()))
863 .children(status_text.map(|status_text| Label::new(format!("({status_text})"))))
864 .button(
865 Button::new("shutdown", "Shutdown")
866 .style(ButtonStyle::Subtle)
867 .disabled(self.kernel.is_shutting_down())
868 .on_click(cx.listener(move |session, _, window, cx| {
869 session.shutdown(window, cx);
870 })),
871 )
872 .buttons(interrupt_button)
873 }
874}
875
876impl KernelSession for Session {
877 fn route(&mut self, message: &JupyterMessage, window: &mut Window, cx: &mut Context<Self>) {
878 let parent_message_id = match message.parent_header.as_ref() {
879 Some(header) => &header.msg_id,
880 None => return,
881 };
882
883 match &message.content {
884 JupyterMessageContent::Status(status) => {
885 self.kernel.set_execution_state(&status.execution_state);
886
887 telemetry::event!(
888 "Kernel Status Changed",
889 kernel_language = self.kernel_specification.language(),
890 kernel_status = KernelStatus::from(&self.kernel).to_string(),
891 repl_session_id = cx.entity_id().to_string(),
892 );
893
894 cx.notify();
895 }
896 JupyterMessageContent::KernelInfoReply(reply) => {
897 self.kernel.set_kernel_info(reply);
898 cx.notify();
899 }
900 JupyterMessageContent::UpdateDisplayData(update) => {
901 let display_id = if let Some(display_id) = update.transient.display_id.clone() {
902 display_id
903 } else {
904 return;
905 };
906
907 self.blocks.iter_mut().for_each(|(_, block)| {
908 block.execution_view.update(cx, |execution_view, cx| {
909 execution_view.update_display_data(&update.data, &display_id, window, cx);
910 });
911 });
912 return;
913 }
914 _ => {}
915 }
916
917 if let Some(block) = self.blocks.get_mut(parent_message_id) {
918 block.handle_message(message, window, cx);
919 }
920 }
921
922 fn kernel_errored(&mut self, error_message: String, cx: &mut Context<Self>) {
923 self.kernel_errored(error_message, cx);
924 }
925}