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