Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:49:55.089Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

repl_editor.rs

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