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