repl: Add Jupyter kernel completion and inspection support

Integrate Jupyter's `complete_request`/`complete_reply` and
`inspect_request`/`inspect_reply` protocols into Zed's editor completion
system. When a REPL session is active, a composite completion provider
wraps the existing LSP provider to fetch completions from both the
language server and the Jupyter kernel in parallel. Selecting a kernel
completion triggers an inspect request to populate documentation.

Context sent to the kernel is informed by how mainstream kernels
(IJulia, ipykernel/xeus-python, IRkernel) actually implement these
handlers:

- For completion, send the runnable chunk surrounding the cursor (the
  same unit `run` would execute) rather than the whole buffer. Kernels
  expect cell-sized input, and sending unrelated code can mislead jedi
  or be wasted on REPL-style completers (IJulia, IRkernel) that only
  look at the local syntactic context. Reusing `runnable_ranges` keeps
  this aligned with the user's mental model and lets future improvements
  to chunk detection (e.g. Tree-sitter top-level node selection) carry
  over automatically.
- For inspect, send only the candidate symbol with the cursor at its
  end. Every surveyed kernel resolves documentation by extracting the
  token at the cursor (`get_token` in IJulia, `token_at_cursor` in
  ipykernel, `.guessTokenFromLine` in IRkernel, regex in xeus-cling),
  and ipykernel's `do_inspect` does a live-namespace lookup rather than
  jedi-based scope analysis, so surrounding context yields no benefit.
  IHaskell additionally ignores `cursor_pos` and uses the last
  whitespace-separated chunk, so injecting any code after the symbol
  would actively break inspection there.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Shuhei Kadowaki 2026-04-15 12:00:05 +09:00
parent 700b0b5de6
commit 94bf3a5b1f
4 changed files with 653 additions and 11 deletions

View file

