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