tinymist-preview 0.14.20-rc1

A previewer for the Typst typesetting system.
use std::sync::Arc;

use reflexo_typst::debug_loc::DocumentPosition;
use serde::{Deserialize, Serialize};
use tinymist_std::error::IgnoreLogging;
use tokio::sync::broadcast;
use tokio::sync::mpsc;

use crate::actor::render::RenderActorRequest;
use crate::debug_loc::{InternQuery, SpanInterner};
use crate::outline::Outline;
use crate::{
    ChangeCursorPositionRequest, DocToSrcJumpInfo, EditorServer, MemoryFiles, MemoryFilesShort,
    ResolveSourceLocRequest, ViewerWindowStateMessage,
};

use super::webview::WebviewActorRequest;
#[derive(Debug, Clone, Deserialize)]
pub struct DocToSrcJumpResolveRequest {
    /// Span id in hex-format.
    pub span: String,
}

#[derive(Debug, Clone, Deserialize)]
pub struct PanelScrollByPositionRequest {
    position: DocumentPosition,
}

#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", content = "data")]
pub enum CompileStatus {
    Compiling,
    CompileSuccess,
    CompileError,
}

#[derive(Debug)]
pub enum EditorActorRequest {
    Shutdown,
    DocToSrcJumpResolve(DocToSrcJumpResolveRequest),
    DocToSrcJump(DocToSrcJumpInfo),
    ViewerWindowState(ViewerWindowStateMessage),
    Outline(Outline),
    CompileStatus(CompileStatus),
}

pub struct ControlPlaneTx {
    pub is_standalone: bool,
    pub resp_tx: mpsc::UnboundedSender<ControlPlaneResponse>,
    pub ctl_rx: mpsc::UnboundedReceiver<ControlPlaneMessage>,
    pub shutdown_tx: mpsc::Sender<()>,
}

pub struct ControlPlaneRx {
    pub resp_rx: mpsc::UnboundedReceiver<ControlPlaneResponse>,
    pub ctl_tx: mpsc::UnboundedSender<ControlPlaneMessage>,
    pub shutdown_rx: mpsc::Receiver<()>,
}

impl ControlPlaneTx {
    pub fn new(need_sync_files: bool) -> (ControlPlaneTx, ControlPlaneRx) {
        let (resp_tx, resp_rx) = mpsc::unbounded_channel();
        let (ctl_tx, ctl_rx) = mpsc::unbounded_channel();
        let (shutdown_tx, shutdown_rx) = mpsc::channel(1);

        (
            Self {
                is_standalone: need_sync_files,
                resp_tx,
                ctl_rx,
                shutdown_tx,
            },
            ControlPlaneRx {
                resp_rx,
                ctl_tx,
                shutdown_rx,
            },
        )
    }
}

impl ControlPlaneTx {
    fn need_sync_files(&self) -> bool {
        self.is_standalone
    }

    async fn sync_editor_changes(&mut self) {
        self.resp_ctl_plane(
            "SyncEditorChanges",
            ControlPlaneResponse::SyncEditorChanges(()),
        )
        .await;
    }

    async fn resp_ctl_plane(&mut self, loc: &str, resp: ControlPlaneResponse) -> bool {
        let sent = self.resp_tx.send(resp).is_ok();
        if !sent {
            log::warn!("failed to send {loc} response to editor");
        }

        sent
    }

    async fn next(&mut self) -> Option<ControlPlaneMessage> {
        self.ctl_rx.recv().await
    }
}

pub struct EditorActor<T> {
    server: Arc<T>,
    mailbox: mpsc::UnboundedReceiver<EditorActorRequest>,
    editor_conn: ControlPlaneTx,

    renderer_sender: broadcast::Sender<RenderActorRequest>,
    webview_sender: broadcast::Sender<WebviewActorRequest>,

    span_interner: SpanInterner,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "event")]
pub enum ControlPlaneMessage {
    #[serde(rename = "changeCursorPosition")]
    ChangeCursorPosition(ChangeCursorPositionRequest),
    #[serde(rename = "panelScrollTo")]
    ResolveSourceLoc(ResolveSourceLocRequest),
    #[serde(rename = "panelScrollByPosition")]
    PanelScrollByPosition(PanelScrollByPositionRequest),
    #[serde(rename = "sourceScrollBySpan")]
    DocToSrcJumpResolve(DocToSrcJumpResolveRequest),
    #[serde(rename = "syncMemoryFiles")]
    SyncMemoryFiles(MemoryFiles),
    #[serde(rename = "updateMemoryFiles")]
    UpdateMemoryFiles(MemoryFiles),
    #[serde(rename = "removeMemoryFiles")]
    RemoveMemoryFiles(MemoryFilesShort),
}

#[derive(Debug, Serialize)]
#[serde(tag = "event")]
pub enum ControlPlaneResponse {
    #[serde(rename = "editorScrollTo")]
    EditorScrollTo(DocToSrcJumpInfo),
    #[serde(rename = "syncEditorChanges")]
    SyncEditorChanges(()),
    #[serde(rename = "compileStatus")]
    CompileStatus(CompileStatus),
    #[serde(rename = "outline")]
    Outline(Outline),
    #[serde(rename = "viewerWindowState")]
    ViewerWindowState(ViewerWindowStateMessage),
}

