Files
zed/crates/repl/src/repl_editor.rs
T
Matt Lui f3e5e3e6fe repl: Add inlay repl output display, skip empty lines on execution, and gutter execution display (#44523)
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
2026-01-06 18:08:38 -06:00

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());
}
}