Files
oak-gpui/crates/repl/src/repl_editor.rs
T
Lukas Wirth 9af6e82e65 language: Only block the foreground on buffer reparsing when necessary (#43888)
Gist is we only need to block the foreground thread for reparsing if
immediate language changes are useful to the user. That is usually only
the case when they edit the buffer

Release Notes:

- Improved performance of large project searches and project diffs

Co-authored by: David Kleingeld <david@zed.dev>
2025-12-01 14:57:15 +01:00

825 lines
25 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);
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_immediate(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_immediate(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_immediate(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_immediate(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_immediate(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")
"#
},]
);
}
}