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);
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 .ok_or_else(|| anyhow::anyhow!("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
173#[allow(clippy::large_enum_variant)]
174pub enum SessionSupport {
175 ActiveSession(Entity<Session>),
176 Inactive(KernelSpecification),
177 RequiresSetup(LanguageName),
178 Unsupported,
179}
180
181pub fn worktree_id_for_editor(editor: WeakEntity<Editor>, cx: &mut App) -> Option<WorktreeId> {
182 editor.upgrade().and_then(|editor| {
183 editor
184 .read(cx)
185 .buffer()
186 .read(cx)
187 .as_singleton()?
188 .read(cx)
189 .project_path(cx)
190 .map(|path| path.worktree_id)
191 })
192}
193
194pub fn session(editor: WeakEntity<Editor>, cx: &mut App) -> SessionSupport {
195 let store = ReplStore::global(cx);
196 let entity_id = editor.entity_id();
197
198 if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
199 return SessionSupport::ActiveSession(session);
200 };
201
202 let Some(language) = get_language(editor.clone(), cx) else {
203 return SessionSupport::Unsupported;
204 };
205
206 let worktree_id = worktree_id_for_editor(editor.clone(), cx);
207
208 let Some(worktree_id) = worktree_id else {
209 return SessionSupport::Unsupported;
210 };
211
212 let kernelspec = store
213 .read(cx)
214 .active_kernelspec(worktree_id, Some(language.clone()), cx);
215
216 match kernelspec {
217 Some(kernelspec) => SessionSupport::Inactive(kernelspec),
218 None => {
219 if language_supported(&language.clone()) {
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) -> (Vec<Range<Point>>, Option<Point>) {
419 if let Some(language) = buffer.language() {
420 if language.name() == "Markdown".into() {
421 return (markdown_code_blocks(buffer, range.clone()), None);
422 }
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(buffer: &BufferSnapshot, range: Range<Point>) -> Vec<Range<Point>> {
447 buffer
448 .injections_intersecting_range(range)
449 .filter(|(_, language)| language_supported(language))
450 .map(|(content_range, _)| {
451 buffer.offset_to_point(content_range.start)..buffer.offset_to_point(content_range.end)
452 })
453 .collect()
454}
455
456fn language_supported(language: &Arc<Language>) -> bool {
457 match language.name().as_ref() {
458 "TypeScript" | "Python" => true,
459 _ => false,
460 }
461}
462
463fn get_language(editor: WeakEntity<Editor>, cx: &mut App) -> Option<Arc<Language>> {
464 editor
465 .update(cx, |editor, cx| {
466 let selection = editor.selections.newest::<usize>(cx);
467 let buffer = editor.buffer().read(cx).snapshot(cx);
468 buffer.language_at(selection.head()).cloned()
469 })
470 .ok()
471 .flatten()
472}
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477 use gpui::App;
478 use indoc::indoc;
479 use language::{Buffer, Language, LanguageConfig, LanguageRegistry};
480
481 #[gpui::test]
482 fn test_snippet_ranges(cx: &mut App) {
483 // Create a test language
484 let test_language = Arc::new(Language::new(
485 LanguageConfig {
486 name: "TestLang".into(),
487 line_comments: vec!["# ".into()],
488 ..Default::default()
489 },
490 None,
491 ));
492
493 let buffer = cx.new(|cx| {
494 Buffer::local(
495 indoc! { r#"
496 print(1 + 1)
497 print(2 + 2)
498
499 print(4 + 4)
500
501
502 "# },
503 cx,
504 )
505 .with_language(test_language, cx)
506 });
507 let snapshot = buffer.read(cx).snapshot();
508
509 // Single-point selection
510 let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 4)..Point::new(0, 4));
511 let snippets = snippets
512 .into_iter()
513 .map(|range| snapshot.text_for_range(range).collect::<String>())
514 .collect::<Vec<_>>();
515 assert_eq!(snippets, vec!["print(1 + 1)"]);
516
517 // Multi-line selection
518 let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(2, 0));
519 let snippets = snippets
520 .into_iter()
521 .map(|range| snapshot.text_for_range(range).collect::<String>())
522 .collect::<Vec<_>>();
523 assert_eq!(
524 snippets,
525 vec![indoc! { r#"
526 print(1 + 1)
527 print(2 + 2)"# }]
528 );
529
530 // Trimming multiple trailing blank lines
531 let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(5, 0));
532
533 let snippets = snippets
534 .into_iter()
535 .map(|range| snapshot.text_for_range(range).collect::<String>())
536 .collect::<Vec<_>>();
537 assert_eq!(
538 snippets,
539 vec![indoc! { r#"
540 print(1 + 1)
541 print(2 + 2)
542
543 print(4 + 4)"# }]
544 );
545 }
546
547 #[gpui::test]
548 fn test_jupytext_snippet_ranges(cx: &mut App) {
549 // Create a test language
550 let test_language = Arc::new(Language::new(
551 LanguageConfig {
552 name: "TestLang".into(),
553 line_comments: vec!["# ".into()],
554 ..Default::default()
555 },
556 None,
557 ));
558
559 let buffer = cx.new(|cx| {
560 Buffer::local(
561 indoc! { r#"
562 # Hello!
563 # %% [markdown]
564 # This is some arithmetic
565 print(1 + 1)
566 print(2 + 2)
567
568 # %%
569 print(3 + 3)
570 print(4 + 4)
571
572 print(5 + 5)
573
574
575
576 "# },
577 cx,
578 )
579 .with_language(test_language, cx)
580 });
581 let snapshot = buffer.read(cx).snapshot();
582
583 // Jupytext snippet surrounding an empty selection
584 let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(2, 5));
585
586 let snippets = snippets
587 .into_iter()
588 .map(|range| snapshot.text_for_range(range).collect::<String>())
589 .collect::<Vec<_>>();
590 assert_eq!(
591 snippets,
592 vec![indoc! { r#"
593 # %% [markdown]
594 # This is some arithmetic
595 print(1 + 1)
596 print(2 + 2)"# }]
597 );
598
599 // Jupytext snippets intersecting a non-empty selection
600 let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(6, 2));
601 let snippets = snippets
602 .into_iter()
603 .map(|range| snapshot.text_for_range(range).collect::<String>())
604 .collect::<Vec<_>>();
605 assert_eq!(
606 snippets,
607 vec![
608 indoc! { r#"
609 # %% [markdown]
610 # This is some arithmetic
611 print(1 + 1)
612 print(2 + 2)"#
613 },
614 indoc! { r#"
615 # %%
616 print(3 + 3)
617 print(4 + 4)
618
619 print(5 + 5)"#
620 }
621 ]
622 );
623 }
624
625 #[gpui::test]
626 fn test_markdown_code_blocks(cx: &mut App) {
627 let markdown = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
628 let typescript = languages::language(
629 "typescript",
630 tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
631 );
632 let python = languages::language("python", tree_sitter_python::LANGUAGE.into());
633 let language_registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
634 language_registry.add(markdown.clone());
635 language_registry.add(typescript.clone());
636 language_registry.add(python.clone());
637
638 // Two code blocks intersecting with selection
639 let buffer = cx.new(|cx| {
640 let mut buffer = Buffer::local(
641 indoc! { r#"
642 Hey this is Markdown!
643
644 ```typescript
645 let foo = 999;
646 console.log(foo + 1999);
647 ```
648
649 ```typescript
650 console.log("foo")
651 ```
652 "#
653 },
654 cx,
655 );
656 buffer.set_language_registry(language_registry.clone());
657 buffer.set_language(Some(markdown.clone()), cx);
658 buffer
659 });
660 let snapshot = buffer.read(cx).snapshot();
661
662 let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(8, 5));
663 let snippets = snippets
664 .into_iter()
665 .map(|range| snapshot.text_for_range(range).collect::<String>())
666 .collect::<Vec<_>>();
667
668 assert_eq!(
669 snippets,
670 vec![
671 indoc! { r#"
672 let foo = 999;
673 console.log(foo + 1999);
674 "#
675 },
676 "console.log(\"foo\")\n"
677 ]
678 );
679
680 // Three code blocks intersecting with selection
681 let buffer = cx.new(|cx| {
682 let mut buffer = Buffer::local(
683 indoc! { r#"
684 Hey this is Markdown!
685
686 ```typescript
687 let foo = 999;
688 console.log(foo + 1999);
689 ```
690
691 ```ts
692 console.log("foo")
693 ```
694
695 ```typescript
696 console.log("another code block")
697 ```
698 "# },
699 cx,
700 );
701 buffer.set_language_registry(language_registry.clone());
702 buffer.set_language(Some(markdown.clone()), cx);
703 buffer
704 });
705 let snapshot = buffer.read(cx).snapshot();
706
707 let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(12, 5));
708 let snippets = snippets
709 .into_iter()
710 .map(|range| snapshot.text_for_range(range).collect::<String>())
711 .collect::<Vec<_>>();
712
713 assert_eq!(
714 snippets,
715 vec![
716 indoc! { r#"
717 let foo = 999;
718 console.log(foo + 1999);
719 "#
720 },
721 "console.log(\"foo\")\n",
722 "console.log(\"another code block\")\n",
723 ]
724 );
725
726 // Python code block
727 let buffer = cx.new(|cx| {
728 let mut buffer = Buffer::local(
729 indoc! { r#"
730 Hey this is Markdown!
731
732 ```python
733 print("hello there")
734 print("hello there")
735 print("hello there")
736 ```
737 "# },
738 cx,
739 );
740 buffer.set_language_registry(language_registry.clone());
741 buffer.set_language(Some(markdown.clone()), cx);
742 buffer
743 });
744 let snapshot = buffer.read(cx).snapshot();
745
746 let (snippets, _) = runnable_ranges(&snapshot, Point::new(4, 5)..Point::new(5, 5));
747 let snippets = snippets
748 .into_iter()
749 .map(|range| snapshot.text_for_range(range).collect::<String>())
750 .collect::<Vec<_>>();
751
752 assert_eq!(
753 snippets,
754 vec![indoc! { r#"
755 print("hello there")
756 print("hello there")
757 print("hello there")
758 "#
759 },]
760 );
761 }
762}