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
437 // Check if the snippet range is entirely blank, if so, skip forward to find code
438 let is_blank =
439 (snippet_range.start.row..=snippet_range.end.row).all(|row| buffer.is_line_blank(row));
440
441 if is_blank {
442 // Search forward for the next non-blank line
443 let max_row = buffer.max_point().row;
444 let mut next_row = snippet_range.end.row + 1;
445 while next_row <= max_row && buffer.is_line_blank(next_row) {
446 next_row += 1;
447 }
448
449 if next_row <= max_row {
450 // Found a non-blank line, find the extent of this cell
451 let next_snippet_range = cell_range(buffer, next_row, next_row);
452 let start_language = buffer.language_at(next_snippet_range.start);
453 let end_language = buffer.language_at(next_snippet_range.end);
454
455 if start_language
456 .zip(end_language)
457 .is_some_and(|(start, end)| start == end)
458 {
459 return (vec![next_snippet_range], None);
460 }
461 }
462
463 return (Vec::new(), None);
464 }
465
466 let start_language = buffer.language_at(snippet_range.start);
467 let end_language = buffer.language_at(snippet_range.end);
468
469 if start_language
470 .zip(end_language)
471 .is_some_and(|(start, end)| start == end)
472 {
473 (vec![snippet_range], None)
474 } else {
475 (Vec::new(), None)
476 }
477}
478
479// We allow markdown code blocks to end in a trailing newline in order to render the output
480// below the final code fence. This is different than our behavior for selections and Jupytext cells.
481fn markdown_code_blocks(
482 buffer: &BufferSnapshot,
483 range: Range<Point>,
484 cx: &mut App,
485) -> Vec<Range<Point>> {
486 buffer
487 .injections_intersecting_range(range)
488 .filter(|(_, language)| language_supported(language, cx))
489 .map(|(content_range, _)| {
490 buffer.offset_to_point(content_range.start)..buffer.offset_to_point(content_range.end)
491 })
492 .collect()
493}
494
495fn language_supported(language: &Arc<Language>, cx: &mut App) -> bool {
496 let store = ReplStore::global(cx);
497 let store_read = store.read(cx);
498
499 // Since we're just checking for general language support, we only need to look at
500 // the pure Jupyter kernels - these are all the globally available ones
501 store_read.pure_jupyter_kernel_specifications().any(|spec| {
502 // Convert to lowercase for case-insensitive comparison since kernels might report "python" while our language is "Python"
503 spec.language().as_ref().to_lowercase() == language.name().as_ref().to_lowercase()
504 })
505}
506
507fn get_language(editor: WeakEntity<Editor>, cx: &mut App) -> Option<Arc<Language>> {
508 editor
509 .update(cx, |editor, cx| {
510 let display_snapshot = editor.display_snapshot(cx);
511 let selection = editor
512 .selections
513 .newest::<MultiBufferOffset>(&display_snapshot);
514 display_snapshot
515 .buffer_snapshot()
516 .language_at(selection.head())
517 .cloned()
518 })
519 .ok()
520 .flatten()
521}
522
523#[cfg(test)]
524mod tests {
525 use super::*;
526 use gpui::App;
527 use indoc::indoc;
528 use language::{Buffer, Language, LanguageConfig, LanguageRegistry};
529
530 #[gpui::test]
531 fn test_snippet_ranges(cx: &mut App) {
532 // Create a test language
533 let test_language = Arc::new(Language::new(
534 LanguageConfig {
535 name: "TestLang".into(),
536 line_comments: vec!["# ".into()],
537 ..Default::default()
538 },
539 None,
540 ));
541
542 let buffer = cx.new(|cx| {
543 Buffer::local(
544 indoc! { r#"
545 print(1 + 1)
546 print(2 + 2)
547
548 print(4 + 4)
549
550
551 "# },
552 cx,
553 )
554 .with_language(test_language, cx)
555 });
556 let snapshot = buffer.read(cx).snapshot();
557
558 // Single-point selection
559 let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 4)..Point::new(0, 4), cx);
560 let snippets = snippets
561 .into_iter()
562 .map(|range| snapshot.text_for_range(range).collect::<String>())
563 .collect::<Vec<_>>();
564 assert_eq!(snippets, vec!["print(1 + 1)"]);
565
566 // Multi-line selection
567 let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(2, 0), cx);
568 let snippets = snippets
569 .into_iter()
570 .map(|range| snapshot.text_for_range(range).collect::<String>())
571 .collect::<Vec<_>>();
572 assert_eq!(
573 snippets,
574 vec![indoc! { r#"
575 print(1 + 1)
576 print(2 + 2)"# }]
577 );
578
579 // Trimming multiple trailing blank lines
580 let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(5, 0), cx);
581
582 let snippets = snippets
583 .into_iter()
584 .map(|range| snapshot.text_for_range(range).collect::<String>())
585 .collect::<Vec<_>>();
586 assert_eq!(
587 snippets,
588 vec![indoc! { r#"
589 print(1 + 1)
590 print(2 + 2)
591
592 print(4 + 4)"# }]
593 );
594 }
595
596 #[gpui::test]
597 fn test_jupytext_snippet_ranges(cx: &mut App) {
598 // Create a test language
599 let test_language = Arc::new(Language::new(
600 LanguageConfig {
601 name: "TestLang".into(),
602 line_comments: vec!["# ".into()],
603 ..Default::default()
604 },
605 None,
606 ));
607
608 let buffer = cx.new(|cx| {
609 Buffer::local(
610 indoc! { r#"
611 # Hello!
612 # %% [markdown]
613 # This is some arithmetic
614 print(1 + 1)
615 print(2 + 2)
616
617 # %%
618 print(3 + 3)
619 print(4 + 4)
620
621 print(5 + 5)
622
623
624
625 "# },
626 cx,
627 )
628 .with_language(test_language, cx)
629 });
630 let snapshot = buffer.read(cx).snapshot();
631
632 // Jupytext snippet surrounding an empty selection
633 let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(2, 5), cx);
634
635 let snippets = snippets
636 .into_iter()
637 .map(|range| snapshot.text_for_range(range).collect::<String>())
638 .collect::<Vec<_>>();
639 assert_eq!(
640 snippets,
641 vec![indoc! { r#"
642 # %% [markdown]
643 # This is some arithmetic
644 print(1 + 1)
645 print(2 + 2)"# }]
646 );
647
648 // Jupytext snippets intersecting a non-empty selection
649 let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(6, 2), cx);
650 let snippets = snippets
651 .into_iter()
652 .map(|range| snapshot.text_for_range(range).collect::<String>())
653 .collect::<Vec<_>>();
654 assert_eq!(
655 snippets,
656 vec![
657 indoc! { r#"
658 # %% [markdown]
659 # This is some arithmetic
660 print(1 + 1)
661 print(2 + 2)"#
662 },
663 indoc! { r#"
664 # %%
665 print(3 + 3)
666 print(4 + 4)
667
668 print(5 + 5)"#
669 }
670 ]
671 );
672 }
673
674 #[gpui::test]
675 fn test_markdown_code_blocks(cx: &mut App) {
676 use crate::kernels::LocalKernelSpecification;
677 use jupyter_protocol::JupyterKernelspec;
678
679 // Initialize settings
680 settings::init(cx);
681 editor::init(cx);
682
683 // Initialize the ReplStore with a fake filesystem
684 let fs = Arc::new(project::RealFs::new(None, cx.background_executor().clone()));
685 ReplStore::init(fs, cx);
686
687 // Add mock kernel specifications for TypeScript and Python
688 let store = ReplStore::global(cx);
689 store.update(cx, |store, cx| {
690 let typescript_spec = KernelSpecification::Jupyter(LocalKernelSpecification {
691 name: "typescript".into(),
692 kernelspec: JupyterKernelspec {
693 argv: vec![],
694 display_name: "TypeScript".into(),
695 language: "typescript".into(),
696 interrupt_mode: None,
697 metadata: None,
698 env: None,
699 },
700 path: std::path::PathBuf::new(),
701 });
702
703 let python_spec = KernelSpecification::Jupyter(LocalKernelSpecification {
704 name: "python".into(),
705 kernelspec: JupyterKernelspec {
706 argv: vec![],
707 display_name: "Python".into(),
708 language: "python".into(),
709 interrupt_mode: None,
710 metadata: None,
711 env: None,
712 },
713 path: std::path::PathBuf::new(),
714 });
715
716 store.set_kernel_specs_for_testing(vec![typescript_spec, python_spec], cx);
717 });
718
719 let markdown = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
720 let typescript = languages::language(
721 "typescript",
722 tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
723 );
724 let python = languages::language("python", tree_sitter_python::LANGUAGE.into());
725 let language_registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
726 language_registry.add(markdown.clone());
727 language_registry.add(typescript);
728 language_registry.add(python);
729
730 // Two code blocks intersecting with selection
731 let buffer = cx.new(|cx| {
732 let mut buffer = Buffer::local(
733 indoc! { r#"
734 Hey this is Markdown!
735
736 ```typescript
737 let foo = 999;
738 console.log(foo + 1999);
739 ```
740
741 ```typescript
742 console.log("foo")
743 ```
744 "#
745 },
746 cx,
747 );
748 buffer.set_language_registry(language_registry.clone());
749 buffer.set_language(Some(markdown.clone()), cx);
750 buffer
751 });
752 let snapshot = buffer.read(cx).snapshot();
753
754 let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(8, 5), cx);
755 let snippets = snippets
756 .into_iter()
757 .map(|range| snapshot.text_for_range(range).collect::<String>())
758 .collect::<Vec<_>>();
759
760 assert_eq!(
761 snippets,
762 vec![
763 indoc! { r#"
764 let foo = 999;
765 console.log(foo + 1999);
766 "#
767 },
768 "console.log(\"foo\")\n"
769 ]
770 );
771
772 // Three code blocks intersecting with selection
773 let buffer = cx.new(|cx| {
774 let mut buffer = Buffer::local(
775 indoc! { r#"
776 Hey this is Markdown!
777
778 ```typescript
779 let foo = 999;
780 console.log(foo + 1999);
781 ```
782
783 ```ts
784 console.log("foo")
785 ```
786
787 ```typescript
788 console.log("another code block")
789 ```
790 "# },
791 cx,
792 );
793 buffer.set_language_registry(language_registry.clone());
794 buffer.set_language(Some(markdown.clone()), cx);
795 buffer
796 });
797 let snapshot = buffer.read(cx).snapshot();
798
799 let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(12, 5), cx);
800 let snippets = snippets
801 .into_iter()
802 .map(|range| snapshot.text_for_range(range).collect::<String>())
803 .collect::<Vec<_>>();
804
805 assert_eq!(
806 snippets,
807 vec![
808 indoc! { r#"
809 let foo = 999;
810 console.log(foo + 1999);
811 "#
812 },
813 "console.log(\"foo\")\n",
814 "console.log(\"another code block\")\n",
815 ]
816 );
817
818 // Python code block
819 let buffer = cx.new(|cx| {
820 let mut buffer = Buffer::local(
821 indoc! { r#"
822 Hey this is Markdown!
823
824 ```python
825 print("hello there")
826 print("hello there")
827 print("hello there")
828 ```
829 "# },
830 cx,
831 );
832 buffer.set_language_registry(language_registry.clone());
833 buffer.set_language(Some(markdown.clone()), cx);
834 buffer
835 });
836 let snapshot = buffer.read(cx).snapshot();
837
838 let (snippets, _) = runnable_ranges(&snapshot, Point::new(4, 5)..Point::new(5, 5), cx);
839 let snippets = snippets
840 .into_iter()
841 .map(|range| snapshot.text_for_range(range).collect::<String>())
842 .collect::<Vec<_>>();
843
844 assert_eq!(
845 snippets,
846 vec![indoc! { r#"
847 print("hello there")
848 print("hello there")
849 print("hello there")
850 "#
851 },]
852 );
853 }
854
855 #[gpui::test]
856 fn test_skip_blank_lines_to_next_cell(cx: &mut App) {
857 let test_language = Arc::new(Language::new(
858 LanguageConfig {
859 name: "TestLang".into(),
860 line_comments: vec!["# ".into()],
861 ..Default::default()
862 },
863 None,
864 ));
865
866 let buffer = cx.new(|cx| {
867 Buffer::local(
868 indoc! { r#"
869 print(1 + 1)
870
871 print(2 + 2)
872 "# },
873 cx,
874 )
875 .with_language(test_language.clone(), cx)
876 });
877 let snapshot = buffer.read(cx).snapshot();
878
879 // Selection on blank line should skip to next non-blank cell
880 let (snippets, _) = runnable_ranges(&snapshot, Point::new(1, 0)..Point::new(1, 0), cx);
881 let snippets = snippets
882 .into_iter()
883 .map(|range| snapshot.text_for_range(range).collect::<String>())
884 .collect::<Vec<_>>();
885 assert_eq!(snippets, vec!["print(2 + 2)"]);
886
887 // Multiple blank lines should also skip forward
888 let buffer = cx.new(|cx| {
889 Buffer::local(
890 indoc! { r#"
891 print(1 + 1)
892
893
894
895 print(2 + 2)
896 "# },
897 cx,
898 )
899 .with_language(test_language.clone(), cx)
900 });
901 let snapshot = buffer.read(cx).snapshot();
902
903 let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 0)..Point::new(2, 0), cx);
904 let snippets = snippets
905 .into_iter()
906 .map(|range| snapshot.text_for_range(range).collect::<String>())
907 .collect::<Vec<_>>();
908 assert_eq!(snippets, vec!["print(2 + 2)"]);
909
910 // Blank lines at end of file should return nothing
911 let buffer = cx.new(|cx| {
912 Buffer::local(
913 indoc! { r#"
914 print(1 + 1)
915
916 "# },
917 cx,
918 )
919 .with_language(test_language, cx)
920 });
921 let snapshot = buffer.read(cx).snapshot();
922
923 let (snippets, _) = runnable_ranges(&snapshot, Point::new(1, 0)..Point::new(1, 0), cx);
924 assert!(snippets.is_empty());
925 }
926}