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