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