repl_editor.rs

  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}