Adds various useful things to the repl inspired by ipynb and the julia vscode extension which can be best seen with this video:  https://github.com/user-attachments/assets/6589715e-3783-456c-8f4b-e2d5a1c4090d To summarize: ## Inline outputs Added small, single-line outputs displayed inline at the end of the code line instead of in a separate block. This provides a cleaner, more compact view for simple results like numbers or short strings. This occurs for execution views who only output a single mimetype/plain OR output nothing, otherwise the default behavior of creating a block will occur. It looks like this: <img width="258" height="35" alt="image" src="https://github.com/user-attachments/assets/ccdeca3f-c3b7-4387-a4de-53d8b9a25132" /> or with a Output <img width="346" height="55" alt="image" src="https://github.com/user-attachments/assets/0b4effc9-1bd7-4e8c-802f-8733cdcc77d1" /> This was inspired by julia vscode extension, but now it can be used with any replanguage! Hooray! <img width="524" height="450" alt="image" src="https://github.com/user-attachments/assets/a3551e51-f5f7-4d3e-994a-213c9d2f948c" /> It saves lots of space compared to the ugly and distracting: <img width="531" height="546" alt="image" src="https://github.com/user-attachments/assets/7cf65bae-8ec1-4279-ab19-f0d4ec4052a2" /> ## Gutters and execution numbers Added gutters + execution number to display exactly what was executed. The gutter highlighting is useful for when selecting multiple cells manually to run, but you dont remember which ones Ran at different times: <img width="257" height="58" alt="image" src="https://github.com/user-attachments/assets/6002ab16-156a-4598-9964-5a6b188e989c" /> Ran together: <img width="306" height="64" alt="image" src="https://github.com/user-attachments/assets/2690ea35-2bd3-4207-b039-6c0f98dad6e4" /> The execution number is useful in the same way that a normal jupyter notebook execution number is useful. If a gutter-region does not have a block assigned to it, when you edit the text in the gutter region, the gutter will disappear, which is useful for telling when you have modified your code, but does not delete useful experiment results in blocks: <img width="280" height="38" alt="image" src="https://github.com/user-attachments/assets/d7f29224-87e4-4c14-8d9f-41cb10ab5009" /> <img width="254" height="31" alt="image" src="https://github.com/user-attachments/assets/586c9e1d-f53c-4973-affb-c8ca05a7563b" /> <img width="264" height="29" alt="image" src="https://github.com/user-attachments/assets/f306c364-1c92-44bd-9050-ecce1b7822a0" /> ## Skip empty line This is a minor fix which is intended to make lab workflow less tedious. Currently when you execute on an empty line (which might be there for formatting purposes) nothing will occur. This PR adds the ability to, when executing from an empty line, skip ahead the range of inclusion until you reach actual code, and then execute. Before: ``` code //run execute //empty space, so you have to move your cursor down or use arrow key code //run execute code //run execute ``` After: ``` code //run execute //empty space, you can now run execute on it and it will include the next line of code //empty space code //automatically executed code //run execute ``` Currently the only piece of tested code is related to this, i still have to write tests for the gutter annotation api i added and all of the gutter + inline related code. Also still have to add more config for this stuff. @rgbkrk would appreciate a review :D Closes #22678 Release Notes: - repl: Added an inline display of execution results (as opposed to the large execution view) for simple REPL cells - repl: Improved how execution of empty lines are handled - repl: Added gutter execution display
927 lines
28 KiB
Rust
927 lines
28 KiB
Rust
//! REPL operations on an [`Editor`].
|
|
|
|
use std::ops::Range;
|
|
use std::sync::Arc;
|
|
|
|
use anyhow::{Context as _, Result};
|
|
use editor::{Editor, MultiBufferOffset};
|
|
use gpui::{App, Entity, WeakEntity, Window, prelude::*};
|
|
use language::{BufferSnapshot, Language, LanguageName, Point};
|
|
use project::{ProjectItem as _, WorktreeId};
|
|
|
|
use crate::repl_store::ReplStore;
|
|
use crate::session::SessionEvent;
|
|
use crate::{
|
|
ClearOutputs, Interrupt, JupyterSettings, KernelSpecification, Restart, Session, Shutdown,
|
|
};
|
|
|
|
pub fn assign_kernelspec(
|
|
kernel_specification: KernelSpecification,
|
|
weak_editor: WeakEntity<Editor>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> Result<()> {
|
|
let store = ReplStore::global(cx);
|
|
if !store.read(cx).is_enabled() {
|
|
return Ok(());
|
|
}
|
|
|
|
let worktree_id = crate::repl_editor::worktree_id_for_editor(weak_editor.clone(), cx)
|
|
.context("editor is not in a worktree")?;
|
|
|
|
store.update(cx, |store, cx| {
|
|
store.set_active_kernelspec(worktree_id, kernel_specification.clone(), cx);
|
|
});
|
|
|
|
let fs = store.read(cx).fs().clone();
|
|
|
|
if let Some(session) = store.read(cx).get_session(weak_editor.entity_id()).cloned() {
|
|
// Drop previous session, start new one
|
|
session.update(cx, |session, cx| {
|
|
session.clear_outputs(cx);
|
|
session.shutdown(window, cx);
|
|
cx.notify();
|
|
});
|
|
}
|
|
|
|
let session =
|
|
cx.new(|cx| Session::new(weak_editor.clone(), fs, kernel_specification, window, cx));
|
|
|
|
weak_editor
|
|
.update(cx, |_editor, cx| {
|
|
cx.notify();
|
|
|
|
cx.subscribe(&session, {
|
|
let store = store.clone();
|
|
move |_this, _session, event, cx| match event {
|
|
SessionEvent::Shutdown(shutdown_event) => {
|
|
store.update(cx, |store, _cx| {
|
|
store.remove_session(shutdown_event.entity_id());
|
|
});
|
|
}
|
|
}
|
|
})
|
|
.detach();
|
|
})
|
|
.ok();
|
|
|
|
store.update(cx, |store, _cx| {
|
|
store.insert_session(weak_editor.entity_id(), session.clone());
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn run(
|
|
editor: WeakEntity<Editor>,
|
|
move_down: bool,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> Result<()> {
|
|
let store = ReplStore::global(cx);
|
|
if !store.read(cx).is_enabled() {
|
|
return Ok(());
|
|
}
|
|
|
|
let editor = editor.upgrade().context("editor was dropped")?;
|
|
let selected_range = editor
|
|
.update(cx, |editor, cx| {
|
|
editor
|
|
.selections
|
|
.newest_adjusted(&editor.display_snapshot(cx))
|
|
})
|
|
.range();
|
|
let multibuffer = editor.read(cx).buffer().clone();
|
|
let Some(buffer) = multibuffer.read(cx).as_singleton() else {
|
|
return Ok(());
|
|
};
|
|
|
|
let Some(project_path) = buffer.read(cx).project_path(cx) else {
|
|
return Ok(());
|
|
};
|
|
|
|
let (runnable_ranges, next_cell_point) =
|
|
runnable_ranges(&buffer.read(cx).snapshot(), selected_range, cx);
|
|
|
|
for runnable_range in runnable_ranges {
|
|
let Some(language) = multibuffer.read(cx).language_at(runnable_range.start, cx) else {
|
|
continue;
|
|
};
|
|
|
|
let kernel_specification = store
|
|
.read(cx)
|
|
.active_kernelspec(project_path.worktree_id, Some(language.clone()), cx)
|
|
.with_context(|| format!("No kernel found for language: {}", language.name()))?;
|
|
|
|
let fs = store.read(cx).fs().clone();
|
|
|
|
let session = if let Some(session) = store.read(cx).get_session(editor.entity_id()).cloned()
|
|
{
|
|
session
|
|
} else {
|
|
let weak_editor = editor.downgrade();
|
|
let session =
|
|
cx.new(|cx| Session::new(weak_editor, fs, kernel_specification, window, cx));
|
|
|
|
editor.update(cx, |_editor, cx| {
|
|
cx.notify();
|
|
|
|
cx.subscribe(&session, {
|
|
let store = store.clone();
|
|
move |_this, _session, event, cx| match event {
|
|
SessionEvent::Shutdown(shutdown_event) => {
|
|
store.update(cx, |store, _cx| {
|
|
store.remove_session(shutdown_event.entity_id());
|
|
});
|
|
}
|
|
}
|
|
})
|
|
.detach();
|
|
});
|
|
|
|
store.update(cx, |store, _cx| {
|
|
store.insert_session(editor.entity_id(), session.clone());
|
|
});
|
|
|
|
session
|
|
};
|
|
|
|
let selected_text;
|
|
let anchor_range;
|
|
let next_cursor;
|
|
{
|
|
let snapshot = multibuffer.read(cx).read(cx);
|
|
selected_text = snapshot
|
|
.text_for_range(runnable_range.clone())
|
|
.collect::<String>();
|
|
anchor_range = snapshot.anchor_before(runnable_range.start)
|
|
..snapshot.anchor_after(runnable_range.end);
|
|
next_cursor = next_cell_point.map(|point| snapshot.anchor_after(point));
|
|
}
|
|
|
|
session.update(cx, |session, cx| {
|
|
session.execute(
|
|
selected_text,
|
|
anchor_range,
|
|
next_cursor,
|
|
move_down,
|
|
window,
|
|
cx,
|
|
);
|
|
});
|
|
}
|
|
|
|
anyhow::Ok(())
|
|
}
|
|
|
|
pub enum SessionSupport {
|
|
ActiveSession(Entity<Session>),
|
|
Inactive(KernelSpecification),
|
|
RequiresSetup(LanguageName),
|
|
Unsupported,
|
|
}
|
|
|
|
pub fn worktree_id_for_editor(editor: WeakEntity<Editor>, cx: &mut App) -> Option<WorktreeId> {
|
|
editor.upgrade().and_then(|editor| {
|
|
editor
|
|
.read(cx)
|
|
.buffer()
|
|
.read(cx)
|
|
.as_singleton()?
|
|
.read(cx)
|
|
.project_path(cx)
|
|
.map(|path| path.worktree_id)
|
|
})
|
|
}
|
|
|
|
pub fn session(editor: WeakEntity<Editor>, cx: &mut App) -> SessionSupport {
|
|
let store = ReplStore::global(cx);
|
|
let entity_id = editor.entity_id();
|
|
|
|
if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
|
|
return SessionSupport::ActiveSession(session);
|
|
};
|
|
|
|
let Some(language) = get_language(editor.clone(), cx) else {
|
|
return SessionSupport::Unsupported;
|
|
};
|
|
|
|
let worktree_id = worktree_id_for_editor(editor, cx);
|
|
|
|
let Some(worktree_id) = worktree_id else {
|
|
return SessionSupport::Unsupported;
|
|
};
|
|
|
|
let kernelspec = store
|
|
.read(cx)
|
|
.active_kernelspec(worktree_id, Some(language.clone()), cx);
|
|
|
|
match kernelspec {
|
|
Some(kernelspec) => SessionSupport::Inactive(kernelspec),
|
|
None => {
|
|
// For language_supported, need to check available kernels for language
|
|
if language_supported(&language, cx) {
|
|
SessionSupport::RequiresSetup(language.name())
|
|
} else {
|
|
SessionSupport::Unsupported
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn clear_outputs(editor: WeakEntity<Editor>, cx: &mut App) {
|
|
let store = ReplStore::global(cx);
|
|
let entity_id = editor.entity_id();
|
|
let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
|
|
return;
|
|
};
|
|
session.update(cx, |session, cx| {
|
|
session.clear_outputs(cx);
|
|
cx.notify();
|
|
});
|
|
}
|
|
|
|
pub fn interrupt(editor: WeakEntity<Editor>, cx: &mut App) {
|
|
let store = ReplStore::global(cx);
|
|
let entity_id = editor.entity_id();
|
|
let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
|
|
return;
|
|
};
|
|
|
|
session.update(cx, |session, cx| {
|
|
session.interrupt(cx);
|
|
cx.notify();
|
|
});
|
|
}
|
|
|
|
pub fn shutdown(editor: WeakEntity<Editor>, window: &mut Window, cx: &mut App) {
|
|
let store = ReplStore::global(cx);
|
|
let entity_id = editor.entity_id();
|
|
let Some(session) = store.read(cx).get_session(entity_id).cloned() else {
|
|
return;
|
|
};
|
|
|
|
session.update(cx, |session, cx| {
|
|
session.shutdown(window, cx);
|
|
cx.notify();
|
|
});
|
|
}
|
|
|
|
pub fn restart(editor: WeakEntity<Editor>, window: &mut Window, cx: &mut App) {
|
|
let Some(editor) = editor.upgrade() else {
|
|
return;
|
|
};
|
|
|
|
let entity_id = editor.entity_id();
|
|
|
|
let Some(session) = ReplStore::global(cx)
|
|
.read(cx)
|
|
.get_session(entity_id)
|
|
.cloned()
|
|
else {
|
|
return;
|
|
};
|
|
|
|
session.update(cx, |session, cx| {
|
|
session.restart(window, cx);
|
|
cx.notify();
|
|
});
|
|
}
|
|
|
|
pub fn setup_editor_session_actions(editor: &mut Editor, editor_handle: WeakEntity<Editor>) {
|
|
editor
|
|
.register_action({
|
|
let editor_handle = editor_handle.clone();
|
|
move |_: &ClearOutputs, _, cx| {
|
|
if !JupyterSettings::enabled(cx) {
|
|
return;
|
|
}
|
|
|
|
crate::clear_outputs(editor_handle.clone(), cx);
|
|
}
|
|
})
|
|
.detach();
|
|
|
|
editor
|
|
.register_action({
|
|
let editor_handle = editor_handle.clone();
|
|
move |_: &Interrupt, _, cx| {
|
|
if !JupyterSettings::enabled(cx) {
|
|
return;
|
|
}
|
|
|
|
crate::interrupt(editor_handle.clone(), cx);
|
|
}
|
|
})
|
|
.detach();
|
|
|
|
editor
|
|
.register_action({
|
|
let editor_handle = editor_handle.clone();
|
|
move |_: &Shutdown, window, cx| {
|
|
if !JupyterSettings::enabled(cx) {
|
|
return;
|
|
}
|
|
|
|
crate::shutdown(editor_handle.clone(), window, cx);
|
|
}
|
|
})
|
|
.detach();
|
|
|
|
editor
|
|
.register_action({
|
|
let editor_handle = editor_handle;
|
|
move |_: &Restart, window, cx| {
|
|
if !JupyterSettings::enabled(cx) {
|
|
return;
|
|
}
|
|
|
|
crate::restart(editor_handle.clone(), window, cx);
|
|
}
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
fn cell_range(buffer: &BufferSnapshot, start_row: u32, end_row: u32) -> Range<Point> {
|
|
let mut snippet_end_row = end_row;
|
|
while buffer.is_line_blank(snippet_end_row) && snippet_end_row > start_row {
|
|
snippet_end_row -= 1;
|
|
}
|
|
Point::new(start_row, 0)..Point::new(snippet_end_row, buffer.line_len(snippet_end_row))
|
|
}
|
|
|
|
// Returns the ranges of the snippets in the buffer and the next point for moving the cursor to
|
|
fn jupytext_cells(
|
|
buffer: &BufferSnapshot,
|
|
range: Range<Point>,
|
|
) -> (Vec<Range<Point>>, Option<Point>) {
|
|
let mut current_row = range.start.row;
|
|
|
|
let Some(language) = buffer.language() else {
|
|
return (Vec::new(), None);
|
|
};
|
|
|
|
let default_scope = language.default_scope();
|
|
let comment_prefixes = default_scope.line_comment_prefixes();
|
|
if comment_prefixes.is_empty() {
|
|
return (Vec::new(), None);
|
|
}
|
|
|
|
let jupytext_prefixes = comment_prefixes
|
|
.iter()
|
|
.map(|comment_prefix| format!("{comment_prefix}%%"))
|
|
.collect::<Vec<_>>();
|
|
|
|
let mut snippet_start_row = None;
|
|
loop {
|
|
if jupytext_prefixes
|
|
.iter()
|
|
.any(|prefix| buffer.contains_str_at(Point::new(current_row, 0), prefix))
|
|
{
|
|
snippet_start_row = Some(current_row);
|
|
break;
|
|
} else if current_row > 0 {
|
|
current_row -= 1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
let mut snippets = Vec::new();
|
|
if let Some(mut snippet_start_row) = snippet_start_row {
|
|
for current_row in range.start.row + 1..=buffer.max_point().row {
|
|
if jupytext_prefixes
|
|
.iter()
|
|
.any(|prefix| buffer.contains_str_at(Point::new(current_row, 0), prefix))
|
|
{
|
|
snippets.push(cell_range(buffer, snippet_start_row, current_row - 1));
|
|
|
|
if current_row <= range.end.row {
|
|
snippet_start_row = current_row;
|
|
} else {
|
|
// Return our snippets as well as the next point for moving the cursor to
|
|
return (snippets, Some(Point::new(current_row, 0)));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Go to the end of the buffer (no more jupytext cells found)
|
|
snippets.push(cell_range(
|
|
buffer,
|
|
snippet_start_row,
|
|
buffer.max_point().row,
|
|
));
|
|
}
|
|
|
|
(snippets, None)
|
|
}
|
|
|
|
fn runnable_ranges(
|
|
buffer: &BufferSnapshot,
|
|
range: Range<Point>,
|
|
cx: &mut App,
|
|
) -> (Vec<Range<Point>>, Option<Point>) {
|
|
if let Some(language) = buffer.language()
|
|
&& language.name() == "Markdown".into()
|
|
{
|
|
return (markdown_code_blocks(buffer, range, cx), None);
|
|
}
|
|
|
|
let (jupytext_snippets, next_cursor) = jupytext_cells(buffer, range.clone());
|
|
if !jupytext_snippets.is_empty() {
|
|
return (jupytext_snippets, next_cursor);
|
|
}
|
|
|
|
let snippet_range = cell_range(buffer, range.start.row, range.end.row);
|
|
|
|
// Check if the snippet range is entirely blank, if so, skip forward to find code
|
|
let is_blank =
|
|
(snippet_range.start.row..=snippet_range.end.row).all(|row| buffer.is_line_blank(row));
|
|
|
|
if is_blank {
|
|
// Search forward for the next non-blank line
|
|
let max_row = buffer.max_point().row;
|
|
let mut next_row = snippet_range.end.row + 1;
|
|
while next_row <= max_row && buffer.is_line_blank(next_row) {
|
|
next_row += 1;
|
|
}
|
|
|
|
if next_row <= max_row {
|
|
// Found a non-blank line, find the extent of this cell
|
|
let next_snippet_range = cell_range(buffer, next_row, next_row);
|
|
let start_language = buffer.language_at(next_snippet_range.start);
|
|
let end_language = buffer.language_at(next_snippet_range.end);
|
|
|
|
if start_language
|
|
.zip(end_language)
|
|
.is_some_and(|(start, end)| start == end)
|
|
{
|
|
return (vec![next_snippet_range], None);
|
|
}
|
|
}
|
|
|
|
return (Vec::new(), None);
|
|
}
|
|
|
|
let start_language = buffer.language_at(snippet_range.start);
|
|
let end_language = buffer.language_at(snippet_range.end);
|
|
|
|
if start_language
|
|
.zip(end_language)
|
|
.is_some_and(|(start, end)| start == end)
|
|
{
|
|
(vec![snippet_range], None)
|
|
} else {
|
|
(Vec::new(), None)
|
|
}
|
|
}
|
|
|
|
// We allow markdown code blocks to end in a trailing newline in order to render the output
|
|
// below the final code fence. This is different than our behavior for selections and Jupytext cells.
|
|
fn markdown_code_blocks(
|
|
buffer: &BufferSnapshot,
|
|
range: Range<Point>,
|
|
cx: &mut App,
|
|
) -> Vec<Range<Point>> {
|
|
buffer
|
|
.injections_intersecting_range(range)
|
|
.filter(|(_, language)| language_supported(language, cx))
|
|
.map(|(content_range, _)| {
|
|
buffer.offset_to_point(content_range.start)..buffer.offset_to_point(content_range.end)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn language_supported(language: &Arc<Language>, cx: &mut App) -> bool {
|
|
let store = ReplStore::global(cx);
|
|
let store_read = store.read(cx);
|
|
|
|
// Since we're just checking for general language support, we only need to look at
|
|
// the pure Jupyter kernels - these are all the globally available ones
|
|
store_read.pure_jupyter_kernel_specifications().any(|spec| {
|
|
// Convert to lowercase for case-insensitive comparison since kernels might report "python" while our language is "Python"
|
|
spec.language().as_ref().to_lowercase() == language.name().as_ref().to_lowercase()
|
|
})
|
|
}
|
|
|
|
fn get_language(editor: WeakEntity<Editor>, cx: &mut App) -> Option<Arc<Language>> {
|
|
editor
|
|
.update(cx, |editor, cx| {
|
|
let display_snapshot = editor.display_snapshot(cx);
|
|
let selection = editor
|
|
.selections
|
|
.newest::<MultiBufferOffset>(&display_snapshot);
|
|
display_snapshot
|
|
.buffer_snapshot()
|
|
.language_at(selection.head())
|
|
.cloned()
|
|
})
|
|
.ok()
|
|
.flatten()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use gpui::App;
|
|
use indoc::indoc;
|
|
use language::{Buffer, Language, LanguageConfig, LanguageRegistry};
|
|
|
|
#[gpui::test]
|
|
fn test_snippet_ranges(cx: &mut App) {
|
|
// Create a test language
|
|
let test_language = Arc::new(Language::new(
|
|
LanguageConfig {
|
|
name: "TestLang".into(),
|
|
line_comments: vec!["# ".into()],
|
|
..Default::default()
|
|
},
|
|
None,
|
|
));
|
|
|
|
let buffer = cx.new(|cx| {
|
|
Buffer::local(
|
|
indoc! { r#"
|
|
print(1 + 1)
|
|
print(2 + 2)
|
|
|
|
print(4 + 4)
|
|
|
|
|
|
"# },
|
|
cx,
|
|
)
|
|
.with_language(test_language, cx)
|
|
});
|
|
let snapshot = buffer.read(cx).snapshot();
|
|
|
|
// Single-point selection
|
|
let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 4)..Point::new(0, 4), cx);
|
|
let snippets = snippets
|
|
.into_iter()
|
|
.map(|range| snapshot.text_for_range(range).collect::<String>())
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(snippets, vec!["print(1 + 1)"]);
|
|
|
|
// Multi-line selection
|
|
let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(2, 0), cx);
|
|
let snippets = snippets
|
|
.into_iter()
|
|
.map(|range| snapshot.text_for_range(range).collect::<String>())
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(
|
|
snippets,
|
|
vec![indoc! { r#"
|
|
print(1 + 1)
|
|
print(2 + 2)"# }]
|
|
);
|
|
|
|
// Trimming multiple trailing blank lines
|
|
let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(5, 0), cx);
|
|
|
|
let snippets = snippets
|
|
.into_iter()
|
|
.map(|range| snapshot.text_for_range(range).collect::<String>())
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(
|
|
snippets,
|
|
vec![indoc! { r#"
|
|
print(1 + 1)
|
|
print(2 + 2)
|
|
|
|
print(4 + 4)"# }]
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn test_jupytext_snippet_ranges(cx: &mut App) {
|
|
// Create a test language
|
|
let test_language = Arc::new(Language::new(
|
|
LanguageConfig {
|
|
name: "TestLang".into(),
|
|
line_comments: vec!["# ".into()],
|
|
..Default::default()
|
|
},
|
|
None,
|
|
));
|
|
|
|
let buffer = cx.new(|cx| {
|
|
Buffer::local(
|
|
indoc! { r#"
|
|
# Hello!
|
|
# %% [markdown]
|
|
# This is some arithmetic
|
|
print(1 + 1)
|
|
print(2 + 2)
|
|
|
|
# %%
|
|
print(3 + 3)
|
|
print(4 + 4)
|
|
|
|
print(5 + 5)
|
|
|
|
|
|
|
|
"# },
|
|
cx,
|
|
)
|
|
.with_language(test_language, cx)
|
|
});
|
|
let snapshot = buffer.read(cx).snapshot();
|
|
|
|
// Jupytext snippet surrounding an empty selection
|
|
let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(2, 5), cx);
|
|
|
|
let snippets = snippets
|
|
.into_iter()
|
|
.map(|range| snapshot.text_for_range(range).collect::<String>())
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(
|
|
snippets,
|
|
vec![indoc! { r#"
|
|
# %% [markdown]
|
|
# This is some arithmetic
|
|
print(1 + 1)
|
|
print(2 + 2)"# }]
|
|
);
|
|
|
|
// Jupytext snippets intersecting a non-empty selection
|
|
let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(6, 2), cx);
|
|
let snippets = snippets
|
|
.into_iter()
|
|
.map(|range| snapshot.text_for_range(range).collect::<String>())
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(
|
|
snippets,
|
|
vec![
|
|
indoc! { r#"
|
|
# %% [markdown]
|
|
# This is some arithmetic
|
|
print(1 + 1)
|
|
print(2 + 2)"#
|
|
},
|
|
indoc! { r#"
|
|
# %%
|
|
print(3 + 3)
|
|
print(4 + 4)
|
|
|
|
print(5 + 5)"#
|
|
}
|
|
]
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn test_markdown_code_blocks(cx: &mut App) {
|
|
use crate::kernels::LocalKernelSpecification;
|
|
use jupyter_protocol::JupyterKernelspec;
|
|
|
|
// Initialize settings
|
|
settings::init(cx);
|
|
editor::init(cx);
|
|
|
|
// Initialize the ReplStore with a fake filesystem
|
|
let fs = Arc::new(project::RealFs::new(None, cx.background_executor().clone()));
|
|
ReplStore::init(fs, cx);
|
|
|
|
// Add mock kernel specifications for TypeScript and Python
|
|
let store = ReplStore::global(cx);
|
|
store.update(cx, |store, cx| {
|
|
let typescript_spec = KernelSpecification::Jupyter(LocalKernelSpecification {
|
|
name: "typescript".into(),
|
|
kernelspec: JupyterKernelspec {
|
|
argv: vec![],
|
|
display_name: "TypeScript".into(),
|
|
language: "typescript".into(),
|
|
interrupt_mode: None,
|
|
metadata: None,
|
|
env: None,
|
|
},
|
|
path: std::path::PathBuf::new(),
|
|
});
|
|
|
|
let python_spec = KernelSpecification::Jupyter(LocalKernelSpecification {
|
|
name: "python".into(),
|
|
kernelspec: JupyterKernelspec {
|
|
argv: vec![],
|
|
display_name: "Python".into(),
|
|
language: "python".into(),
|
|
interrupt_mode: None,
|
|
metadata: None,
|
|
env: None,
|
|
},
|
|
path: std::path::PathBuf::new(),
|
|
});
|
|
|
|
store.set_kernel_specs_for_testing(vec![typescript_spec, python_spec], cx);
|
|
});
|
|
|
|
let markdown = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
|
|
let typescript = languages::language(
|
|
"typescript",
|
|
tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
|
|
);
|
|
let python = languages::language("python", tree_sitter_python::LANGUAGE.into());
|
|
let language_registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
|
|
language_registry.add(markdown.clone());
|
|
language_registry.add(typescript);
|
|
language_registry.add(python);
|
|
|
|
// Two code blocks intersecting with selection
|
|
let buffer = cx.new(|cx| {
|
|
let mut buffer = Buffer::local(
|
|
indoc! { r#"
|
|
Hey this is Markdown!
|
|
|
|
```typescript
|
|
let foo = 999;
|
|
console.log(foo + 1999);
|
|
```
|
|
|
|
```typescript
|
|
console.log("foo")
|
|
```
|
|
"#
|
|
},
|
|
cx,
|
|
);
|
|
buffer.set_language_registry(language_registry.clone());
|
|
buffer.set_language(Some(markdown.clone()), cx);
|
|
buffer
|
|
});
|
|
let snapshot = buffer.read(cx).snapshot();
|
|
|
|
let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(8, 5), cx);
|
|
let snippets = snippets
|
|
.into_iter()
|
|
.map(|range| snapshot.text_for_range(range).collect::<String>())
|
|
.collect::<Vec<_>>();
|
|
|
|
assert_eq!(
|
|
snippets,
|
|
vec![
|
|
indoc! { r#"
|
|
let foo = 999;
|
|
console.log(foo + 1999);
|
|
"#
|
|
},
|
|
"console.log(\"foo\")\n"
|
|
]
|
|
);
|
|
|
|
// Three code blocks intersecting with selection
|
|
let buffer = cx.new(|cx| {
|
|
let mut buffer = Buffer::local(
|
|
indoc! { r#"
|
|
Hey this is Markdown!
|
|
|
|
```typescript
|
|
let foo = 999;
|
|
console.log(foo + 1999);
|
|
```
|
|
|
|
```ts
|
|
console.log("foo")
|
|
```
|
|
|
|
```typescript
|
|
console.log("another code block")
|
|
```
|
|
"# },
|
|
cx,
|
|
);
|
|
buffer.set_language_registry(language_registry.clone());
|
|
buffer.set_language(Some(markdown.clone()), cx);
|
|
buffer
|
|
});
|
|
let snapshot = buffer.read(cx).snapshot();
|
|
|
|
let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(12, 5), cx);
|
|
let snippets = snippets
|
|
.into_iter()
|
|
.map(|range| snapshot.text_for_range(range).collect::<String>())
|
|
.collect::<Vec<_>>();
|
|
|
|
assert_eq!(
|
|
snippets,
|
|
vec![
|
|
indoc! { r#"
|
|
let foo = 999;
|
|
console.log(foo + 1999);
|
|
"#
|
|
},
|
|
"console.log(\"foo\")\n",
|
|
"console.log(\"another code block\")\n",
|
|
]
|
|
);
|
|
|
|
// Python code block
|
|
let buffer = cx.new(|cx| {
|
|
let mut buffer = Buffer::local(
|
|
indoc! { r#"
|
|
Hey this is Markdown!
|
|
|
|
```python
|
|
print("hello there")
|
|
print("hello there")
|
|
print("hello there")
|
|
```
|
|
"# },
|
|
cx,
|
|
);
|
|
buffer.set_language_registry(language_registry.clone());
|
|
buffer.set_language(Some(markdown.clone()), cx);
|
|
buffer
|
|
});
|
|
let snapshot = buffer.read(cx).snapshot();
|
|
|
|
let (snippets, _) = runnable_ranges(&snapshot, Point::new(4, 5)..Point::new(5, 5), cx);
|
|
let snippets = snippets
|
|
.into_iter()
|
|
.map(|range| snapshot.text_for_range(range).collect::<String>())
|
|
.collect::<Vec<_>>();
|
|
|
|
assert_eq!(
|
|
snippets,
|
|
vec![indoc! { r#"
|
|
print("hello there")
|
|
print("hello there")
|
|
print("hello there")
|
|
"#
|
|
},]
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn test_skip_blank_lines_to_next_cell(cx: &mut App) {
|
|
let test_language = Arc::new(Language::new(
|
|
LanguageConfig {
|
|
name: "TestLang".into(),
|
|
line_comments: vec!["# ".into()],
|
|
..Default::default()
|
|
},
|
|
None,
|
|
));
|
|
|
|
let buffer = cx.new(|cx| {
|
|
Buffer::local(
|
|
indoc! { r#"
|
|
print(1 + 1)
|
|
|
|
print(2 + 2)
|
|
"# },
|
|
cx,
|
|
)
|
|
.with_language(test_language.clone(), cx)
|
|
});
|
|
let snapshot = buffer.read(cx).snapshot();
|
|
|
|
// Selection on blank line should skip to next non-blank cell
|
|
let (snippets, _) = runnable_ranges(&snapshot, Point::new(1, 0)..Point::new(1, 0), cx);
|
|
let snippets = snippets
|
|
.into_iter()
|
|
.map(|range| snapshot.text_for_range(range).collect::<String>())
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(snippets, vec!["print(2 + 2)"]);
|
|
|
|
// Multiple blank lines should also skip forward
|
|
let buffer = cx.new(|cx| {
|
|
Buffer::local(
|
|
indoc! { r#"
|
|
print(1 + 1)
|
|
|
|
|
|
|
|
print(2 + 2)
|
|
"# },
|
|
cx,
|
|
)
|
|
.with_language(test_language.clone(), cx)
|
|
});
|
|
let snapshot = buffer.read(cx).snapshot();
|
|
|
|
let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 0)..Point::new(2, 0), cx);
|
|
let snippets = snippets
|
|
.into_iter()
|
|
.map(|range| snapshot.text_for_range(range).collect::<String>())
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(snippets, vec!["print(2 + 2)"]);
|
|
|
|
// Blank lines at end of file should return nothing
|
|
let buffer = cx.new(|cx| {
|
|
Buffer::local(
|
|
indoc! { r#"
|
|
print(1 + 1)
|
|
|
|
"# },
|
|
cx,
|
|
)
|
|
.with_language(test_language, cx)
|
|
});
|
|
let snapshot = buffer.read(cx).snapshot();
|
|
|
|
let (snippets, _) = runnable_ranges(&snapshot, Point::new(1, 0)..Point::new(1, 0), cx);
|
|
assert!(snippets.is_empty());
|
|
}
|
|
}
|