refactor: rearrange preview entry for future improvement (#426)

* dev: refactor preview entry for future improvement

* dev: move watcher ahead

* dev: move host ahead
This commit is contained in:
Myriad-Dreamin 2024-07-18 13:53:18 +08:00 committed by GitHub
parent 8413c66c51
commit 1d011155cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 257 additions and 222 deletions

View file

@ -380,10 +380,10 @@ where
self
}
pub fn with_command(
pub fn with_command<T: Serialize + 'static>(
mut self,
cmd: &'static str,
handler: fn(&mut Args::S, Vec<JsonValue>) -> AnySchedulableResponse,
handler: fn(&mut Args::S, Vec<JsonValue>) -> SchedulableResponse<T>,
) -> Self {
self.exec_cmds.insert(
cmd,

View file

@ -51,7 +51,10 @@ impl PreviewActor {
};
// Unregister preview early
tab.compile_handler.unregister_preview(&task_id);
let unregistered = tab.compile_handler.unregister_preview(&task_id);
if !unregistered {
log::warn!("PreviewTask({task_id}): failed to unregister preview");
}
let client = self.client.clone();
self.client.handle.spawn(async move {

View file

@ -226,24 +226,27 @@ impl CompileHandler {
Ok(f(&mut analysis))
}
// todo: multiple preview support
#[cfg(feature = "preview")]
pub fn register_preview(&self, handle: Arc<typst_preview::CompileWatcher>) {
// todo: conflict detection
*self.inner.write() = Some(handle);
#[must_use]
pub fn register_preview(&self, handle: &Arc<typst_preview::CompileWatcher>) -> bool {
let mut p = self.inner.write();
if p.as_ref().is_some() {
return false;
}
*p = Some(handle.clone());
true
}
#[cfg(feature = "preview")]
pub fn unregister_preview(&self, task_id: &str) {
#[must_use]
pub fn unregister_preview(&self, task_id: &str) -> bool {
let mut p = self.inner.write();
if p.as_ref().is_some_and(|p| p.task_id() == task_id) {
*p = None;
return true;
}
}
// todo: multiple preview support
#[cfg(feature = "preview")]
pub fn registered_preview(&self) -> bool {
self.inner.read().is_some()
false
}
}

View file

@ -93,7 +93,10 @@ impl LanguageState {
/// Start a preview instance.
#[cfg(feature = "preview")]
pub fn start_preview(&mut self, mut args: Vec<JsonValue>) -> AnySchedulableResponse {
pub fn start_preview(
&mut self,
mut args: Vec<JsonValue>,
) -> SchedulableResponse<crate::tool::preview::StartPreviewResponse> {
use std::path::Path;
use crate::tool::preview::PreviewCliArgs;
@ -104,7 +107,7 @@ impl LanguageState {
let cli_args = ["preview"]
.into_iter()
.chain(cli_args.iter().map(|e| e.as_str()));
let cli_args =
let mut cli_args =
PreviewCliArgs::try_parse_from(cli_args).map_err(|e| invalid_params(e.to_string()))?;
// todo: preview specific arguments are not used
@ -121,17 +124,21 @@ impl LanguageState {
return Err(invalid_params("entry file must be absolute path"));
};
// todo: race condition
let handle = self.primary().handle.clone();
if handle.registered_preview() {
// Disble control plane host
cli_args.preview.control_plane_host = String::default();
let primary = self.primary().handle.clone();
let previewer = typst_preview::PreviewBuilder::new(cli_args.preview.clone());
if !primary.register_preview(previewer.compile_watcher()) {
return Err(internal_error("preview is already running"));
}
// todo: recover pin status reliably
self.pin_entry(Some(entry))
.map_err(|e| internal_error(format!("could not pin file: {e}")))?;
self.preview.start(cli_args, handle)
self.preview.start(cli_args, previewer, primary)
}
/// Kill a preview instance.

View file

@ -18,9 +18,9 @@ use typst::syntax::{LinkedNode, Source, Span, SyntaxKind, VirtualPath};
use typst::World;
pub use typst_preview::CompileStatus;
use typst_preview::{
preview, CompileHost, ControlPlaneMessage, ControlPlaneResponse, DocToSrcJumpInfo,
EditorServer, Location, LspControlPlaneRx, LspControlPlaneTx, MemoryFiles, MemoryFilesShort,
PreviewArgs, PreviewMode, Previewer, SourceFileServer,
CompileHost, ControlPlaneMessage, ControlPlaneResponse, DocToSrcJumpInfo, EditorServer,
Location, LspControlPlaneRx, LspControlPlaneTx, MemoryFiles, MemoryFilesShort, PreviewArgs,
PreviewBuilder, PreviewMode, Previewer, SourceFileServer,
};
use typst_ts_compiler::vfs::notify::{FileChangeSet, MemoryEvent};
use typst_ts_compiler::EntryReader;
@ -224,9 +224,10 @@ impl PreviewState {
}
}
/// Response for starting a preview.
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct StartPreviewResponse {
pub struct StartPreviewResponse {
static_server_port: Option<u16>,
static_server_addr: Option<String>,
data_plane_port: Option<u16>,
@ -236,15 +237,13 @@ impl PreviewState {
/// Start a preview on a given compiler.
pub fn start(
&self,
mut args: PreviewCliArgs,
args: PreviewCliArgs,
mut previewer: PreviewBuilder,
compile_handler: Arc<CompileHandler>,
) -> AnySchedulableResponse {
) -> SchedulableResponse<StartPreviewResponse> {
let task_id = args.preview.task_id.clone();
log::info!("PreviewTask({task_id}): arguments: {args:#?}");
// Disble control plane host
args.preview.control_plane_host = String::default();
let (lsp_tx, lsp_rx) = LspControlPlaneTx::new();
let LspControlPlaneRx {
resp_rx,
@ -253,12 +252,8 @@ impl PreviewState {
} = lsp_rx;
// Create a previewer
let previewer = preview(
args.preview,
compile_handler.clone(),
Some(lsp_tx),
TYPST_PREVIEW_HTML,
);
previewer = previewer.with_lsp_connection(Some(lsp_tx));
let previewer = previewer.start(compile_handler.clone(), TYPST_PREVIEW_HTML);
// Forward preview responses to lsp client
let tid = task_id.clone();
@ -301,7 +296,6 @@ impl PreviewState {
let preview_tx = self.preview_tx.clone();
just_future(async move {
let previewer = previewer.await;
compile_handler.register_preview(previewer.compile_watcher().clone());
// Put a fence to ensure the previewer can receive the first compilation. z
// The fence must be put after the previewer is initialized.
@ -327,7 +321,7 @@ impl PreviewState {
}));
sent.map_err(|_| internal_error("failed to register preview tab"))?;
Ok(serde_json::to_value(resp).unwrap())
Ok(resp)
})
}
@ -496,9 +490,10 @@ pub async fn preview_main(args: PreviewCliArgs) -> anyhow::Result<()> {
(service, handle)
};
let previewer = preview(args.preview, handle.clone(), None, TYPST_PREVIEW_HTML).await;
handle.register_preview(previewer.compile_watcher().clone());
let previewer = PreviewBuilder::new(args.preview);
let registered = handle.register_preview(previewer.compile_watcher());
assert!(registered, "failed to register preview");
let previewer = previewer.start(handle.clone(), TYPST_PREVIEW_HTML).await;
tokio::spawn(service.spawn());
let (static_server_addr, _tx, static_server_handle) =

View file

@ -35,30 +35,7 @@ pub struct TypstActor<T> {
renderer_sender: broadcast::Sender<RenderActorRequest>,
}
type MpScChannel<T> = (mpsc::UnboundedSender<T>, mpsc::UnboundedReceiver<T>);
type BroadcastChannel<T> = (broadcast::Sender<T>, broadcast::Receiver<T>);
pub struct Channels {
pub typst_mailbox: MpScChannel<TypstActorRequest>,
pub renderer_mailbox: BroadcastChannel<RenderActorRequest>,
pub editor_conn: MpScChannel<EditorActorRequest>,
pub webview_conn: BroadcastChannel<WebviewActorRequest>,
}
impl<T> TypstActor<T> {
pub fn set_up_channels() -> Channels {
let typst_mailbox = mpsc::unbounded_channel();
let renderer_mailbox = broadcast::channel(1024);
let editor_conn = mpsc::unbounded_channel();
let webview_conn = broadcast::channel(32);
Channels {
typst_mailbox,
renderer_mailbox,
editor_conn,
webview_conn,
}
}
pub fn new(
client: Arc<T>,
mailbox: mpsc::UnboundedReceiver<TypstActorRequest>,

View file

@ -6,7 +6,9 @@ mod outline;
pub use actor::editor::{
CompileStatus, ControlPlaneMessage, ControlPlaneResponse, LspControlPlaneRx, LspControlPlaneTx,
};
use actor::webview::WebviewActorRequest;
pub use args::*;
use once_cell::sync::OnceCell;
pub use outline::Outline;
use std::pin::Pin;
@ -29,93 +31,7 @@ use typst_ts_core::{ImmutStr, TypstDocument as Document};
use crate::actor::editor::EditorActorRequest;
use crate::actor::render::RenderActorRequest;
use actor::editor::{EditorActor, EditorConnection};
use actor::typst::TypstActor;
#[derive(Debug, Serialize, Deserialize)]
pub struct DocToSrcJumpInfo {
pub filepath: String,
pub start: Option<(usize, usize)>, // row, column
pub end: Option<(usize, usize)>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ChangeCursorPositionRequest {
filepath: PathBuf,
line: usize,
/// fixme: character is 0-based, UTF-16 code unit.
/// We treat it as UTF-8 now.
character: usize,
}
#[derive(Debug, Deserialize)]
pub struct SrcToDocJumpRequest {
filepath: PathBuf,
line: usize,
/// fixme: character is 0-based, UTF-16 code unit.
/// We treat it as UTF-8 now.
character: usize,
}
impl SrcToDocJumpRequest {
pub fn to_byte_offset(&self, src: &typst::syntax::Source) -> Option<usize> {
src.line_column_to_byte(self.line, self.character)
}
}
#[derive(Debug, Deserialize)]
pub struct MemoryFiles {
pub files: HashMap<PathBuf, String>,
}
#[derive(Debug, Deserialize)]
pub struct MemoryFilesShort {
pub files: Vec<PathBuf>,
// mtime: Option<u64>,
}
pub struct CompileWatcher {
task_id: String,
refresh_style: RefreshStyle,
doc_sender: watch::Sender<Option<Arc<Document>>>,
editor_tx: mpsc::UnboundedSender<EditorActorRequest>,
render_tx: broadcast::Sender<RenderActorRequest>,
}
impl CompileWatcher {
pub fn task_id(&self) -> &str {
&self.task_id
}
pub fn status(&self, status: CompileStatus) {
let _ = self
.editor_tx
.send(EditorActorRequest::CompileStatus(status));
}
pub fn notify_compile(&self, res: Result<Arc<Document>, CompileStatus>, is_on_saved: bool) {
if self.refresh_style == RefreshStyle::OnSave && !is_on_saved {
return;
}
match res {
Ok(doc) => {
// it is ok to ignore the error here
let _ = self.doc_sender.send(Some(doc));
// todo: is it right that ignore zero broadcast receiver?
let _ = self.render_tx.send(RenderActorRequest::RenderIncremental);
let _ = self.editor_tx.send(EditorActorRequest::CompileStatus(
CompileStatus::CompileSuccess,
));
}
Err(status) => {
let _ = self
.editor_tx
.send(EditorActorRequest::CompileStatus(status));
}
}
}
}
use actor::typst::{TypstActor, TypstActorRequest};
type StopFuture = Pin<Box<dyn Future<Output = ()> + Send + Sync>>;
@ -125,7 +41,6 @@ pub struct Previewer {
data_plane_handle: tokio::task::JoinHandle<()>,
control_plane_handle: tokio::task::JoinHandle<()>,
data_plane_port: u16,
compile_watcher: Arc<CompileWatcher>,
}
impl Previewer {
@ -138,10 +53,6 @@ impl Previewer {
self.data_plane_port
}
pub fn compile_watcher(&self) -> &Arc<CompileWatcher> {
&self.compile_watcher
}
/// Join the previewer actors.
pub async fn join(self) {
let _ = tokio::join!(self.data_plane_handle, self.control_plane_handle);
@ -154,86 +65,38 @@ impl Previewer {
}
}
pub type SourceLocation = typst_ts_core::debug_loc::SourceLocation;
pub enum Location {
Src(SourceLocation),
}
pub trait SourceFileServer {
fn resolve_source_span(
&self,
_by: Location,
) -> impl Future<Output = Result<Option<SourceSpanOffset>, Error>> + Send {
async { Ok(None) }
}
fn resolve_document_position(
&self,
_by: Location,
) -> impl Future<Output = Result<Option<Position>, Error>> + Send {
async { Ok(None) }
}
fn resolve_source_location(
&self,
_s: Span,
_offset: Option<usize>,
) -> impl Future<Output = Result<Option<DocToSrcJumpInfo>, Error>> + Send {
async { Ok(None) }
}
}
pub trait EditorServer {
fn update_memory_files(
&self,
_files: MemoryFiles,
_reset_shadow: bool,
) -> impl Future<Output = Result<(), Error>> + Send {
async { Ok(()) }
}
fn remove_shadow_files(
&self,
_files: MemoryFilesShort,
) -> impl Future<Output = Result<(), Error>> + Send {
async { Ok(()) }
}
}
pub trait CompileHost: SourceFileServer + EditorServer {}
pub async fn preview<T: CompileHost + Send + Sync + 'static>(
arguments: PreviewArgs,
client: Arc<T>,
lsp_connection: Option<LspControlPlaneTx>,
html: &str,
) -> Previewer {
let enable_partial_rendering = arguments.enable_partial_rendering;
let invert_colors = arguments.invert_colors;
let idle_timeout = Duration::from_secs(5);
PreviewBuilder::new(arguments).start(client, html).await
}
// Creates the world that serves sources, fonts and files.
let actor::typst::Channels {
async fn preview_<T: CompileHost + Send + Sync + 'static>(
builder: PreviewBuilder,
client: Arc<T>,
html: &str,
) -> Previewer {
let PreviewBuilder {
arguments,
lsp_connection,
typst_mailbox,
renderer_mailbox,
editor_conn,
webview_conn: (webview_tx, _),
} = TypstActor::<()>::set_up_channels();
doc_sender,
..
} = builder;
let enable_partial_rendering = arguments.enable_partial_rendering;
let invert_colors = arguments.invert_colors;
let idle_timeout = Duration::from_secs(5);
// Shared resource
let span_interner = SpanInterner::new();
// Set callback
let doc_watcher = watch::channel::<Option<Arc<Document>>>(None);
let compile_watcher = CompileWatcher {
task_id: arguments.task_id,
refresh_style: arguments.refresh_style,
doc_sender: doc_watcher.0,
editor_tx: editor_conn.0.clone(),
render_tx: renderer_mailbox.0.clone(),
};
// Spawns the typst actor
let typst_actor = TypstActor::new(
client,
@ -309,7 +172,7 @@ pub async fn preview<T: CompileHost + Send + Sync + 'static>(
});
let render_actor = actor::render::RenderActor::new(
renderer_tx.subscribe(),
doc_watcher.1.clone(),
doc_sender.subscribe(),
typst_tx,
svg.0,
webview_tx,
@ -317,7 +180,7 @@ pub async fn preview<T: CompileHost + Send + Sync + 'static>(
tokio::spawn(render_actor.run());
let outline_render_actor = actor::render::OutlineRenderActor::new(
renderer_tx.subscribe(),
doc_watcher.1.clone(),
doc_sender.subscribe(),
editor_tx.clone(),
span_interner,
);
@ -415,7 +278,194 @@ pub async fn preview<T: CompileHost + Send + Sync + 'static>(
control_plane_handle,
data_plane_port,
stop: Some(Box::new(stop)),
compile_watcher: Arc::new(compile_watcher),
}
}
type MpScChannel<T> = (mpsc::UnboundedSender<T>, mpsc::UnboundedReceiver<T>);
type BroadcastChannel<T> = (broadcast::Sender<T>, broadcast::Receiver<T>);
pub struct PreviewBuilder {
arguments: PreviewArgs,
lsp_connection: Option<LspControlPlaneTx>,
typst_mailbox: MpScChannel<TypstActorRequest>,
renderer_mailbox: BroadcastChannel<RenderActorRequest>,
editor_conn: MpScChannel<EditorActorRequest>,
webview_conn: BroadcastChannel<WebviewActorRequest>,
doc_sender: watch::Sender<Option<Arc<Document>>>,
compile_watcher: OnceCell<Arc<CompileWatcher>>,
}
impl PreviewBuilder {
pub fn new(arguments: PreviewArgs) -> Self {
Self {
arguments,
lsp_connection: None,
typst_mailbox: mpsc::unbounded_channel(),
renderer_mailbox: broadcast::channel(1024),
editor_conn: mpsc::unbounded_channel(),
webview_conn: broadcast::channel(32),
doc_sender: watch::channel(None).0,
compile_watcher: OnceCell::new(),
}
}
pub fn with_lsp_connection(mut self, lsp_connection: Option<LspControlPlaneTx>) -> Self {
self.lsp_connection = lsp_connection;
self
}
pub fn compile_watcher(&self) -> &Arc<CompileWatcher> {
self.compile_watcher.get_or_init(|| {
Arc::new(CompileWatcher {
task_id: self.arguments.task_id.clone(),
refresh_style: self.arguments.refresh_style,
doc_sender: self.doc_sender.clone(),
editor_tx: self.editor_conn.0.clone(),
render_tx: self.renderer_mailbox.0.clone(),
})
})
}
pub async fn start<T>(self, client: Arc<T>, html: &str) -> Previewer
where
T: CompileHost + Send + Sync + 'static,
{
preview_(self, client, html).await
}
}
pub type SourceLocation = typst_ts_core::debug_loc::SourceLocation;
pub enum Location {
Src(SourceLocation),
}
pub trait SourceFileServer {
fn resolve_source_span(
&self,
_by: Location,
) -> impl Future<Output = Result<Option<SourceSpanOffset>, Error>> + Send {
async { Ok(None) }
}
fn resolve_document_position(
&self,
_by: Location,
) -> impl Future<Output = Result<Option<Position>, Error>> + Send {
async { Ok(None) }
}
fn resolve_source_location(
&self,
_s: Span,
_offset: Option<usize>,
) -> impl Future<Output = Result<Option<DocToSrcJumpInfo>, Error>> + Send {
async { Ok(None) }
}
}
pub trait EditorServer {
fn update_memory_files(
&self,
_files: MemoryFiles,
_reset_shadow: bool,
) -> impl Future<Output = Result<(), Error>> + Send {
async { Ok(()) }
}
fn remove_shadow_files(
&self,
_files: MemoryFilesShort,
) -> impl Future<Output = Result<(), Error>> + Send {
async { Ok(()) }
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DocToSrcJumpInfo {
pub filepath: String,
pub start: Option<(usize, usize)>, // row, column
pub end: Option<(usize, usize)>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ChangeCursorPositionRequest {
filepath: PathBuf,
line: usize,
/// fixme: character is 0-based, UTF-16 code unit.
/// We treat it as UTF-8 now.
character: usize,
}
#[derive(Debug, Deserialize)]
pub struct SrcToDocJumpRequest {
filepath: PathBuf,
line: usize,
/// fixme: character is 0-based, UTF-16 code unit.
/// We treat it as UTF-8 now.
character: usize,
}
impl SrcToDocJumpRequest {
pub fn to_byte_offset(&self, src: &typst::syntax::Source) -> Option<usize> {
src.line_column_to_byte(self.line, self.character)
}
}
#[derive(Debug, Deserialize)]
pub struct MemoryFiles {
pub files: HashMap<PathBuf, String>,
}
#[derive(Debug, Deserialize)]
pub struct MemoryFilesShort {
pub files: Vec<PathBuf>,
// mtime: Option<u64>,
}
pub struct CompileWatcher {
task_id: String,
refresh_style: RefreshStyle,
doc_sender: watch::Sender<Option<Arc<Document>>>,
editor_tx: mpsc::UnboundedSender<EditorActorRequest>,
render_tx: broadcast::Sender<RenderActorRequest>,
}
impl CompileWatcher {
pub fn task_id(&self) -> &str {
&self.task_id
}
pub fn status(&self, status: CompileStatus) {
let _ = self
.editor_tx
.send(EditorActorRequest::CompileStatus(status));
}
pub fn notify_compile(&self, res: Result<Arc<Document>, CompileStatus>, is_on_saved: bool) {
if self.refresh_style == RefreshStyle::OnSave && !is_on_saved {
return;
}
match res {
Ok(doc) => {
// it is ok to ignore the error here
let _ = self.doc_sender.send(Some(doc));
// todo: is it right that ignore zero broadcast receiver?
let _ = self.render_tx.send(RenderActorRequest::RenderIncremental);
let _ = self.editor_tx.send(EditorActorRequest::CompileStatus(
CompileStatus::CompileSuccess,
));
}
Err(status) => {
let _ = self
.editor_tx
.send(EditorActorRequest::CompileStatus(status));
}
}
}
}