1//! REPL operations on an [`Editor`].
2
3use std::ops::Range;
4use std::sync::Arc;
5
6use anyhow::{Context as _, Result};
7use editor::Editor;
8use gpui::{App, Entity, WeakEntity, Window, prelude::*};
9use language::{BufferSnapshot, Language, LanguageName, Point};
10use project::{ProjectItem as _, WorktreeId};
11
12use crate::repl_store::ReplStore;
13use crate::session::SessionEvent;
14use crate::{
15 ClearOutputs, Interrupt, JupyterSettings, KernelSpecification, Restart, Session, Shutdown,
16};
17
18pub fn assign_kernelspec(
19 kernel_specification: KernelSpecification,
20 weak_editor: WeakEntity<Editor>,
21 window: &mut Window,
22 cx: &mut App,
23) -> Result<()> {
24 let store = ReplStore::global(cx);
25 if !store.read(cx).is_enabled() {
26 return Ok(());
27 }
28
29 let worktree_id = crate::repl_editor::worktree_id_for_editor(weak_editor.clone(), cx)
30 .context("editor is not in a worktree")?;
31
32 store.update(cx, |store, cx| {
33 store.set_active_kernelspec(worktree_id, kernel_specification.clone(), cx);
34 });
35
36 let fs = store.read(cx).fs().clone();
37
38 if let Some(session) = store.read(cx).get_session(weak_editor.entity_id()).cloned() {
39 // Drop previous session, start new one
40 session.update(cx, |session, cx| {
41 session.clear_outputs(cx);
42 session.shutdown(window, cx);
43 cx.notify();
44 });
45 }
46
47 let session =
48 cx.new(|cx| Session::new(weak_editor.clone(), fs, kernel_specification, window, cx));
49
50 weak_editor
51 .update(cx, |_editor, cx| {
52 cx.notify();
53
54 cx.subscribe(&session, {
55 let store = store.clone();
56 move |_this, _session, event, cx| match event {
57 SessionEvent::Shutdown(shutdown_event) => {
58 store.update(cx, |store, _cx| {
59 store.remove_session(shutdown_event.entity_id());
60 });
61 }
62 }
63 })
64 .detach();
65 })
66 .ok();
67
68 store.update(cx, |store, _cx| {
69 store.insert_session(weak_editor.entity_id(), session.clone());
70 });
71
72 Ok(())
73}
74
75pub fn run(
76 editor: WeakEntity<Editor>,
77 move_down: bool,
78 window: &mut Window,
79 cx: &mut App,
80) -> Result<()> {
81 let store = ReplStore::global(cx);
82 if !store.read(cx).is_enabled() {
83 return Ok(());
84 }
85
86 let editor = editor.upgrade().context("editor was dropped")?;
87 let selected_range = editor
88 .update(cx, |editor, cx| {
89 editor
90 .selections
91 .newest_adjusted(&editor.display_snapshot(cx))
92 })
93 .range();
94 let multibuffer = editor.read(cx).buffer().clone();
95 let Some(buffer) = multibuffer.read(cx).as_singleton() else {
96 return Ok(());
97 };
98
99 let Some(project_path) = buffer.read(cx).project_path(cx) else {
100 return Ok(());
101 };
102
103 let (runnable_ranges, next_cell_point) =
104 runnable_ranges(&buffer.read(cx).snapshot(), selected_range, cx);
105
106 for runnable_range in runnable_ranges {
107 let Some(language) = multibuffer.read(cx).language_at(runnable_range.start, cx) else {
108 continue;
109 };
110
111 let kernel_specification = store
112 .read(cx)
113 .active_kernelspec(project_path.worktree_id, Some(language.clone()), cx)
114 .with_context(|| format!("No kernel found for language: {}", language.name()))?;
115
116 let fs = store.read(cx).fs().clone();
117
118 let session = if let Some(session) = store.read(cx).get_session(editor.entity_id()).cloned()
119 {
120 session
121 } else {
122 let weak_editor = editor.downgrade();
123 let session =
124 cx.new(|cx| Session::new(weak_editor, fs, kernel_specification, window, cx));
125
126 editor.update(cx, |_editor, cx| {
127 cx.notify();
128
129 cx.subscribe(&session, {
130 let store = store.clone();
131 move |_this, _session, event, cx| match event {
132 SessionEvent::Shutdown(shutdown_event) => {
133 store.update(cx, |store, _cx| {
134 store.remove_session(shutdown_event.entity_id());
135 });
136 }
137 }
138 })
139 .detach();
140 });
141
142 store.update(cx, |store, _cx| {
143 store.insert_session(editor.entity_id(), session.clone());
144 });
145
146 session
147 };
148
149 let selected_text;
150 let anchor_range;
151 let next_cursor;
152 {
153 let snapshot = multibuffer.read(cx).read(cx);
154 selected_text = snapshot
155 .text_for_range(runnable_range.clone())
156 .collect::<String>();
157 anchor_range = snapshot.anchor_before(runnable_range.start)
158 ..snapshot.anchor_after(runnable_range.end);
159 next_cursor = next_cell_point.map(|point| snapshot.anchor_after(point));
160 }
161
162 session.update(cx, |session, cx| {
163 session.execute(
164 selected_text,
165 anchor_range,
166 next_cursor,
167 move_down,
168 window,
169 cx,
170 );
171 });
172 }
173
174 anyhow::Ok(())
175}
176
177pub enum SessionSupport {
178 ActiveSession(Entity<Session>),
179 Inactive(KernelSpecification),
180 RequiresSetup(LanguageName),
181 Unsupported,
182}
183
184pub fn worktree_id_for_editor(editor: WeakEntity<Editor>, cx: &mut App) -> Option<WorktreeId> {
185 editor.upgrade().and_then(|editor| {
186 editor
187 .read(cx)
188 .buffer()
189 .read(cx)
190 .as_singleton()?
191 .read(cx)
192 .project_path(cx)
193 .map(|path| path.worktree_id)
194 })
195}
196
197pub fn session(editor: WeakEntity<Editor>, cx: &mut App) -> SessionSupport {
198 let store = ReplStore::global(cx);
199 let entity_id = editor.entity_id();
200
201 if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
202 return SessionSupport::ActiveSession(session);
203 };
204
205 let Some(language) = get_language(editor.clone(), cx) else {
206 return SessionSupport::Unsupported;
207 };
208
209 let worktree_id = worktree_id_for_editor(editor, cx);
210
211 let Some(worktree_id) = worktree_id else {
212 return SessionSupport::Unsupported;
213 };
214
215 let kernelspec = store
216 .read(cx)
217 .active_kernelspec(worktree_id, Some(language.clone()), cx);
218
219 match kernelspec {
220 Some(kernelspec) => SessionSupport::Inactive(kernelspec),
221 None => {
222 // For language_supported, need to check available kernels for language
223 if language_supported(&language, cx) {
224 SessionSupport::RequiresSetup(language.name())
225 } else {
226 SessionSupport::Unsupported
227 }
228 }
229 }
230}
231
232pub fn clear_outputs(editor: WeakEntity<Editor>, cx: &mut App) {
233 let store = ReplStore::global(cx);
234 let entity_id = editor.entity_id();
235 let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
236 return;
237 };
238 session.update(cx, |session, cx| {
239 session.clear_outputs(cx);
240 cx.notify();
241 });
242}
243
244pub fn interrupt(editor: WeakEntity<Editor>, cx: &mut App) {
245 let store = ReplStore::global(cx);
246 let entity_id = editor.entity_id();
247 let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
248 return;
249 };
250
251 session.update(cx, |session, cx| {
252 session.interrupt(cx);
253 cx.notify();
254 });
255}
256
257pub fn shutdown(editor: WeakEntity<Editor>, window: &mut Window, cx: &mut App) {
258 let store = ReplStore::global(cx);
259 let entity_id = editor.entity_id();
260 let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
261 return;
262 };
263
264 session.update(cx, |session, cx| {
265 session.shutdown(window, cx);
266 cx.notify();
267 });
268}
269
270pub fn restart(editor: WeakEntity<Editor>, window: &mut Window, cx: &mut App) {
271 let Some(editor) = editor.upgrade() else {
272 return;
273 };
274
275 let entity_id = editor.entity_id();
276
277 let Some(session) = ReplStore::global(cx)
278 .read(cx)
279 .get_session(entity_id)
280 .cloned()
281 else {
282 return;
283 };
284
285 session.update(cx, |session, cx| {
286 session.restart(window, cx);
287 cx.notify();
288 });
289}
290
291pub fn setup_editor_session_actions(editor: &mut Editor, editor_handle: WeakEntity<Editor>) {
292 editor
293 .register_action({
294 let editor_handle = editor_handle.clone();
295 move |_: &ClearOutputs, _, cx| {
296 if !JupyterSettings::enabled(cx) {
297 return;
298 }
299
300 crate::clear_outputs(editor_handle.clone(), cx);
301 }
302 })
303 .detach();
304
305 editor
306 .register_action({
307 let editor_handle = editor_handle.clone();
308 move |_: &Interrupt, _, cx| {
309 if !JupyterSettings::enabled(cx) {
310 return;
311 }
312
313 crate::interrupt(editor_handle.clone(), cx);
314 }
315 })
316 .detach();
317
318 editor
319 .register_action({
320 let editor_handle = editor_handle.clone();
321 move |_: &Shutdown, window, cx| {
322 if !JupyterSettings::enabled(cx) {
323 return;
324 }
325
326 crate::shutdown(editor_handle.clone(), window, cx);
327 }
328 })
329 .detach();
330
331 editor
332 .register_action({
333 let editor_handle = editor_handle;
334 move |_: &Restart, window, cx| {
335 if !JupyterSettings::enabled(cx) {
336 return;
337 }
338
339 crate::restart(editor_handle.clone(), window, cx);
340 }
341 })
342 .detach();
343}
344
345fn cell_range(buffer: &BufferSnapshot, start_row: u32, end_row: u32) -> Range<Point> {
346 let mut snippet_end_row = end_row;
347 while buffer.is_line_blank(snippet_end_row) && snippet_end_row > start_row {
348 snippet_end_row -= 1;
349 }
350 Point::new(start_row, 0)..Point::new(snippet_end_row, buffer.line_len(snippet_end_row))
351}
352
353// Returns the ranges of the snippets in the buffer and the next point for moving the cursor to
354fn jupytext_cells(
355 buffer: &BufferSnapshot,
356 range: Range<Point>,
357) -> (Vec<Range<Point>>, Option<Point>) {
358 let mut current_row = range.start.row;
359
360 let Some(language) = buffer.language() else {
361 return (Vec::new(), None);
362 };
363
364 let default_scope = language.default_scope();
365 let comment_prefixes = default_scope.line_comment_prefixes();
366 if comment_prefixes.is_empty() {
367 return (Vec::new(), None);
368 }
369
370 let jupytext_prefixes = comment_prefixes
371 .iter()
372 .map(|comment_prefix| format!("{comment_prefix}%%"))
373 .collect::<Vec<_>>();
374
375 let mut snippet_start_row = None;
376 loop {
377 if jupytext_prefixes
378 .iter()
379 .any(|prefix| buffer.contains_str_at(Point::new(current_row, 0), prefix))
380 {
381 snippet_start_row = Some(current_row);
382 break;
383 } else if current_row > 0 {
384 current_row -= 1;
385 } else {
386 break;
387 }
388 }
389
390 let mut snippets = Vec::new();
391 if let Some(mut snippet_start_row) = snippet_start_row {
392 for current_row in range.start.row + 1..=buffer.max_point().row {
393 if jupytext_prefixes
394 .iter()
395 .any(|prefix| buffer.contains_str_at(Point::new(current_row, 0), prefix))
396 {
397 snippets.push(cell_range(buffer, snippet_start_row, current_row - 1));
398
399 if current_row <= range.end.row {
400 snippet_start_row = current_row;
401 } else {
402 // Return our snippets as well as the next point for moving the cursor to
403 return (snippets, Some(Point::new(current_row, 0)));
404 }
405 }
406 }
407
408 // Go to the end of the buffer (no more jupytext cells found)
409 snippets.push(cell_range(
410 buffer,
411 snippet_start_row,
412 buffer.max_point().row,
413 ));
414 }
415
416 (snippets, None)
417}
418
419fn runnable_ranges(
420 buffer: &BufferSnapshot,
421 range: Range<Point>,
422 cx: &mut App,
423) -> (Vec<Range<Point>>, Option<Point>) {
424 if let Some(language) = buffer.language()
425 && language.name() == "Markdown".into()
426 {
427 return (markdown_code_blocks(buffer, range, cx), None);
428 }
429
430 let (jupytext_snippets, next_cursor) = jupytext_cells(buffer, range.clone());
431 if !jupytext_snippets.is_empty() {
432 return (jupytext_snippets, next_cursor);
433 }
434
435 let snippet_range = cell_range(buffer, range.start.row, range.end.row);
436 let start_language = buffer.language_at(snippet_range.start);
437 let end_language = buffer.language_at(snippet_range.end);
438
439 if start_language
440 .zip(end_language)
441 .is_some_and(|(start, end)| start == end)
442 {
443 (vec![snippet_range], None)
444 } else {
445 (Vec::new(), None)
446 }
447}
448
449// We allow markdown code blocks to end in a trailing newline in order to render the output
450// below the final code fence. This is different than our behavior for selections and Jupytext cells.
451fn markdown_code_blocks(
452 buffer: &BufferSnapshot,
453 range: Range<Point>,
454 cx: &mut App,
455) -> Vec<Range<Point>> {
456 buffer
457 .injections_intersecting_range(range)
458 .filter(|(_, language)| language_supported(language, cx))
459 .map(|(content_range, _)| {
460 buffer.offset_to_point(content_range.start)..buffer.offset_to_point(content_range.end)
461 })
462 .collect()
463}
464
465fn language_supported(language: &Arc<Language>, cx: &mut App) -> bool {
466 let store = ReplStore::global(cx);
467 let store_read = store.read(cx);
468
469 // Since we're just checking for general language support, we only need to look at
470 // the pure Jupyter kernels - these are all the globally available ones
471 store_read.pure_jupyter_kernel_specifications().any(|spec| {
472 // Convert to lowercase for case-insensitive comparison since kernels might report "python" while our language is "Python"
473 spec.language().as_ref().to_lowercase() == language.name().as_ref().to_lowercase()
474 })
475}
476
477fn get_language(editor: WeakEntity<Editor>, cx: &mut App) -> Option<Arc<Language>> {
478 editor
479 .update(cx, |editor, cx| {
480 let display_snapshot = editor.display_snapshot(cx);
481 let selection = editor.selections.newest::<usize>(&display_snapshot);
482 display_snapshot
483 .buffer_snapshot()
484 .language_at(selection.head())
485 .cloned()
486 })
487 .ok()
488 .flatten()
489}
490
491#[cfg(test)]
492mod tests {
493 use super::*;
494 use gpui::App;
495 use indoc::indoc;
496 use language::{Buffer, Language, LanguageConfig, LanguageRegistry};
497
498 #[gpui::test]
499 fn test_snippet_ranges(cx: &mut App) {
500 // Create a test language
501 let test_language = Arc::new(Language::new(
502 LanguageConfig {
503 name: "TestLang".into(),
504 line_comments: vec!["# ".into()],
505 ..Default::default()
506 },
507 None,
508 ));
509
510 let buffer = cx.new(|cx| {
511 Buffer::local(
512 indoc! { r#"
513 print(1 + 1)
514 print(2 + 2)
515
516 print(4 + 4)
517
518
519 "# },
520 cx,
521 )
522 .with_language(test_language, cx)
523 });
524 let snapshot = buffer.read(cx).snapshot();
525
526 // Single-point selection
527 let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 4)..Point::new(0, 4), cx);
528 let snippets = snippets
529 .into_iter()
530 .map(|range| snapshot.text_for_range(range).collect::<String>())
531 .collect::<Vec<_>>();
532 assert_eq!(snippets, vec!["print(1 + 1)"]);
533
534 // Multi-line selection
535 let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(2, 0), cx);
536 let snippets = snippets
537 .into_iter()
538 .map(|range| snapshot.text_for_range(range).collect::<String>())
539 .collect::<Vec<_>>();
540 assert_eq!(
541 snippets,
542 vec![indoc! { r#"
543 print(1 + 1)
544 print(2 + 2)"# }]
545 );
546
547 // Trimming multiple trailing blank lines
548 let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(5, 0), cx);
549
550 let snippets = snippets
551 .into_iter()
552 .map(|range| snapshot.text_for_range(range).collect::<String>())
553 .collect::<Vec<_>>();
554 assert_eq!(
555 snippets,
556 vec![indoc! { r#"
557 print(1 + 1)
558 print(2 + 2)
559
560 print(4 + 4)"# }]
561 );
562 }
563
564 #[gpui::test]
565 fn test_jupytext_snippet_ranges(cx: &mut App) {
566 // Create a test language
567 let test_language = Arc::new(Language::new(
568 LanguageConfig {
569 name: "TestLang".into(),
570 line_comments: vec!["# ".into()],
571 ..Default::default()
572 },
573 None,
574 ));
575
576 let buffer = cx.new(|cx| {
577 Buffer::local(
578 indoc! { r#"
579 # Hello!
580 # %% [markdown]
581 # This is some arithmetic
582 print(1 + 1)
583 print(2 + 2)
584
585 # %%
586 print(3 + 3)
587 print(4 + 4)
588
589 print(5 + 5)
590
591
592
593 "# },
594 cx,
595 )
596 .with_language(test_language, cx)
597 });
598 let snapshot = buffer.read(cx).snapshot();
599
600 // Jupytext snippet surrounding an empty selection
601 let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(2, 5), cx);
602
603 let snippets = snippets
604 .into_iter()
605 .map(|range| snapshot.text_for_range(range).collect::<String>())
606 .collect::<Vec<_>>();
607 assert_eq!(
608 snippets,
609 vec![indoc! { r#"
610 # %% [markdown]
611 # This is some arithmetic
612 print(1 + 1)
613 print(2 + 2)"# }]
614 );
615
616 // Jupytext snippets intersecting a non-empty selection
617 let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(6, 2), cx);
618 let snippets = snippets
619 .into_iter()
620 .map(|range| snapshot.text_for_range(range).collect::<String>())
621 .collect::<Vec<_>>();
622 assert_eq!(
623 snippets,
624 vec![
625 indoc! { r#"
626 # %% [markdown]
627 # This is some arithmetic
628 print(1 + 1)
629 print(2 + 2)"#
630 },
631 indoc! { r#"
632 # %%
633 print(3 + 3)
634 print(4 + 4)
635
636 print(5 + 5)"#
637 }
638 ]
639 );
640 }
641
642 #[gpui::test]
643 fn test_markdown_code_blocks(cx: &mut App) {
644 use crate::kernels::LocalKernelSpecification;
645 use jupyter_protocol::JupyterKernelspec;
646
647 // Initialize settings
648 settings::init(cx);
649 editor::init(cx);
650
651 // Initialize the ReplStore with a fake filesystem
652 let fs = Arc::new(project::RealFs::new(None, cx.background_executor().clone()));
653 ReplStore::init(fs, cx);
654
655 // Add mock kernel specifications for TypeScript and Python
656 let store = ReplStore::global(cx);
657 store.update(cx, |store, cx| {
658 let typescript_spec = KernelSpecification::Jupyter(LocalKernelSpecification {
659 name: "typescript".into(),
660 kernelspec: JupyterKernelspec {
661 argv: vec![],
662 display_name: "TypeScript".into(),
663 language: "typescript".into(),
664 interrupt_mode: None,
665 metadata: None,
666 env: None,
667 },
668 path: std::path::PathBuf::new(),
669 });
670
671 let python_spec = KernelSpecification::Jupyter(LocalKernelSpecification {
672 name: "python".into(),
673 kernelspec: JupyterKernelspec {
674 argv: vec![],
675 display_name: "Python".into(),
676 language: "python".into(),
677 interrupt_mode: None,
678 metadata: None,
679 env: None,
680 },
681 path: std::path::PathBuf::new(),
682 });
683
684 store.set_kernel_specs_for_testing(vec![typescript_spec, python_spec], cx);
685 });
686
687 let markdown = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
688 let typescript = languages::language(
689 "typescript",
690 tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
691 );
692 let python = languages::language("python", tree_sitter_python::LANGUAGE.into());
693 let language_registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
694 language_registry.add(markdown.clone());
695 language_registry.add(typescript);
696 language_registry.add(python);
697
698 // Two code blocks intersecting with selection
699 let buffer = cx.new(|cx| {
700 let mut buffer = Buffer::local(
701 indoc! { r#"
702 Hey this is Markdown!
703
704 ```typescript
705 let foo = 999;
706 console.log(foo + 1999);
707 ```
708
709 ```typescript
710 console.log("foo")
711 ```
712 "#
713 },
714 cx,
715 );
716 buffer.set_language_registry(language_registry.clone());
717 buffer.set_language(Some(markdown.clone()), cx);
718 buffer
719 });
720 let snapshot = buffer.read(cx).snapshot();
721
722 let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(8, 5), cx);
723 let snippets = snippets
724 .into_iter()
725 .map(|range| snapshot.text_for_range(range).collect::<String>())
726 .collect::<Vec<_>>();
727
728 assert_eq!(
729 snippets,
730 vec![
731 indoc! { r#"
732 let foo = 999;
733 console.log(foo + 1999);
734 "#
735 },
736 "console.log(\"foo\")\n"
737 ]
738 );
739
740 // Three code blocks intersecting with selection
741 let buffer = cx.new(|cx| {
742 let mut buffer = Buffer::local(
743 indoc! { r#"
744 Hey this is Markdown!
745
746 ```typescript
747 let foo = 999;
748 console.log(foo + 1999);
749 ```
750
751 ```ts
752 console.log("foo")
753 ```
754
755 ```typescript
756 console.log("another code block")
757 ```
758 "# },
759 cx,
760 );
761 buffer.set_language_registry(language_registry.clone());
762 buffer.set_language(Some(markdown.clone()), cx);
763 buffer
764 });
765 let snapshot = buffer.read(cx).snapshot();
766
767 let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(12, 5), cx);
768 let snippets = snippets
769 .into_iter()
770 .map(|range| snapshot.text_for_range(range).collect::<String>())
771 .collect::<Vec<_>>();
772
773 assert_eq!(
774 snippets,
775 vec![
776 indoc! { r#"
777 let foo = 999;
778 console.log(foo + 1999);
779 "#
780 },
781 "console.log(\"foo\")\n",
782 "console.log(\"another code block\")\n",
783 ]
784 );
785
786 // Python code block
787 let buffer = cx.new(|cx| {
788 let mut buffer = Buffer::local(
789 indoc! { r#"
790 Hey this is Markdown!
791
792 ```python
793 print("hello there")
794 print("hello there")
795 print("hello there")
796 ```
797 "# },
798 cx,
799 );
800 buffer.set_language_registry(language_registry.clone());
801 buffer.set_language(Some(markdown.clone()), cx);
802 buffer
803 });
804 let snapshot = buffer.read(cx).snapshot();
805
806 let (snippets, _) = runnable_ranges(&snapshot, Point::new(4, 5)..Point::new(5, 5), cx);
807 let snippets = snippets
808 .into_iter()
809 .map(|range| snapshot.text_for_range(range).collect::<String>())
810 .collect::<Vec<_>>();
811
812 assert_eq!(
813 snippets,
814 vec![indoc! { r#"
815 print("hello there")
816 print("hello there")
817 print("hello there")
818 "#
819 },]
820 );
821 }
822}