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