ZIm/crates/repl/src/repl_editor.rs
Piotr Osiewicz 0f17e82154
chore: Bump Rust to 1.87 (#30739)
Closes #ISSUE

Release Notes:

- N/A
2025-05-15 22:28:52 +00:00

761 lines
22 KiB
Rust

//! REPL operations on an [`Editor`].
use std::ops::Range;
use std::sync::Arc;
use anyhow::{Context as _, Result};
use editor::Editor;
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(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);
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)
.ok_or_else(|| anyhow::anyhow!("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.clone(), 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 => {
if language_supported(&language.clone()) {
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.clone();
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>,
) -> (Vec<Range<Point>>, Option<Point>) {
if let Some(language) = buffer.language() {
if language.name() == "Markdown".into() {
return (markdown_code_blocks(buffer, range.clone()), 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);
let start_language = buffer.language_at(snippet_range.start);
let end_language = buffer.language_at(snippet_range.end);
if start_language
.zip(end_language)
.map_or(false, |(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>) -> Vec<Range<Point>> {
buffer
.injections_intersecting_range(range)
.filter(|(_, language)| language_supported(language))
.map(|(content_range, _)| {
buffer.offset_to_point(content_range.start)..buffer.offset_to_point(content_range.end)
})
.collect()
}
fn language_supported(language: &Arc<Language>) -> bool {
match language.name().as_ref() {
"TypeScript" | "Python" => true,
_ => false,
}
}
fn get_language(editor: WeakEntity<Editor>, cx: &mut App) -> Option<Arc<Language>> {
editor
.update(cx, |editor, cx| {
let selection = editor.selections.newest::<usize>(cx);
let buffer = editor.buffer().read(cx).snapshot(cx);
buffer.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));
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));
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));
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));
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));
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) {
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.clone());
language_registry.add(python.clone());
// 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));
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));
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));
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")
"#
},]
);
}
}