Add kernel detection for language support of runnable markdown cells (#29664)

Closes #27757

Release Notes:

- List of runnable markdown cells is now based on detected jupyter
kernels instead of hardcoded to Python and TypeScript
This commit is contained in:
Jon Gretar Borgthorsson 2025-05-22 06:23:05 +03:00 committed by GitHub
parent dce22a965e
commit 66667d1eef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 82 additions and 18 deletions

View file

@ -97,7 +97,7 @@ pub fn run(
};
let (runnable_ranges, next_cell_point) =
runnable_ranges(&buffer.read(cx).snapshot(), selected_range);
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 {
@ -215,7 +215,8 @@ pub fn session(editor: WeakEntity<Editor>, cx: &mut App) -> SessionSupport {
match kernelspec {
Some(kernelspec) => SessionSupport::Inactive(kernelspec),
None => {
if language_supported(&language.clone()) {
// For language_supported, need to check available kernels for language
if language_supported(&language.clone(), cx) {
SessionSupport::RequiresSetup(language.name())
} else {
SessionSupport::Unsupported
@ -414,10 +415,11 @@ fn jupytext_cells(
fn runnable_ranges(
buffer: &BufferSnapshot,
range: Range<Point>,
cx: &mut App,
) -> (Vec<Range<Point>>, Option<Point>) {
if let Some(language) = buffer.language() {
if language.name() == "Markdown".into() {
return (markdown_code_blocks(buffer, range.clone()), None);
return (markdown_code_blocks(buffer, range.clone(), cx), None);
}
}
@ -442,21 +444,30 @@ fn runnable_ranges(
// 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>> {
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))
.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>) -> bool {
match language.name().as_ref() {
"TypeScript" | "Python" => true,
_ => false,
}
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>> {
@ -506,7 +517,7 @@ mod tests {
let snapshot = buffer.read(cx).snapshot();
// Single-point selection
let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 4)..Point::new(0, 4));
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>())
@ -514,7 +525,7 @@ mod tests {
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, _) = 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>())
@ -527,7 +538,7 @@ mod tests {
);
// Trimming multiple trailing blank lines
let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(5, 0));
let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(5, 0), cx);
let snippets = snippets
.into_iter()
@ -580,7 +591,7 @@ mod tests {
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, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(2, 5), cx);
let snippets = snippets
.into_iter()
@ -596,7 +607,7 @@ mod tests {
);
// Jupytext snippets intersecting a non-empty selection
let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(6, 2));
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>())
@ -623,6 +634,49 @@ mod tests {
#[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",
@ -658,7 +712,7 @@ mod tests {
});
let snapshot = buffer.read(cx).snapshot();
let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(8, 5));
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>())
@ -703,7 +757,7 @@ mod tests {
});
let snapshot = buffer.read(cx).snapshot();
let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(12, 5));
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>())
@ -742,7 +796,7 @@ mod tests {
});
let snapshot = buffer.read(cx).snapshot();
let (snippets, _) = runnable_ranges(&snapshot, Point::new(4, 5)..Point::new(5, 5));
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>())

View file

@ -279,4 +279,14 @@ impl ReplStore {
pub fn remove_session(&mut self, entity_id: EntityId) {
self.sessions.remove(&entity_id);
}
#[cfg(test)]
pub fn set_kernel_specs_for_testing(
&mut self,
specs: Vec<KernelSpecification>,
cx: &mut Context<Self>,
) {
self.kernel_specifications = specs;
cx.notify();
}
}