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