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