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