From 6a142dca0147e9954bba56a84bb469510b400108 Mon Sep 17 00:00:00 2001
From: Myriad-Dreamin <35292584+Myriad-Dreamin@users.noreply.github.com>
Date: Sun, 14 Jul 2024 17:40:01 +0800
Subject: [PATCH] dev: refactor tasks (#411)
* dev: improve export task
* aba aba
* dev: less indent
* dev: reorder
* dev: reduce some clone
* dev: improve format task
* fix: documentation links
* dev: doesn't rely on hash entry state
---
.../tinymist-query/src/color_presentation.rs | 5 +-
crates/tinymist-query/src/on_enter.rs | 6 +-
crates/tinymist-query/src/syntax/module.rs | 5 +-
crates/tinymist/src/actor/editor.rs | 4 +-
crates/tinymist/src/actor/mod.rs | 8 +-
crates/tinymist/src/actor/typ_client.rs | 47 +--
crates/tinymist/src/actor/typ_server.rs | 35 +--
crates/tinymist/src/init.rs | 101 ++++++-
crates/tinymist/src/server.rs | 12 +-
crates/tinymist/src/task/export.rs | 276 ++++++------------
crates/tinymist/src/task/format.rs | 63 ++--
crates/tinymist/src/task/mod.rs | 22 +-
12 files changed, 276 insertions(+), 308 deletions(-)
diff --git a/crates/tinymist-query/src/color_presentation.rs b/crates/tinymist-query/src/color_presentation.rs
index 0ca5e999..f4c5f982 100644
--- a/crates/tinymist-query/src/color_presentation.rs
+++ b/crates/tinymist-query/src/color_presentation.rs
@@ -18,8 +18,9 @@ use crate::prelude::*;
/// This request was introduced in specification version 3.6.0.
///
/// This request has no special capabilities and registration options since it
-/// is sent as a resolve request for the
-/// [`textDocument/documentColor`](Self::document_color) request.
+/// is sent as a resolve request for the [`textDocument/documentColor`] request.
+///
+/// [`textDocument/documentColor`]: https://microsoft.github.io/language-server-protocol/specification#textDocument_documentColor
#[derive(Debug, Clone)]
pub struct ColorPresentationRequest {
/// The path of the document to request color presentations for.
diff --git a/crates/tinymist-query/src/on_enter.rs b/crates/tinymist-query/src/on_enter.rs
index 0357afd8..408326f5 100644
--- a/crates/tinymist-query/src/on_enter.rs
+++ b/crates/tinymist-query/src/on_enter.rs
@@ -5,9 +5,9 @@ use crate::{prelude::*, SyntaxRequest};
/// The [`experimental/onEnter`] request is sent from client to server to handle
/// the Enter key press.
///
-/// - kbd:[Enter] inside triple-slash comments automatically inserts `///`
-/// - kbd:[Enter] in the middle or after a trailing space in `//` inserts `//`
-/// - kbd:[Enter] inside `//!` doc comments automatically inserts `//!`
+/// - `kbd:Enter` inside triple-slash comments automatically inserts `///`
+/// - `kbd:Enter` in the middle or after a trailing space in `//` inserts `//`
+/// - `kbd:Enter` inside `//!` doc comments automatically inserts `//!`
///
/// [`experimental/onEnter`]: https://github.com/rust-lang/rust-analyzer/blob/master/docs/dev/lsp-extensions.md#on-enter
///
diff --git a/crates/tinymist-query/src/syntax/module.rs b/crates/tinymist-query/src/syntax/module.rs
index 692f01fe..5df8b791 100644
--- a/crates/tinymist-query/src/syntax/module.rs
+++ b/crates/tinymist-query/src/syntax/module.rs
@@ -15,8 +15,9 @@ pub struct ModuleDependency {
/// Construct the module dependencies of the given context.
///
-/// It will scan all the files in the context, using [`AnalysisContext::files`],
-/// and find the dependencies and dependents of each file.
+/// It will scan all the files in the context, using
+/// [`AnalysisContext::source_files`], and find the dependencies and dependents
+/// of each file.
pub fn construct_module_dependencies(
ctx: &mut AnalysisContext,
) -> HashMap {
diff --git a/crates/tinymist/src/actor/editor.rs b/crates/tinymist/src/actor/editor.rs
index 5eb897d1..1598dd67 100644
--- a/crates/tinymist/src/actor/editor.rs
+++ b/crates/tinymist/src/actor/editor.rs
@@ -4,7 +4,7 @@
use std::collections::HashMap;
use log::info;
-use lsp_types::{Diagnostic, Url};
+use lsp_types::Url;
use tinymist_query::{DiagnosticsMap, LspDiagnostic};
use tokio::sync::mpsc;
@@ -115,7 +115,7 @@ impl EditorActor {
}
}
- fn publish_inner(&mut self, group: &str, url: Url, next: Option>) {
+ fn publish_inner(&mut self, group: &str, url: Url, next: Option>) {
let mut to_publish = Vec::new();
// Get the diagnostics from other groups
diff --git a/crates/tinymist/src/actor/mod.rs b/crates/tinymist/src/actor/mod.rs
index 9628c324..acc1eabd 100644
--- a/crates/tinymist/src/actor/mod.rs
+++ b/crates/tinymist/src/actor/mod.rs
@@ -20,7 +20,7 @@ use self::{
typ_server::CompileServerActor,
};
use crate::{
- task::{ExportConfig, ExportTask, ExportTaskConf},
+ task::{ExportConfig, ExportTask, ExportUserConfig},
world::{ImmutDict, LspWorldBuilder},
LanguageState,
};
@@ -52,11 +52,11 @@ impl LanguageState {
let (intr_tx, intr_rx) = mpsc::unbounded_channel();
// Run Export actors before preparing cluster to avoid loss of events
- let export = ExportTask::new(ExportTaskConf {
+ let export = ExportTask::new(ExportConfig {
group: editor_group.clone(),
editor_tx: Some(self.editor_tx.clone()),
- config: ExportConfig {
- substitute_pattern: self.compile_config().output_path.clone(),
+ config: ExportUserConfig {
+ output: self.compile_config().output_path.clone(),
mode: self.compile_config().export_pdf,
},
kind: ExportKind::Pdf,
diff --git a/crates/tinymist/src/actor/typ_client.rs b/crates/tinymist/src/actor/typ_client.rs
index 7d2fbf22..1ac6517f 100644
--- a/crates/tinymist/src/actor/typ_client.rs
+++ b/crates/tinymist/src/actor/typ_client.rs
@@ -10,20 +10,17 @@
//! └─────┬────────────────────▲─────┘ └────────────┘
//! │ │
//! ┌─────▼────────────────────┴─────┐ handler ┌────────────┐
-//! │ compiler::compile_driver ├────────►│ rest actors│
+//! │ compiler::compile_handler ├────────►│ rest actors│
//! └────────────────────────────────┘ └────────────┘
//! ```
//!
-//! We generally use typst in two ways.
-//! + creates a [`CompileDriver`] and run compilation in fly.
-//! + creates a [`CompileServerActor`], wraps the driver, and runs
-//! [`CompileDriver`] incrementally.
+//! We use typst by creating a
+//! [`CompileServerActor`][`crate::actor::typ_server::CompileServerActor`] and
+//! running compiler with callbacking [`CompileHandler`] incrementally. An
+//! additional [`CompileClientActor`] is also created to control the
+//! [`CompileServerActor`][`crate::actor::typ_server::CompileServerActor`].
//!
-//! For latter case, an additional [`CompileClientActor`] is created to
-//! control the [`CompileServerActor`].
-//!
-//! The [`CompileDriver`] will also keep a [`CompileHandler`] to push
-//! information to other actors.
+//! The [`CompileHandler`] will push information to other actors.
use std::{
collections::HashMap,
@@ -61,7 +58,7 @@ use super::{
typ_server::{CompilationHandle, CompileSnapshot, CompiledArtifact, Interrupt},
};
use crate::{
- task::{ExportConfig, ExportSignal, ExportTask},
+ task::{ExportTask, ExportUserConfig},
world::{LspCompilerFeat, LspWorld},
CompileConfig,
};
@@ -295,20 +292,12 @@ impl CompilationHandle for CompileHandler {
snap.env.tracer.as_ref().map(|e| e.clone().warnings()),
);
- if snap.flags.triggered_by_entry_update {
- self.export.signal(snap, ExportSignal::EntryChanged);
- } else if snap.flags.triggered_by_mem_events && snap.flags.triggered_by_fs_events {
- self.export.signal(snap, ExportSignal::TypedAndSaved);
- } else if snap.flags.triggered_by_mem_events {
- self.export.signal(snap, ExportSignal::Typed);
- } else if snap.flags.triggered_by_fs_events {
- self.export.signal(snap, ExportSignal::Saved);
- }
-
if let Ok(doc) = &snap.doc {
let _ = self.doc_tx.send(Some(doc.clone()));
}
+ self.export.signal(snap, snap.signal);
+
self.editor_tx
.send(EditorRequest::Status(
self.diag_group.clone(),
@@ -326,7 +315,7 @@ impl CompilationHandle for CompileHandler {
.doc
.clone()
.map_err(|_| typst_preview::CompileStatus::CompileError);
- inner.notify_compile(res, snap.flags.triggered_by_fs_events);
+ inner.notify_compile(res, snap.signal.by_fs_events);
}
}
}
@@ -368,25 +357,17 @@ impl CompileClientActor {
self.config = config;
}
- pub(crate) fn change_export_config(&mut self, config: ExportConfig) {
+ pub(crate) fn change_export_config(&mut self, config: ExportUserConfig) {
self.handle.export.change_config(config);
}
pub fn on_export(&self, kind: ExportKind, path: PathBuf) -> QueryFuture {
let snap = self.snapshot()?;
- let export = self.handle.export.task();
let entry = self.config.determine_entry(Some(path.as_path().into()));
-
+ let export = self.handle.export.oneshot(snap, Some(entry), kind);
just_future(async move {
- let snap = snap.snapshot().await?;
- let snap = snap.task(TaskInputs {
- entry: Some(entry),
- ..Default::default()
- });
-
- let artifact = snap.compile().await;
- let res = export.oneshot(&artifact, kind).await;
+ let res = export.await?;
log::info!("CompileActor: on export end: {path:?} as {res:?}");
Ok(tinymist_query::CompilerQueryResponse::OnExport(res))
diff --git a/crates/tinymist/src/actor/typ_server.rs b/crates/tinymist/src/actor/typ_server.rs
index 14c8808f..e2af7a1d 100644
--- a/crates/tinymist/src/actor/typ_server.rs
+++ b/crates/tinymist/src/actor/typ_server.rs
@@ -29,21 +29,23 @@ use typst_ts_core::{exporter_builtins::GroupExporter, Exporter, GenericExporter,
type CompileRawResult = Deferred<(SourceResult>, CompileEnv)>;
type DocState = once_cell::sync::OnceCell;
-#[derive(Clone, Copy)]
-pub struct CompileFlags {
- /// The compiler-thread local logical tick when the snapshot is taken.
- pub compile_tick: usize,
+/// A signal that possibly triggers an export.
+///
+/// Whether to export depends on the current state of the document and the user
+/// settings.
+#[derive(Debug, Clone, Copy)]
+pub struct ExportSignal {
/// Whether the revision is annotated by memory events.
- pub triggered_by_mem_events: bool,
+ pub by_mem_events: bool,
/// Whether the revision is annotated by file system events.
- pub triggered_by_fs_events: bool,
+ pub by_fs_events: bool,
/// Whether the revision is annotated by entry update.
- pub triggered_by_entry_update: bool,
+ pub by_entry_update: bool,
}
pub struct CompileSnapshot {
- /// All the flags for the document.
- pub flags: CompileFlags,
+ /// The export signal for the document.
+ pub flags: ExportSignal,
/// Using env
pub env: CompileEnv,
/// Using world
@@ -97,7 +99,7 @@ impl CompileSnapshot {
pub async fn compile(&self) -> CompiledArtifact {
let (doc, env) = self.start().wait().clone();
CompiledArtifact {
- flags: self.flags,
+ signal: self.flags,
world: self.world.clone(),
env,
doc,
@@ -120,8 +122,8 @@ impl Clone for CompileSnapshot {
#[derive(Clone)]
pub struct CompiledArtifact {
- /// All the flags for the document.
- pub flags: CompileFlags,
+ /// All the export signal for the document.
+ pub signal: ExportSignal,
/// Used world
pub world: Arc>,
/// Used env
@@ -484,11 +486,10 @@ impl CompileServerActor {
CompileSnapshot {
world: Arc::new(world.clone()),
env: env.clone(),
- flags: CompileFlags {
- compile_tick: self.logical_tick,
- triggered_by_entry_update: reason.by_entry_update,
- triggered_by_mem_events: reason.by_memory_events,
- triggered_by_fs_events: reason.by_fs_events,
+ flags: ExportSignal {
+ by_entry_update: reason.by_entry_update,
+ by_mem_events: reason.by_memory_events,
+ by_fs_events: reason.by_fs_events,
},
doc_state: Arc::new(OnceCell::new()),
success_doc: self.latest_success_doc.clone(),
diff --git a/crates/tinymist/src/init.rs b/crates/tinymist/src/init.rs
index 396a7cd1..c98e3c1e 100644
--- a/crates/tinymist/src/init.rs
+++ b/crates/tinymist/src/init.rs
@@ -7,7 +7,8 @@ use comemo::Prehashed;
use itertools::Itertools;
use lsp_types::*;
use once_cell::sync::{Lazy, OnceCell};
-use serde::Deserialize;
+use reflexo::path::PathClean;
+use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value as JsonValue};
use tinymist_query::{get_semantic_tokens_options, PositionEncoding};
use tinymist_render::PeriscopeArgs;
@@ -401,7 +402,7 @@ pub struct CompileConfig {
/// The workspace roots from initialization.
pub roots: Vec,
/// The output directory for PDF export.
- pub output_path: String,
+ pub output_path: PathPattern,
/// The mode of PDF export.
pub export_pdf: ExportMode,
/// Specifies the root path of the project manually.
@@ -438,7 +439,8 @@ impl CompileConfig {
/// Updates the configuration with a map.
pub fn update_by_map(&mut self, update: &Map) -> anyhow::Result<()> {
- self.output_path = try_or_default(|| Some(update.get("outputPath")?.as_str()?.to_owned()));
+ self.output_path =
+ try_or_default(|| PathPattern::deserialize(update.get("outputPath")?).ok());
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_status = match try_(|| update.get("compileStatus")?.as_str()) {
@@ -726,6 +728,71 @@ pub struct CompileExtraOpts {
pub font_paths: Vec,
}
+/// The path pattern that could be substituted.
+///
+/// # Examples
+/// - `$root` is the root of the project.
+/// - `$root/$dir` is the parent directory of the input (main) file.
+/// - `$root/main` will help store pdf file to `$root/main.pdf` constantly.
+/// - (default) `$root/$dir/$name` will help store pdf file along with the input
+/// file.
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
+pub struct PathPattern(pub String);
+
+impl PathPattern {
+ /// Creates a new path pattern.
+ pub fn new(pattern: &str) -> Self {
+ Self(pattern.to_owned())
+ }
+
+ /// Substitutes the path pattern with `$root`, and `$dir/$name`.
+ pub fn substitute(&self, entry: &EntryState) -> Option {
+ self.substitute_impl(entry.root(), entry.main())
+ }
+
+ #[comemo::memoize]
+ fn substitute_impl(&self, root: Option, main: Option) -> Option {
+ log::info!("Check path {main:?} and root {root:?} with output directory {self:?}");
+
+ let (root, main) = root.zip(main)?;
+
+ // Files in packages are not exported
+ if main.package().is_some() {
+ return None;
+ }
+ // Files without a path are not exported
+ let path = main.vpath().resolve(&root)?;
+
+ // todo: handle untitled path
+ 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 self.0.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 = self.0.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::*;
@@ -754,7 +821,7 @@ mod tests {
config.update(&update).unwrap();
- assert_eq!(config.compile.output_path, "out");
+ assert_eq!(config.compile.output_path, PathPattern::new("out"));
assert_eq!(config.compile.export_pdf, ExportMode::OnSave);
assert_eq!(config.compile.root_path, Some(PathBuf::from(root_path)));
assert_eq!(config.semantic_tokens, SemanticTokensMode::Enable);
@@ -799,4 +866,30 @@ mod tests {
let err = format!("{}", config.update(&update).unwrap_err());
assert!(err.contains("absolute path"), "unexpected error: {err}");
}
+
+ #[test]
+ fn test_substitute_path() {
+ let root = Path::new("/root");
+ let entry = EntryState::new_rooted(
+ root.into(),
+ Some(FileId::new(None, VirtualPath::new("/dir1/dir2/file.txt"))),
+ );
+
+ assert_eq!(
+ PathPattern::new("/substitute/$dir/$name").substitute(&entry),
+ Some(PathBuf::from("/substitute/dir1/dir2/file.txt").into())
+ );
+ assert_eq!(
+ PathPattern::new("/substitute/$dir/../$name").substitute(&entry),
+ Some(PathBuf::from("/substitute/dir1/file.txt").into())
+ );
+ assert_eq!(
+ PathPattern::new("/substitute/$name").substitute(&entry),
+ Some(PathBuf::from("/substitute/file.txt").into())
+ );
+ assert_eq!(
+ PathPattern::new("/substitute/target/$dir/$name").substitute(&entry),
+ Some(PathBuf::from("/substitute/target/dir1/dir2/file.txt").into())
+ );
+ }
}
diff --git a/crates/tinymist/src/server.rs b/crates/tinymist/src/server.rs
index 34e679a2..0ef0d6fe 100644
--- a/crates/tinymist/src/server.rs
+++ b/crates/tinymist/src/server.rs
@@ -15,7 +15,7 @@ use once_cell::sync::OnceCell;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value as JsonValue};
use sync_lsp::*;
-use task::{ExportConfig, FormatConfig, FormatTask, UserActionTask};
+use task::{ExportUserConfig, FormatTask, FormatUserConfig, UserActionTask};
use tinymist_query::{
get_semantic_tokens_options, get_semantic_tokens_registration,
get_semantic_tokens_unregistration, PageSelection, SemanticTokenContext,
@@ -111,7 +111,7 @@ impl LanguageState {
const_config.tokens_overlapping_token_support,
const_config.tokens_multiline_token_support,
);
- let formatter = FormatTask::new(FormatConfig {
+ let formatter = FormatTask::new(FormatUserConfig {
mode: config.formatter,
width: config.formatter_print_width,
position_encoding: const_config.position_encoding,
@@ -481,8 +481,8 @@ impl LanguageState {
if config.compile.output_path != self.config.compile.output_path
|| config.compile.export_pdf != self.config.compile.export_pdf
{
- let config = ExportConfig {
- substitute_pattern: self.config.compile.output_path.clone(),
+ let config = ExportUserConfig {
+ output: self.config.compile.output_path.clone(),
mode: self.config.compile.export_pdf,
};
@@ -513,7 +513,7 @@ impl LanguageState {
error!("could not change formatter config: {err}");
}
- self.formatter.change_config(FormatConfig {
+ self.formatter.change_config(FormatUserConfig {
mode: self.config.formatter,
width: self.config.formatter_print_width,
position_encoding: self.const_config.position_encoding,
@@ -667,7 +667,7 @@ impl LanguageState {
let source = self
.query_source(path, |source: typst::syntax::Source| Ok(source))
.map_err(|e| internal_error(format!("could not format document: {e}")))?;
- self.client.schedule(req_id, self.formatter.exec(source))
+ self.client.schedule(req_id, self.formatter.run(source))
}
fn inlay_hint(&mut self, req_id: RequestId, params: InlayHintParams) -> ScheduledResult {
diff --git a/crates/tinymist/src/task/export.rs b/crates/tinymist/src/task/export.rs
index 8068de9a..5cddab16 100644
--- a/crates/tinymist/src/task/export.rs
+++ b/crates/tinymist/src/task/export.rs
@@ -1,75 +1,53 @@
-//! The actor that handles PDF/SVG/PNG export.
+//! The actor that handles various document export, like PDF and SVG export.
-use std::{
- path::{Path, PathBuf},
- sync::Arc,
-};
+use std::{path::PathBuf, sync::Arc};
-use anyhow::bail;
-use anyhow::Context;
-use log::{error, info};
+use anyhow::{bail, Context};
use once_cell::sync::Lazy;
use tinymist_query::{ExportKind, PageSelection};
use tokio::{sync::mpsc, task::spawn_blocking};
use typst::{foundations::Smart, layout::Abs, layout::Frame, visualize::Color};
-use typst_ts_compiler::EntryReader;
-use typst_ts_core::{path::PathClean, ImmutPath};
+use typst_ts_compiler::{EntryReader, EntryState, TaskInputs};
use crate::{
- actor::{editor::EditorRequest, typ_server::CompiledArtifact},
+ actor::{
+ editor::EditorRequest,
+ typ_client::QuerySnap,
+ typ_server::{CompiledArtifact, ExportSignal},
+ },
tool::word_count,
world::LspCompilerFeat,
- ExportMode,
+ ExportMode, PathPattern,
};
use super::*;
+/// User configuration for export.
#[derive(Debug, Clone, Default)]
-pub struct ExportConfig {
- pub substitute_pattern: String,
+pub struct ExportUserConfig {
+ /// The output path pattern.
+ pub output: PathPattern,
+ /// The export mode.
pub mode: ExportMode,
}
-#[derive(Debug, Clone, Copy)]
-pub enum ExportSignal {
- Typed,
- Saved,
- TypedAndSaved,
- EntryChanged,
-}
-
-impl ExportSignal {
- pub fn is_typed(&self) -> bool {
- matches!(self, ExportSignal::Typed | ExportSignal::TypedAndSaved)
- }
-
- pub fn is_saved(&self) -> bool {
- matches!(self, ExportSignal::Saved | ExportSignal::TypedAndSaved)
- }
-
- fn is_entry_change(&self) -> bool {
- matches!(self, ExportSignal::EntryChanged)
- }
-}
-
#[derive(Clone, Default)]
pub struct ExportTask {
- factory: SyncTaskFactory,
+ factory: SyncTaskFactory,
export_folder: FutureFolder,
count_word_folder: FutureFolder,
}
impl ExportTask {
- pub fn new(data: ExportTaskConf) -> Self {
+ pub fn new(data: ExportConfig) -> Self {
Self {
- factory: SyncTaskFactory(Arc::new(std::sync::RwLock::new(Arc::new(data)))),
- export_folder: FutureFolder::default(),
- count_word_folder: FutureFolder::default(),
+ factory: SyncTaskFactory::new(data),
+ ..ExportTask::default()
}
}
- pub fn task(&self) -> Arc {
- self.factory.task()
+ pub fn change_config(&self, config: ExportUserConfig) {
+ self.factory.mutate(|data| data.config = config);
}
pub fn signal(&self, snap: &CompiledArtifact, s: ExportSignal) {
@@ -77,30 +55,36 @@ impl ExportTask {
task.signal(snap, s, self);
}
- pub fn change_config(&self, config: ExportConfig) {
- self.factory.mutate(|data| data.config = config);
+ pub fn oneshot(
+ &self,
+ snap: QuerySnap,
+ entry: Option,
+ kind: ExportKind,
+ ) -> impl Future