impl<T: EditorServer> EditorActor<T> {
    pub fn new(
        server: Arc<T>,
        mailbox: mpsc::UnboundedReceiver<EditorActorRequest>,
        editor_websocket_conn: ControlPlaneTx,
        renderer_sender: broadcast::Sender<RenderActorRequest>,
        webview_sender: broadcast::Sender<WebviewActorRequest>,
        span_interner: SpanInterner,
    ) -> Self {
        Self {
            server,
            mailbox,
            editor_conn: editor_websocket_conn,
            renderer_sender,
            webview_sender,

            span_interner,
        }
    }

    pub async fn run(mut self) {
        if self.editor_conn.need_sync_files() {
            self.editor_conn.sync_editor_changes().await;
        }

        loop {
            tokio::select! {
                Some(msg) = self.mailbox.recv() => {
                    log::trace!("EditorActor: received message from mailbox: {msg:?}");
                   let sent = match msg {
                        EditorActorRequest::Shutdown => {
                            log::info!("EditorActor: received exit message");
                            break;
                        },
                        EditorActorRequest::DocToSrcJump(jump_info) => {
                            self.editor_conn.resp_ctl_plane("DocToSrcJump", ControlPlaneResponse::EditorScrollTo(jump_info)).await
                        },
                        EditorActorRequest::ViewerWindowState(state) => {
                            self.editor_conn.resp_ctl_plane("ViewerWindowState", ControlPlaneResponse::ViewerWindowState(state)).await
                        },
                        EditorActorRequest::DocToSrcJumpResolve(req) => {
                            self.source_scroll_by_span(req.span)
                                .await;

                            false
                        },
                        EditorActorRequest::CompileStatus(status) => {
                            self.editor_conn.resp_ctl_plane("CompileStatus", ControlPlaneResponse::CompileStatus(status)).await
                        },
                        EditorActorRequest::Outline(outline) => {
                            self.editor_conn.resp_ctl_plane("Outline", ControlPlaneResponse::Outline(outline)).await
                        }
                    };

                    if !sent {
                        break;
                    }
                }
                Some(msg) = self.editor_conn.next() => {
                    match msg {
                        ControlPlaneMessage::ChangeCursorPosition(cursor_info) => {
                            log::debug!("EditorActor: received message from editor: {cursor_info:?}");
                            self.renderer_sender.send(RenderActorRequest::ChangeCursorPosition(cursor_info)).log_error("EditorActor");
                        }
                        ControlPlaneMessage::ResolveSourceLoc(jump_info) => {
                            log::debug!("EditorActor: received message from editor: {jump_info:?}");
                            self.renderer_sender.send(RenderActorRequest::ResolveSourceLoc(jump_info)).log_error("EditorActor");
                        }
                        ControlPlaneMessage::PanelScrollByPosition(jump_info) => {
                            log::debug!("EditorActor: received message from editor: {jump_info:?}");
                            self.webview_sender.send(WebviewActorRequest::ViewportPosition(jump_info.position)).log_error("EditorActor");
                        }
                        ControlPlaneMessage::DocToSrcJumpResolve(jump_info) => {
                            log::debug!("EditorActor: received message from editor: {jump_info:?}");

                            self.source_scroll_by_span(jump_info.span)
                                .await;
                        }
                        ControlPlaneMessage::SyncMemoryFiles(req) => {
                            log::debug!(
                                "EditorActor: processing SYNC memory files: {:?}",
                                req.files.keys().collect::<Vec<_>>()
                            );
                            handle_error(
                                "SyncMemoryFiles",
                                self.server.update_memory_files(req, true).await,
                            );
                        }
                        ControlPlaneMessage::UpdateMemoryFiles(req) => {
                            log::debug!(
                                "EditorActor: processing UPDATE memory files: {:?}",
                                req.files.keys().collect::<Vec<_>>()
                            );
                            handle_error(
                                "UpdateMemoryFiles",
                                self.server.update_memory_files(req, false).await,
                            );
                        }
                        ControlPlaneMessage::RemoveMemoryFiles(req) => {
                            log::debug!("EditorActor: processing REMOVE memory files: {:?}", req.files);
                            handle_error(
                                "RemoveMemoryFiles",
                                self.server.remove_memory_files(req).await,
                            );
                        }
                    };
                }
            }
        }

        log::info!("EditorActor: editor disconnected");

        if self.editor_conn.is_standalone {
            log::info!("EditorActor: shutting down whole program");
            std::process::exit(0);
        }
    }

    async fn source_scroll_by_span(&mut self, span: String) {
        let jump_info = {
            match self.span_interner.span_by_str(&span).await {
                InternQuery::Ok(s) => s,
                InternQuery::UseAfterFree => {
                    log::warn!("EditorActor: out of date span id: {span}");
                    return;
                }
            }
        };
        if let Some(span) = jump_info {
            let span_and_offset = span.into();
            self.renderer_sender
                .send(RenderActorRequest::EditorResolveSpanRange(
                    span_and_offset..span_and_offset,
                ))
                .log_error("EditorActor");
        };
    }
}

fn handle_error<T>(loc: &'static str, m: Result<T, reflexo_typst::Error>) -> Option<T> {
    if let Err(err) = &m {
        log::error!("EditorActor: failed to {loc}: {err:#}");
    }

    m.ok()
}