@ -0,0 +1,392 @@
use std::cell::RefCell;
use std::ops::Range;
use std::rc::Rc;
use std::time::Duration;
use anyhow::Result;
use editor::{CompletionContext, CompletionProvider, Editor};
use gpui::{Context, Entity, Task, WeakEntity, Window};
use language::{self, CodeLabel, ToOffset, ToPoint};
use project::{
Completion, CompletionDisplayOptions, CompletionResponse, CompletionSource, Project,
lsp_store::CompletionDocumentation,
};
use runtimelib::media::MediaType;
use crate::repl_editor::{CompletionChunk, completion_chunk};
use crate::session::Session;
pub(crate) struct JupyterCompletionProvider {
project: Entity<Project>,
session: WeakEntity<Session>,
}
impl JupyterCompletionProvider {
pub fn new(project: Entity<Project>, session: WeakEntity<Session>) -> Self {
Self { project, session }
}
}
impl CompletionProvider for JupyterCompletionProvider {
fn completions(
&self,
buffer: &Entity<language::Buffer>,
buffer_position: language::Anchor,
trigger: CompletionContext,
window: &mut Window,
cx: &mut Context<Editor>,
) -> Task<Result<Vec<CompletionResponse>>> {
let lsp_completions =
self.project
.completions(buffer, buffer_position, trigger, window, cx);
let snapshot = buffer.read(cx).snapshot();
let cursor_point = buffer_position.to_point(&snapshot);
let chunk = completion_chunk(&snapshot, cursor_point, cx);
let jupyter_receiver = chunk.as_ref().and_then(|chunk| {
let session = self.session.upgrade()?;
session.update(cx, |session, cx| {
session.request_completions(chunk.code.clone(), chunk.cursor_pos, cx)
})
});
let timer = cx.background_executor().timer(Duration::from_secs(5));
cx.spawn(async move |_, _cx| {
let mut responses = lsp_completions.await.unwrap_or_default();
if let (Some(rx), Some(chunk)) = (jupyter_receiver, chunk) {
let reply = match futures::future::select(std::pin::pin!(rx), std::pin::pin!(timer))
.await
{
futures::future::Either::Left((Ok(reply), _)) => Some(reply),
_ => None,
};
if let Some(reply) = reply
&& reply.status == runtimelib::ReplyStatus::Ok
{
if let Some(response) = jupyter_reply_to_completion_response(
&reply,
&chunk,
&snapshot,
buffer_position,
) {
responses.push(response);
}
}
}
Ok(responses)
})
}
fn resolve_completions(
&self,
buffer: Entity<language::Buffer>,
completion_indices: Vec<usize>,
completions: Rc<RefCell<Box<[Completion]>>>,
cx: &mut Context<Editor>,
) -> Task<Result<bool>> {
let mut lsp_indices = Vec::new();
let mut jupyter_indices = Vec::new();
for &index in &completion_indices {
match &completions.borrow()[index].source {
CompletionSource::Custom => jupyter_indices.push(index),
_ => lsp_indices.push(index),
}
}
let lsp_task = if lsp_indices.is_empty() {
Task::ready(Ok(false))
} else {
self.project
.resolve_completions(buffer, lsp_indices, completions.clone(), cx)
};
let jupyter_receivers: Vec<_> = jupyter_indices
.iter()
.filter_map(|&index| {
let session = self.session.upgrade()?;
let new_text = completions.borrow()[index].new_text.clone();
let cursor_pos = new_text.chars().count();
let rx = session.update(cx, |session, cx| {
session.request_inspect(new_text, cursor_pos, cx)
})?;
Some((index, rx))
})
.collect();
let executor = cx.background_executor().clone();
cx.spawn(async move |_, _cx| {
let lsp_resolved = lsp_task.await.unwrap_or(false);
let mut jupyter_resolved = false;
for (index, rx) in jupyter_receivers {
let timeout = executor.timer(Duration::from_secs(5));
let reply = match futures::future::select(
std::pin::pin!(rx),
std::pin::pin!(timeout),
)
.await
{
futures::future::Either::Left((Ok(reply), _)) => Some(reply),
_ => None,
};
if let Some(reply) = reply
&& reply.found
&& reply.status == runtimelib::ReplyStatus::Ok
{
if let Some(documentation) = inspect_reply_to_documentation(&reply) {
completions.borrow_mut()[index].documentation = Some(documentation);
jupyter_resolved = true;
}
}
}
Ok(lsp_resolved || jupyter_resolved)
})
}
fn apply_additional_edits_for_completion(
&self,
buffer: Entity<language::Buffer>,
completions: Rc<RefCell<Box<[Completion]>>>,
completion_index: usize,
push_to_history: bool,
all_commit_ranges: Vec<Range<language::Anchor>>,
cx: &mut Context<Editor>,
) -> Task<Result<Option<language::Transaction>>> {
self.project.apply_additional_edits_for_completion(
buffer,
completions,
completion_index,
push_to_history,
all_commit_ranges,
cx,
)
}
fn is_completion_trigger(
&self,
buffer: &Entity<language::Buffer>,
position: language::Anchor,
text: &str,
trigger_in_words: bool,
cx: &mut Context<Editor>,
) -> bool {
self.project
.is_completion_trigger(buffer, position, text, trigger_in_words, cx)
}
fn sort_completions(&self) -> bool {
true
}
fn filter_completions(&self) -> bool {
true
}
fn show_snippets(&self) -> bool {
true
}
}
fn char_offset_to_byte_offset(text: &str, char_offset: usize) -> usize {
text.char_indices()
.nth(char_offset)
.map(|(byte_idx, _)| byte_idx)
.unwrap_or(text.len())
}
fn jupyter_reply_to_completion_response(
reply: &runtimelib::CompleteReply,
chunk: &CompletionChunk,
snapshot: &language::BufferSnapshot,
buffer_position: language::Anchor,
) -> Option<CompletionResponse> {
if reply.matches.is_empty() {
return None;
}
let start_byte = chunk.start_byte + char_offset_to_byte_offset(&chunk.code, reply.cursor_start);
let end_byte = chunk.start_byte + char_offset_to_byte_offset(&chunk.code, reply.cursor_end);
let replace_start = snapshot.anchor_after(start_byte);
let replace_end = snapshot.anchor_before(end_byte);
let replace_range = replace_start..replace_end;
let byte_offset = buffer_position.to_offset(snapshot);
let match_start_byte = start_byte.min(byte_offset);
let match_start = snapshot.anchor_after(match_start_byte);
let completions = reply
.matches
.iter()
.map(|match_text| Completion {
replace_range: replace_range.clone(),
new_text: match_text.clone(),
label: CodeLabel::filtered(
format!("{match_text} Jupyter"),
match_text.len(),
None,
vec![],
),
documentation: None,
source: CompletionSource::Custom,
icon_path: None,
match_start: Some(match_start),
snippet_deduplication_key: None,
insert_text_mode: None,
confirm: None,
group: None,
})
.collect();
Some(CompletionResponse {
completions,
display_options: CompletionDisplayOptions::default(),
is_incomplete: false,
})
}
fn inspect_reply_to_documentation(
reply: &runtimelib::InspectReply,
) -> Option<CompletionDocumentation> {
for media_type in &reply.data.content {
if let MediaType::Markdown(text) = media_type {
let trimmed = text.trim();
if !trimmed.is_empty() {
return Some(CompletionDocumentation::MultiLineMarkdown(
trimmed.to_string().into(),
));
}
}
}
for media_type in &reply.data.content {
if let MediaType::Plain(text) = media_type {
let trimmed = text.trim();
if !trimmed.is_empty() {
return Some(CompletionDocumentation::MultiLinePlainText(
trimmed.to_string().into(),
));
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use gpui::{App, AppContext as _};
use language::Buffer;
fn make_chunk(code: &str, start_byte: usize) -> CompletionChunk {
CompletionChunk {
code: code.into(),
cursor_pos: code.chars().count(),
start_byte,
}
}
#[gpui::test]
fn test_jupyter_reply_empty_matches(cx: &mut App) {
let buffer = cx.new(|cx| Buffer::local("foo bar", cx));
let snapshot = buffer.read(cx).snapshot();
let buffer_position = snapshot.anchor_after(4);
let chunk = make_chunk("foo bar", 0);
let reply = runtimelib::CompleteReply::default();
assert!(
jupyter_reply_to_completion_response(&reply, &chunk, &snapshot, buffer_position)
.is_none()
);
}
#[gpui::test]
fn test_jupyter_reply_translates_offsets(cx: &mut App) {
// Buffer matches the chunk one-to-one; chunk starts at byte 0.
let text = "import numpy as np\nnp.ar";
let buffer = cx.new(|cx| Buffer::local(text, cx));
let snapshot = buffer.read(cx).snapshot();
let buffer_position = snapshot.anchor_after(text.len());
let chunk = make_chunk(text, 0);
let reply = runtimelib::CompleteReply {
matches: vec!["np.array".into(), "np.arange".into()],
cursor_start: 19, // start of "np.ar" (after "import numpy as np\n")
cursor_end: 24, // end of "np.ar"
..Default::default()
};
let response =
jupyter_reply_to_completion_response(&reply, &chunk, &snapshot, buffer_position)
.unwrap();
assert_eq!(response.completions.len(), 2);
let completion = &response.completions[0];
assert_eq!(completion.replace_range.start.to_offset(&snapshot), 19);
assert_eq!(completion.replace_range.end.to_offset(&snapshot), 24);
assert_eq!(completion.new_text, "np.array");
// In the common case, `match_start` equals the replace-range start.
assert_eq!(completion.match_start.unwrap().to_offset(&snapshot), 19);
}
#[gpui::test]
fn test_jupyter_reply_chunk_offset_into_buffer(cx: &mut App) {
// The chunk lives at byte 12 of the buffer; replace_range must be
// shifted by chunk.start_byte to land in the right buffer span.
let prelude = "x = 1\ny = 2\n"; // 12 bytes
let chunk_text = "z.fo";
let mut text = String::from(prelude);
text.push_str(chunk_text);
let buffer = cx.new({
let text = text.clone();
|cx| Buffer::local(text, cx)
});
let snapshot = buffer.read(cx).snapshot();
let buffer_position = snapshot.anchor_after(text.len());
let chunk = make_chunk(chunk_text, 12);
let reply = runtimelib::CompleteReply {
matches: vec!["z.foo".into()],
cursor_start: 0,
cursor_end: 4,
..Default::default()
};
let response =
jupyter_reply_to_completion_response(&reply, &chunk, &snapshot, buffer_position)
.unwrap();
let completion = &response.completions[0];
assert_eq!(completion.replace_range.start.to_offset(&snapshot), 12);
assert_eq!(completion.replace_range.end.to_offset(&snapshot), 16);
}
#[gpui::test]
fn test_jupyter_reply_multibyte_offsets(cx: &mut App) {
// "π" is 2 bytes / 1 char, so char-offset and byte-offset diverge.
let text = "x = π.";
let buffer = cx.new(|cx| Buffer::local(text, cx));
let snapshot = buffer.read(cx).snapshot();
let buffer_position = snapshot.anchor_after(text.len());
let chunk = make_chunk(text, 0);
let reply = runtimelib::CompleteReply {
matches: vec!["π.bit_length".into()],
cursor_start: 4, // char offset of "π"
cursor_end: 6, // char offset after "."
..Default::default()
};
let response =
jupyter_reply_to_completion_response(&reply, &chunk, &snapshot, buffer_position)
.unwrap();
let completion = &response.completions[0];
// char offset 4 → byte 4 ("x = " = 4 bytes)
// char offset 6 → byte 7 ("x = π." = 4 + 2 + 1 = 7 bytes)
assert_eq!(completion.replace_range.start.to_offset(&snapshot), 4);
assert_eq!(completion.replace_range.end.to_offset(&snapshot), 7);
}
}

View file

@ -1,4 +1,5 @@
pub mod components; pub mod components;
mod jupyter_completion_provider;
mod jupyter_settings; mod jupyter_settings;
pub mod kernels; pub mod kernels;
pub mod notebook; pub mod notebook;

View file

@ -1,15 +1,17 @@
//! REPL operations on an [`Editor`]. //! REPL operations on an [`Editor`].
use std::ops::Range; use std::ops::Range;
use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use editor::{Editor, MultiBufferOffset}; use editor::{Editor, MultiBufferOffset};
use gpui::{App, Entity, WeakEntity, Window, prelude::*}; use gpui::{App, Entity, WeakEntity, Window, prelude::*};
use language::{BufferSnapshot, Language, LanguageName, Point}; use language::{BufferSnapshot, Language, LanguageName, Point, ToOffset as _};
use project::{ProjectItem as _, WorktreeId}; use project::{ProjectItem as _, WorktreeId};
use workspace::{Workspace, notifications::NotificationId}; use workspace::{Workspace, notifications::NotificationId};
use crate::jupyter_completion_provider::JupyterCompletionProvider;
use crate::kernels::PythonEnvKernelSpecification; use crate::kernels::PythonEnvKernelSpecification;
use crate::repl_store::ReplStore; use crate::repl_store::ReplStore;
use crate::session::SessionEvent; use crate::session::SessionEvent;
@ -51,15 +53,24 @@ pub fn assign_kernelspec(
cx.new(|cx| Session::new(weak_editor.clone(), fs, kernel_specification, window, cx)); cx.new(|cx| Session::new(weak_editor.clone(), fs, kernel_specification, window, cx));
weak_editor weak_editor
.update(cx, |_editor, cx| { .update(cx, |editor, cx| {
if let Some(project) = editor.project().cloned() {
let provider =
Rc::new(JupyterCompletionProvider::new(project, session.downgrade()));
editor.set_completion_provider(Some(provider));
}
cx.notify(); cx.notify();
cx.subscribe(&session, { cx.subscribe(&session, {
let store = store.clone(); let store = store.clone();
move |_this, _session, event, cx| match event { move |this, _session, event, cx| match event {
SessionEvent::Shutdown(shutdown_event) => { SessionEvent::Shutdown(shutdown_editor) => {
if let Some(project) = this.project().cloned() {
this.set_completion_provider(Some(Rc::new(project)));
}
store.update(cx, |store, _cx| { store.update(cx, |store, _cx| {
store.remove_session(shutdown_event.entity_id()); store.remove_session(shutdown_editor.entity_id());
}); });
} }
} }
@ -253,15 +264,24 @@ pub fn run(
let session = let session =
cx.new(|cx| Session::new(weak_editor, fs, kernel_specification, window, cx)); cx.new(|cx| Session::new(weak_editor, fs, kernel_specification, window, cx));
editor.update(cx, |_editor, cx| { editor.update(cx, |editor, cx| {
if let Some(project) = editor.project().cloned() {
let provider =
Rc::new(JupyterCompletionProvider::new(project, session.downgrade()));
editor.set_completion_provider(Some(provider));
}
cx.notify(); cx.notify();
cx.subscribe(&session, { cx.subscribe(&session, {
let store = store.clone(); let store = store.clone();
move |_this, _session, event, cx| match event { move |this, _session, event, cx| match event {
SessionEvent::Shutdown(shutdown_event) => { SessionEvent::Shutdown(shutdown_editor) => {
if let Some(project) = this.project().cloned() {
this.set_completion_provider(Some(Rc::new(project)));
}
store.update(cx, |store, _cx| { store.update(cx, |store, _cx| {
store.remove_session(shutdown_event.entity_id()); store.remove_session(shutdown_editor.entity_id());
}); });
} }
} }
@ -503,6 +523,42 @@ pub fn setup_editor_session_actions(editor: &mut Editor, editor_handle: WeakEnti
.detach(); .detach();
} }
pub(crate) struct CompletionChunk {
pub code: String,
/// Char offset of the cursor within `code`.
pub cursor_pos: usize,
/// Byte offset within the buffer where `code` starts.
pub start_byte: usize,
}
/// Returns the runnable chunk surrounding `cursor` along with the cursor's char
/// offset within that chunk. The chunk is the same unit that would be sent to
/// the kernel if the user invoked `run` at this position, so completion and
/// inspection requests share the kernel's mental model of "the current cell".
pub(crate) fn completion_chunk(
buffer: &BufferSnapshot,
cursor: Point,
cx: &mut App,
) -> Option<CompletionChunk> {
let (ranges, _) = runnable_ranges(buffer, cursor..cursor, cx);
// `runnable_ranges` may skip forward to the next cell when the cursor is on
// a blank line; that range is not useful as completion context, so only
// accept a chunk that actually contains the cursor.
let range = ranges
.into_iter()
.find(|r| r.start <= cursor && cursor <= r.end)?;
let start_byte = range.start.to_offset(buffer);
let cursor_byte = cursor.to_offset(buffer);
let code: String = buffer.text_for_range(range).collect();
let cursor_byte_in_chunk = cursor_byte.saturating_sub(start_byte).min(code.len());
let cursor_pos = code[..cursor_byte_in_chunk].chars().count();
Some(CompletionChunk {
code,
cursor_pos,
start_byte,
})
}
fn cell_range(buffer: &BufferSnapshot, start_row: u32, end_row: u32) -> Range<Point> { fn cell_range(buffer: &BufferSnapshot, start_row: u32, end_row: u32) -> Range<Point> {
let mut snippet_end_row = end_row; let mut snippet_end_row = end_row;
while buffer.is_line_blank(snippet_end_row) && snippet_end_row > start_row { while buffer.is_line_blank(snippet_end_row) && snippet_end_row > start_row {
@ -1081,4 +1137,131 @@ mod tests {
let (snippets, _) = runnable_ranges(&snapshot, Point::new(1, 0)..Point::new(1, 0), cx); let (snippets, _) = runnable_ranges(&snapshot, Point::new(1, 0)..Point::new(1, 0), cx);
assert!(snippets.is_empty()); assert!(snippets.is_empty());
} }
#[gpui::test]
fn test_completion_chunk_basic(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, cx)
});
let snapshot = buffer.read(cx).snapshot();
let chunk = completion_chunk(&snapshot, Point::new(0, 6), cx).unwrap();
assert_eq!(chunk.code, "print(1 + 1)");
assert_eq!(chunk.cursor_pos, 6);
assert_eq!(chunk.start_byte, 0);
// Second line: chunk starts after "print(1 + 1)\n" (13 bytes).
let chunk = completion_chunk(&snapshot, Point::new(1, 4), cx).unwrap();
assert_eq!(chunk.code, "print(2 + 2)");
assert_eq!(chunk.cursor_pos, 4);
assert_eq!(chunk.start_byte, 13);
}
#[gpui::test]
fn test_completion_chunk_jupytext(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#"
# %%
x = 1
y = 2
# %%
z = 3
"# },
cx,
)
.with_language(test_language, cx)
});
let snapshot = buffer.read(cx).snapshot();
// Cursor inside the first cell.
let chunk = completion_chunk(&snapshot, Point::new(2, 4), cx).unwrap();
assert_eq!(chunk.code, "# %%\nx = 1\ny = 2");
assert_eq!(chunk.cursor_pos, 15);
assert_eq!(chunk.start_byte, 0);
// Cursor inside the second cell; chunk starts at row 3 (byte 17).
let chunk = completion_chunk(&snapshot, Point::new(4, 2), cx).unwrap();
assert_eq!(chunk.code, "# %%\nz = 3");
assert_eq!(chunk.cursor_pos, 7);
assert_eq!(chunk.start_byte, 17);
}
#[gpui::test]
fn test_completion_chunk_blank_line_returns_none(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, cx)
});
let snapshot = buffer.read(cx).snapshot();
// `runnable_ranges` skips forward to the next non-blank cell, but the
// returned range does not contain the cursor on row 1, so
// `completion_chunk` rejects it.
assert!(completion_chunk(&snapshot, Point::new(1, 0), cx).is_none());
}
#[gpui::test]
fn test_completion_chunk_multibyte(cx: &mut App) {
let test_language = Arc::new(Language::new(
LanguageConfig {
name: "TestLang".into(),
line_comments: vec!["# ".into()],
..Default::default()
},
None,
));
// "π" is 2 bytes, so byte and char counts diverge inside the chunk.
let buffer = cx.new(|cx| Buffer::local("x = π * 2", cx).with_language(test_language, cx));
let snapshot = buffer.read(cx).snapshot();
// Byte column 6 = right after "π" (which occupies bytes 4..6).
let chunk = completion_chunk(&snapshot, Point::new(0, 6), cx).unwrap();
assert_eq!(chunk.code, "x = π * 2");
// chunk.code[..6] = "x = π" → 5 chars
assert_eq!(chunk.cursor_pos, 5);
assert_eq!(chunk.start_byte, 0);
}
} }

View file

@ -35,8 +35,8 @@ use gpui::{
use language::Point; use language::Point;
use project::Fs; use project::Fs;
use runtimelib::{ use runtimelib::{
ExecuteRequest, ExecutionState, InputReply, InterruptRequest, JupyterMessage, CompleteRequest, ExecuteRequest, ExecutionState, InputReply, InspectRequest, InterruptRequest,
JupyterMessageContent, KernelInfoRequest, ReplyStatus, ShutdownRequest, JupyterMessage, JupyterMessageContent, KernelInfoRequest, ReplyStatus, ShutdownRequest,
}; };
use settings::Settings as _; use settings::Settings as _;
use std::{env::temp_dir, ops::Range, sync::Arc, time::Duration}; use std::{env::temp_dir, ops::Range, sync::Arc, time::Duration};
@ -53,6 +53,10 @@ pub struct Session {
blocks: HashMap<String, EditorBlock>, blocks: HashMap<String, EditorBlock>,
result_inlays: HashMap<String, (InlayId, Range<Anchor>, usize)>, result_inlays: HashMap<String, (InlayId, Range<Anchor>, usize)>,
next_inlay_id: usize, next_inlay_id: usize,
pending_completion_replies:
HashMap<String, futures::channel::oneshot::Sender<runtimelib::CompleteReply>>,
pending_inspect_replies:
HashMap<String, futures::channel::oneshot::Sender<runtimelib::InspectReply>>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
@ -257,6 +261,8 @@ impl Session {
blocks: HashMap::default(), blocks: HashMap::default(),
result_inlays: HashMap::default(), result_inlays: HashMap::default(),
next_inlay_id: 0, next_inlay_id: 0,
pending_completion_replies: HashMap::default(),
pending_inspect_replies: HashMap::default(),
kernel_specification, kernel_specification,
_subscriptions: vec![subscription], _subscriptions: vec![subscription],
}; };
@ -486,6 +492,54 @@ impl Session {
anyhow::Ok(()) anyhow::Ok(())
} }
pub fn request_completions(
&mut self,
code: String,
cursor_pos: usize,
cx: &mut Context<Self>,
) -> Option<futures::channel::oneshot::Receiver<runtimelib::CompleteReply>> {
if !matches!(&self.kernel, Kernel::RunningKernel(_)) {
return None;
}
self.pending_completion_replies.clear();
let request = CompleteRequest { code, cursor_pos };
let message: JupyterMessage = JupyterMessageContent::CompleteRequest(request).into();
let msg_id = message.header.msg_id.clone();
let (tx, rx) = futures::channel::oneshot::channel();
self.pending_completion_replies.insert(msg_id, tx);
self.send(message, cx).log_err();
Some(rx)
}
pub fn request_inspect(
&mut self,
code: String,
cursor_pos: usize,
cx: &mut Context<Self>,
) -> Option<futures::channel::oneshot::Receiver<runtimelib::InspectReply>> {
if !matches!(&self.kernel, Kernel::RunningKernel(_)) {
return None;
}
let request = InspectRequest {
code,
cursor_pos,
detail_level: Some(0),
};
let message: JupyterMessage = JupyterMessageContent::InspectRequest(request).into();
let msg_id = message.header.msg_id.clone();
let (tx, rx) = futures::channel::oneshot::channel();
self.pending_inspect_replies.insert(msg_id, tx);
self.send(message, cx).log_err();
Some(rx)
}
fn send_stdin_reply( fn send_stdin_reply(
&mut self, &mut self,
value: String, value: String,
@ -999,6 +1053,18 @@ impl KernelSession for Session {
self.kernel.set_kernel_info(reply); self.kernel.set_kernel_info(reply);
cx.notify(); cx.notify();
} }
JupyterMessageContent::CompleteReply(reply) => {
if let Some(tx) = self.pending_completion_replies.remove(parent_message_id) {
tx.send(reply.clone()).ok();
}
return;
}
JupyterMessageContent::InspectReply(reply) => {
if let Some(tx) = self.pending_inspect_replies.remove(parent_message_id) {
tx.send(reply.clone()).ok();
}
return;
}
JupyterMessageContent::UpdateDisplayData(update) => { JupyterMessageContent::UpdateDisplayData(update) => {
let display_id = if let Some(display_id) = update.transient.display_id.clone() { let display_id = if let Some(display_id) = update.transient.display_id.clone() {
display_id display_id