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(¬ification_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(¬ification_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}