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