These changes attempt to expand on the work introduced by https://github.com/zed-industries/zed/pull/54778 by introducing a new `GoToDefinitionScrollStrategy::Preserve` variant that attempts to keep the cursor at the same vertical offset within the viewport when navigating to a definition. Most of the machinery for this was already in place. To support cases where the user's scroll position isn't snapped to an exact display row, for example, after scrolling with the mmouse, `Autoscroll::TopRelative` and `Autoscroll::BottomRelative` were updated from `usize` to `ScrollOffset`, allowing fractional offsets. When the cursor is offscreen at the moment the `editor: go to definition` action is invoked, `Preserve` falls back to `Autoscroll::center`, matching the existing default for `go_to_definition_scroll_strategy`. This avoids attempting to preserve an offset where the cursor isn't visible which would lead to the cursor being offscreen when jumping to the definition. Documentation has also been updated to reflect this new strategy value. Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Relates to #52173 Release Notes: - Added a new `preserve` option to `go_to_definition_scroll_strategy` that keeps the cursor at the same vertical position within the viewport when navigating to a definition
1028 lines
36 KiB
Rust
1028 lines
36 KiB
Rust
use crate::components::KernelListItem;
|
|
use crate::setup_editor_session_actions;
|
|
use crate::{
|
|
KernelStatus,
|
|
kernels::{
|
|
Kernel, KernelSession, KernelSpecification, NativeRunningKernel, RemoteRunningKernel,
|
|
SshRunningKernel, WslRunningKernel,
|
|
},
|
|
outputs::{
|
|
ExecutionStatus, ExecutionView, ExecutionViewFinishedEmpty, ExecutionViewFinishedSmall,
|
|
InputReplyEvent,
|
|
},
|
|
repl_settings::ReplSettings,
|
|
};
|
|
use anyhow::Context as _;
|
|
use collections::{HashMap, HashSet};
|
|
use editor::SelectionEffects;
|
|
use editor::{
|
|
Anchor, AnchorRangeExt as _, Editor, Inlay, MultiBuffer, ToOffset, ToPoint,
|
|
display_map::{
|
|
BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId,
|
|
RenderBlock,
|
|
},
|
|
scroll::Autoscroll,
|
|
};
|
|
use project::InlayId;
|
|
|
|
/// Marker types
|
|
enum ReplExecutedRange {}
|
|
|
|
use futures::FutureExt as _;
|
|
use gpui::{
|
|
Context, Entity, EventEmitter, Render, Subscription, Task, WeakEntity, Window, div, prelude::*,
|
|
};
|
|
use language::Point;
|
|
use project::Fs;
|
|
use runtimelib::{
|
|
ExecuteRequest, ExecutionState, InputReply, InterruptRequest, JupyterMessage,
|
|
JupyterMessageContent, KernelInfoRequest, ReplyStatus, ShutdownRequest,
|
|
};
|
|
use settings::Settings as _;
|
|
use std::{env::temp_dir, ops::Range, sync::Arc, time::Duration};
|
|
use theme::ActiveTheme;
|
|
use ui::{IconButtonShape, Tooltip, prelude::*};
|
|
use util::ResultExt as _;
|
|
|
|
pub struct Session {
|
|
fs: Arc<dyn Fs>,
|
|
editor: WeakEntity<Editor>,
|
|
pub kernel: Kernel,
|
|
pub kernel_specification: KernelSpecification,
|
|
|
|
blocks: HashMap<String, EditorBlock>,
|
|
result_inlays: HashMap<String, (InlayId, Range<Anchor>, usize)>,
|
|
next_inlay_id: usize,
|
|
|
|
_subscriptions: Vec<Subscription>,
|
|
}
|
|
|
|
struct EditorBlock {
|
|
code_range: Range<Anchor>,
|
|
invalidation_anchor: Anchor,
|
|
block_id: CustomBlockId,
|
|
execution_view: Entity<ExecutionView>,
|
|
}
|
|
|
|
type CloseBlockFn =
|
|
Arc<dyn for<'a> Fn(CustomBlockId, &'a mut Window, &mut App) + Send + Sync + 'static>;
|
|
|
|
impl EditorBlock {
|
|
fn new(
|
|
editor: WeakEntity<Editor>,
|
|
code_range: Range<Anchor>,
|
|
status: ExecutionStatus,
|
|
on_close: CloseBlockFn,
|
|
cx: &mut Context<Session>,
|
|
) -> anyhow::Result<Self> {
|
|
let editor = editor.upgrade().context("editor is not open")?;
|
|
let workspace = editor.read(cx).workspace().context("workspace dropped")?;
|
|
|
|
let execution_view = cx.new(|cx| ExecutionView::new(status, workspace.downgrade(), cx));
|
|
|
|
let (block_id, invalidation_anchor) = editor.update(cx, |editor, cx| {
|
|
let buffer = editor.buffer().clone();
|
|
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
|
let end_point = code_range.end.to_point(&buffer_snapshot);
|
|
let next_row_start = end_point + Point::new(1, 0);
|
|
if next_row_start > buffer_snapshot.max_point() {
|
|
buffer.update(cx, |buffer, cx| {
|
|
buffer.edit(
|
|
[(
|
|
buffer_snapshot.max_point()..buffer_snapshot.max_point(),
|
|
"\n",
|
|
)],
|
|
None,
|
|
cx,
|
|
)
|
|
});
|
|
}
|
|
|
|
// Re-read snapshot after potential buffer edit and create a fresh anchor for
|
|
// block placement. Using anchor_before (Bias::Left) ensures the anchor stays
|
|
// at the end of the code line regardless of whether we inserted a newline.
|
|
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
|
let block_placement_anchor = buffer_snapshot.anchor_before(end_point);
|
|
let invalidation_anchor = buffer_snapshot.anchor_before(next_row_start);
|
|
let block = BlockProperties {
|
|
placement: BlockPlacement::Below(block_placement_anchor),
|
|
// Take up at least one height for status, allow the editor to determine the real height based on the content from render
|
|
height: Some(1),
|
|
style: BlockStyle::Sticky,
|
|
render: Self::create_output_area_renderer(execution_view.clone(), on_close.clone()),
|
|
priority: 0,
|
|
};
|
|
|
|
let block_id = editor.insert_blocks([block], None, cx)[0];
|
|
(block_id, invalidation_anchor)
|
|
});
|
|
|
|
anyhow::Ok(Self {
|
|
code_range,
|
|
invalidation_anchor,
|
|
block_id,
|
|
execution_view,
|
|
})
|
|
}
|
|
|
|
fn handle_message(
|
|
&mut self,
|
|
message: &JupyterMessage,
|
|
window: &mut Window,
|
|
cx: &mut Context<Session>,
|
|
) {
|
|
self.execution_view.update(cx, |execution_view, cx| {
|
|
if matches!(&message.content, JupyterMessageContent::InputRequest(_)) {
|
|
execution_view.handle_input_request(message, window, cx);
|
|
} else {
|
|
execution_view.push_message(&message.content, window, cx);
|
|
}
|
|
});
|
|
}
|
|
|
|
fn create_output_area_renderer(
|
|
execution_view: Entity<ExecutionView>,
|
|
on_close: CloseBlockFn,
|
|
) -> RenderBlock {
|
|
Arc::new(move |cx: &mut BlockContext| {
|
|
let execution_view = execution_view.clone();
|
|
let text_style = crate::outputs::plain::text_style(cx.window, cx.app);
|
|
|
|
let editor_margins = cx.margins;
|
|
let gutter = editor_margins.gutter;
|
|
|
|
let block_id = cx.block_id;
|
|
let on_close = on_close.clone();
|
|
|
|
let rem_size = cx.window.rem_size();
|
|
|
|
let text_line_height = text_style.line_height_in_pixels(rem_size);
|
|
let output_settings = ReplSettings::get_global(cx.app);
|
|
let output_max_height = if output_settings.output_max_height_lines > 0 {
|
|
Some(text_line_height * output_settings.output_max_height_lines as f32)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let close_button = h_flex()
|
|
.flex_none()
|
|
.items_center()
|
|
.justify_center()
|
|
.absolute()
|
|
.top(text_line_height / 2.)
|
|
.right(
|
|
// 2px is a magic number to nudge the button just a bit closer to
|
|
// the line number start
|
|
gutter.full_width() / 2.0 - text_line_height / 2.0 - px(2.),
|
|
)
|
|
.w(text_line_height)
|
|
.h(text_line_height)
|
|
.child(
|
|
IconButton::new("close_output_area", IconName::Close)
|
|
.icon_size(IconSize::Small)
|
|
.icon_color(Color::Muted)
|
|
.size(ButtonSize::Compact)
|
|
.shape(IconButtonShape::Square)
|
|
.tooltip(Tooltip::text("Close output area"))
|
|
.on_click(move |_, window, cx| {
|
|
if let BlockId::Custom(block_id) = block_id {
|
|
(on_close)(block_id, window, cx)
|
|
}
|
|
}),
|
|
);
|
|
|
|
div()
|
|
.id(cx.block_id)
|
|
.block_mouse_except_scroll()
|
|
.flex()
|
|
.items_start()
|
|
.min_h(text_line_height)
|
|
.w_full()
|
|
.border_y_1()
|
|
.border_color(cx.theme().colors().border)
|
|
.bg(cx.theme().colors().background)
|
|
.child(
|
|
div()
|
|
.relative()
|
|
.w(gutter.full_width())
|
|
.h(text_line_height * 2)
|
|
.child(close_button),
|
|
)
|
|
.child(
|
|
div()
|
|
.id((ElementId::from(cx.block_id), "output-scroll"))
|
|
.flex_1()
|
|
.overflow_x_hidden()
|
|
.py(text_line_height / 2.)
|
|
.mr(editor_margins.right)
|
|
.pr_2()
|
|
.when_some(output_max_height, |div, max_h| {
|
|
div.max_h(max_h).overflow_y_scroll()
|
|
})
|
|
.child(execution_view),
|
|
)
|
|
.into_any_element()
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Session {
|
|
pub fn new(
|
|
editor: WeakEntity<Editor>,
|
|
fs: Arc<dyn Fs>,
|
|
kernel_specification: KernelSpecification,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Self {
|
|
let subscription = match editor.upgrade() {
|
|
Some(editor) => {
|
|
let buffer = editor.read(cx).buffer().clone();
|
|
cx.subscribe(&buffer, Self::on_buffer_event)
|
|
}
|
|
None => Subscription::new(|| {}),
|
|
};
|
|
|
|
let editor_handle = editor.clone();
|
|
|
|
editor
|
|
.update(cx, |editor, _cx| {
|
|
setup_editor_session_actions(editor, editor_handle);
|
|
})
|
|
.ok();
|
|
|
|
let mut session = Self {
|
|
fs,
|
|
editor,
|
|
kernel: Kernel::StartingKernel(Task::ready(()).shared()),
|
|
blocks: HashMap::default(),
|
|
result_inlays: HashMap::default(),
|
|
next_inlay_id: 0,
|
|
kernel_specification,
|
|
_subscriptions: vec![subscription],
|
|
};
|
|
|
|
session.start_kernel(window, cx);
|
|
session
|
|
}
|
|
|
|
fn start_kernel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
let kernel_language = self.kernel_specification.language();
|
|
let entity_id = self.editor.entity_id();
|
|
|
|
// For WSL Remote kernels, use project root instead of potentially temporary working directory
|
|
// which causes .venv/bin/python checks to fail
|
|
let is_remote_execution = matches!(
|
|
self.kernel_specification,
|
|
crate::KernelSpecification::WslRemote(_) | crate::KernelSpecification::SshRemote(_)
|
|
);
|
|
|
|
let working_directory = if is_remote_execution {
|
|
// For WSL Remote kernels, use project root instead of potentially temporary working directory
|
|
// which causes .venv/bin/python checks to fail
|
|
self.editor
|
|
.upgrade()
|
|
.and_then(|editor| editor.read(cx).project().cloned())
|
|
.and_then(|project| {
|
|
project
|
|
.read(cx)
|
|
.worktrees(cx)
|
|
.next()
|
|
.map(|worktree| worktree.read(cx).abs_path().to_path_buf())
|
|
})
|
|
.unwrap_or_else(temp_dir)
|
|
} else {
|
|
self.editor
|
|
.upgrade()
|
|
.and_then(|editor| editor.read(cx).working_directory(cx))
|
|
.unwrap_or_else(temp_dir)
|
|
};
|
|
|
|
telemetry::event!(
|
|
"Kernel Status Changed",
|
|
kernel_language,
|
|
kernel_status = KernelStatus::Starting.to_string(),
|
|
repl_session_id = cx.entity_id().to_string(),
|
|
);
|
|
|
|
let session_view = cx.entity();
|
|
|
|
let kernel = match self.kernel_specification.clone() {
|
|
KernelSpecification::Jupyter(kernel_specification) => NativeRunningKernel::new(
|
|
kernel_specification,
|
|
entity_id,
|
|
working_directory,
|
|
self.fs.clone(),
|
|
session_view,
|
|
window,
|
|
cx,
|
|
),
|
|
KernelSpecification::PythonEnv(env_specification) => NativeRunningKernel::new(
|
|
env_specification.as_local_spec(),
|
|
entity_id,
|
|
working_directory,
|
|
self.fs.clone(),
|
|
session_view,
|
|
window,
|
|
cx,
|
|
),
|
|
KernelSpecification::JupyterServer(remote_kernel_specification) => {
|
|
RemoteRunningKernel::new(
|
|
remote_kernel_specification,
|
|
working_directory,
|
|
session_view,
|
|
window,
|
|
cx,
|
|
)
|
|
}
|
|
KernelSpecification::SshRemote(spec) => {
|
|
let project = self
|
|
.editor
|
|
.upgrade()
|
|
.and_then(|editor| editor.read(cx).project().cloned());
|
|
if let Some(project) = project {
|
|
SshRunningKernel::new(
|
|
spec,
|
|
working_directory,
|
|
project,
|
|
session_view,
|
|
window,
|
|
cx,
|
|
)
|
|
} else {
|
|
Task::ready(Err(anyhow::anyhow!("No project associated with editor")))
|
|
}
|
|
}
|
|
KernelSpecification::WslRemote(spec) => WslRunningKernel::new(
|
|
spec,
|
|
entity_id,
|
|
working_directory,
|
|
self.fs.clone(),
|
|
session_view,
|
|
window,
|
|
cx,
|
|
),
|
|
};
|
|
|
|
let pending_kernel = cx
|
|
.spawn(async move |this, cx| {
|
|
let kernel: anyhow::Result<Box<dyn crate::kernels::RunningKernel>> = kernel.await;
|
|
|
|
match kernel {
|
|
Ok(kernel) => {
|
|
this.update(cx, |session, cx| {
|
|
session.kernel(Kernel::RunningKernel(kernel), cx);
|
|
let request =
|
|
JupyterMessageContent::KernelInfoRequest(KernelInfoRequest {});
|
|
session.send(request.into(), cx).log_err();
|
|
})
|
|
.ok();
|
|
}
|
|
Err(err) => {
|
|
this.update(cx, |session, cx| {
|
|
session.kernel_errored(err.to_string(), cx);
|
|
})
|
|
.ok();
|
|
}
|
|
}
|
|
})
|
|
.shared();
|
|
|
|
self.kernel(Kernel::StartingKernel(pending_kernel), cx);
|
|
cx.notify();
|
|
}
|
|
|
|
pub fn kernel_errored(&mut self, error_message: String, cx: &mut Context<Self>) {
|
|
self.kernel(Kernel::ErroredLaunch(error_message.clone()), cx);
|
|
|
|
self.blocks.values().for_each(|block| {
|
|
block.execution_view.update(cx, |execution_view, cx| {
|
|
match execution_view.status {
|
|
ExecutionStatus::Finished => {
|
|
// Do nothing when the output was good
|
|
}
|
|
_ => {
|
|
// All other cases, set the status to errored
|
|
execution_view.status =
|
|
ExecutionStatus::KernelErrored(error_message.clone())
|
|
}
|
|
}
|
|
cx.notify();
|
|
});
|
|
});
|
|
}
|
|
|
|
fn on_buffer_event(
|
|
&mut self,
|
|
buffer: Entity<MultiBuffer>,
|
|
event: &multi_buffer::Event,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if let multi_buffer::Event::Edited { .. } = event {
|
|
let snapshot = buffer.read(cx).snapshot(cx);
|
|
|
|
let mut blocks_to_remove: HashSet<CustomBlockId> = HashSet::default();
|
|
let mut gutter_ranges_to_remove: Vec<Range<Anchor>> = Vec::new();
|
|
let mut keys_to_remove: Vec<String> = Vec::new();
|
|
|
|
self.blocks.retain(|id, block| {
|
|
if block.invalidation_anchor.is_valid(&snapshot) {
|
|
true
|
|
} else {
|
|
blocks_to_remove.insert(block.block_id);
|
|
gutter_ranges_to_remove.push(block.code_range.clone());
|
|
keys_to_remove.push(id.clone());
|
|
false
|
|
}
|
|
});
|
|
|
|
let mut inlays_to_remove: Vec<InlayId> = Vec::new();
|
|
|
|
self.result_inlays
|
|
.retain(|id, (inlay_id, code_range, original_len)| {
|
|
let start_offset = code_range.start.to_offset(&snapshot);
|
|
let end_offset = code_range.end.to_offset(&snapshot);
|
|
let current_len = end_offset.saturating_sub(start_offset);
|
|
|
|
if current_len != *original_len {
|
|
inlays_to_remove.push(*inlay_id);
|
|
gutter_ranges_to_remove.push(code_range.clone());
|
|
keys_to_remove.push(id.clone());
|
|
false
|
|
} else {
|
|
true
|
|
}
|
|
});
|
|
|
|
if !blocks_to_remove.is_empty()
|
|
|| !inlays_to_remove.is_empty()
|
|
|| !gutter_ranges_to_remove.is_empty()
|
|
{
|
|
self.editor
|
|
.update(cx, |editor, cx| {
|
|
if !blocks_to_remove.is_empty() {
|
|
editor.remove_blocks(blocks_to_remove, None, cx);
|
|
}
|
|
if !inlays_to_remove.is_empty() {
|
|
editor.splice_inlays(&inlays_to_remove, vec![], cx);
|
|
}
|
|
if !gutter_ranges_to_remove.is_empty() {
|
|
editor.remove_gutter_highlights::<ReplExecutedRange>(
|
|
gutter_ranges_to_remove,
|
|
cx,
|
|
);
|
|
}
|
|
})
|
|
.ok();
|
|
cx.notify();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn send(&mut self, message: JupyterMessage, _cx: &mut Context<Self>) -> anyhow::Result<()> {
|
|
if let Kernel::RunningKernel(kernel) = &mut self.kernel {
|
|
kernel.request_tx().try_send(message).ok();
|
|
}
|
|
|
|
anyhow::Ok(())
|
|
}
|
|
|
|
fn send_stdin_reply(
|
|
&mut self,
|
|
value: String,
|
|
parent_message: &JupyterMessage,
|
|
_cx: &mut Context<Self>,
|
|
) {
|
|
if let Kernel::RunningKernel(kernel) = &mut self.kernel {
|
|
let reply = InputReply {
|
|
value,
|
|
status: ReplyStatus::Ok,
|
|
error: None,
|
|
};
|
|
let message = reply.as_child_of(parent_message);
|
|
kernel.stdin_tx().try_send(message).log_err();
|
|
}
|
|
}
|
|
|
|
fn replace_block_with_inlay(&mut self, message_id: &str, text: &str, cx: &mut Context<Self>) {
|
|
let Some(block) = self.blocks.remove(message_id) else {
|
|
return;
|
|
};
|
|
|
|
let Some(editor) = self.editor.upgrade() else {
|
|
return;
|
|
};
|
|
|
|
let code_range = block.code_range.clone();
|
|
|
|
editor.update(cx, |editor, cx| {
|
|
let mut block_ids = HashSet::default();
|
|
block_ids.insert(block.block_id);
|
|
editor.remove_blocks(block_ids, None, cx);
|
|
|
|
let buffer = editor.buffer().read(cx).snapshot(cx);
|
|
let start_offset = code_range.start.to_offset(&buffer);
|
|
let end_offset = code_range.end.to_offset(&buffer);
|
|
let original_len = end_offset.saturating_sub(start_offset);
|
|
|
|
let end_point = code_range.end.to_point(&buffer);
|
|
let inlay_position = buffer.anchor_after(end_point);
|
|
|
|
let inlay_id = self.next_inlay_id;
|
|
self.next_inlay_id += 1;
|
|
|
|
let inlay = Inlay::repl_result(inlay_id, inlay_position, format!(" {}", text));
|
|
|
|
editor.splice_inlays(&[], vec![inlay], cx);
|
|
self.result_inlays.insert(
|
|
message_id.to_string(),
|
|
(
|
|
InlayId::ReplResult(inlay_id),
|
|
code_range.clone(),
|
|
original_len,
|
|
),
|
|
);
|
|
|
|
editor.insert_gutter_highlight::<ReplExecutedRange>(
|
|
code_range,
|
|
|cx| cx.theme().status().success,
|
|
cx,
|
|
);
|
|
});
|
|
|
|
cx.notify();
|
|
}
|
|
|
|
pub fn clear_outputs(&mut self, cx: &mut Context<Self>) {
|
|
let blocks_to_remove: HashSet<CustomBlockId> =
|
|
self.blocks.values().map(|block| block.block_id).collect();
|
|
|
|
let inlays_to_remove: Vec<InlayId> =
|
|
self.result_inlays.values().map(|(id, _, _)| *id).collect();
|
|
|
|
self.editor
|
|
.update(cx, |editor, cx| {
|
|
editor.remove_blocks(blocks_to_remove, None, cx);
|
|
editor.splice_inlays(&inlays_to_remove, vec![], cx);
|
|
editor.clear_gutter_highlights::<ReplExecutedRange>(cx);
|
|
})
|
|
.ok();
|
|
|
|
self.blocks.clear();
|
|
self.result_inlays.clear();
|
|
}
|
|
|
|
pub fn clear_output_at_position(&mut self, position: Anchor, cx: &mut Context<Self>) {
|
|
let Some(editor) = self.editor.upgrade() else {
|
|
return;
|
|
};
|
|
|
|
let (block_id, code_range, msg_id) = {
|
|
let snapshot = editor.read(cx).buffer().read(cx).read(cx);
|
|
let pos_range = position..position;
|
|
|
|
let block_to_remove = self
|
|
.blocks
|
|
.iter()
|
|
.find(|(_, block)| block.code_range.includes(&pos_range, &snapshot));
|
|
|
|
let Some((msg_id, block)) = block_to_remove else {
|
|
return;
|
|
};
|
|
|
|
(block.block_id, block.code_range.clone(), msg_id.clone())
|
|
};
|
|
|
|
let inlay_to_remove = self.result_inlays.get(&msg_id).map(|(id, _, _)| *id);
|
|
|
|
self.blocks.remove(&msg_id);
|
|
if inlay_to_remove.is_some() {
|
|
self.result_inlays.remove(&msg_id);
|
|
}
|
|
|
|
self.editor
|
|
.update(cx, |editor, cx| {
|
|
let mut block_ids = HashSet::default();
|
|
block_ids.insert(block_id);
|
|
editor.remove_blocks(block_ids, None, cx);
|
|
|
|
if let Some(inlay_id) = inlay_to_remove {
|
|
editor.splice_inlays(&[inlay_id], vec![], cx);
|
|
}
|
|
|
|
editor.remove_gutter_highlights::<ReplExecutedRange>(vec![code_range], cx);
|
|
})
|
|
.ok();
|
|
|
|
cx.notify();
|
|
}
|
|
|
|
pub fn execute(
|
|
&mut self,
|
|
code: String,
|
|
anchor_range: Range<Anchor>,
|
|
next_cell: Option<Anchor>,
|
|
move_down: bool,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let Some(editor) = self.editor.upgrade() else {
|
|
return;
|
|
};
|
|
|
|
if code.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let execute_request = ExecuteRequest {
|
|
code,
|
|
allow_stdin: true,
|
|
..ExecuteRequest::default()
|
|
};
|
|
|
|
let message: JupyterMessage = execute_request.into();
|
|
|
|
let mut blocks_to_remove: HashSet<CustomBlockId> = HashSet::default();
|
|
let mut inlays_to_remove: Vec<InlayId> = Vec::new();
|
|
let mut gutter_ranges_to_remove: Vec<Range<Anchor>> = Vec::new();
|
|
|
|
let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
|
|
|
|
self.blocks.retain(|_key, block| {
|
|
if anchor_range.overlaps(&block.code_range, &buffer) {
|
|
blocks_to_remove.insert(block.block_id);
|
|
false
|
|
} else {
|
|
true
|
|
}
|
|
});
|
|
|
|
self.result_inlays
|
|
.retain(|_key, (inlay_id, inlay_range, _)| {
|
|
if anchor_range.overlaps(inlay_range, &buffer) {
|
|
inlays_to_remove.push(*inlay_id);
|
|
gutter_ranges_to_remove.push(inlay_range.clone());
|
|
false
|
|
} else {
|
|
true
|
|
}
|
|
});
|
|
|
|
self.editor
|
|
.update(cx, |editor, cx| {
|
|
editor.remove_blocks(blocks_to_remove, None, cx);
|
|
if !inlays_to_remove.is_empty() {
|
|
editor.splice_inlays(&inlays_to_remove, vec![], cx);
|
|
}
|
|
if !gutter_ranges_to_remove.is_empty() {
|
|
editor
|
|
.remove_gutter_highlights::<ReplExecutedRange>(gutter_ranges_to_remove, cx);
|
|
}
|
|
})
|
|
.ok();
|
|
|
|
let status = match &self.kernel {
|
|
Kernel::Restarting => ExecutionStatus::Restarting,
|
|
Kernel::RunningKernel(_) => ExecutionStatus::Queued,
|
|
Kernel::StartingKernel(_) => ExecutionStatus::ConnectingToKernel,
|
|
Kernel::ErroredLaunch(error) => ExecutionStatus::KernelErrored(error.clone()),
|
|
Kernel::ShuttingDown => ExecutionStatus::ShuttingDown,
|
|
Kernel::Shutdown => ExecutionStatus::Shutdown,
|
|
};
|
|
|
|
let parent_message_id = message.header.msg_id.clone();
|
|
let session_view = cx.entity().downgrade();
|
|
let weak_editor = self.editor.clone();
|
|
let code_range_for_close = anchor_range.clone();
|
|
|
|
let on_close: CloseBlockFn = Arc::new(
|
|
move |block_id: CustomBlockId, _: &mut Window, cx: &mut App| {
|
|
if let Some(session) = session_view.upgrade() {
|
|
session.update(cx, |session, cx| {
|
|
session.blocks.remove(&parent_message_id);
|
|
cx.notify();
|
|
});
|
|
}
|
|
|
|
if let Some(editor) = weak_editor.upgrade() {
|
|
editor.update(cx, |editor, cx| {
|
|
let mut block_ids = HashSet::default();
|
|
block_ids.insert(block_id);
|
|
editor.remove_blocks(block_ids, None, cx);
|
|
editor.remove_gutter_highlights::<ReplExecutedRange>(
|
|
vec![code_range_for_close.clone()],
|
|
cx,
|
|
);
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
let Ok(editor_block) = EditorBlock::new(
|
|
self.editor.clone(),
|
|
anchor_range.clone(),
|
|
status,
|
|
on_close,
|
|
cx,
|
|
) else {
|
|
return;
|
|
};
|
|
|
|
self.editor
|
|
.update(cx, |editor, cx| {
|
|
editor.insert_gutter_highlight::<ReplExecutedRange>(
|
|
anchor_range.clone(),
|
|
|cx| cx.theme().status().success,
|
|
cx,
|
|
);
|
|
})
|
|
.ok();
|
|
|
|
let new_cursor_pos = if let Some(next_cursor) = next_cell {
|
|
next_cursor
|
|
} else {
|
|
editor_block.invalidation_anchor
|
|
};
|
|
|
|
let msg_id = message.header.msg_id.clone();
|
|
let subscription = cx.subscribe(
|
|
&editor_block.execution_view,
|
|
move |session, _execution_view, _event: &ExecutionViewFinishedEmpty, cx| {
|
|
session.replace_block_with_inlay(&msg_id, "✓", cx);
|
|
},
|
|
);
|
|
self._subscriptions.push(subscription);
|
|
|
|
let msg_id = message.header.msg_id.clone();
|
|
let subscription = cx.subscribe(
|
|
&editor_block.execution_view,
|
|
move |session, _execution_view, event: &ExecutionViewFinishedSmall, cx| {
|
|
session.replace_block_with_inlay(&msg_id, &event.0, cx);
|
|
},
|
|
);
|
|
self._subscriptions.push(subscription);
|
|
|
|
let subscription = cx.subscribe(
|
|
&editor_block.execution_view,
|
|
|session, _execution_view, event: &InputReplyEvent, cx| {
|
|
session.send_stdin_reply(event.value.clone(), &event.parent_message, cx);
|
|
},
|
|
);
|
|
self._subscriptions.push(subscription);
|
|
|
|
self.blocks
|
|
.insert(message.header.msg_id.clone(), editor_block);
|
|
|
|
match &self.kernel {
|
|
Kernel::RunningKernel(_) => {
|
|
self.send(message, cx).ok();
|
|
}
|
|
Kernel::StartingKernel(task) => {
|
|
// Queue up the execution as a task to run after the kernel starts
|
|
let task = task.clone();
|
|
|
|
cx.spawn(async move |this, cx| {
|
|
task.await;
|
|
this.update(cx, |session, cx| {
|
|
session.send(message, cx).ok();
|
|
})
|
|
.ok();
|
|
})
|
|
.detach();
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
if move_down {
|
|
editor.update(cx, move |editor, cx| {
|
|
editor.change_selections(
|
|
SelectionEffects::scroll(Autoscroll::top_relative(8.0)),
|
|
window,
|
|
cx,
|
|
|selections| {
|
|
selections.select_ranges([new_cursor_pos..new_cursor_pos]);
|
|
},
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
pub fn interrupt(&mut self, cx: &mut Context<Self>) {
|
|
match &mut self.kernel {
|
|
Kernel::RunningKernel(_kernel) => {
|
|
self.send(InterruptRequest {}.into(), cx).ok();
|
|
}
|
|
Kernel::StartingKernel(_task) => {
|
|
// NOTE: If we switch to a literal queue instead of chaining on to the task, clear all queued executions
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
pub fn kernel(&mut self, kernel: Kernel, cx: &mut Context<Self>) {
|
|
if let Kernel::Shutdown = kernel {
|
|
cx.emit(SessionEvent::Shutdown(self.editor.clone()));
|
|
}
|
|
|
|
let kernel_status = KernelStatus::from(&kernel).to_string();
|
|
let kernel_language = self.kernel_specification.language();
|
|
|
|
telemetry::event!(
|
|
"Kernel Status Changed",
|
|
kernel_language,
|
|
kernel_status,
|
|
repl_session_id = cx.entity_id().to_string(),
|
|
);
|
|
|
|
self.kernel = kernel;
|
|
}
|
|
|
|
pub fn shutdown(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
let kernel = std::mem::replace(&mut self.kernel, Kernel::ShuttingDown);
|
|
|
|
match kernel {
|
|
Kernel::RunningKernel(mut kernel) => {
|
|
let mut request_tx = kernel.request_tx();
|
|
|
|
let forced = kernel.force_shutdown(window, cx);
|
|
|
|
cx.spawn(async move |this, cx| {
|
|
let message: JupyterMessage = ShutdownRequest { restart: false }.into();
|
|
request_tx.try_send(message).ok();
|
|
|
|
forced.await.log_err();
|
|
|
|
// Give the kernel a bit of time to clean up
|
|
cx.background_executor().timer(Duration::from_secs(3)).await;
|
|
|
|
this.update(cx, |session, cx| {
|
|
session.clear_outputs(cx);
|
|
session.kernel(Kernel::Shutdown, cx);
|
|
cx.notify();
|
|
})
|
|
.ok();
|
|
})
|
|
.detach();
|
|
}
|
|
_ => {
|
|
self.kernel(Kernel::Shutdown, cx);
|
|
}
|
|
}
|
|
cx.notify();
|
|
}
|
|
|
|
pub fn restart(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
let kernel = std::mem::replace(&mut self.kernel, Kernel::Restarting);
|
|
|
|
match kernel {
|
|
Kernel::Restarting => {
|
|
// Do nothing if already restarting
|
|
}
|
|
Kernel::RunningKernel(mut kernel) => {
|
|
let mut request_tx = kernel.request_tx();
|
|
|
|
let forced = kernel.force_shutdown(window, cx);
|
|
|
|
cx.spawn_in(window, async move |this, cx| {
|
|
// Send shutdown request with restart flag
|
|
log::debug!("restarting kernel");
|
|
let message: JupyterMessage = ShutdownRequest { restart: true }.into();
|
|
request_tx.try_send(message).ok();
|
|
|
|
// Wait for kernel to shutdown
|
|
cx.background_executor().timer(Duration::from_secs(1)).await;
|
|
|
|
// Force kill the kernel if it hasn't shut down
|
|
forced.await.log_err();
|
|
|
|
// Start a new kernel
|
|
this.update_in(cx, |session, window, cx| {
|
|
// TODO: Differentiate between restart and restart+clear-outputs
|
|
session.clear_outputs(cx);
|
|
session.start_kernel(window, cx);
|
|
})
|
|
.ok();
|
|
})
|
|
.detach();
|
|
}
|
|
_ => {
|
|
self.clear_outputs(cx);
|
|
self.start_kernel(window, cx);
|
|
}
|
|
}
|
|
cx.notify();
|
|
}
|
|
}
|
|
|
|
pub enum SessionEvent {
|
|
Shutdown(WeakEntity<Editor>),
|
|
}
|
|
|
|
impl EventEmitter<SessionEvent> for Session {}
|
|
|
|
impl Render for Session {
|
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let (status_text, interrupt_button) = match &self.kernel {
|
|
Kernel::RunningKernel(kernel) => (
|
|
kernel
|
|
.kernel_info()
|
|
.as_ref()
|
|
.map(|info| info.language_info.name.clone()),
|
|
Some(
|
|
Button::new("interrupt", "Interrupt")
|
|
.style(ButtonStyle::Subtle)
|
|
.on_click(cx.listener(move |session, _, _, cx| {
|
|
session.interrupt(cx);
|
|
})),
|
|
),
|
|
),
|
|
Kernel::StartingKernel(_) => (Some("Starting".into()), None),
|
|
Kernel::ErroredLaunch(err) => (Some(format!("Error: {err}")), None),
|
|
Kernel::ShuttingDown => (Some("Shutting Down".into()), None),
|
|
Kernel::Shutdown => (Some("Shutdown".into()), None),
|
|
Kernel::Restarting => (Some("Restarting".into()), None),
|
|
};
|
|
|
|
KernelListItem::new(self.kernel_specification.clone())
|
|
.status_color(match &self.kernel {
|
|
Kernel::RunningKernel(kernel) => match kernel.execution_state() {
|
|
ExecutionState::Idle => Color::Success,
|
|
ExecutionState::Busy => Color::Modified,
|
|
ExecutionState::Unknown => Color::Modified,
|
|
ExecutionState::Starting => Color::Modified,
|
|
ExecutionState::Restarting => Color::Modified,
|
|
ExecutionState::Terminating => Color::Disabled,
|
|
ExecutionState::AutoRestarting => Color::Modified,
|
|
ExecutionState::Dead => Color::Disabled,
|
|
ExecutionState::Other(_) => Color::Modified,
|
|
},
|
|
Kernel::StartingKernel(_) => Color::Modified,
|
|
Kernel::ErroredLaunch(_) => Color::Error,
|
|
Kernel::ShuttingDown => Color::Modified,
|
|
Kernel::Shutdown => Color::Disabled,
|
|
Kernel::Restarting => Color::Modified,
|
|
})
|
|
.child(Label::new(self.kernel_specification.name()))
|
|
.children(status_text.map(|status_text| Label::new(format!("({status_text})"))))
|
|
.button(
|
|
Button::new("shutdown", "Shutdown")
|
|
.style(ButtonStyle::Subtle)
|
|
.disabled(self.kernel.is_shutting_down())
|
|
.on_click(cx.listener(move |session, _, window, cx| {
|
|
session.shutdown(window, cx);
|
|
})),
|
|
)
|
|
.buttons(interrupt_button)
|
|
}
|
|
}
|
|
|
|
impl KernelSession for Session {
|
|
fn route(&mut self, message: &JupyterMessage, window: &mut Window, cx: &mut Context<Self>) {
|
|
let parent_message_id = match message.parent_header.as_ref() {
|
|
Some(header) => &header.msg_id,
|
|
None => return,
|
|
};
|
|
|
|
match &message.content {
|
|
JupyterMessageContent::Status(status) => {
|
|
self.kernel.set_execution_state(&status.execution_state);
|
|
|
|
telemetry::event!(
|
|
"Kernel Status Changed",
|
|
kernel_language = self.kernel_specification.language(),
|
|
kernel_status = KernelStatus::from(&self.kernel).to_string(),
|
|
repl_session_id = cx.entity_id().to_string(),
|
|
);
|
|
|
|
cx.notify();
|
|
}
|
|
JupyterMessageContent::KernelInfoReply(reply) => {
|
|
self.kernel.set_kernel_info(reply);
|
|
cx.notify();
|
|
}
|
|
JupyterMessageContent::UpdateDisplayData(update) => {
|
|
let display_id = if let Some(display_id) = update.transient.display_id.clone() {
|
|
display_id
|
|
} else {
|
|
return;
|
|
};
|
|
|
|
self.blocks.iter_mut().for_each(|(_, block)| {
|
|
block.execution_view.update(cx, |execution_view, cx| {
|
|
execution_view.update_display_data(&update.data, &display_id, window, cx);
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
if let Some(block) = self.blocks.get_mut(parent_message_id) {
|
|
block.handle_message(message, window, cx);
|
|
}
|
|
}
|
|
|
|
fn kernel_errored(&mut self, error_message: String, cx: &mut Context<Self>) {
|
|
self.kernel_errored(error_message, cx);
|
|
}
|
|
}
|