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