1027 lines · 36.4 KB · Rust
RawDownload
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 typesenum 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,            };

Lines 1–500 of 1027.