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