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