diff --git a/crates/tinymist-query/src/lib.rs b/crates/tinymist-query/src/lib.rs index 3e855c84..f16e6ae3 100644 --- a/crates/tinymist-query/src/lib.rs +++ b/crates/tinymist-query/src/lib.rs @@ -132,17 +132,15 @@ mod polymorphic { use super::*; #[derive(Debug, Clone, Copy, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] pub enum PageSelection { - #[serde(rename = "first")] First, - #[serde(rename = "merged")] Merged, } #[derive(Debug, Clone)] pub enum ExportKind { Pdf, - WordCount, Svg { page: PageSelection }, Png { page: PageSelection }, } @@ -151,7 +149,6 @@ mod polymorphic { pub fn extension(&self) -> &str { match self { Self::Pdf => "pdf", - Self::WordCount => "txt", Self::Svg { .. } => "svg", Self::Png { .. } => "png", } @@ -179,12 +176,11 @@ mod polymorphic { pub struct ServerInfoRequest {} #[derive(Debug, Clone, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] pub struct ServerInfoResponse { pub root: Option, - #[serde(rename = "fontPaths")] pub font_paths: Vec, pub inputs: Dict, - #[serde(rename = "estimatedMemoryUsage")] pub estimated_memory_usage: HashMap, } diff --git a/crates/tinymist-render/src/lib.rs b/crates/tinymist-render/src/lib.rs index 1b8f3359..6fed1425 100644 --- a/crates/tinymist-render/src/lib.rs +++ b/crates/tinymist-render/src/lib.rs @@ -27,17 +27,15 @@ impl ExportFeature for PeriscopeExportFeature { /// The arguments for periscope renderer. #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] pub struct PeriscopeArgs { /// The distance above the center line. - #[serde(rename = "yAbove")] pub y_above: f32, /// The distance below the center line. - #[serde(rename = "yBelow")] pub y_below: f32, /// The scale of the image. pub scale: f32, /// Whether to invert the color. (will become smarter in the future) - #[serde(rename = "invertColor")] pub invert_color: String, } diff --git a/crates/tinymist/src/actor/cluster.rs b/crates/tinymist/src/actor/editor.rs similarity index 84% rename from crates/tinymist/src/actor/cluster.rs rename to crates/tinymist/src/actor/editor.rs index e9b94fa9..e82fbf97 100644 --- a/crates/tinymist/src/actor/cluster.rs +++ b/crates/tinymist/src/actor/editor.rs @@ -1,4 +1,4 @@ -//! The cluster actor running in background +//! The actor that send notifications to the client. use std::collections::HashMap; @@ -9,29 +9,44 @@ use tokio::sync::mpsc; use crate::{tools::word_count::WordsCount, LspHost, TypstLanguageServer}; -pub enum CompileClusterRequest { +pub enum EditorRequest { Diag(String, Option), Status(String, TinymistCompileStatusEnum), - WordCount(String, Option), + WordCount(String, WordsCount), } pub struct EditorActor { - pub host: LspHost, - pub diag_rx: mpsc::UnboundedReceiver, + host: LspHost, + editor_rx: mpsc::UnboundedReceiver, - pub diagnostics: HashMap>>, - pub affect_map: HashMap>, - pub published_primary: bool, - pub notify_compile_status: bool, + diagnostics: HashMap>>, + affect_map: HashMap>, + published_primary: bool, + notify_compile_status: bool, } impl EditorActor { + pub fn new( + host: LspHost, + editor_rx: mpsc::UnboundedReceiver, + notify_compile_status: bool, + ) -> Self { + Self { + host, + editor_rx, + diagnostics: HashMap::new(), + affect_map: HashMap::new(), + published_primary: false, + notify_compile_status, + } + } + pub async fn run(mut self) { let mut compile_status = TinymistCompileStatusEnum::Compiling; let mut words_count = None; - while let Some(req) = self.diag_rx.recv().await { + while let Some(req) = self.editor_rx.recv().await { match req { - CompileClusterRequest::Diag(group, diagnostics) => { + EditorRequest::Diag(group, diagnostics) => { info!( "received diagnostics from {group}: diag({:?})", diagnostics.as_ref().map(|e| e.len()) @@ -52,7 +67,7 @@ impl EditorActor { self.published_primary = again_with_primary; } } - CompileClusterRequest::Status(group, status) => { + EditorRequest::Status(group, status) => { log::debug!("received status request"); if self.notify_compile_status && group == "primary" { compile_status = status; @@ -64,10 +79,10 @@ impl EditorActor { ); } } - CompileClusterRequest::WordCount(group, wc) => { + EditorRequest::WordCount(group, wc) => { log::debug!("received word count request"); if self.notify_compile_status && group == "primary" { - words_count = wc; + words_count = Some(wc); self.host.send_notification::( TinymistCompileStatus { status: compile_status.clone(), @@ -170,9 +185,9 @@ pub enum TinymistCompileStatusEnum { } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] pub struct TinymistCompileStatus { pub status: TinymistCompileStatusEnum, - #[serde(rename = "wordsCount")] pub words_count: Option, } diff --git a/crates/tinymist/src/actor/export.rs b/crates/tinymist/src/actor/export.rs new file mode 100644 index 00000000..3d6e18d7 --- /dev/null +++ b/crates/tinymist/src/actor/export.rs @@ -0,0 +1,269 @@ +//! The actor that handles PDF export. + +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; + +use anyhow::bail; +use anyhow::Context; +use log::{error, info}; +use once_cell::sync::Lazy; +use tinymist_query::{ExportKind, PageSelection}; +use tokio::sync::{mpsc, oneshot, watch}; +use typst::{foundations::Smart, layout::Abs, layout::Frame, visualize::Color}; +use typst_ts_core::{config::compiler::EntryState, path::PathClean, ImmutPath, TypstDocument}; + +use crate::{tools::word_count, ExportMode}; + +use super::editor::EditorRequest; + +#[derive(Debug, Clone, Default)] +pub struct ExportConfig { + pub substitute_pattern: String, + pub entry: EntryState, + pub mode: ExportMode, +} + +#[derive(Debug)] +pub enum ExportRequest { + OnTyped, + OnSaved(PathBuf), + Oneshot(Option, oneshot::Sender>), + ChangeConfig(ExportConfig), + ChangeExportPath(EntryState), +} + +pub struct ExportActor { + group: String, + editor_tx: mpsc::UnboundedSender, + export_rx: mpsc::UnboundedReceiver, + document: watch::Receiver>>, + + config: ExportConfig, + kind: ExportKind, + count_words: bool, +} + +impl ExportActor { + pub fn new( + group: String, + document: watch::Receiver>>, + editor_tx: mpsc::UnboundedSender, + export_rx: mpsc::UnboundedReceiver, + config: ExportConfig, + kind: ExportKind, + count_words: bool, + ) -> Self { + Self { + group, + editor_tx, + export_rx, + document, + config, + kind, + count_words, + } + } + + pub async fn run(mut self) { + while let Some(mut req) = self.export_rx.recv().await { + let Some(doc) = self.document.borrow().clone() else { + info!("RenderActor: document is not ready"); + continue; + }; + + let mut need_export = false; + + 'accumulate: loop { + log::debug!("RenderActor: received request: {req:?}"); + match req { + ExportRequest::ChangeConfig(cfg) => self.config = cfg, + ExportRequest::ChangeExportPath(entry) => self.config.entry = entry, + ExportRequest::OnTyped => need_export |= self.config.mode == ExportMode::OnType, + ExportRequest::OnSaved(..) => match self.config.mode { + ExportMode::OnSave => need_export = true, + ExportMode::OnDocumentHasTitle => need_export |= doc.title.is_some(), + _ => {} + }, + ExportRequest::Oneshot(kind, callback) => { + // Do oneshot export instantly without accumulation. + let kind = kind.as_ref().unwrap_or(&self.kind); + let resp = self.check_mode_and_export(kind, &doc).await; + if let Err(err) = callback.send(resp) { + error!("RenderActor(@{kind:?}): failed to send response: {err:?}"); + } + } + } + + // Try to accumulate more requests. + match self.export_rx.try_recv() { + Ok(new_req) => req = new_req, + _ => break 'accumulate, + } + } + + if need_export { + self.check_mode_and_export(&self.kind, &doc).await; + } + + if self.count_words { + let wc = word_count::word_count(&doc); + log::debug!("word count: {wc:?}"); + let _ = self + .editor_tx + .send(EditorRequest::WordCount(self.group.clone(), wc)); + } + } + info!("RenderActor(@{:?}): stopped", &self.kind); + } + + async fn check_mode_and_export( + &self, + kind: &ExportKind, + doc: &TypstDocument, + ) -> Option { + // pub entry: EntryState, + let root = self.config.entry.root(); + let main = self.config.entry.main(); + + info!( + "RenderActor: check path {:?} and root {:?} with output directory {}", + main, root, self.config.substitute_pattern + ); + + let root = root?; + let main = main?; + + // todo: package?? + if main.package().is_some() { + return None; + } + + let path = main.vpath().resolve(&root)?; + + match self.export(kind, doc, &root, &path).await { + Ok(pdf) => Some(pdf), + Err(err) => { + error!("RenderActor({kind:?}): failed to export {err}"); + None + } + } + } + + async fn export( + &self, + kind: &ExportKind, + doc: &TypstDocument, + root: &Path, + path: &Path, + ) -> anyhow::Result { + use ExportKind::*; + use PageSelection::*; + + let Some(to) = substitute_path(&self.config.substitute_pattern, root, path) else { + bail!("RenderActor({kind:?}): failed to substitute path"); + }; + if to.is_relative() { + bail!("RenderActor({kind:?}): path is relative: {to:?}"); + } + if to.is_dir() { + bail!("RenderActor({kind:?}): path is a directory: {to:?}"); + } + + let to = to.with_extension(kind.extension()); + info!("RenderActor({kind:?}): exporting {path:?} to {to:?}"); + + if let Some(e) = to.parent() { + if !e.exists() { + std::fs::create_dir_all(e).with_context(|| { + format!("RenderActor({kind:?}): failed to create directory") + })?; + } + } + + static BLANK: Lazy = Lazy::new(Frame::default); + let first_frame = || doc.pages.first().map(|f| &f.frame).unwrap_or(&*BLANK); + let data = match kind { + Pdf => { + // todo: Some(pdf_uri.as_str()) + // todo: timestamp world.now() + typst_pdf::pdf(doc, Smart::Auto, None) + } + Svg { page: First } => typst_svg::svg(first_frame()).into_bytes(), + Svg { page: Merged } => typst_svg::svg_merged(doc, Abs::zero()).into_bytes(), + Png { page: First } => typst_render::render(first_frame(), 3., Color::WHITE) + .encode_png() + .map_err(|err| anyhow::anyhow!("failed to encode PNG ({err})"))?, + Png { page: Merged } => { + typst_render::render_merged(doc, 3., Color::WHITE, Abs::zero(), Color::WHITE) + .encode_png() + .map_err(|err| anyhow::anyhow!("failed to encode PNG ({err})"))? + } + }; + + std::fs::write(&to, data) + .with_context(|| format!("RenderActor({kind:?}): failed to export"))?; + + info!("RenderActor({kind:?}): export complete"); + Ok(to) + } +} + +#[comemo::memoize] +fn substitute_path(substitute_pattern: &str, root: &Path, path: &Path) -> Option { + if let Ok(path) = path.strip_prefix("/untitled") { + let tmp = std::env::temp_dir(); + let path = tmp.join("typst").join(path); + return Some(path.as_path().into()); + } + + if substitute_pattern.is_empty() { + return Some(path.to_path_buf().clean().into()); + } + + let path = path.strip_prefix(root).ok()?; + let dir = path.parent(); + let file_name = path.file_name().unwrap_or_default(); + + let w = root.to_string_lossy(); + let f = file_name.to_string_lossy(); + + // replace all $root + let mut path = substitute_pattern.replace("$root", &w); + if let Some(dir) = dir { + let d = dir.to_string_lossy(); + path = path.replace("$dir", &d); + } + path = path.replace("$name", &f); + + Some(PathBuf::from(path).clean().into()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_substitute_path() { + let root = Path::new("/root"); + let path = Path::new("/root/dir1/dir2/file.txt"); + + assert_eq!( + substitute_path("/substitute/$dir/$name", root, path), + Some(PathBuf::from("/substitute/dir1/dir2/file.txt").into()) + ); + assert_eq!( + substitute_path("/substitute/$dir/../$name", root, path), + Some(PathBuf::from("/substitute/dir1/file.txt").into()) + ); + assert_eq!( + substitute_path("/substitute/$name", root, path), + Some(PathBuf::from("/substitute/file.txt").into()) + ); + assert_eq!( + substitute_path("/substitute/target/$dir/$name", root, path), + Some(PathBuf::from("/substitute/target/dir1/dir2/file.txt").into()) + ); + } +} diff --git a/crates/tinymist/src/actor/formatting.rs b/crates/tinymist/src/actor/format.rs similarity index 63% rename from crates/tinymist/src/actor/formatting.rs rename to crates/tinymist/src/actor/format.rs index 2da54283..5ea9fe0c 100644 --- a/crates/tinymist/src/actor/formatting.rs +++ b/crates/tinymist/src/actor/format.rs @@ -1,66 +1,63 @@ +//! The actor that handles formatting. + +use std::iter::zip; + use lsp_server::RequestId; use lsp_types::TextEdit; use tinymist_query::{typst_to_lsp, PositionEncoding}; use typst::syntax::Source; -use crate::{result_to_response_, FormatterMode, LspHost, LspResult, TypstLanguageServer}; +use crate::{result_to_response, FormatterMode, LspHost, LspResult, TypstLanguageServer}; #[derive(Debug, Clone)] -pub struct FormattingConfig { +pub struct FormatConfig { pub mode: FormatterMode, pub width: u32, } -pub enum FormattingRequest { - ChangeConfig(FormattingConfig), - Formatting((RequestId, Source)), +pub enum FormatRequest { + ChangeConfig(FormatConfig), + Format(RequestId, Source), } pub fn run_format_thread( - init_c: FormattingConfig, - rx_req: crossbeam_channel::Receiver, + config: FormatConfig, + format_rx: crossbeam_channel::Receiver, client: LspHost, position_encoding: PositionEncoding, ) { type FmtFn = Box LspResult>>>; - let compile = |c: FormattingConfig| -> FmtFn { + let compile = |c: FormatConfig| -> FmtFn { log::info!("formatting thread with config: {c:#?}"); match c.mode { FormatterMode::Typstyle => { let cw = c.width as usize; - let f: FmtFn = Box::new(move |e: Source| { + Box::new(move |e: Source| { let res = typstyle_core::Typstyle::new_with_src(e.clone(), cw).pretty_print(); Ok(calc_diff(e, res, position_encoding)) - }); - f + }) } FormatterMode::Typstfmt => { let config = typstfmt_lib::Config { max_line_length: c.width as usize, ..typstfmt_lib::Config::default() }; - let f: FmtFn = Box::new(move |e: Source| { + Box::new(move |e: Source| { let res = typstfmt_lib::format(e.text(), config); Ok(calc_diff(e, res, position_encoding)) - }); - f - } - FormatterMode::Disable => { - let f: FmtFn = Box::new(|_| Ok(None)); - f + }) } + FormatterMode::Disable => Box::new(|_| Ok(None)), } }; - let mut f: FmtFn = compile(init_c); - while let Ok(req) = rx_req.recv() { + let mut f: FmtFn = compile(config); + while let Ok(req) = format_rx.recv() { match req { - FormattingRequest::ChangeConfig(c) => f = compile(c), - FormattingRequest::Formatting((id, source)) => { + FormatRequest::ChangeConfig(c) => f = compile(c), + FormatRequest::Format(id, source) => { let res = f(source); - if let Ok(response) = result_to_response_(id, res) { - client.respond(response); - } + client.respond(result_to_response(id, res)); } } } @@ -74,10 +71,7 @@ fn calc_diff(prev: Source, next: String, encoding: PositionEncoding) -> Option Option; impl CompileServer { @@ -47,41 +44,25 @@ impl CompileServer { snapshot: FileChangeSet, ) -> CompileClientActor { let (doc_tx, doc_rx) = watch::channel(None); - let (render_tx, _) = broadcast::channel(10); - - let config = ExportConfig { - substitute_pattern: self.config.output_path.clone(), - entry: entry.clone(), - mode: self.config.export_pdf, - }; + let (export_tx, export_rx) = mpsc::unbounded_channel(); // Run Export actors before preparing cluster to avoid loss of events self.handle.spawn( ExportActor::new( editor_group.clone(), - doc_rx.clone(), - self.diag_tx.clone(), - render_tx.subscribe(), - config.clone(), + doc_rx, + self.editor_tx.clone(), + export_rx, + ExportConfig { + substitute_pattern: self.config.output_path.clone(), + entry: entry.clone(), + mode: self.config.export_pdf, + }, ExportKind::Pdf, + self.config.notify_compile_status, ) .run(), ); - if self.config.notify_compile_status { - let mut config = config; - config.mode = ExportMode::OnType; - self.handle.spawn( - ExportActor::new( - editor_group.clone(), - doc_rx.clone(), - self.diag_tx.clone(), - render_tx.subscribe(), - config, - ExportKind::WordCount, - ) - .run(), - ); - } // Create the server let inner = Deferred::new({ @@ -91,8 +72,8 @@ impl CompileServer { inner: std::sync::Arc::new(parking_lot::Mutex::new(None)), diag_group: editor_group.clone(), doc_tx, - render_tx: render_tx.clone(), - editor_tx: self.diag_tx.clone(), + export_tx: export_tx.clone(), + editor_tx: self.editor_tx.clone(), }; let position_encoding = self.const_config().position_encoding; @@ -138,7 +119,7 @@ impl CompileServer { } }); - CompileClientActor::new(editor_group, self.config.clone(), entry, inner, render_tx) + CompileClientActor::new(editor_group, self.config.clone(), entry, inner, export_tx) } } @@ -161,24 +142,23 @@ impl TypstLanguageServer { } let (tx_req, rx_req) = crossbeam_channel::unbounded(); - self.format_thread = Some(tx_req.clone()); + self.format_thread = Some(tx_req); let client = self.client.clone(); let mode = self.config.formatter; let enc = self.const_config.position_encoding; - std::thread::spawn(move || { - run_format_thread(FormattingConfig { mode, width: 120 }, rx_req, client, enc) - }); + let config = format::FormatConfig { mode, width: 120 }; + std::thread::spawn(move || run_format_thread(config, rx_req, client, enc)); } pub fn run_user_action_thread(&mut self) { - if self.user_action_threads.is_some() { + if self.user_action_thread.is_some() { log::error!("user action threads are already started"); return; } let (tx_req, rx_req) = crossbeam_channel::unbounded(); - self.user_action_threads = Some(tx_req.clone()); + self.user_action_thread = Some(tx_req); let client = self.client.clone(); std::thread::spawn(move || run_user_action_thread(rx_req, client)); diff --git a/crates/tinymist/src/actor/render.rs b/crates/tinymist/src/actor/render.rs deleted file mode 100644 index 71e7099c..00000000 --- a/crates/tinymist/src/actor/render.rs +++ /dev/null @@ -1,349 +0,0 @@ -//! The (PDF) render actor - -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; - -use anyhow::Context; -use log::{error, info}; -use once_cell::sync::Lazy; -use parking_lot::Mutex; -use tinymist_query::{ExportKind, PageSelection}; -use tokio::sync::{ - broadcast::{self, error::RecvError}, - mpsc, oneshot, watch, -}; -use typst::{foundations::Smart, layout::Frame}; -use typst_ts_core::{config::compiler::EntryState, path::PathClean, ImmutPath, TypstDocument}; - -use crate::{tools::word_count, ExportMode}; - -use super::cluster::CompileClusterRequest; - -#[derive(Debug, Clone)] -pub struct OneshotRendering { - pub kind: Option, - // todo: bad arch... - pub callback: Arc>>>>, -} - -#[derive(Debug, Clone)] -pub enum RenderActorRequest { - OnTyped, - Oneshot(OneshotRendering), - OnSaved(PathBuf), - ChangeExportPath(PathVars), - ChangeConfig(ExportConfig), -} - -#[derive(Debug, Clone)] -pub struct PathVars { - pub entry: EntryState, -} - -#[derive(Debug, Clone, Default)] -pub struct ExportConfig { - pub substitute_pattern: String, - pub entry: EntryState, - pub mode: ExportMode, -} - -pub struct ExportActor { - group: String, - editor_tx: mpsc::UnboundedSender, - render_rx: broadcast::Receiver, - document: watch::Receiver>>, - - pub substitute_pattern: String, - pub entry: EntryState, - pub mode: ExportMode, - pub kind: ExportKind, -} - -impl ExportActor { - pub fn new( - group: String, - document: watch::Receiver>>, - editor_tx: mpsc::UnboundedSender, - render_rx: broadcast::Receiver, - config: ExportConfig, - kind: ExportKind, - ) -> Self { - Self { - group, - editor_tx, - render_rx, - document, - substitute_pattern: config.substitute_pattern, - entry: config.entry, - mode: config.mode, - kind, - } - } - - pub async fn run(mut self) { - let kind = &self.kind; - loop { - let req = match self.render_rx.recv().await { - Ok(req) => req, - Err(RecvError::Closed) => { - info!("RenderActor(@{kind:?}): channel closed"); - break; - } - Err(RecvError::Lagged(_)) => { - info!("RenderActor(@{kind:?}): channel lagged"); - continue; - } - }; - - log::debug!("RenderActor: received request: {req:?}"); - match req { - RenderActorRequest::ChangeConfig(cfg) => { - self.substitute_pattern = cfg.substitute_pattern; - self.entry = cfg.entry; - self.mode = cfg.mode; - } - RenderActorRequest::ChangeExportPath(cfg) => { - self.entry = cfg.entry; - } - _ => { - let cb = match &req { - RenderActorRequest::Oneshot(oneshot) => Some(oneshot.callback.clone()), - _ => None, - }; - let resp = self.check_mode_and_export(req).await; - if let Some(cb) = cb { - let Some(cb) = cb.lock().take() else { - error!("RenderActor(@{kind:?}): oneshot.callback is None"); - continue; - }; - if let Err(e) = cb.send(resp) { - error!("RenderActor(@{kind:?}): failed to send response: {e:?}"); - } - } - } - } - } - info!("RenderActor(@{kind:?}): stopped"); - } - - async fn check_mode_and_export(&self, req: RenderActorRequest) -> Option { - let Some(document) = self.document.borrow().clone() else { - info!("RenderActor: document is not ready"); - return None; - }; - - let eq_mode = match req { - RenderActorRequest::OnTyped => ExportMode::OnType, - RenderActorRequest::Oneshot(..) => ExportMode::OnSave, - RenderActorRequest::OnSaved(..) => ExportMode::OnSave, - _ => unreachable!(), - }; - - let kind = match &req { - RenderActorRequest::Oneshot(oneshot) => oneshot.kind.as_ref(), - _ => None, - }; - let kind = kind.unwrap_or(&self.kind); - - // pub entry: EntryState, - let root = self.entry.root(); - let main = self.entry.main(); - - info!( - "RenderActor: check path {:?} and root {:?} with output directory {}", - main, root, self.substitute_pattern - ); - - let root = root?; - let main = main?; - - // todo: package?? - if main.package().is_some() { - return None; - } - - let path = main.vpath().resolve(&root)?; - - let should_do = matches!(req, RenderActorRequest::Oneshot(..)) || eq_mode == self.mode || { - let mode = self.mode; - info!( - "RenderActor: validating document for export mode {mode:?} title is {title}", - title = document.title.is_some() - ); - mode == ExportMode::OnDocumentHasTitle - && document.title.is_some() - && matches!(req, RenderActorRequest::OnSaved(..)) - }; - if should_do { - return match self.export(kind, &document, &root, &path).await { - Ok(pdf) => Some(pdf), - Err(err) => { - error!("RenderActor({kind:?}): failed to export {err}"); - None - } - }; - } - - None - } - - async fn export( - &self, - kind: &ExportKind, - doc: &TypstDocument, - root: &Path, - path: &Path, - ) -> anyhow::Result { - let Some(to) = substitute_path(&self.substitute_pattern, root, path) else { - return Err(anyhow::anyhow!( - "RenderActor({kind:?}): failed to substitute path" - )); - }; - if to.is_relative() { - return Err(anyhow::anyhow!( - "RenderActor({kind:?}): path is relative: {to:?}" - )); - } - if to.is_dir() { - return Err(anyhow::anyhow!( - "RenderActor({kind:?}): path is a directory: {to:?}" - )); - } - - let to = to.with_extension(kind.extension()); - info!("RenderActor({kind:?}): exporting {path:?} to {to:?}"); - - if let Some(e) = to.parent() { - if !e.exists() { - std::fs::create_dir_all(e).with_context(|| { - format!("RenderActor({kind:?}): failed to create directory") - })?; - } - } - - static DEFAULT_FRAME: Lazy = Lazy::new(Frame::default); - let data = match kind { - ExportKind::Pdf => { - // todo: Some(pdf_uri.as_str()) - // todo: timestamp world.now() - typst_pdf::pdf(doc, Smart::Auto, None) - } - ExportKind::Svg { - page: PageSelection::First, - } => typst_svg::svg( - doc.pages - .first() - .map(|f| &f.frame) - .unwrap_or(&*DEFAULT_FRAME), - ) - .into_bytes(), - ExportKind::Svg { - page: PageSelection::Merged, - } => typst_svg::svg_merged(doc, typst::layout::Abs::zero()).into_bytes(), - ExportKind::Png { - page: PageSelection::First, - } => { - let pixmap = typst_render::render( - doc.pages - .first() - .map(|f| &f.frame) - .unwrap_or(&*DEFAULT_FRAME), - 3., - typst::visualize::Color::WHITE, - ); - pixmap - .encode_png() - .map_err(|err| anyhow::anyhow!("failed to encode PNG ({err})"))? - } - ExportKind::Png { - page: PageSelection::Merged, - } => { - let pixmap = typst_render::render_merged( - doc, - 3., - typst::visualize::Color::WHITE, - typst::layout::Abs::zero(), - typst::visualize::Color::WHITE, - ); - pixmap - .encode_png() - .map_err(|err| anyhow::anyhow!("failed to encode PNG ({err})"))? - } - ExportKind::WordCount => { - let wc = word_count::word_count(doc); - log::debug!("word count: {wc:?}"); - let _ = self.editor_tx.send(CompileClusterRequest::WordCount( - self.group.clone(), - Some(wc), - )); - return Ok(PathBuf::new()); - } - }; - - std::fs::write(&to, data) - .with_context(|| format!("RenderActor({kind:?}): failed to export"))?; - - info!("RenderActor({kind:?}): export complete"); - Ok(to) - } -} - -#[comemo::memoize] -fn substitute_path(substitute_pattern: &str, root: &Path, path: &Path) -> Option { - if let Ok(path) = path.strip_prefix("/untitled") { - let tmp = std::env::temp_dir(); - let path = tmp.join("typst").join(path); - return Some(path.as_path().into()); - } - - if substitute_pattern.is_empty() { - return Some(path.to_path_buf().clean().into()); - } - - let path = path.strip_prefix(root).ok()?; - let dir = path.parent(); - let file_name = path.file_name().unwrap_or_default(); - - let w = root.to_string_lossy(); - let f = file_name.to_string_lossy(); - - // replace all $root - let mut path = substitute_pattern.replace("$root", &w); - if let Some(dir) = dir { - let d = dir.to_string_lossy(); - path = path.replace("$dir", &d); - } - path = path.replace("$name", &f); - - Some(PathBuf::from(path).clean().into()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_substitute_path() { - let root = Path::new("/root"); - let path = Path::new("/root/dir1/dir2/file.txt"); - - assert_eq!( - substitute_path("/substitute/$dir/$name", root, path), - Some(PathBuf::from("/substitute/dir1/dir2/file.txt").into()) - ); - assert_eq!( - substitute_path("/substitute/$dir/../$name", root, path), - Some(PathBuf::from("/substitute/dir1/file.txt").into()) - ); - assert_eq!( - substitute_path("/substitute/$name", root, path), - Some(PathBuf::from("/substitute/file.txt").into()) - ); - assert_eq!( - substitute_path("/substitute/target/$dir/$name", root, path), - Some(PathBuf::from("/substitute/target/dir1/dir2/file.txt").into()) - ); - } -} diff --git a/crates/tinymist/src/actor/typ_client.rs b/crates/tinymist/src/actor/typ_client.rs index 908de8ed..00f13137 100644 --- a/crates/tinymist/src/actor/typ_client.rs +++ b/crates/tinymist/src/actor/typ_client.rs @@ -1,4 +1,4 @@ -//! The typst actors running compilations. +//! The actor that runs compilations. //! //! ```ascii //! ┌────────────────────────────────┐ @@ -32,7 +32,7 @@ use std::{ sync::Arc, }; -use anyhow::anyhow; +use anyhow::{anyhow, bail}; use log::{error, info, trace}; use parking_lot::Mutex; use tinymist_query::{ @@ -40,7 +40,7 @@ use tinymist_query::{ DiagnosticsMap, ExportKind, ServerInfoResponse, VersionedDocument, }; use tinymist_render::PeriscopeRenderer; -use tokio::sync::{broadcast, mpsc, oneshot, watch}; +use tokio::sync::{mpsc, oneshot, watch}; use typst::{ diag::{PackageError, SourceDiagnostic, SourceResult}, layout::Position, @@ -60,12 +60,12 @@ use typst_ts_core::{ }; use super::{ - cluster::{CompileClusterRequest, TinymistCompileStatusEnum}, - render::ExportConfig, + editor::{EditorRequest, TinymistCompileStatusEnum}, + export::ExportConfig, typ_server::{CompileClient as TsCompileClient, CompileServerActor}, }; use crate::{ - actor::render::{OneshotRendering, PathVars, RenderActorRequest}, + actor::export::ExportRequest, actor::typ_server::EntryStateExt, compiler_init::CompileConfig, tools::preview::{CompilationHandle, CompileStatus}, @@ -77,7 +77,7 @@ type CompileDriverInner = CompileDriverImpl; type CompileService = CompileServerActor; type CompileClient = TsCompileClient; -type EditorSender = mpsc::UnboundedSender; +type EditorSender = mpsc::UnboundedSender; pub struct CompileHandler { pub(super) diag_group: String, @@ -86,7 +86,7 @@ pub struct CompileHandler { pub(super) inner: Arc>>, pub(super) doc_tx: watch::Sender>>, - pub(super) render_tx: broadcast::Sender, + pub(super) export_tx: mpsc::UnboundedSender, pub(super) editor_tx: EditorSender, } @@ -101,12 +101,11 @@ impl CompilationHandle for CompileHandler { fn notify_compile(&self, res: Result, CompileStatus>) { if let Ok(doc) = res.clone() { let _ = self.doc_tx.send(Some(doc.clone())); - // todo: is it right that ignore zero broadcast receiver? - let _ = self.render_tx.send(RenderActorRequest::OnTyped); + let _ = self.export_tx.send(ExportRequest::OnTyped); } self.editor_tx - .send(CompileClusterRequest::Status( + .send(EditorRequest::Status( self.diag_group.clone(), if res.is_ok() { TinymistCompileStatusEnum::CompileSuccess @@ -125,10 +124,9 @@ impl CompilationHandle for CompileHandler { impl CompileHandler { fn push_diagnostics(&mut self, diagnostics: Option) { - let res = self.editor_tx.send(CompileClusterRequest::Diag( - self.diag_group.clone(), - diagnostics, - )); + let res = self + .editor_tx + .send(EditorRequest::Diag(self.diag_group.clone(), diagnostics)); if let Err(err) = res { error!("failed to send diagnostics: {err:#}"); } @@ -157,7 +155,7 @@ impl CompileMiddleware for CompileDriver { fn wrap_compile(&mut self, env: &mut CompileEnv) -> SourceResult> { self.handler .editor_tx - .send(CompileClusterRequest::Status( + .send(EditorRequest::Status( self.handler.diag_group.clone(), TinymistCompileStatusEnum::Compiling, )) @@ -217,11 +215,11 @@ impl CompileDriver { let Some(main) = w.main_id() else { error!("TypstActor: main file is not set"); - return Err(anyhow!("main file is not set")); + bail!("main file is not set"); }; let Some(root) = w.entry.root() else { error!("TypstActor: root is not set"); - return Err(anyhow!("root is not set")); + bail!("root is not set"); }; w.source(main).map_err(|err| { info!("TypstActor: failed to prepare main file: {err:?}"); @@ -277,7 +275,7 @@ pub struct CompileClientActor { pub config: CompileConfig, entry: EntryState, inner: Deferred, - render_tx: broadcast::Sender, + export_tx: mpsc::UnboundedSender, } impl CompileClientActor { @@ -286,14 +284,14 @@ impl CompileClientActor { config: CompileConfig, entry: EntryState, inner: Deferred, - render_tx: broadcast::Sender, + export_tx: mpsc::UnboundedSender, ) -> Self { Self { diag_group, config, entry, inner, - render_tx, + export_tx, } } @@ -364,8 +362,8 @@ impl CompileClientActor { })??; let entry = next_entry.clone(); - let req = RenderActorRequest::ChangeExportPath(PathVars { entry }); - self.render_tx.send(req).unwrap(); + let req = ExportRequest::ChangeExportPath(entry); + let _ = self.export_tx.send(req); // todo: better way to trigger recompile let files = FileChangeSet::new_inserts(vec![]); @@ -381,14 +379,13 @@ impl CompileClientActor { } pub(crate) fn change_export_pdf(&mut self, config: ExportConfig) { + let entry = self.entry.clone(); let _ = self - .render_tx - .send(RenderActorRequest::ChangeConfig(ExportConfig { - substitute_pattern: config.substitute_pattern, - entry: self.entry.clone(), - mode: config.mode, - })) - .unwrap(); + .export_tx + .send(ExportRequest::ChangeConfig(ExportConfig { + entry, + ..config + })); } pub fn clear_cache(&self) { @@ -422,25 +419,16 @@ impl CompileClientActor { info!("CompileActor: on export: {}", path.display()); let (tx, rx) = oneshot::channel(); - - let callback = Arc::new(Mutex::new(Some(tx))); - self.render_tx - .send(RenderActorRequest::Oneshot(OneshotRendering { - kind: Some(kind), - callback, - })) - .map_err(map_string_err("failed to send to sync_render"))?; - + let _ = self.export_tx.send(ExportRequest::Oneshot(Some(kind), tx)); let res: Option = utils::threaded_receive(rx)?; info!("CompileActor: on export end: {path:?} as {res:?}"); - Ok(res) } pub fn on_save_export(&self, path: PathBuf) -> anyhow::Result<()> { info!("CompileActor: on save export: {}", path.display()); - let _ = self.render_tx.send(RenderActorRequest::OnSaved(path)); + let _ = self.export_tx.send(ExportRequest::OnSaved(path)); Ok(()) } diff --git a/crates/tinymist/src/actor/typ_server.rs b/crates/tinymist/src/actor/typ_server.rs index 0d2c4baa..eff6f438 100644 --- a/crates/tinymist/src/actor/typ_server.rs +++ b/crates/tinymist/src/actor/typ_server.rs @@ -242,24 +242,28 @@ where // Wait for first events. 'event_loop: while let Some(mut event) = self.steal_rx.blocking_recv() { - // Accumulate events, the order of processing which is critical. let mut need_compile = false; 'accumulate: loop { // Warp the logical clock by one. self.logical_tick += 1; + // If settle, stop the actor. if let Interrupt::Settle(e) = event { log::info!("CompileServerActor: requested stop"); e.send(()).ok(); break 'event_loop; } + + // Ensure complied before executing tasks. if matches!(event, Interrupt::Task(_)) && need_compile { self.compile(&compiler_ack); need_compile = false; } + need_compile |= self.process(event, &compiler_ack); + // Try to accumulate more events. match self.steal_rx.try_recv() { Ok(new_event) => event = new_event, _ => break 'accumulate, diff --git a/crates/tinymist/src/actor/user_action.rs b/crates/tinymist/src/actor/user_action.rs index 669ca3d6..23089cc7 100644 --- a/crates/tinymist/src/actor/user_action.rs +++ b/crates/tinymist/src/actor/user_action.rs @@ -1,3 +1,5 @@ +//! The actor that runs user actions. + use std::path::PathBuf; use anyhow::bail; @@ -6,36 +8,33 @@ use lsp_server::RequestId; use serde::{Deserialize, Serialize}; use typst_ts_core::TypstDict; -use crate::{internal_error, result_to_response_, LspHost, TypstLanguageServer}; +use crate::{internal_error, result_to_response, LspHost, TypstLanguageServer}; #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UserActionTraceRequest { - #[serde(rename = "compilerProgram")] +#[serde(rename_all = "camelCase")] +pub struct TraceParams { pub compiler_program: PathBuf, pub root: PathBuf, pub main: PathBuf, pub inputs: TypstDict, - #[serde(rename = "fontPaths")] pub font_paths: Vec, } pub enum UserActionRequest { - Trace((RequestId, UserActionTraceRequest)), + Trace(RequestId, TraceParams), } pub fn run_user_action_thread( - rx_req: crossbeam_channel::Receiver, + user_action_rx: crossbeam_channel::Receiver, client: LspHost, ) { - while let Ok(req) = rx_req.recv() { + while let Ok(req) = user_action_rx.recv() { match req { - UserActionRequest::Trace((id, req)) => { - let res = run_trace_program(req) + UserActionRequest::Trace(id, params) => { + let res = run_trace_program(params) .map_err(|e| internal_error(format!("failed to run trace program: {:?}", e))); - if let Ok(response) = result_to_response_(id, res) { - client.respond(response); - } + client.respond(result_to_response(id, res)); } } } @@ -44,26 +43,26 @@ pub fn run_user_action_thread( } /// Run a perf trace to some typst program -fn run_trace_program(req: UserActionTraceRequest) -> anyhow::Result { +fn run_trace_program(params: TraceParams) -> anyhow::Result { // Typst compile root, input, font paths, inputs - let mut cmd = std::process::Command::new(&req.compiler_program); + let mut cmd = std::process::Command::new(¶ms.compiler_program); let mut cmd = &mut cmd; cmd = cmd.arg("compile"); cmd = cmd .arg("--root") - .arg(req.root.as_path()) - .arg(req.main.as_path()); + .arg(params.root.as_path()) + .arg(params.main.as_path()); // todo: test space in input? - for (k, v) in req.inputs.iter() { + for (k, v) in params.inputs.iter() { let typst::foundations::Value::Str(s) = v else { bail!("input value must be string, got {:?} for {:?}", v, k); }; cmd = cmd.arg(format!("--input={k}={}", s.as_str())); } - for p in &req.font_paths { + for p in ¶ms.font_paths { cmd = cmd.arg(format!("--font-path={}", p.as_path().display())); } @@ -89,7 +88,7 @@ fn run_trace_program(req: UserActionTraceRequest) -> anyhow::Result let stderr = base64::engine::general_purpose::STANDARD.encode(stderr); Ok(TraceReport { - request: req, + request: params, messages, stderr, }) @@ -97,7 +96,7 @@ fn run_trace_program(req: UserActionTraceRequest) -> anyhow::Result #[derive(Debug, Clone, Serialize, Deserialize)] struct TraceReport { - request: UserActionTraceRequest, + request: TraceParams, messages: Vec, stderr: String, } diff --git a/crates/tinymist/src/harness.rs b/crates/tinymist/src/harness.rs index 019cb976..3f946b5b 100644 --- a/crates/tinymist/src/harness.rs +++ b/crates/tinymist/src/harness.rs @@ -1,17 +1,14 @@ use std::sync::Arc; use std::time::Instant; -use log::{info, trace, warn}; -use lsp_types::InitializedParams; -use parking_lot::RwLock; -use serde::{de::DeserializeOwned, Serialize}; - +use anyhow::bail; +use log::{error, info, trace, warn}; use lsp_server::{Connection, Message, Response}; - use lsp_types::notification::PublishDiagnostics; use lsp_types::request::{RegisterCapability, UnregisterCapability}; use lsp_types::*; -use parking_lot::Mutex; +use parking_lot::{Mutex, RwLock}; +use serde::{de::DeserializeOwned, Serialize}; // Enforces drop order pub struct Handle { @@ -55,7 +52,7 @@ impl LspHost { let mut req_queue = self.req_queue.lock(); let sender = self.sender.read(); let Some(sender) = sender.as_ref() else { - warn!("closed connection, failed to send request"); + warn!("failed to send request: connection closed"); return; }; let request = req_queue @@ -82,7 +79,7 @@ impl LspHost { let sender = self.sender.read(); let Some(sender) = sender.as_ref() else { - warn!("closed connection, failed to send request"); + warn!("failed to send notification: connection closed"); return; }; let Err(res) = sender.send(not.into()) else { @@ -102,12 +99,13 @@ impl LspHost { (request.method.clone(), request_received), ); } + pub fn respond(&self, response: lsp_server::Response) { let mut req_queue = self.req_queue.lock(); if let Some((method, start)) = req_queue.incoming.complete(response.id.clone()) { let sender = self.sender.read(); let Some(sender) = sender.as_ref() else { - warn!("closed connection, failed to send request"); + warn!("failed to send response: connection closed"); return; }; @@ -146,7 +144,7 @@ impl LspHost { pub fn register_capability(&self, registrations: Vec) -> anyhow::Result<()> { self.send_request::(RegistrationParams { registrations }, |_, resp| { if let Some(err) = resp.error { - log::error!("failed to register capability: {err:?}"); + error!("failed to register capability: {err:?}"); } }); Ok(()) @@ -160,7 +158,7 @@ impl LspHost { UnregistrationParams { unregisterations }, |_, resp| { if let Some(err) = resp.error { - log::error!("failed to unregister capability: {err:?}"); + error!("failed to unregister capability: {err:?}"); } }, ); @@ -198,7 +196,7 @@ pub fn lsp_harness( let (initialize_id, initialize_params) = match connection.initialize_start() { Ok(it) => it, Err(e) => { - log::error!("failed to initialize: {e}"); + error!("failed to initialize: {e}"); *force_exit = !e.channel_is_disconnected(); return Err(e.into()); } @@ -208,7 +206,7 @@ pub fn lsp_harness( let sender = Arc::new(RwLock::new(Some(connection.sender))); let host = LspHost::new(sender.clone()); - let _drop_connection = ForceDrop(sender); + let _drop_guard = ForceDrop(sender); let req = lsp_server::Request::new(initialize_id, "initialize".to_owned(), initialize_params); host.register_request(&req, request_received); @@ -234,15 +232,13 @@ pub fn lsp_harness( r#"expected initialized notification, got: {msg:?}"# ))), Err(e) => { - log::error!("failed to receive initialized notification: {e}"); + error!("failed to receive initialized notification: {e}"); Err(ProtocolError::disconnected()) } }; if let Err(e) = initialized_ack { *force_exit = !e.channel_is_disconnected(); - return Err(anyhow::anyhow!( - "failed to receive initialized notification: {e:?}" - )); + bail!("failed to receive initialized notification: {e:?}"); } service.initialized(InitializedParams {}); @@ -270,7 +266,7 @@ impl ProtocolError { struct ForceDrop(Arc>>); impl Drop for ForceDrop { fn drop(&mut self) { - self.0.write().take(); + *self.0.write() = None; } } @@ -279,5 +275,5 @@ pub fn from_json( json: &serde_json::Value, ) -> anyhow::Result { serde_json::from_value(json.clone()) - .map_err(|e| anyhow::format_err!("Failed to deserialize {what}: {e}; {json}")) + .map_err(|e| anyhow::anyhow!("Failed to deserialize {what}: {e}; {json}")) } diff --git a/crates/tinymist/src/main.rs b/crates/tinymist/src/main.rs index 4079da65..abe76d97 100644 --- a/crates/tinymist/src/main.rs +++ b/crates/tinymist/src/main.rs @@ -4,24 +4,24 @@ mod args; use std::{path::PathBuf, sync::Arc}; -use args::CompileArgs; +use anyhow::bail; use clap::Parser; use comemo::Prehashed; use lsp_types::{InitializeParams, InitializedParams}; use once_cell::sync::Lazy; use parking_lot::RwLock; +use tokio::sync::mpsc; +use typst::{eval::Tracer, foundations::IntoValue, syntax::Span}; +use typst_ts_compiler::service::{CompileEnv, Compiler, EntryManager}; +use typst_ts_core::{typst::prelude::EcoVec, TypstDict}; + +use crate::args::{CliArguments, Commands, CompileArgs, LspArgs}; use tinymist::{ compiler_init::{CompileInit, CompileInitializeParams}, harness::{lsp_harness, InitializedLspDriver, LspDriver, LspHost}, transport::with_stdio_transport, CompileFontOpts, Init, LspWorld, TypstLanguageServer, }; -use tokio::sync::mpsc; -use typst::{eval::Tracer, foundations::IntoValue, syntax::Span}; -use typst_ts_compiler::service::{CompileEnv, Compiler, EntryManager}; -use typst_ts_core::{typst::prelude::EcoVec, TypstDict}; - -use crate::args::{CliArguments, Commands, LspArgs}; #[cfg(feature = "dhat-heap")] #[global_allocator] @@ -114,7 +114,7 @@ pub fn lsp_main(args: LspArgs) -> anyhow::Result<()> { } pub fn compiler_main(args: CompileArgs) -> anyhow::Result<()> { - let (diag_tx, _diag_rx) = mpsc::unbounded_channel(); + let (editor_tx, _editor_rx) = mpsc::unbounded_channel(); let mut input = PathBuf::from(args.compile.input.unwrap()); @@ -127,9 +127,7 @@ pub fn compiler_main(args: CompileArgs) -> anyhow::Result<()> { input = std::env::current_dir()?.join(input); } if !input.starts_with(&root_path) { - return Err(anyhow::anyhow!( - "input file is not within the root path: {input:?} not in {root_path:?}" - )); + bail!("input file is not within the root path: {input:?} not in {root_path:?}"); } let inputs = Arc::new(Prehashed::new(if args.compile.inputs.is_empty() { @@ -147,7 +145,7 @@ pub fn compiler_main(args: CompileArgs) -> anyhow::Result<()> { no_system_fonts: args.compile.font.no_system_fonts, ..Default::default() }, - diag_tx, + editor_tx, }; if args.persist { log::info!("starting compile server"); @@ -163,7 +161,7 @@ pub fn compiler_main(args: CompileArgs) -> anyhow::Result<()> { let sender = Arc::new(RwLock::new(Some(s))); let host = LspHost::new(sender.clone()); - let _drop_connection = ForceDrop(sender); + let _drop_guard = ForceDrop(sender); let (mut service, res) = init.initialize( host, @@ -252,9 +250,10 @@ pub fn compiler_main(args: CompileArgs) -> anyhow::Result<()> { } struct ForceDrop(Arc>>); + impl Drop for ForceDrop { fn drop(&mut self) { - self.0.write().take(); + *self.0.write() = None; } } diff --git a/crates/tinymist/src/resource/symbols.rs b/crates/tinymist/src/resource/symbols.rs index 3ec0005e..ba9951bd 100644 --- a/crates/tinymist/src/resource/symbols.rs +++ b/crates/tinymist/src/resource/symbols.rs @@ -1,11 +1,10 @@ pub use super::prelude::*; #[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] struct ResourceSymbolResponse { symbols: HashMap, - #[serde(rename = "fontSelects")] font_selects: Vec, - #[serde(rename = "glyphDefs")] glyph_defs: String, } @@ -17,43 +16,34 @@ struct ResourceSymbolItem { } #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] enum SymCategory { - #[serde(rename = "accent")] Accent, - #[serde(rename = "greek")] Greek, - #[serde(rename = "misc")] Misc, } #[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] struct ResourceGlyphDesc { - #[serde(rename = "fontIndex")] font_index: u32, - #[serde(rename = "xAdvance")] x_advance: Option, - #[serde(rename = "yAdvance")] y_advance: Option, - #[serde(rename = "xMin")] x_min: Option, - #[serde(rename = "xMax")] x_max: Option, - #[serde(rename = "yMin")] y_min: Option, - #[serde(rename = "yMax")] y_max: Option, name: Option, shape: Option, } #[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] struct FontItem { family: String, - #[serde(rename = "capHeight")] cap_height: f32, ascender: f32, descender: f32, - #[serde(rename = "unitsPerEm")] units_per_em: f32, // vertical: bool, } diff --git a/crates/tinymist/src/server/compiler.rs b/crates/tinymist/src/server/compiler.rs index f2522977..df9a0d58 100644 --- a/crates/tinymist/src/server/compiler.rs +++ b/crates/tinymist/src/server/compiler.rs @@ -3,9 +3,8 @@ use std::{collections::HashMap, path::Path, sync::Arc, time::Instant}; use crossbeam_channel::{select, Receiver}; use log::{error, info, warn}; -use lsp_server::{Notification, Request, ResponseError}; +use lsp_server::{ErrorCode, Message, Notification, Request, RequestId, Response, ResponseError}; use lsp_types::{notification::Notification as _, ExecuteCommandParams}; -use paste::paste; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value as JsonValue}; use tinymist_query::{ExportKind, PageSelection}; @@ -15,7 +14,7 @@ use typst_ts_compiler::vfs::notify::FileChangeSet; use typst_ts_core::{config::compiler::DETACHED_ENTRY, ImmutPath}; use crate::{ - actor::{cluster::CompileClusterRequest, render::ExportConfig, typ_client::CompileClientActor}, + actor::{editor::EditorRequest, export::ExportConfig, typ_client::CompileClientActor}, compiler_init::{CompileConfig, CompilerConstConfig}, harness::InitializedLspDriver, internal_error, invalid_params, method_not_found, run_query, @@ -46,9 +45,7 @@ macro_rules! request_fn { const E: LspMethod = |this, req| { let req: <$desc as lsp_types::request::Request>::Params = serde_json::from_value(req).unwrap(); // todo: soft unwrap - let res = this.$method(req)?; - let res = serde_json::to_value(res).unwrap(); // todo: soft unwrap - Ok(res) + this.$method(req) }; E }) @@ -69,15 +66,6 @@ macro_rules! notify_fn { }; } -pub struct CompileServerArgs { - pub client: LspHost, - pub compile_config: CompileConfig, - pub const_config: CompilerConstConfig, - pub diag_tx: mpsc::UnboundedSender, - pub font: Deferred, - pub handle: tokio::runtime::Handle, -} - /// The object providing the language server functionality. pub struct CompileServer { /// The language server client. @@ -110,25 +98,23 @@ pub struct CompileServer { /// Source synchronized with client pub memory_changes: HashMap, MemoryFileMeta>, /// The diagnostics sender to send diagnostics to `crate::actor::cluster`. - pub diag_tx: mpsc::UnboundedSender, + pub editor_tx: mpsc::UnboundedSender, /// The compiler actor. pub compiler: Option, } impl CompileServer { - pub fn new(args: CompileServerArgs) -> Self { - let CompileServerArgs { - client, - compile_config, - const_config, - diag_tx, - font, - handle, - } = args; - + pub fn new( + client: LspHost, + compile_config: CompileConfig, + const_config: CompilerConstConfig, + editor_tx: mpsc::UnboundedSender, + font: Deferred, + handle: tokio::runtime::Handle, + ) -> Self { CompileServer { client, - diag_tx, + editor_tx, shutdown_requested: false, config: compile_config, const_config, @@ -210,7 +196,7 @@ impl CompileServer { #[derive(Debug)] enum Event { - Lsp(lsp_server::Message), + Lsp(Message), } impl fmt::Display for Event { @@ -224,14 +210,11 @@ impl fmt::Display for Event { impl InitializedLspDriver for CompileServer { fn initialized(&mut self, _params: lsp_types::InitializedParams) {} - fn main_loop( - &mut self, - inbox: crossbeam_channel::Receiver, - ) -> anyhow::Result<()> { + fn main_loop(&mut self, inbox: crossbeam_channel::Receiver) -> anyhow::Result<()> { while let Some(event) = self.next_event(&inbox) { if matches!( &event, - Event::Lsp(lsp_server::Message::Notification(Notification { method, .. })) + Event::Lsp(Message::Notification(Notification { method, .. })) if method == lsp_types::notification::Exit::METHOD ) { return Ok(()); @@ -245,7 +228,7 @@ impl InitializedLspDriver for CompileServer { } impl CompileServer { - fn next_event(&self, inbox: &Receiver) -> Option { + fn next_event(&self, inbox: &Receiver) -> Option { select! { recv(inbox) -> msg => msg.ok().map(Event::Lsp), @@ -258,11 +241,9 @@ impl CompileServer { // let was_quiescent = self.is_quiescent(); match event { Event::Lsp(msg) => match msg { - lsp_server::Message::Request(req) => self.on_new_request(loop_start, req), - lsp_server::Message::Notification(not) => self.on_notification(loop_start, not)?, - lsp_server::Message::Response(resp) => { - self.client.clone().complete_request(self, resp) - } + Message::Request(req) => self.on_request(loop_start, req), + Message::Notification(not) => self.on_notification(loop_start, not)?, + Message::Response(resp) => self.client.clone().complete_request(self, resp), }, } Ok(()) @@ -270,17 +251,13 @@ impl CompileServer { /// Registers and handles a request. This should only be called once per /// incoming request. - fn on_new_request(&mut self, request_received: Instant, req: Request) { + fn on_request(&mut self, request_received: Instant, req: Request) { self.client.register_request(&req, request_received); - self.on_request(req); - } - /// Handles a request. - fn on_request(&mut self, req: Request) { if self.shutdown_requested { - self.client.respond(lsp_server::Response::new_err( + self.client.respond(Response::new_err( req.id.clone(), - lsp_server::ErrorCode::InvalidRequest as i32, + ErrorCode::InvalidRequest as i32, "Shutdown already requested.".to_owned(), )); return; @@ -298,12 +275,12 @@ impl CompileServer { } fn result_to_response( - id: lsp_server::RequestId, + id: RequestId, result: Result, - ) -> Result { + ) -> Result { let res = match result { - Ok(resp) => lsp_server::Response::new_ok(id, resp), - Err(e) => lsp_server::Response::new_err(id, e.code, e.message), + Ok(resp) => Response::new_ok(id, resp), + Err(e) => Response::new_err(id, e.code, e.message), }; Ok(res) } @@ -404,7 +381,7 @@ impl CompileServer { } /// The entry point for the `workspace/executeCommand` request. - fn execute_command(&mut self, params: ExecuteCommandParams) -> LspResult> { + fn execute_command(&mut self, params: ExecuteCommandParams) -> LspResult { let ExecuteCommandParams { command, arguments, @@ -414,8 +391,7 @@ impl CompileServer { error!("asked to execute unknown command"); return Err(method_not_found()); }; - - Ok(Some(handler(self, arguments)?)) + handler(self, arguments) } /// Export the current document as a PDF file. diff --git a/crates/tinymist/src/server/compiler_init.rs b/crates/tinymist/src/server/compiler_init.rs index ff788bff..492739ae 100644 --- a/crates/tinymist/src/server/compiler_init.rs +++ b/crates/tinymist/src/server/compiler_init.rs @@ -12,15 +12,15 @@ use tinymist_query::PositionEncoding; use tinymist_render::PeriscopeArgs; use tokio::sync::mpsc; use typst::foundations::IntoValue; -use typst::syntax::FileId; -use typst::syntax::VirtualPath; +use typst::syntax::{FileId, VirtualPath}; use typst::util::Deferred; use typst_ts_core::config::compiler::EntryState; use typst_ts_core::{ImmutPath, TypstDict}; -use crate::actor::cluster::CompileClusterRequest; -use crate::compiler::{CompileServer, CompileServerArgs}; +use crate::actor::editor::EditorRequest; +use crate::compiler::CompileServer; use crate::harness::LspDriver; +use crate::utils::{try_, try_or_default}; use crate::world::{ImmutDict, SharedFontResolver}; use crate::{CompileExtraOpts, CompileFontOpts, ExportMode, LspHost}; @@ -124,68 +124,29 @@ impl CompileConfig { /// # Errors /// Errors if the update is invalid. pub fn update_by_map(&mut self, update: &Map) -> anyhow::Result<()> { - if let Some(JsonValue::String(output_path)) = update.get("outputPath") { - output_path.clone_into(&mut self.output_path); - } else { - self.output_path = String::new(); - } - - let export_pdf = update - .get("exportPdf") - .map(ExportMode::deserialize) - .and_then(Result::ok); - if let Some(export_pdf) = export_pdf { - self.export_pdf = export_pdf; - } else { - self.export_pdf = ExportMode::default(); - } - - let root_path = update.get("rootPath"); - if let Some(root_path) = root_path { - if root_path.is_null() { - self.root_path = None; - } - if let Some(root_path) = root_path.as_str().map(PathBuf::from) { - self.root_path = Some(root_path); - } - } else { - self.root_path = None; - } - - let compile_status = update.get("compileStatus").and_then(|x| x.as_str()); - if let Some(word_count) = compile_status { - if !matches!(word_count, "enable" | "disable") { - bail!("compileStatus must be either 'enable' or 'disable'"); - } - } - self.notify_compile_status = compile_status.map_or(false, |e| e != "disable"); - - let preferred_theme = update.get("preferredTheme").and_then(|x| x.as_str()); - self.preferred_theme = preferred_theme.map(str::to_owned); + self.output_path = try_or_default(|| Some(update.get("outputPath")?.as_str()?.to_owned())); + self.export_pdf = try_or_default(|| ExportMode::deserialize(update.get("exportPdf")?).ok()); + self.root_path = try_(|| Some(update.get("rootPath")?.as_str()?.into())); + self.notify_compile_status = match try_(|| update.get("compileStatus")?.as_str()) { + Some("enable") => true, + Some("disable") | None => false, + _ => bail!("compileStatus must be either 'enable' or 'disable'"), + }; + self.preferred_theme = try_(|| Some(update.get("preferredTheme")?.as_str()?.to_owned())); // periscope_args - let periscope_args = update.get("hoverPeriscope"); - let periscope_args: Option = match periscope_args { + self.periscope_args = match update.get("hoverPeriscope") { Some(serde_json::Value::String(e)) if e == "enable" => Some(PeriscopeArgs::default()), Some(serde_json::Value::Null | serde_json::Value::String(..)) | None => None, Some(periscope_args) => match serde_json::from_value(periscope_args.clone()) { Ok(e) => Some(e), - Err(e) => { - log::error!("failed to parse hoverPeriscope: {e}"); - return Ok(()); - } + Err(e) => bail!("failed to parse hoverPeriscope: {e}"), }, }; - if let Some(mut periscope_args) = periscope_args { - if periscope_args.invert_color == "auto" - && self.preferred_theme.as_ref().is_some_and(|t| t == "dark") - { - "always".clone_into(&mut periscope_args.invert_color); + if let Some(args) = self.periscope_args.as_mut() { + if args.invert_color == "auto" && self.preferred_theme.as_deref() == Some("dark") { + args.invert_color = "always".to_owned(); } - - self.periscope_args = Some(periscope_args); - } else { - self.periscope_args = None; } 'parse_extra_args: { @@ -193,10 +154,7 @@ impl CompileConfig { let typst_args: Vec = match serde_json::from_value(typst_extra_args.clone()) { Ok(e) => e, - Err(e) => { - log::error!("failed to parse typstExtraArgs: {e}"); - return Ok(()); - } + Err(e) => bail!("failed to parse typstExtraArgs: {e}"), }; let command = match CompileOnceArgs::try_parse_from( @@ -229,9 +187,7 @@ impl CompileConfig { } self.has_default_entry_path = self.determine_default_entry_path().is_some(); - self.validate()?; - - Ok(()) + self.validate() } pub fn determine_root(&self, entry: Option<&ImmutPath>) -> Option { @@ -239,18 +195,8 @@ impl CompileConfig { return Some(path.as_path().into()); } - if let Some(extras) = &self.typst_extra_args { - if let Some(root) = &extras.root_dir { - return Some(root.as_path().into()); - } - } - - if let Some(path) = &self - .typst_extra_args - .as_ref() - .and_then(|x| x.root_dir.clone()) - { - return Some(path.as_path().into()); + if let Some(root) = try_(|| self.typst_extra_args.as_ref()?.root_dir.as_ref()) { + return Some(root.as_path().into()); } if let Some(entry) = entry { @@ -277,15 +223,15 @@ impl CompileConfig { } pub fn determine_default_entry_path(&self) -> Option { - self.typst_extra_args.as_ref().and_then(|e| { - if let Some(e) = &e.entry { - if e.is_relative() { - let root = self.determine_root(None)?; - return Some(root.join(e).as_path().into()); - } + let extras = self.typst_extra_args.as_ref()?; + // todo: pre-compute this when updating config + if let Some(entry) = &extras.entry { + if entry.is_relative() { + let root = self.determine_root(None)?; + return Some(root.join(entry).as_path().into()); } - e.entry.clone() - }) + } + extras.entry.clone() } pub fn determine_entry(&self, entry: Option) -> EntryState { @@ -370,7 +316,7 @@ impl Default for CompilerConstConfig { pub struct CompileInit { pub handle: tokio::runtime::Handle, pub font: CompileFontOpts, - pub diag_tx: mpsc::UnboundedSender, + pub editor_tx: mpsc::UnboundedSender, } #[derive(Debug, Deserialize)] @@ -410,10 +356,10 @@ impl LspDriver for CompileInit { Deferred::new(|| SharedFontResolver::new(opts).expect("failed to create font book")) }; - let args = CompileServerArgs { + let mut service = CompileServer::new( client, compile_config, - const_config: CompilerConstConfig { + CompilerConstConfig { position_encoding: params .position_encoding .map(|x| match x.as_str() { @@ -422,12 +368,10 @@ impl LspDriver for CompileInit { }) .unwrap_or_default(), }, - diag_tx: self.diag_tx, - handle: self.handle, + self.editor_tx, font, - }; - - let mut service = CompileServer::new(args); + self.handle, + ); let primary = service.server( "primary".to_owned(), @@ -435,10 +379,9 @@ impl LspDriver for CompileInit { service.config.determine_inputs(), service.vfs_snapshot(), ); - if service.compiler.is_some() { + if service.compiler.replace(primary).is_some() { panic!("primary already initialized"); } - service.compiler = Some(primary); (service, Ok(())) } diff --git a/crates/tinymist/src/server/lsp.rs b/crates/tinymist/src/server/lsp.rs index ba78bcef..d901cb65 100644 --- a/crates/tinymist/src/server/lsp.rs +++ b/crates/tinymist/src/server/lsp.rs @@ -1,6 +1,5 @@ //! tinymist LSP mode -use core::fmt; use std::ops::Deref; use std::path::Path; use std::sync::Arc; @@ -8,16 +7,13 @@ use std::time::Instant; use std::{collections::HashMap, path::PathBuf}; use anyhow::{bail, Context}; -use crossbeam_channel::select; -use crossbeam_channel::Receiver; use futures::future::BoxFuture; use log::{error, info, trace, warn}; -use lsp_server::{ErrorCode, Message, Notification, Request, RequestId, ResponseError}; +use lsp_server::{ErrorCode, Message, Notification, Request, RequestId, Response, ResponseError}; use lsp_types::notification::Notification as NotificationTrait; use lsp_types::request::{GotoDeclarationParams, GotoDeclarationResponse, WorkspaceConfiguration}; use lsp_types::*; -use parking_lot::lock_api::RwLock; -use paste::paste; +use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value as JsonValue}; use tinymist_query::{ @@ -33,12 +29,11 @@ use typst_ts_core::path::PathClean; use typst_ts_core::{error::prelude::*, ImmutPath}; use super::lsp_init::*; -use crate::actor::cluster::CompileClusterRequest; +use crate::actor::editor::EditorRequest; +use crate::actor::format::{FormatConfig, FormatRequest}; use crate::actor::typ_client::CompileClientActor; -use crate::actor::{ - FormattingConfig, FormattingRequest, UserActionRequest, UserActionTraceRequest, -}; -use crate::compiler::{CompileServer, CompileServerArgs}; +use crate::actor::user_action::{TraceParams, UserActionRequest}; +use crate::compiler::CompileServer; use crate::compiler_init::CompilerConstConfig; use crate::harness::{InitializedLspDriver, LspHost}; use crate::tools::package::InitTask; @@ -47,21 +42,6 @@ use crate::{run_query, LspResult}; pub type MaySyncResult<'a> = Result>; -#[derive(Debug)] -enum Event { - Lsp(lsp_server::Message), -} - -impl fmt::Display for Event { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Event::Lsp(_) => write!(f, "Event::Lsp"), - } - } -} - -pub(crate) struct Cancelled; - type LspMethod = fn(srv: &mut TypstLanguageServer, args: JsonValue) -> LspResult; type LspHandler = fn(srv: &mut TypstLanguageServer, args: Req) -> LspResult; @@ -69,7 +49,7 @@ type LspHandler = fn(srv: &mut TypstLanguageServer, args: Req) -> LspR /// Returns Ok(None) -> Need to respond none /// Returns Err(..) -> Need to respond error type LspRawHandler = - fn(srv: &mut TypstLanguageServer, args: (RequestId, T)) -> LspResult>; + fn(srv: &mut TypstLanguageServer, req_id: RequestId, args: T) -> LspResult>; type ExecuteCmdMap = HashMap<&'static str, LspRawHandler>>; type NotifyCmdMap = HashMap<&'static str, LspMethod<()>>; @@ -86,7 +66,7 @@ macro_rules! resource_fn { macro_rules! request_fn_ { ($desc: ty, Self::$method: ident) => { (<$desc>::METHOD, { - const E: LspRawHandler = |this, (req_id, req)| { + const E: LspRawHandler = |this, req_id, req| { let req: <$desc as lsp_types::request::Request>::Params = serde_json::from_value(req).unwrap(); // todo: soft unwrap this.$method(req_id, req) @@ -99,26 +79,12 @@ macro_rules! request_fn_ { macro_rules! request_fn { ($desc: ty, Self::$method: ident) => { (<$desc>::METHOD, { - const E: LspRawHandler = |this, (req_id, req)| { + const E: LspRawHandler = |this, req_id, req| { let req: <$desc as lsp_types::request::Request>::Params = serde_json::from_value(req).unwrap(); // todo: soft unwrap - let res = this - .$method(req) - .map(|res| serde_json::to_value(res).unwrap()); // todo: soft unwrap + let res = this.$method(req); - if let Ok(response) = result_to_response(req_id, res) { - this.client.respond(response); - } - - // todo: cancellation - // Err(e) => match e.downcast::() { - // Ok(cancelled) => return Err(cancelled), - // Err(e) => lsp_server::Response::new_err( - // id, - // lsp_server::ErrorCode::InternalError as i32, - // e.to_string(), - // ), - // }, + this.client.respond(result_to_response(req_id, res)); Ok(Some(())) }; @@ -130,11 +96,8 @@ macro_rules! request_fn { macro_rules! exec_fn_ { ($key: expr, Self::$method: ident) => { ($key, { - { - const E: LspRawHandler> = - |this, (req_id, req)| this.$method(req_id, req); - E - } + const E: LspRawHandler> = |this, req_id, req| this.$method(req_id, req); + E }) }; } @@ -142,13 +105,9 @@ macro_rules! exec_fn_ { macro_rules! exec_fn { ($key: expr, Self::$method: ident) => { ($key, { - const E: LspRawHandler> = |this, (req_id, args)| { + const E: LspRawHandler> = |this, req_id, args| { let res = this.$method(args); - - if let Ok(response) = result_to_response(req_id, res) { - this.client.respond(response); - } - + this.client.respond(result_to_response(req_id, res)); Ok(Some(())) }; E @@ -181,14 +140,6 @@ fn as_path_pos(inp: TextDocumentPositionParams) -> (PathBuf, Position) { (as_path(inp.text_document), inp.position) } -pub struct TypstLanguageServerArgs { - pub handle: tokio::runtime::Handle, - pub client: LspHost, - pub const_config: ConstConfig, - pub diag_tx: mpsc::UnboundedSender, - pub font: Deferred, -} - /// The object providing the language server functionality. pub struct TypstLanguageServer { /// The language server client. @@ -198,9 +149,9 @@ pub struct TypstLanguageServer { /// Whether the server is shutting down. pub shutdown_requested: bool, /// Whether the server has registered semantic tokens capabilities. - pub sema_tokens_registered: Option, + pub sema_tokens_registered: bool, /// Whether the server has registered document formatter capabilities. - pub formatter_registered: Option, + pub formatter_registered: bool, /// Whether client is pinning a file. pub pinning: bool, /// The client focusing file. @@ -236,41 +187,47 @@ pub struct TypstLanguageServer { pub dedicates: Vec, /// The formatter thread running in backend. /// Note: The thread will exit if you drop the sender. - pub format_thread: Option>, + pub format_thread: Option>, /// The user action thread running in backend. /// Note: The thread will exit if you drop the sender. - pub user_action_threads: Option>, + pub user_action_thread: Option>, } /// Getters and the main loop. impl TypstLanguageServer { /// Create a new language server. - pub fn new(args: TypstLanguageServerArgs) -> Self { + pub fn new( + client: LspHost, + const_config: ConstConfig, + editor_tx: mpsc::UnboundedSender, + font: Deferred, + handle: tokio::runtime::Handle, + ) -> Self { let tokens_ctx = SemanticTokenContext::new( - args.const_config.position_encoding, - args.const_config.sema_tokens_overlapping_token_support, - args.const_config.sema_tokens_multiline_token_support, + const_config.position_encoding, + const_config.tokens_overlapping_token_support, + const_config.tokens_multiline_token_support, ); Self { - client: args.client.clone(), - primary: CompileServer::new(CompileServerArgs { - client: LspHost::new(Arc::new(RwLock::new(None))), - compile_config: Default::default(), - const_config: CompilerConstConfig { - position_encoding: args.const_config.position_encoding, + client, + primary: CompileServer::new( + LspHost::new(Arc::new(RwLock::new(None))), + Default::default(), + CompilerConstConfig { + position_encoding: const_config.position_encoding, }, - diag_tx: args.diag_tx, - font: args.font, - handle: args.handle, - }), + editor_tx, + font, + handle, + ), dedicates: Vec::new(), shutdown_requested: false, ever_focusing_by_activities: false, ever_manual_focusing: false, - sema_tokens_registered: None, - formatter_registered: None, + sema_tokens_registered: false, + formatter_registered: false, config: Default::default(), - const_config: args.const_config, + const_config, exec_cmds: Self::get_exec_commands(), regular_cmds: Self::get_regular_cmds(), @@ -281,7 +238,7 @@ impl TypstLanguageServer { focusing: None, tokens_ctx, format_thread: None, - user_action_threads: None, + user_action_thread: None, } } @@ -350,7 +307,7 @@ impl InitializedLspDriver for TypstLanguageServer { /// The server can use the `initialized` notification, for example, to /// dynamically register capabilities with the client. fn initialized(&mut self, params: InitializedParams) { - if self.const_config().sema_tokens_dynamic_registration + if self.const_config().tokens_dynamic_registration && self.config.semantic_tokens == SemanticTokensMode::Enable { let err = self.enable_sema_token_caps(true); @@ -413,15 +370,15 @@ impl InitializedLspDriver for TypstLanguageServer { // SetThreadPriority(thread, thread_priority_above_normal); // } - while let Some(event) = self.next_event(&inbox) { - if matches!( - &event, - Event::Lsp(lsp_server::Message::Notification(Notification { method, .. })) - if method == lsp_types::notification::Exit::METHOD - ) { - return Ok(()); + while let Ok(msg) = inbox.recv() { + const EXIT_METHOD: &str = lsp_types::notification::Exit::METHOD; + let loop_start = Instant::now(); + match msg { + Message::Notification(not) if not.method == EXIT_METHOD => return Ok(()), + Message::Notification(not) => self.on_notification(loop_start, not)?, + Message::Request(req) => self.on_request(loop_start, req), + Message::Response(resp) => self.client.clone().complete_request(self, resp), } - self.handle_event(event)?; } warn!("client exited without proper shutdown sequence"); @@ -430,44 +387,15 @@ impl InitializedLspDriver for TypstLanguageServer { } impl TypstLanguageServer { - /// Receives the next event from event sources. - fn next_event(&self, inbox: &Receiver) -> Option { - select! { - recv(inbox) -> msg => - msg.ok().map(Event::Lsp), - } - } - - /// Handles an incoming event. - fn handle_event(&mut self, event: Event) -> anyhow::Result<()> { - let loop_start = Instant::now(); - - // let was_quiescent = self.is_quiescent(); - match event { - Event::Lsp(msg) => match msg { - lsp_server::Message::Request(req) => self.on_new_request(loop_start, req), - lsp_server::Message::Notification(not) => self.on_notification(loop_start, not)?, - lsp_server::Message::Response(resp) => { - self.client.clone().complete_request(self, resp) - } - }, - } - Ok(()) - } - /// Registers and handles a request. This should only be called once per /// incoming request. - fn on_new_request(&mut self, request_received: Instant, req: Request) { + fn on_request(&mut self, request_received: Instant, req: Request) { self.client.register_request(&req, request_received); - self.on_request(req); - } - /// Handles a request. - fn on_request(&mut self, req: Request) { if self.shutdown_requested { - self.client.respond(lsp_server::Response::new_err( + self.client.respond(Response::new_err( req.id.clone(), - lsp_server::ErrorCode::InvalidRequest as i32, + ErrorCode::InvalidRequest as i32, "Shutdown already requested.".to_owned(), )); return; @@ -478,14 +406,7 @@ impl TypstLanguageServer { return; }; - let res = handler(self, (req.id.clone(), req.params)); - if matches!(res, Ok(Some(()))) { - return; - } - - if let Ok(response) = result_to_response_(req.id, res) { - self.client.respond(response); - } + let _ = handler(self, req.id.clone(), req.params); } /// The entry point for the `workspace/executeCommand` request. @@ -495,16 +416,14 @@ impl TypstLanguageServer { params: ExecuteCommandParams, ) -> LspResult> { let ExecuteCommandParams { - command, - arguments, - work_done_progress_params: _, + command, arguments, .. } = params; let Some(handler) = self.exec_cmds.get(command.as_str()) else { error!("asked to execute unknown command"); return Err(method_not_found()); }; - handler(self, (req_id.clone(), arguments)) + handler(self, req_id.clone(), arguments) } /// Handles an incoming notification. @@ -540,33 +459,29 @@ impl TypstLanguageServer { /// Registers or unregisters semantic tokens. fn enable_sema_token_caps(&mut self, enable: bool) -> anyhow::Result<()> { - if !self.const_config().sema_tokens_dynamic_registration { + if !self.const_config().tokens_dynamic_registration { trace!("skip register semantic by config"); return Ok(()); } - let res = match (enable, self.sema_tokens_registered) { - (true, None | Some(false)) => { + match (enable, self.sema_tokens_registered) { + (true, false) => { trace!("registering semantic tokens"); let options = get_semantic_tokens_options(); self.client .register_capability(vec![get_semantic_tokens_registration(options)]) + .inspect(|_| self.sema_tokens_registered = enable) .context("could not register semantic tokens") } - (false, Some(true)) => { + (false, true) => { trace!("unregistering semantic tokens"); self.client .unregister_capability(vec![get_semantic_tokens_unregistration()]) + .inspect(|_| self.sema_tokens_registered = enable) .context("could not unregister semantic tokens") } - (true, Some(true)) | (false, None | Some(false)) => Ok(()), - }; - - if res.is_ok() { - self.sema_tokens_registered = Some(enable); + _ => Ok(()), } - - res } /// Registers or unregisters document formatter. @@ -594,27 +509,23 @@ impl TypstLanguageServer { } } - let res = match (enable, self.formatter_registered) { - (true, None | Some(false)) => { + match (enable, self.formatter_registered) { + (true, false) => { trace!("registering formatter"); self.client .register_capability(vec![get_formatting_registration()]) + .inspect(|_| self.formatter_registered = enable) .context("could not register formatter") } - (false, Some(true)) => { + (false, true) => { trace!("unregistering formatter"); self.client .unregister_capability(vec![get_formatting_unregistration()]) + .inspect(|_| self.formatter_registered = enable) .context("could not unregister formatter") } - (true, Some(true)) | (false, None | Some(false)) => Ok(()), - }; - - if res.is_ok() { - self.formatter_registered = Some(enable); + _ => Ok(()), } - - res } } @@ -731,7 +642,7 @@ impl TypstLanguageServer { let self_path = std::env::current_exe() .map_err(|e| internal_error(format!("Cannot get typst compiler {e}")))?; - let thread = self.user_action_threads.clone(); + let thread = self.user_action_thread.clone(); let entry = self.config.compile.determine_entry(Some(path)); let res = self @@ -750,16 +661,16 @@ impl TypstLanguageServer { .ok_or_else(|| anyhow::anyhow!("main file must be resolved, got {entry:?}"))?; if let Some(f) = thread { - f.send(UserActionRequest::Trace(( + f.send(UserActionRequest::Trace( req_id, - UserActionTraceRequest { + TraceParams { compiler_program: self_path, root: root.as_ref().to_owned(), main, inputs: cc.world().inputs.as_ref().deref().clone(), font_paths: cc.world().font_resolver.font_paths().to_owned(), }, - ))) + )) .context("cannot send trace request")?; } else { bail!("user action thread is not available"); @@ -847,8 +758,8 @@ impl TypstLanguageServer { use crate::tools::package::{self, determine_latest_version, TemplateSource}; #[derive(Debug, Serialize)] + #[serde(rename_all = "camelCase")] struct InitResult { - #[serde(rename = "entryPath")] entry_path: PathBuf, } @@ -1049,7 +960,7 @@ impl TypstLanguageServer { error!("could not change formatter config: {err}"); } if let Some(f) = &self.format_thread { - let err = f.send(FormattingRequest::ChangeConfig(FormattingConfig { + let err = f.send(FormatRequest::ChangeConfig(FormatConfig { mode: self.config.formatter, width: self.config.formatter_print_width, })); @@ -1182,7 +1093,7 @@ impl TypstLanguageServer { let path = as_path(params.text_document).as_path().into(); self.query_source(path, |source| { if let Some(f) = &self.format_thread { - f.send(FormattingRequest::Formatting((req_id, source.clone())))?; + f.send(FormatRequest::Format(req_id, source.clone()))?; } else { bail!("formatter thread is not available"); } @@ -1323,32 +1234,20 @@ pub fn method_not_found() -> ResponseError { } } -pub(crate) fn result_to_response_( - id: lsp_server::RequestId, +pub(crate) fn result_to_response( + id: RequestId, result: Result, -) -> Result { - let res = match result { - Ok(resp) => { - let resp = serde_json::to_value(resp); - match resp { - Ok(resp) => lsp_server::Response::new_ok(id, resp), - Err(e) => return result_to_response(id, Err(internal_error(e.to_string()))), +) -> Response { + match result { + Ok(resp) => match serde_json::to_value(resp) { + Ok(resp) => Response::new_ok(id, resp), + Err(e) => { + let e = internal_error(e.to_string()); + Response::new_err(id, e.code, e.message) } - } - Err(e) => lsp_server::Response::new_err(id, e.code, e.message), - }; - Ok(res) -} - -fn result_to_response( - id: lsp_server::RequestId, - result: Result, -) -> Result { - let res = match result { - Ok(resp) => lsp_server::Response::new_ok(id, resp), - Err(e) => lsp_server::Response::new_err(id, e.code, e.message), - }; - Ok(res) + }, + Err(e) => Response::new_err(id, e.code, e.message), + } } #[test] diff --git a/crates/tinymist/src/server/lsp_init.rs b/crates/tinymist/src/server/lsp_init.rs index 34c37046..af582fef 100644 --- a/crates/tinymist/src/server/lsp_init.rs +++ b/crates/tinymist/src/server/lsp_init.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, path::PathBuf}; +use std::path::PathBuf; use anyhow::bail; use itertools::Itertools; @@ -11,13 +11,12 @@ use tokio::sync::mpsc; use typst::util::Deferred; use typst_ts_core::ImmutPath; -use crate::actor::cluster::EditorActor; +use crate::actor::editor::EditorActor; use crate::compiler_init::CompileConfig; use crate::harness::LspHost; +use crate::utils::{try_, try_or}; use crate::world::{ImmutDict, SharedFontResolver}; -use crate::{ - invalid_params, CompileFontOpts, LspResult, TypstLanguageServer, TypstLanguageServerArgs, -}; +use crate::{invalid_params, CompileFontOpts, LspResult, TypstLanguageServer}; // todo: svelte-language-server responds to a Goto Definition request with // LocationLink[] even if the client does not report the @@ -68,13 +67,10 @@ pub enum SemanticTokensMode { pub struct CompileExtraOpts { /// The root directory for compilation routine. pub root_dir: Option, - /// Path to entry pub entry: Option, - /// Additional input arguments to compile the entry file. pub inputs: ImmutDict, - /// will remove later pub font_paths: Vec, } @@ -153,38 +149,14 @@ impl Config { /// # Errors /// Errors if the update is invalid. pub fn update_by_map(&mut self, update: &Map) -> anyhow::Result<()> { - let semantic_tokens = update - .get("semanticTokens") - .map(SemanticTokensMode::deserialize) - .and_then(Result::ok); - if let Some(semantic_tokens) = semantic_tokens { - self.semantic_tokens = semantic_tokens; - } - - let formatter = update - .get("formatterMode") - .map(FormatterMode::deserialize) - .and_then(Result::ok); - if let Some(formatter) = formatter { - self.formatter = formatter; - } - - let print_width = update - .get("formatterPrintWidth") - .and_then(|e| serde_json::from_value::(e.clone()).ok()); - if let Some(formatter) = print_width { - self.formatter_print_width = formatter; - } - + try_(|| SemanticTokensMode::deserialize(update.get("semanticTokens")?).ok()) + .inspect(|v| self.semantic_tokens = *v); + try_(|| FormatterMode::deserialize(update.get("formatterMode")?).ok()) + .inspect(|v| self.formatter = *v); + try_(|| u32::deserialize(update.get("formatterPrintWidth")?).ok()) + .inspect(|v| self.formatter_print_width = *v); self.compile.update_by_map(update)?; - self.validate()?; - Ok(()) - } - - fn validate(&self) -> anyhow::Result<()> { - self.compile.validate()?; - - Ok(()) + self.compile.validate() } } @@ -198,11 +170,11 @@ pub struct ConstConfig { /// Allow dynamic registration of configuration changes. pub cfg_change_registration: bool, /// Allow dynamic registration of semantic tokens. - pub sema_tokens_dynamic_registration: bool, + pub tokens_dynamic_registration: bool, /// Allow overlapping tokens. - pub sema_tokens_overlapping_token_support: bool, + pub tokens_overlapping_token_support: bool, /// Allow multiline tokens. - pub sema_tokens_multiline_token_support: bool, + pub tokens_multiline_token_support: bool, /// Allow line folding on documents. pub doc_line_folding_only: bool, /// Allow dynamic registration of document formatting. @@ -211,16 +183,12 @@ pub struct ConstConfig { impl From<&InitializeParams> for ConstConfig { fn from(params: &InitializeParams) -> Self { - const DEFAULT_ENCODING: &[PositionEncodingKind; 1] = &[PositionEncodingKind::UTF16]; + const DEFAULT_ENCODING: &[PositionEncodingKind] = &[PositionEncodingKind::UTF16]; let position_encoding = { - let encodings = params - .capabilities - .general - .as_ref() - .and_then(|general| general.position_encodings.as_ref()) - .map(|encodings| encodings.as_slice()) - .unwrap_or(DEFAULT_ENCODING); + let general = params.capabilities.general.as_ref(); + let encodings = try_(|| Some(general?.position_encodings.as_ref()?.as_slice())); + let encodings = encodings.unwrap_or(DEFAULT_ENCODING); if encodings.contains(&PositionEncodingKind::UTF8) { PositionEncoding::Utf8 @@ -229,42 +197,20 @@ impl From<&InitializeParams> for ConstConfig { } }; - let workspace_caps = params.capabilities.workspace.as_ref(); - let supports_config_change_registration = workspace_caps - .and_then(|workspace| workspace.configuration) - .unwrap_or(false); - - let doc_caps = params.capabilities.text_document.as_ref(); - let folding_caps = doc_caps.and_then(|doc| doc.folding_range.as_ref()); - let line_folding_only = folding_caps - .and_then(|folding| folding.line_folding_only) - .unwrap_or(true); - - let semantic_tokens_caps = doc_caps.and_then(|doc| doc.semantic_tokens.as_ref()); - let supports_semantic_tokens_dynamic_registration = semantic_tokens_caps - .and_then(|semantic_tokens| semantic_tokens.dynamic_registration) - .unwrap_or(false); - let supports_semantic_tokens_overlapping_token_support = semantic_tokens_caps - .and_then(|semantic_tokens| semantic_tokens.overlapping_token_support) - .unwrap_or(false); - let supports_semantic_tokens_multiline_token_support = semantic_tokens_caps - .and_then(|semantic_tokens| semantic_tokens.multiline_token_support) - .unwrap_or(false); - - let formatter_caps = doc_caps.and_then(|doc| doc.formatting.as_ref()); - let supports_document_formatting_dynamic_registration = formatter_caps - .and_then(|formatting| formatting.dynamic_registration) - .unwrap_or(false); + let workspace = params.capabilities.workspace.as_ref(); + let doc = params.capabilities.text_document.as_ref(); + let sema = try_(|| doc?.semantic_tokens.as_ref()); + let fold = try_(|| doc?.folding_range.as_ref()); + let format = try_(|| doc?.formatting.as_ref()); Self { position_encoding, - sema_tokens_dynamic_registration: supports_semantic_tokens_dynamic_registration, - sema_tokens_overlapping_token_support: - supports_semantic_tokens_overlapping_token_support, - sema_tokens_multiline_token_support: supports_semantic_tokens_multiline_token_support, - doc_fmt_dynamic_registration: supports_document_formatting_dynamic_registration, - cfg_change_registration: supports_config_change_registration, - doc_line_folding_only: line_folding_only, + cfg_change_registration: try_or(|| workspace?.configuration, false), + tokens_dynamic_registration: try_or(|| sema?.dynamic_registration, false), + tokens_overlapping_token_support: try_or(|| sema?.overlapping_token_support, false), + tokens_multiline_token_support: try_or(|| sema?.multiline_token_support, false), + doc_line_folding_only: try_or(|| fold?.line_folding_only, true), + doc_fmt_dynamic_registration: try_or(|| format?.dynamic_registration, false), } } } @@ -299,19 +245,14 @@ impl Init { // Initialize configurations let cc = ConstConfig::from(¶ms); - info!( - "initialized with const_config {const_config:?}", - const_config = cc - ); + info!("initialized with const_config {cc:?}"); let mut config = Config { compile: CompileConfig { roots: match params.workspace_folders.as_ref() { Some(roots) => roots .iter() - .map(|root| &root.uri) - .map(Url::to_file_path) - .collect::, _>>() - .unwrap(), + .filter_map(|root| root.uri.to_file_path().ok()) + .collect::>(), #[allow(deprecated)] // `params.root_path` is marked as deprecated None => params .root_uri @@ -352,15 +293,15 @@ impl Init { }; // Bootstrap server - let (diag_tx, diag_rx) = mpsc::unbounded_channel(); + let (editor_tx, editor_rx) = mpsc::unbounded_channel(); - let mut service = TypstLanguageServer::new(TypstLanguageServerArgs { - client: self.host.clone(), - const_config: cc.clone(), - diag_tx, - handle: self.handle.clone(), + let mut service = TypstLanguageServer::new( + self.host.clone(), + cc.clone(), + editor_tx, font, - }); + self.handle.clone(), + ); if let Err(err) = res { return (service, Err(err)); @@ -373,14 +314,11 @@ impl Init { service.run_format_thread(); service.run_user_action_thread(); - let cluster_actor = EditorActor { - host: self.host.clone(), - diag_rx, - diagnostics: HashMap::new(), - affect_map: HashMap::new(), - published_primary: false, - notify_compile_status: service.config.compile.notify_compile_status, - }; + let editor_actor = EditorActor::new( + self.host.clone(), + editor_rx, + service.config.compile.notify_compile_status, + ); let fallback = service.config.compile.determine_default_entry_path(); let primary = service.server( @@ -394,14 +332,14 @@ impl Init { service.primary.compiler = Some(primary); // Run the cluster in the background after we referencing it - self.handle.spawn(cluster_actor.run()); + self.handle.spawn(editor_actor.run()); // Respond to the host (LSP client) // Register these capabilities statically if the client does not support dynamic // registration let semantic_tokens_provider = match service.config.semantic_tokens { - SemanticTokensMode::Enable if !cc.sema_tokens_dynamic_registration => { + SemanticTokensMode::Enable if !cc.tokens_dynamic_registration => { Some(get_semantic_tokens_options().into()) } _ => None, diff --git a/crates/tinymist/src/state.rs b/crates/tinymist/src/state.rs index f2eb8874..dd9f76b0 100644 --- a/crates/tinymist/src/state.rs +++ b/crates/tinymist/src/state.rs @@ -2,13 +2,13 @@ use std::path::PathBuf; -use ::typst::{diag::FileResult, syntax::Source}; use anyhow::anyhow; use lsp_types::TextDocumentContentChangeEvent; use tinymist_query::{ lsp_to_typst, CompilerQueryRequest, CompilerQueryResponse, FoldRequestFeature, OnExportRequest, OnSaveExportRequest, PositionEncoding, SemanticRequest, StatefulRequest, SyntaxRequest, }; +use typst::{diag::FileResult, syntax::Source}; use typst_ts_compiler::{ vfs::notify::{FileChangeSet, MemoryEvent}, Time, @@ -30,11 +30,10 @@ impl CompileServer { impl TypstLanguageServer { /// Pin the entry to the given path pub fn pin_entry(&mut self, new_entry: Option) -> Result<(), Error> { - let pinning = new_entry.is_some(); + self.pinning = new_entry.is_some(); self.primary.do_change_entry(new_entry)?; - self.pinning = pinning; - if !pinning { + if !self.pinning { let fallback = self.config.compile.determine_default_entry_path(); let fallback = fallback.or_else(|| self.focusing.clone()); if let Some(e) = fallback { @@ -195,7 +194,7 @@ impl TypstLanguageServer { macro_rules! run_query { ($self: ident.$query: ident ($($arg_key:ident),* $(,)?)) => {{ use tinymist_query::*; - let req = paste! { [<$query Request>] { $($arg_key),* } }; + let req = paste::paste! { [<$query Request>] { $($arg_key),* } }; $self .query(CompilerQueryRequest::$query(req.clone())) .map_err(|err| { @@ -255,7 +254,7 @@ impl TypstLanguageServer { f: impl FnOnce(Source) -> anyhow::Result, ) -> anyhow::Result { let snapshot = self.primary.memory_changes.get(&path); - let snapshot = snapshot.ok_or_else(|| anyhow!("file missing {:?}", path))?; + let snapshot = snapshot.ok_or_else(|| anyhow!("file missing {path:?}"))?; let source = snapshot.content.clone(); f(source) } @@ -292,10 +291,10 @@ impl TypstLanguageServer { assert!(query.fold_feature() != FoldRequestFeature::ContextFreeUnique); match query { - CompilerQueryRequest::OnExport(OnExportRequest { kind, path }) => Ok( + OnExport(OnExportRequest { kind, path }) => Ok( CompilerQueryResponse::OnExport(client.on_export(kind, path)?), ), - CompilerQueryRequest::OnSaveExport(OnSaveExportRequest { path }) => { + OnSaveExport(OnSaveExportRequest { path }) => { client.on_save_export(path)?; Ok(CompilerQueryResponse::OnSaveExport(())) } @@ -312,21 +311,12 @@ impl TypstLanguageServer { Rename(req) => query_state!(client, Rename, req), PrepareRename(req) => query_state!(client, PrepareRename, req), Symbol(req) => query_world!(client, Symbol, req), - DocumentMetrics(req) => query_state!(client, DocumentMetrics, req), ServerInfo(_) => { let res = client.collect_server_info()?; Ok(CompilerQueryResponse::ServerInfo(Some(res))) } - - InteractCodeContext(..) - | FoldingRange(..) - | SelectionRange(..) - | SemanticTokensDelta(..) - | Formatting(..) - | DocumentSymbol(..) - | ColorPresentation(..) - | SemanticTokensFull(..) => unreachable!(), + _ => unreachable!(), } } } diff --git a/crates/tinymist/src/tools/word_count.rs b/crates/tinymist/src/tools/word_count.rs index 815cc756..282790d2 100644 --- a/crates/tinymist/src/tools/word_count.rs +++ b/crates/tinymist/src/tools/word_count.rs @@ -9,6 +9,7 @@ use unicode_script::{Script, UnicodeScript}; /// Words count for a document. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct WordsCount { /// Number of words. pub words: usize, @@ -18,7 +19,6 @@ pub struct WordsCount { /// Multiple consecutive spaces are counted as one. pub spaces: usize, /// Number of CJK characters. - #[serde(rename = "cjkChars")] pub cjk_chars: usize, } diff --git a/crates/tinymist/src/utils.rs b/crates/tinymist/src/utils.rs index 8238ddab..7fdf84f7 100644 --- a/crates/tinymist/src/utils.rs +++ b/crates/tinymist/src/utils.rs @@ -22,6 +22,18 @@ pub fn threaded_receive(f: oneshot::Receiver) -> Result { .map_err(map_string_err("failed to recv from receive data")) } +pub fn try_(f: impl FnOnce() -> Option) -> Option { + f() +} + +pub fn try_or(f: impl FnOnce() -> Option, default: T) -> T { + f().unwrap_or(default) +} + +pub fn try_or_default(f: impl FnOnce() -> Option) -> T { + f().unwrap_or_default() +} + #[cfg(test)] mod tests { fn do_receive() { diff --git a/crates/tinymist/src/world.rs b/crates/tinymist/src/world.rs index 9a9429b4..c6047462 100644 --- a/crates/tinymist/src/world.rs +++ b/crates/tinymist/src/world.rs @@ -26,30 +26,24 @@ pub struct CompileOpts { } #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct CompileOnceOpts { /// The root directory for compilation routine. - #[serde(rename = "rootDir")] pub root_dir: PathBuf, - /// Path to entry pub entry: PathBuf, - /// Additional input arguments to compile the entry file. pub inputs: TypstDict, } #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct CompileFontOpts { /// Path to font profile for cache - #[serde(rename = "fontProfileCachePath")] pub font_profile_cache_path: PathBuf, - /// will remove later - #[serde(rename = "fontPaths")] pub font_paths: Vec, - /// Exclude system font paths - #[serde(rename = "noSystemFonts")] pub no_system_fonts: bool, }