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 selection = editor
481 .selections
482 .newest::<usize>(&editor.display_snapshot(cx));
483 let buffer = editor.buffer().read(cx).snapshot(cx);
484 buffer.language_at(selection.head()).cloned()
485 })
486 .ok()
487 .flatten()
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493 use gpui::App;
494 use indoc::indoc;
495 use language::{Buffer, Language, LanguageConfig, LanguageRegistry};
496
497 #[gpui::test]
498 fn test_snippet_ranges(cx: &mut App) {
499 // Create a test language
500 let test_language = Arc::new(Language::new(
501 LanguageConfig {
502 name: "TestLang".into(),
503 line_comments: vec!["# ".into()],
504 ..Default::default()
505 },
506 None,
507 ));
508
509 let buffer = cx.new(|cx| {
510 Buffer::local(
511 indoc! { r#"
512 print(1 + 1)
513 print(2 + 2)
514
515 print(4 + 4)
516
517
518 "# },
519 cx,
520 )
521 .with_language(test_language, cx)
522 });
523 let snapshot = buffer.read(cx).snapshot();
524
525 // Single-point selection
526 let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 4)..Point::new(0, 4), cx);
527 let snippets = snippets
528 .into_iter()
529 .map(|range| snapshot.text_for_range(range).collect::<String>())
530 .collect::<Vec<_>>();
531 assert_eq!(snippets, vec!["print(1 + 1)"]);
532
533 // Multi-line selection
534 let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(2, 0), cx);
535 let snippets = snippets
536 .into_iter()
537 .map(|range| snapshot.text_for_range(range).collect::<String>())
538 .collect::<Vec<_>>();
539 assert_eq!(
540 snippets,
541 vec![indoc! { r#"
542 print(1 + 1)
543 print(2 + 2)"# }]
544 );
545
546 // Trimming multiple trailing blank lines
547 let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(5, 0), cx);
548
549 let snippets = snippets
550 .into_iter()
551 .map(|range| snapshot.text_for_range(range).collect::<String>())
552 .collect::<Vec<_>>();
553 assert_eq!(
554 snippets,
555 vec![indoc! { r#"
556 print(1 + 1)
557 print(2 + 2)
558
559 print(4 + 4)"# }]
560 );
561 }
562
563 #[gpui::test]
564 fn test_jupytext_snippet_ranges(cx: &mut App) {
565 // Create a test language
566 let test_language = Arc::new(Language::new(
567 LanguageConfig {
568 name: "TestLang".into(),
569 line_comments: vec!["# ".into()],
570 ..Default::default()
571 },
572 None,
573 ));
574
575 let buffer = cx.new(|cx| {
576 Buffer::local(
577 indoc! { r#"
578 # Hello!
579 # %% [markdown]
580 # This is some arithmetic
581 print(1 + 1)
582 print(2 + 2)
583
584 # %%
585 print(3 + 3)
586 print(4 + 4)
587
588 print(5 + 5)
589
590
591
592 "# },
593 cx,
594 )
595 .with_language(test_language, cx)
596 });
597 let snapshot = buffer.read(cx).snapshot();
598
599 // Jupytext snippet surrounding an empty selection
600 let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(2, 5), cx);
601
602 let snippets = snippets
603 .into_iter()
604 .map(|range| snapshot.text_for_range(range).collect::<String>())
605 .collect::<Vec<_>>();
606 assert_eq!(
607 snippets,
608 vec![indoc! { r#"
609 # %% [markdown]
610 # This is some arithmetic
611 print(1 + 1)
612 print(2 + 2)"# }]
613 );
614
615 // Jupytext snippets intersecting a non-empty selection
616 let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(6, 2), cx);
617 let snippets = snippets
618 .into_iter()
619 .map(|range| snapshot.text_for_range(range).collect::<String>())
620 .collect::<Vec<_>>();
621 assert_eq!(
622 snippets,
623 vec![
624 indoc! { r#"
625 # %% [markdown]
626 # This is some arithmetic
627 print(1 + 1)
628 print(2 + 2)"#
629 },
630 indoc! { r#"
631 # %%
632 print(3 + 3)
633 print(4 + 4)
634
635 print(5 + 5)"#
636 }
637 ]
638 );
639 }
640
641 #[gpui::test]
642 fn test_markdown_code_blocks(cx: &mut App) {
643 use crate::kernels::LocalKernelSpecification;
644 use jupyter_protocol::JupyterKernelspec;
645
646 // Initialize settings
647 settings::init(cx);
648 editor::init(cx);
649
650 // Initialize the ReplStore with a fake filesystem
651 let fs = Arc::new(project::RealFs::new(None, cx.background_executor().clone()));
652 ReplStore::init(fs, cx);
653
654 // Add mock kernel specifications for TypeScript and Python
655 let store = ReplStore::global(cx);
656 store.update(cx, |store, cx| {
657 let typescript_spec = KernelSpecification::Jupyter(LocalKernelSpecification {
658 name: "typescript".into(),
659 kernelspec: JupyterKernelspec {
660 argv: vec![],
661 display_name: "TypeScript".into(),
662 language: "typescript".into(),
663 interrupt_mode: None,
664 metadata: None,
665 env: None,
666 },
667 path: std::path::PathBuf::new(),
668 });
669
670 let python_spec = KernelSpecification::Jupyter(LocalKernelSpecification {
671 name: "python".into(),
672 kernelspec: JupyterKernelspec {
673 argv: vec![],
674 display_name: "Python".into(),
675 language: "python".into(),
676 interrupt_mode: None,
677 metadata: None,
678 env: None,
679 },
680 path: std::path::PathBuf::new(),
681 });
682
683 store.set_kernel_specs_for_testing(vec![typescript_spec, python_spec], cx);
684 });
685
686 let markdown = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
687 let typescript = languages::language(
688 "typescript",
689 tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
690 );
691 let python = languages::language("python", tree_sitter_python::LANGUAGE.into());
692 let language_registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
693 language_registry.add(markdown.clone());
694 language_registry.add(typescript);
695 language_registry.add(python);
696
697 // Two code blocks intersecting with selection
698 let buffer = cx.new(|cx| {
699 let mut buffer = Buffer::local(
700 indoc! { r#"
701 Hey this is Markdown!
702
703 ```typescript
704 let foo = 999;
705 console.log(foo + 1999);
706 ```
707
708 ```typescript
709 console.log("foo")
710 ```
711 "#
712 },
713 cx,
714 );
715 buffer.set_language_registry(language_registry.clone());
716 buffer.set_language(Some(markdown.clone()), cx);
717 buffer
718 });
719 let snapshot = buffer.read(cx).snapshot();
720
721 let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(8, 5), cx);
722 let snippets = snippets
723 .into_iter()
724 .map(|range| snapshot.text_for_range(range).collect::<String>())
725 .collect::<Vec<_>>();
726
727 assert_eq!(
728 snippets,
729 vec![
730 indoc! { r#"
731 let foo = 999;
732 console.log(foo + 1999);
733 "#
734 },
735 "console.log(\"foo\")\n"
736 ]
737 );
738
739 // Three code blocks intersecting with selection
740 let buffer = cx.new(|cx| {
741 let mut buffer = Buffer::local(
742 indoc! { r#"
743 Hey this is Markdown!
744
745 ```typescript
746 let foo = 999;
747 console.log(foo + 1999);
748 ```
749
750 ```ts
751 console.log("foo")
752 ```
753
754 ```typescript
755 console.log("another code block")
756 ```
757 "# },
758 cx,
759 );
760 buffer.set_language_registry(language_registry.clone());
761 buffer.set_language(Some(markdown.clone()), cx);
762 buffer
763 });
764 let snapshot = buffer.read(cx).snapshot();
765
766 let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(12, 5), cx);
767 let snippets = snippets
768 .into_iter()
769 .map(|range| snapshot.text_for_range(range).collect::<String>())
770 .collect::<Vec<_>>();
771
772 assert_eq!(
773 snippets,
774 vec![
775 indoc! { r#"
776 let foo = 999;
777 console.log(foo + 1999);
778 "#
779 },
780 "console.log(\"foo\")\n",
781 "console.log(\"another code block\")\n",
782 ]
783 );
784
785 // Python code block
786 let buffer = cx.new(|cx| {
787 let mut buffer = Buffer::local(
788 indoc! { r#"
789 Hey this is Markdown!
790
791 ```python
792 print("hello there")
793 print("hello there")
794 print("hello there")
795 ```
796 "# },
797 cx,
798 );
799 buffer.set_language_registry(language_registry.clone());
800 buffer.set_language(Some(markdown.clone()), cx);
801 buffer
802 });
803 let snapshot = buffer.read(cx).snapshot();
804
805 let (snippets, _) = runnable_ranges(&snapshot, Point::new(4, 5)..Point::new(5, 5), cx);
806 let snippets = snippets
807 .into_iter()
808 .map(|range| snapshot.text_for_range(range).collect::<String>())
809 .collect::<Vec<_>>();
810
811 assert_eq!(
812 snippets,
813 vec![indoc! { r#"
814 print("hello there")
815 print("hello there")
816 print("hello there")
817 "#
818 },]
819 );
820 }
821}