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};
11use workspace::{Workspace, notifications::NotificationId};
12
13use crate::kernels::PythonEnvKernelSpecification;
14use crate::repl_store::ReplStore;
15use crate::session::SessionEvent;
16use crate::{
17 ClearCurrentOutput, ClearOutputs, Interrupt, JupyterSettings, KernelSpecification, Restart,
18 Session, Shutdown,
19};
20
21pub fn assign_kernelspec(
22 kernel_specification: KernelSpecification,
23 weak_editor: WeakEntity<Editor>,
24 window: &mut Window,
25 cx: &mut App,
26) -> Result<()> {
27 let store = ReplStore::global(cx);
28 if !store.read(cx).is_enabled() {
29 return Ok(());
30 }
31
32 let worktree_id = crate::repl_editor::worktree_id_for_editor(weak_editor.clone(), cx)
33 .context("editor is not in a worktree")?;
34
35 store.update(cx, |store, cx| {
36 store.set_active_kernelspec(worktree_id, kernel_specification.clone(), cx);
37 });
38
39 let fs = store.read(cx).fs().clone();
40
41 if let Some(session) = store.read(cx).get_session(weak_editor.entity_id()).cloned() {
42 // Drop previous session, start new one
43 session.update(cx, |session, cx| {
44 session.clear_outputs(cx);
45 session.shutdown(window, cx);
46 cx.notify();
47 });
48 }
49
50 let session =
51 cx.new(|cx| Session::new(weak_editor.clone(), fs, kernel_specification, window, cx));
52
53 weak_editor
54 .update(cx, |_editor, cx| {
55 cx.notify();
56
57 cx.subscribe(&session, {
58 let store = store.clone();
59 move |_this, _session, event, cx| match event {
60 SessionEvent::Shutdown(shutdown_event) => {
61 store.update(cx, |store, _cx| {
62 store.remove_session(shutdown_event.entity_id());
63 });
64 }
65 }
66 })
67 .detach();
68 })
69 .ok();
70
71 store.update(cx, |store, _cx| {
72 store.insert_session(weak_editor.entity_id(), session.clone());
73 });
74
75 Ok(())
76}
77
78pub fn install_ipykernel_and_assign(
79 kernel_specification: KernelSpecification,
80 weak_editor: WeakEntity<Editor>,
81 window: &mut Window,
82 cx: &mut App,
83) -> Result<()> {
84 let KernelSpecification::PythonEnv(ref env_spec) = kernel_specification else {
85 return assign_kernelspec(kernel_specification, weak_editor, window, cx);
86 };
87
88 let python_path = env_spec.path.clone();
89 let env_name = env_spec.name.clone();
90 let env_spec = env_spec.clone();
91
92 struct IpykernelInstall;
93 let notification_id = NotificationId::unique::<IpykernelInstall>();
94
95 let workspace = Workspace::for_window(window, cx);
96 if let Some(workspace) = &workspace {
97 workspace.update(cx, |workspace, cx| {
98 workspace.show_toast(
99 workspace::Toast::new(
100 notification_id.clone(),
101 format!("Installing ipykernel in {}...", env_name),
102 ),
103 cx,
104 );
105 });
106 }
107
108 let weak_workspace = workspace.map(|w| w.downgrade());
109 let window_handle = window.window_handle();
110
111 let install_task = cx.background_spawn(async move {
112 let output = util::command::new_command(python_path.to_string_lossy().as_ref())
113 .args(&["-m", "pip", "install", "ipykernel"])
114 .output()
115 .await
116 .context("failed to run pip install ipykernel")?;
117
118 if output.status.success() {
119 anyhow::Ok(())
120 } else {
121 let stderr = String::from_utf8_lossy(&output.stderr);
122 anyhow::bail!("{}", stderr.lines().last().unwrap_or("unknown error"))
123 }
124 });
125
126 cx.spawn(async move |cx| {
127 let result = install_task.await;
128
129 match result {
130 Ok(()) => {
131 if let Some(weak_workspace) = &weak_workspace {
132 weak_workspace
133 .update(cx, |workspace, cx| {
134 workspace.dismiss_toast(¬ification_id, cx);
135 workspace.show_toast(
136 workspace::Toast::new(
137 notification_id.clone(),
138 format!("ipykernel installed in {}", env_name),
139 )
140 .autohide(),
141 cx,
142 );
143 })
144 .ok();
145 }
146
147 window_handle
148 .update(cx, |_, window, cx| {
149 let updated_spec =
150 KernelSpecification::PythonEnv(PythonEnvKernelSpecification {
151 has_ipykernel: true,
152 ..env_spec
153 });
154 assign_kernelspec(updated_spec, weak_editor, window, cx).ok();
155 })
156 .ok();
157 }
158 Err(error) => {
159 if let Some(weak_workspace) = &weak_workspace {
160 weak_workspace
161 .update(cx, |workspace, cx| {
162 workspace.dismiss_toast(¬ification_id, cx);
163 workspace.show_toast(
164 workspace::Toast::new(
165 notification_id.clone(),
166 format!(
167 "Failed to install ipykernel in {}: {}",
168 env_name, error
169 ),
170 ),
171 cx,
172 );
173 })
174 .ok();
175 }
176 }
177 }
178 })
179 .detach();
180
181 Ok(())
182}
183
184pub fn run(
185 editor: WeakEntity<Editor>,
186 move_down: bool,
187 window: &mut Window,
188 cx: &mut App,
189) -> Result<()> {
190 let store = ReplStore::global(cx);
191 if !store.read(cx).is_enabled() {
192 return Ok(());
193 }
194
195 let editor = editor.upgrade().context("editor was dropped")?;
196 let selected_range = editor
197 .update(cx, |editor, cx| {
198 editor
199 .selections
200 .newest_adjusted(&editor.display_snapshot(cx))
201 })
202 .range();
203 let multibuffer = editor.read(cx).buffer().clone();
204 let Some(buffer) = multibuffer.read(cx).as_singleton() else {
205 return Ok(());
206 };
207
208 let Some(project_path) = buffer.read(cx).project_path(cx) else {
209 return Ok(());
210 };
211
212 let (runnable_ranges, next_cell_point) =
213 runnable_ranges(&buffer.read(cx).snapshot(), selected_range, cx);
214
215 for runnable_range in runnable_ranges {
216 let Some(language) = multibuffer.read(cx).language_at(runnable_range.start, cx) else {
217 continue;
218 };
219
220 let kernel_specification = store
221 .read(cx)
222 .active_kernelspec(project_path.worktree_id, Some(language.clone()), cx)
223 .with_context(|| format!("No kernel found for language: {}", language.name()))?;
224
225 let fs = store.read(cx).fs().clone();
226
227 let session = if let Some(session) = store.read(cx).get_session(editor.entity_id()).cloned()
228 {
229 session
230 } else {
231 let weak_editor = editor.downgrade();
232 let session =
233 cx.new(|cx| Session::new(weak_editor, fs, kernel_specification, window, cx));
234
235 editor.update(cx, |_editor, cx| {
236 cx.notify();
237
238 cx.subscribe(&session, {
239 let store = store.clone();
240 move |_this, _session, event, cx| match event {
241 SessionEvent::Shutdown(shutdown_event) => {
242 store.update(cx, |store, _cx| {
243 store.remove_session(shutdown_event.entity_id());
244 });
245 }
246 }
247 })
248 .detach();
249 });
250
251 store.update(cx, |store, _cx| {
252 store.insert_session(editor.entity_id(), session.clone());
253 });
254
255 session
256 };
257
258 let selected_text;
259 let anchor_range;
260 let next_cursor;
261 {
262 let snapshot = multibuffer.read(cx).read(cx);
263 selected_text = snapshot
264 .text_for_range(runnable_range.clone())
265 .collect::<String>();
266 anchor_range = snapshot.anchor_before(runnable_range.start)
267 ..snapshot.anchor_after(runnable_range.end);
268 next_cursor = next_cell_point.map(|point| snapshot.anchor_after(point));
269 }
270
271 session.update(cx, |session, cx| {
272 session.execute(
273 selected_text,
274 anchor_range,
275 next_cursor,
276 move_down,
277 window,
278 cx,
279 );
280 });
281 }
282
283 anyhow::Ok(())
284}
285
286pub enum SessionSupport {
287 ActiveSession(Entity<Session>),
288 Inactive(KernelSpecification),
289 RequiresSetup(LanguageName),
290 Unsupported,
291}
292
293pub fn worktree_id_for_editor(editor: WeakEntity<Editor>, cx: &mut App) -> Option<WorktreeId> {
294 editor.upgrade().and_then(|editor| {
295 editor
296 .read(cx)
297 .buffer()
298 .read(cx)
299 .as_singleton()?
300 .read(cx)
301 .project_path(cx)
302 .map(|path| path.worktree_id)
303 })
304}
305
306pub fn session(editor: WeakEntity<Editor>, cx: &mut App) -> SessionSupport {
307 let store = ReplStore::global(cx);
308 let entity_id = editor.entity_id();
309
310 if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
311 return SessionSupport::ActiveSession(session);
312 };
313
314 let Some(language) = get_language(editor.clone(), cx) else {
315 return SessionSupport::Unsupported;
316 };
317
318 let worktree_id = worktree_id_for_editor(editor, cx);
319
320 let Some(worktree_id) = worktree_id else {
321 return SessionSupport::Unsupported;
322 };
323
324 let kernelspec = store
325 .read(cx)
326 .active_kernelspec(worktree_id, Some(language.clone()), cx);
327
328 match kernelspec {
329 Some(kernelspec) => SessionSupport::Inactive(kernelspec),
330 None => {
331 // For language_supported, need to check available kernels for language
332 if language_supported(&language, cx) {
333 SessionSupport::RequiresSetup(language.name())
334 } else {
335 SessionSupport::Unsupported
336 }
337 }
338 }
339}
340
341pub fn clear_outputs(editor: WeakEntity<Editor>, cx: &mut App) {
342 let store = ReplStore::global(cx);
343 let entity_id = editor.entity_id();
344 let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
345 return;
346 };
347 session.update(cx, |session, cx| {
348 session.clear_outputs(cx);
349 cx.notify();
350 });
351}
352
353pub fn clear_current_output(editor: WeakEntity<Editor>, cx: &mut App) {
354 let Some(editor_entity) = editor.upgrade() else {
355 return;
356 };
357
358 let store = ReplStore::global(cx);
359 let entity_id = editor.entity_id();
360 let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
361 return;
362 };
363
364 let position = editor_entity.read(cx).selections.newest_anchor().head();
365
366 session.update(cx, |session, cx| {
367 session.clear_output_at_position(position, cx);
368 });
369}
370
371pub fn interrupt(editor: WeakEntity<Editor>, cx: &mut App) {
372 let store = ReplStore::global(cx);
373 let entity_id = editor.entity_id();
374 let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
375 return;
376 };
377
378 session.update(cx, |session, cx| {
379 session.interrupt(cx);
380 cx.notify();
381 });
382}
383
384pub fn shutdown(editor: WeakEntity<Editor>, window: &mut Window, cx: &mut App) {
385 let store = ReplStore::global(cx);
386 let entity_id = editor.entity_id();
387 let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
388 return;
389 };
390
391 session.update(cx, |session, cx| {
392 session.shutdown(window, cx);
393 cx.notify();
394 });
395}
396
397pub fn restart(editor: WeakEntity<Editor>, window: &mut Window, cx: &mut App) {
398 let Some(editor) = editor.upgrade() else {
399 return;
400 };
401
402 let entity_id = editor.entity_id();
403
404 let Some(session) = ReplStore::global(cx)
405 .read(cx)
406 .get_session(entity_id)
407 .cloned()
408 else {
409 return;
410 };
411
412 session.update(cx, |session, cx| {
413 session.restart(window, cx);
414 cx.notify();
415 });
416}
417
418pub fn setup_editor_session_actions(editor: &mut Editor, editor_handle: WeakEntity<Editor>) {
419 editor
420 .register_action({
421 let editor_handle = editor_handle.clone();
422 move |_: &ClearOutputs, _, cx| {
423 if !JupyterSettings::enabled(cx) {
424 return;
425 }
426
427 crate::clear_outputs(editor_handle.clone(), cx);
428 }
429 })
430 .detach();
431
432 editor
433 .register_action({
434 let editor_handle = editor_handle.clone();
435 move |_: &ClearCurrentOutput, _, cx| {
436 if !JupyterSettings::enabled(cx) {
437 return;
438 }
439
440 crate::clear_current_output(editor_handle.clone(), cx);
441 }
442 })
443 .detach();
444
445 editor
446 .register_action({
447 let editor_handle = editor_handle.clone();
448 move |_: &Interrupt, _, cx| {
449 if !JupyterSettings::enabled(cx) {
450 return;
451 }
452
453 crate::interrupt(editor_handle.clone(), cx);
454 }
455 })
456 .detach();
457
458 editor
459 .register_action({
460 let editor_handle = editor_handle.clone();
461 move |_: &Shutdown, window, cx| {
462 if !JupyterSettings::enabled(cx) {
463 return;
464 }
465
466 crate::shutdown(editor_handle.clone(), window, cx);
467 }
468 })
469 .detach();
470
471 editor
472 .register_action({
473 let editor_handle = editor_handle;
474 move |_: &Restart, window, cx| {
475 if !JupyterSettings::enabled(cx) {
476 return;
477 }
478
479 crate::restart(editor_handle.clone(), window, cx);
480 }
481 })
482 .detach();
483}
484
485fn cell_range(buffer: &BufferSnapshot, start_row: u32, end_row: u32) -> Range<Point> {
486 let mut snippet_end_row = end_row;
487 while buffer.is_line_blank(snippet_end_row) && snippet_end_row > start_row {
488 snippet_end_row -= 1;
489 }
490 Point::new(start_row, 0)..Point::new(snippet_end_row, buffer.line_len(snippet_end_row))
491}
492
493// Returns the ranges of the snippets in the buffer and the next point for moving the cursor to
494fn jupytext_cells(
495 buffer: &BufferSnapshot,
496 range: Range<Point>,
497) -> (Vec<Range<Point>>, Option<Point>) {
498 let mut current_row = range.start.row;
499
500 let Some(language) = buffer.language() else {
501 return (Vec::new(), None);
502 };
503
504 let default_scope = language.default_scope();
505 let comment_prefixes = default_scope.line_comment_prefixes();
506 if comment_prefixes.is_empty() {
507 return (Vec::new(), None);
508 }
509
510 let jupytext_prefixes = comment_prefixes
511 .iter()
512 .map(|comment_prefix| format!("{comment_prefix}%%"))
513 .collect::<Vec<_>>();
514
515 let mut snippet_start_row = None;
516 loop {
517 if jupytext_prefixes
518 .iter()
519 .any(|prefix| buffer.contains_str_at(Point::new(current_row, 0), prefix))
520 {
521 snippet_start_row = Some(current_row);
522 break;
523 } else if current_row > 0 {
524 current_row -= 1;
525 } else {
526 break;
527 }
528 }
529
530 let mut snippets = Vec::new();
531 if let Some(mut snippet_start_row) = snippet_start_row {
532 for current_row in range.start.row + 1..=buffer.max_point().row {
533 if jupytext_prefixes
534 .iter()
535 .any(|prefix| buffer.contains_str_at(Point::new(current_row, 0), prefix))
536 {
537 snippets.push(cell_range(buffer, snippet_start_row, current_row - 1));
538
539 if current_row <= range.end.row {
540 snippet_start_row = current_row;
541 } else {
542 // Return our snippets as well as the next point for moving the cursor to
543 return (snippets, Some(Point::new(current_row, 0)));
544 }
545 }
546 }
547
548 // Go to the end of the buffer (no more jupytext cells found)
549 snippets.push(cell_range(
550 buffer,
551 snippet_start_row,
552 buffer.max_point().row,
553 ));
554 }
555
556 (snippets, None)
557}
558
559fn runnable_ranges(
560 buffer: &BufferSnapshot,
561 range: Range<Point>,
562 cx: &mut App,
563) -> (Vec<Range<Point>>, Option<Point>) {
564 if let Some(language) = buffer.language()
565 && language.name() == "Markdown"
566 {
567 return (markdown_code_blocks(buffer, range, cx), None);
568 }
569
570 let (jupytext_snippets, next_cursor) = jupytext_cells(buffer, range.clone());
571 if !jupytext_snippets.is_empty() {
572 return (jupytext_snippets, next_cursor);
573 }
574
575 let snippet_range = cell_range(buffer, range.start.row, range.end.row);
576
577 // Check if the snippet range is entirely blank, if so, skip forward to find code
578 let is_blank =
579 (snippet_range.start.row..=snippet_range.end.row).all(|row| buffer.is_line_blank(row));
580
581 if is_blank {
582 // Search forward for the next non-blank line
583 let max_row = buffer.max_point().row;
584 let mut next_row = snippet_range.end.row + 1;
585 while next_row <= max_row && buffer.is_line_blank(next_row) {
586 next_row += 1;
587 }
588
589 if next_row <= max_row {
590 // Found a non-blank line, find the extent of this cell
591 let next_snippet_range = cell_range(buffer, next_row, next_row);
592 let start_language = buffer.language_at(next_snippet_range.start);
593 let end_language = buffer.language_at(next_snippet_range.end);
594
595 if start_language
596 .zip(end_language)
597 .is_some_and(|(start, end)| start == end)
598 {
599 return (vec![next_snippet_range], None);
600 }
601 }
602
603 return (Vec::new(), None);
604 }
605
606 let start_language = buffer.language_at(snippet_range.start);
607 let end_language = buffer.language_at(snippet_range.end);
608
609 if start_language
610 .zip(end_language)
611 .is_some_and(|(start, end)| start == end)
612 {
613 (vec![snippet_range], None)
614 } else {
615 (Vec::new(), None)
616 }
617}
618
619// We allow markdown code blocks to end in a trailing newline in order to render the output
620// below the final code fence. This is different than our behavior for selections and Jupytext cells.
621fn markdown_code_blocks(
622 buffer: &BufferSnapshot,
623 range: Range<Point>,
624 cx: &mut App,
625) -> Vec<Range<Point>> {
626 buffer
627 .injections_intersecting_range(range)
628 .filter(|(_, language)| language_supported(language, cx))
629 .map(|(content_range, _)| {
630 buffer.offset_to_point(content_range.start)..buffer.offset_to_point(content_range.end)
631 })
632 .collect()
633}
634
635fn language_supported(language: &Arc<Language>, cx: &mut App) -> bool {
636 let store = ReplStore::global(cx);
637 let store_read = store.read(cx);
638
639 // Since we're just checking for general language support, we only need to look at
640 // the pure Jupyter kernels - these are all the globally available ones
641 store_read.pure_jupyter_kernel_specifications().any(|spec| {
642 // Convert to lowercase for case-insensitive comparison since kernels might report "python" while our language is "Python"
643 spec.language().as_ref().to_lowercase() == language.name().as_ref().to_lowercase()
644 })
645}
646
647fn get_language(editor: WeakEntity<Editor>, cx: &mut App) -> Option<Arc<Language>> {
648 editor
649 .update(cx, |editor, cx| {
650 let display_snapshot = editor.display_snapshot(cx);
651 let selection = editor
652 .selections
653 .newest::<MultiBufferOffset>(&display_snapshot);
654 display_snapshot
655 .buffer_snapshot()
656 .language_at(selection.head())
657 .cloned()
658 })
659 .ok()
660 .flatten()
661}
662
663#[cfg(test)]
664mod tests {
665 use super::*;
666 use gpui::App;
667 use indoc::indoc;
668 use language::{Buffer, Language, LanguageConfig, LanguageRegistry};
669
670 #[gpui::test]
671 fn test_snippet_ranges(cx: &mut App) {
672 // Create a test language
673 let test_language = Arc::new(Language::new(
674 LanguageConfig {
675 name: "TestLang".into(),
676 line_comments: vec!["# ".into()],
677 ..Default::default()
678 },
679 None,
680 ));
681
682 let buffer = cx.new(|cx| {
683 Buffer::local(
684 indoc! { r#"
685 print(1 + 1)
686 print(2 + 2)
687
688 print(4 + 4)
689
690
691 "# },
692 cx,
693 )
694 .with_language(test_language, cx)
695 });
696 let snapshot = buffer.read(cx).snapshot();
697
698 // Single-point selection
699 let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 4)..Point::new(0, 4), cx);
700 let snippets = snippets
701 .into_iter()
702 .map(|range| snapshot.text_for_range(range).collect::<String>())
703 .collect::<Vec<_>>();
704 assert_eq!(snippets, vec!["print(1 + 1)"]);
705
706 // Multi-line selection
707 let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(2, 0), cx);
708 let snippets = snippets
709 .into_iter()
710 .map(|range| snapshot.text_for_range(range).collect::<String>())
711 .collect::<Vec<_>>();
712 assert_eq!(
713 snippets,
714 vec![indoc! { r#"
715 print(1 + 1)
716 print(2 + 2)"# }]
717 );
718
719 // Trimming multiple trailing blank lines
720 let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(5, 0), cx);
721
722 let snippets = snippets
723 .into_iter()
724 .map(|range| snapshot.text_for_range(range).collect::<String>())
725 .collect::<Vec<_>>();
726 assert_eq!(
727 snippets,
728 vec![indoc! { r#"
729 print(1 + 1)
730 print(2 + 2)
731
732 print(4 + 4)"# }]
733 );
734 }
735
736 #[gpui::test]
737 fn test_jupytext_snippet_ranges(cx: &mut App) {
738 // Create a test language
739 let test_language = Arc::new(Language::new(
740 LanguageConfig {
741 name: "TestLang".into(),
742 line_comments: vec!["# ".into()],
743 ..Default::default()
744 },
745 None,
746 ));
747
748 let buffer = cx.new(|cx| {
749 Buffer::local(
750 indoc! { r#"
751 # Hello!
752 # %% [markdown]
753 # This is some arithmetic
754 print(1 + 1)
755 print(2 + 2)
756
757 # %%
758 print(3 + 3)
759 print(4 + 4)
760
761 print(5 + 5)
762
763
764
765 "# },
766 cx,
767 )
768 .with_language(test_language, cx)
769 });
770 let snapshot = buffer.read(cx).snapshot();
771
772 // Jupytext snippet surrounding an empty selection
773 let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(2, 5), cx);
774
775 let snippets = snippets
776 .into_iter()
777 .map(|range| snapshot.text_for_range(range).collect::<String>())
778 .collect::<Vec<_>>();
779 assert_eq!(
780 snippets,
781 vec![indoc! { r#"
782 # %% [markdown]
783 # This is some arithmetic
784 print(1 + 1)
785 print(2 + 2)"# }]
786 );
787
788 // Jupytext snippets intersecting a non-empty selection
789 let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(6, 2), cx);
790 let snippets = snippets
791 .into_iter()
792 .map(|range| snapshot.text_for_range(range).collect::<String>())
793 .collect::<Vec<_>>();
794 assert_eq!(
795 snippets,
796 vec![
797 indoc! { r#"
798 # %% [markdown]
799 # This is some arithmetic
800 print(1 + 1)
801 print(2 + 2)"#
802 },
803 indoc! { r#"
804 # %%
805 print(3 + 3)
806 print(4 + 4)
807
808 print(5 + 5)"#
809 }
810 ]
811 );
812 }
813
814 #[gpui::test]
815 fn test_markdown_code_blocks(cx: &mut App) {
816 use crate::kernels::LocalKernelSpecification;
817 use jupyter_protocol::JupyterKernelspec;
818
819 // Initialize settings
820 settings::init(cx);
821 editor::init(cx);
822
823 // Initialize the ReplStore with a fake filesystem
824 let fs = Arc::new(project::RealFs::new(None, cx.background_executor().clone()));
825 ReplStore::init(fs, cx);
826
827 // Add mock kernel specifications for TypeScript and Python
828 let store = ReplStore::global(cx);
829 store.update(cx, |store, cx| {
830 let typescript_spec = KernelSpecification::Jupyter(LocalKernelSpecification {
831 name: "typescript".into(),
832 kernelspec: JupyterKernelspec {
833 argv: vec![],
834 display_name: "TypeScript".into(),
835 language: "typescript".into(),
836 interrupt_mode: None,
837 metadata: None,
838 env: None,
839 },
840 path: std::path::PathBuf::new(),
841 });
842
843 let python_spec = KernelSpecification::Jupyter(LocalKernelSpecification {
844 name: "python".into(),
845 kernelspec: JupyterKernelspec {
846 argv: vec![],
847 display_name: "Python".into(),
848 language: "python".into(),
849 interrupt_mode: None,
850 metadata: None,
851 env: None,
852 },
853 path: std::path::PathBuf::new(),
854 });
855
856 store.set_kernel_specs_for_testing(vec![typescript_spec, python_spec], cx);
857 });
858
859 let markdown = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
860 let typescript = languages::language(
861 "typescript",
862 tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
863 );
864 let python = languages::language("python", tree_sitter_python::LANGUAGE.into());
865 let language_registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
866 language_registry.add(markdown.clone());
867 language_registry.add(typescript);
868 language_registry.add(python);
869
870 // Two code blocks intersecting with selection
871 let buffer = cx.new(|cx| {
872 let mut buffer = Buffer::local(
873 indoc! { r#"
874 Hey this is Markdown!
875
876 ```typescript
877 let foo = 999;
878 console.log(foo + 1999);
879 ```
880
881 ```typescript
882 console.log("foo")
883 ```
884 "#
885 },
886 cx,
887 );
888 buffer.set_language_registry(language_registry.clone());
889 buffer.set_language(Some(markdown.clone()), cx);
890 buffer
891 });
892 let snapshot = buffer.read(cx).snapshot();
893
894 let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(8, 5), cx);
895 let snippets = snippets
896 .into_iter()
897 .map(|range| snapshot.text_for_range(range).collect::<String>())
898 .collect::<Vec<_>>();
899
900 assert_eq!(
901 snippets,
902 vec![
903 indoc! { r#"
904 let foo = 999;
905 console.log(foo + 1999);
906 "#
907 },
908 "console.log(\"foo\")\n"
909 ]
910 );
911
912 // Three code blocks intersecting with selection
913 let buffer = cx.new(|cx| {
914 let mut buffer = Buffer::local(
915 indoc! { r#"
916 Hey this is Markdown!
917
918 ```typescript
919 let foo = 999;
920 console.log(foo + 1999);
921 ```
922
923 ```ts
924 console.log("foo")
925 ```
926
927 ```typescript
928 console.log("another code block")
929 ```
930 "# },
931 cx,
932 );
933 buffer.set_language_registry(language_registry.clone());
934 buffer.set_language(Some(markdown.clone()), cx);
935 buffer
936 });
937 let snapshot = buffer.read(cx).snapshot();
938
939 let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(12, 5), cx);
940 let snippets = snippets
941 .into_iter()
942 .map(|range| snapshot.text_for_range(range).collect::<String>())
943 .collect::<Vec<_>>();
944
945 assert_eq!(
946 snippets,
947 vec![
948 indoc! { r#"
949 let foo = 999;
950 console.log(foo + 1999);
951 "#
952 },
953 "console.log(\"foo\")\n",
954 "console.log(\"another code block\")\n",
955 ]
956 );
957
958 // Python code block
959 let buffer = cx.new(|cx| {
960 let mut buffer = Buffer::local(
961 indoc! { r#"
962 Hey this is Markdown!
963
964 ```python
965 print("hello there")
966 print("hello there")
967 print("hello there")
968 ```
969 "# },
970 cx,
971 );
972 buffer.set_language_registry(language_registry.clone());
973 buffer.set_language(Some(markdown.clone()), cx);
974 buffer
975 });
976 let snapshot = buffer.read(cx).snapshot();
977
978 let (snippets, _) = runnable_ranges(&snapshot, Point::new(4, 5)..Point::new(5, 5), cx);
979 let snippets = snippets
980 .into_iter()
981 .map(|range| snapshot.text_for_range(range).collect::<String>())
982 .collect::<Vec<_>>();
983
984 assert_eq!(
985 snippets,
986 vec![indoc! { r#"
987 print("hello there")
988 print("hello there")
989 print("hello there")
990 "#
991 },]
992 );
993 }
994
995 #[gpui::test]
996 fn test_skip_blank_lines_to_next_cell(cx: &mut App) {
997 let test_language = Arc::new(Language::new(
998 LanguageConfig {
999 name: "TestLang".into(),
1000 line_comments: vec!["# ".into()],
1001 ..Default::default()
1002 },
1003 None,
1004 ));
1005
1006 let buffer = cx.new(|cx| {
1007 Buffer::local(
1008 indoc! { r#"
1009 print(1 + 1)
1010
1011 print(2 + 2)
1012 "# },
1013 cx,
1014 )
1015 .with_language(test_language.clone(), cx)
1016 });
1017 let snapshot = buffer.read(cx).snapshot();
1018
1019 // Selection on blank line should skip to next non-blank cell
1020 let (snippets, _) = runnable_ranges(&snapshot, Point::new(1, 0)..Point::new(1, 0), cx);
1021 let snippets = snippets
1022 .into_iter()
1023 .map(|range| snapshot.text_for_range(range).collect::<String>())
1024 .collect::<Vec<_>>();
1025 assert_eq!(snippets, vec!["print(2 + 2)"]);
1026
1027 // Multiple blank lines should also skip forward
1028 let buffer = cx.new(|cx| {
1029 Buffer::local(
1030 indoc! { r#"
1031 print(1 + 1)
1032
1033
1034
1035 print(2 + 2)
1036 "# },
1037 cx,
1038 )
1039 .with_language(test_language.clone(), cx)
1040 });
1041 let snapshot = buffer.read(cx).snapshot();
1042
1043 let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 0)..Point::new(2, 0), cx);
1044 let snippets = snippets
1045 .into_iter()
1046 .map(|range| snapshot.text_for_range(range).collect::<String>())
1047 .collect::<Vec<_>>();
1048 assert_eq!(snippets, vec!["print(2 + 2)"]);
1049
1050 // Blank lines at end of file should return nothing
1051 let buffer = cx.new(|cx| {
1052 Buffer::local(
1053 indoc! { r#"
1054 print(1 + 1)
1055
1056 "# },
1057 cx,
1058 )
1059 .with_language(test_language, cx)
1060 });
1061 let snapshot = buffer.read(cx).snapshot();
1062
1063 let (snippets, _) = runnable_ranges(&snapshot, Point::new(1, 0)..Point::new(1, 0), cx);
1064 assert!(snippets.is_empty());
1065 }
1066}