feat: move world implementation (#1183)

* feat: move world implementation

* dev: remove vector ir

* fix: errors

* fix: clippy

* fix: don't build world in web

* fix: unused patches

* fix: fmt

* fix: docs example

* fix: doc examples
This commit is contained in:
Myriad-Dreamin 2025-01-19 08:25:35 +08:00 committed by GitHub
parent a9437b2772
commit 6180e343e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
122 changed files with 7829 additions and 439 deletions

140
Cargo.lock generated
View file

@ -749,7 +749,8 @@ dependencies = [
"criterion", "criterion",
"ecow", "ecow",
"insta", "insta",
"tinymist-world", "tinymist-project",
"tinymist-std",
"typst", "typst",
"typst-syntax", "typst-syntax",
] ]
@ -3347,6 +3348,17 @@ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]]
name = "serde-wasm-bindgen"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
dependencies = [
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.215" version = "1.0.215"
@ -3984,9 +3996,10 @@ dependencies = [
"tinymist-assets 0.12.18 (registry+https://github.com/rust-lang/crates.io-index)", "tinymist-assets 0.12.18 (registry+https://github.com/rust-lang/crates.io-index)",
"tinymist-core", "tinymist-core",
"tinymist-fs", "tinymist-fs",
"tinymist-project",
"tinymist-query", "tinymist-query",
"tinymist-render", "tinymist-render",
"tinymist-world", "tinymist-std",
"tokio", "tokio",
"tokio-util", "tokio-util",
"toml", "toml",
@ -4065,6 +4078,33 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "tinymist-project"
version = "0.12.18"
dependencies = [
"anyhow",
"chrono",
"clap",
"comemo",
"dirs",
"ecow",
"log",
"notify",
"parking_lot",
"pathdiff",
"rayon",
"semver",
"serde",
"serde_json",
"tinymist-fs",
"tinymist-std",
"tinymist-world",
"tokio",
"toml",
"typst",
"typst-assets",
]
[[package]] [[package]]
name = "tinymist-query" name = "tinymist-query"
version = "0.12.18" version = "0.12.18"
@ -4091,8 +4131,6 @@ dependencies = [
"pathdiff", "pathdiff",
"percent-encoding", "percent-encoding",
"rayon", "rayon",
"reflexo",
"reflexo-typst",
"regex", "regex",
"rpds", "rpds",
"rust_iso3166", "rust_iso3166",
@ -4106,6 +4144,8 @@ dependencies = [
"strum", "strum",
"tinymist-analysis", "tinymist-analysis",
"tinymist-derive", "tinymist-derive",
"tinymist-project",
"tinymist-std",
"tinymist-world", "tinymist-world",
"toml", "toml",
"triomphe", "triomphe",
@ -4130,6 +4170,49 @@ dependencies = [
"tinymist-query", "tinymist-query",
] ]
[[package]]
name = "tinymist-std"
version = "0.12.18"
dependencies = [
"base64",
"bitvec",
"comemo",
"dashmap",
"ecow",
"fxhash",
"hex",
"js-sys",
"parking_lot",
"path-clean",
"rkyv",
"rustc-hash 2.1.0",
"serde",
"serde_json",
"serde_repr",
"serde_with",
"siphasher 1.0.1",
"typst",
"typst-shim",
"wasm-bindgen",
"web-time",
]
[[package]]
name = "tinymist-vfs"
version = "0.12.18"
dependencies = [
"indexmap 2.7.0",
"js-sys",
"log",
"nohash-hasher",
"parking_lot",
"rpds",
"tinymist-std",
"typst",
"wasm-bindgen",
"web-sys",
]
[[package]] [[package]]
name = "tinymist-world" name = "tinymist-world"
version = "0.12.18" version = "0.12.18"
@ -4137,22 +4220,38 @@ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
"clap", "clap",
"codespan-reporting",
"comemo", "comemo",
"dirs", "dirs",
"ecow",
"flate2",
"fontdb",
"hex",
"js-sys",
"log", "log",
"parking_lot", "parking_lot",
"pathdiff", "pathdiff",
"rayon", "rayon",
"reflexo-typst", "reflexo-typst",
"reflexo-typst-shim", "reflexo-typst-shim",
"reqwest",
"semver", "semver",
"serde", "serde",
"serde-wasm-bindgen",
"serde_json", "serde_json",
"serde_with",
"sha2",
"strum",
"tar",
"tinymist-fs", "tinymist-fs",
"tinymist-std",
"tinymist-vfs",
"tokio", "tokio",
"toml", "toml",
"typst", "typst",
"typst-assets", "typst-assets",
"wasm-bindgen",
"web-sys",
] ]
[[package]] [[package]]
@ -4400,7 +4499,8 @@ dependencies = [
"insta", "insta",
"regex", "regex",
"tinymist-analysis", "tinymist-analysis",
"tinymist-world", "tinymist-project",
"tinymist-std",
"typst", "typst",
"typst-svg", "typst-svg",
"typst-syntax", "typst-syntax",
@ -5471,33 +5571,3 @@ checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768"
dependencies = [ dependencies = [
"zune-core", "zune-core",
] ]
[[patch.unused]]
name = "reflexo"
version = "0.5.1"
source = "git+https://github.com/Myriad-Dreamin/typst.ts/?rev=1b6e29c650ad6d3095e5ea18d93a2428c1ae77b9#1b6e29c650ad6d3095e5ea18d93a2428c1ae77b9"
[[patch.unused]]
name = "reflexo-typst"
version = "0.5.1"
source = "git+https://github.com/Myriad-Dreamin/typst.ts/?rev=1b6e29c650ad6d3095e5ea18d93a2428c1ae77b9#1b6e29c650ad6d3095e5ea18d93a2428c1ae77b9"
[[patch.unused]]
name = "reflexo-typst-shim"
version = "0.5.1"
source = "git+https://github.com/Myriad-Dreamin/typst.ts/?rev=1b6e29c650ad6d3095e5ea18d93a2428c1ae77b9#1b6e29c650ad6d3095e5ea18d93a2428c1ae77b9"
[[patch.unused]]
name = "reflexo-typst2vec"
version = "0.5.1"
source = "git+https://github.com/Myriad-Dreamin/typst.ts/?rev=1b6e29c650ad6d3095e5ea18d93a2428c1ae77b9#1b6e29c650ad6d3095e5ea18d93a2428c1ae77b9"
[[patch.unused]]
name = "reflexo-vec2svg"
version = "0.5.1"
source = "git+https://github.com/Myriad-Dreamin/typst.ts/?rev=1b6e29c650ad6d3095e5ea18d93a2428c1ae77b9#1b6e29c650ad6d3095e5ea18d93a2428c1ae77b9"
[[patch.unused]]
name = "reflexo-world"
version = "0.5.1"
source = "git+https://github.com/Myriad-Dreamin/typst.ts/?rev=1b6e29c650ad6d3095e5ea18d93a2428c1ae77b9#1b6e29c650ad6d3095e5ea18d93a2428c1ae77b9"

View file

@ -45,22 +45,44 @@ parking_lot = "0.12.1"
walkdir = "2" walkdir = "2"
chrono = "0.4" chrono = "0.4"
dirs = "5" dirs = "5"
fontdb = "0.21"
notify = "6"
path-clean = "1.0.1"
windows-sys = "0.59" windows-sys = "0.59"
tempfile = "3.10.1" tempfile = "3.10.1"
same-file = "1.0.6" same-file = "1.0.6"
libc = "0.2.155" libc = "0.2.155"
core-foundation = { version = "0.10.0", features = ["mac_os_10_7_support"] } core-foundation = { version = "0.10.0", features = ["mac_os_10_7_support"] }
# Web
js-sys = "^0.3"
wasm-bindgen = "^0.2"
wasm-bindgen-futures = "^0.4"
wasm-bindgen-test = "0.3.45"
web-sys = "^0.3"
web-time = { version = "1.1.0" }
console_error_panic_hook = { version = "0.1.7" }
# Networking # Networking
hyper = { version = "1", features = ["full"] } hyper = { version = "1", features = ["full"] }
hyper-util = { version = "0.1.7", features = ["tokio"] } hyper-util = { version = "0.1.7", features = ["tokio"] }
hyper-tungstenite = "0.15.0" hyper-tungstenite = "0.15.0"
reqwest = { version = "^0.12", default-features = false, features = [
"rustls-tls",
"blocking",
"multipart",
] }
# Algorithms # Algorithms
base64 = "0.22" base64 = "0.22"
regex = "1.10.5" regex = "1.10.5"
# Cryptography and data processing
rustc-hash = { version = "2", features = ["std"] } rustc-hash = { version = "2", features = ["std"] }
siphasher = "1" siphasher = "1"
fxhash = "0.2.1"
sha2 = "0.10.6"
nohash-hasher = "0.2.0"
# Data Structures # Data Structures
comemo = "0.4" comemo = "0.4"
@ -75,15 +97,21 @@ indexmap = "2.7.0"
rpds = "1" rpds = "1"
# Data/Text Format and Processing # Data/Text Format and Processing
hex = "0.4.3"
flate2 = "1"
tar = "0.4"
biblatex = "0.10" biblatex = "0.10"
pathdiff = "0.2" pathdiff = "0.2"
percent-encoding = "2" percent-encoding = "2"
rust_iso639 = "0.0.3" rust_iso639 = "0.0.3"
rust_iso3166 = "0.1.4" rust_iso3166 = "0.1.4"
rkyv = "0.7.42"
semver = "1" semver = "1"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
serde_yaml = "0.9" serde_yaml = "0.9"
serde_with = { version = "3.6", features = ["base64"] }
serde-wasm-bindgen = "^0.6"
toml = { version = "0.8", default-features = false, features = [ toml = { version = "0.8", default-features = false, features = [
"parse", "parse",
"display", "display",
@ -102,7 +130,6 @@ log = "0.4"
reflexo = { version = "=0.5.4", default-features = false, features = [ reflexo = { version = "=0.5.4", default-features = false, features = [
"flat-vector", "flat-vector",
] } ] }
reflexo-world = { version = "=0.5.4", features = ["system"] }
reflexo-typst = { version = "=0.5.4", features = [ reflexo-typst = { version = "=0.5.4", features = [
"system", "system",
], default-features = false } ], default-features = false }
@ -121,7 +148,7 @@ typstfmt = { git = "https://github.com/Myriad-Dreamin/typstfmt", tag = "v0.12.1"
typst-ansi-hl = "0.3.0" typst-ansi-hl = "0.3.0"
typstyle-core = { version = "=0.12.13", default-features = false } typstyle-core = { version = "=0.12.13", default-features = false }
typlite = { path = "./crates/typlite" } typlite = { path = "./crates/typlite" }
typst-shim = { path = "./crates/typst-shim", features = ["nightly"] } typst-shim = { path = "./crates/typst-shim" }
# LSP # LSP
crossbeam-channel = "0.5.12" crossbeam-channel = "0.5.12"
@ -154,13 +181,15 @@ insta = { version = "1.39", features = ["glob"] }
typst-preview = { path = "./crates/typst-preview" } typst-preview = { path = "./crates/typst-preview" }
tinymist-assets = { version = "0.12.18" } tinymist-assets = { version = "0.12.18" }
tinymist = { path = "./crates/tinymist/" } tinymist = { path = "./crates/tinymist/" }
tinymist-std = { path = "./crates/tinymist-std/", default-features = false }
tinymist-vfs = { path = "./crates/tinymist-vfs/", default-features = false }
tinymist-core = { path = "./crates/tinymist-core/", default-features = false } tinymist-core = { path = "./crates/tinymist-core/", default-features = false }
tinymist-world = { path = "./crates/tinymist-world/", default-features = false }
tinymist-project = { path = "./crates/tinymist-project/" } tinymist-project = { path = "./crates/tinymist-project/" }
tinymist-fs = { path = "./crates/tinymist-fs/" } tinymist-fs = { path = "./crates/tinymist-fs/" }
tinymist-derive = { path = "./crates/tinymist-derive/" } tinymist-derive = { path = "./crates/tinymist-derive/" }
tinymist-analysis = { path = "./crates/tinymist-analysis/" } tinymist-analysis = { path = "./crates/tinymist-analysis/" }
tinymist-query = { path = "./crates/tinymist-query/" } tinymist-query = { path = "./crates/tinymist-query/" }
tinymist-world = { path = "./crates/tinymist-world/" }
tinymist-render = { path = "./crates/tinymist-render/" } tinymist-render = { path = "./crates/tinymist-render/" }
[profile.dev.package.insta] [profile.dev.package.insta]
@ -237,16 +266,14 @@ typst-syntax = { git = "https://github.com/Myriad-Dreamin/typst.git", tag = "tin
# These patches use a different version of `reflexo`. # These patches use a different version of `reflexo`.
# #
# A regular build MUST use `tag` or `rev` to specify the version of the patched crate to ensure stability. # A regular build MUST use `tag` or `rev` to specify the version of the patched crate to ensure stability.
reflexo = { git = "https://github.com/Myriad-Dreamin/typst.ts/", rev = "1b6e29c650ad6d3095e5ea18d93a2428c1ae77b9" } # reflexo = { git = "https://github.com/Myriad-Dreamin/typst.ts/", rev = "1b6e29c650ad6d3095e5ea18d93a2428c1ae77b9" }
reflexo-world = { git = "https://github.com/Myriad-Dreamin/typst.ts/", rev = "1b6e29c650ad6d3095e5ea18d93a2428c1ae77b9" } # reflexo-typst = { git = "https://github.com/Myriad-Dreamin/typst.ts/", rev = "1b6e29c650ad6d3095e5ea18d93a2428c1ae77b9" }
reflexo-typst = { git = "https://github.com/Myriad-Dreamin/typst.ts/", rev = "1b6e29c650ad6d3095e5ea18d93a2428c1ae77b9" } # reflexo-typst2vec = { git = "https://github.com/Myriad-Dreamin/typst.ts/", rev = "1b6e29c650ad6d3095e5ea18d93a2428c1ae77b9" }
reflexo-typst2vec = { git = "https://github.com/Myriad-Dreamin/typst.ts/", rev = "1b6e29c650ad6d3095e5ea18d93a2428c1ae77b9" } # reflexo-vec2svg = { git = "https://github.com/Myriad-Dreamin/typst.ts/", rev = "1b6e29c650ad6d3095e5ea18d93a2428c1ae77b9" }
reflexo-vec2svg = { git = "https://github.com/Myriad-Dreamin/typst.ts/", rev = "1b6e29c650ad6d3095e5ea18d93a2428c1ae77b9" } # reflexo-typst-shim = { git = "https://github.com/Myriad-Dreamin/typst.ts/", rev = "1b6e29c650ad6d3095e5ea18d93a2428c1ae77b9" }
reflexo-typst-shim = { git = "https://github.com/Myriad-Dreamin/typst.ts/", rev = "1b6e29c650ad6d3095e5ea18d93a2428c1ae77b9" }
# These patches use local `reflexo` for development. # These patches use local `reflexo` for development.
# reflexo = { path = "../typst.ts/crates/reflexo/" } # reflexo = { path = "../typst.ts/crates/reflexo/" }
# reflexo-world = { path = "../typst.ts/crates/reflexo-world/" }
# reflexo-typst = { path = "../typst.ts/crates/reflexo-typst/" } # reflexo-typst = { path = "../typst.ts/crates/reflexo-typst/" }
# reflexo-typst2vec = { path = "../typst.ts/crates/conversion/typst2vec/" } # reflexo-typst2vec = { path = "../typst.ts/crates/conversion/typst2vec/" }
# reflexo-vec2svg = { path = "../typst.ts/crates/conversion/vec2svg/" } # reflexo-vec2svg = { path = "../typst.ts/crates/conversion/vec2svg/" }

View file

@ -29,7 +29,8 @@ criterion = "0.5.1"
# criterion = { path = "../../target/criterion.rs" } # criterion = { path = "../../target/criterion.rs" }
comemo.workspace = true comemo.workspace = true
ecow.workspace = true ecow.workspace = true
tinymist-world.workspace = true tinymist-std.workspace = true
tinymist-project.workspace = true
typst.workspace = true typst.workspace = true
typst-syntax.workspace = true typst-syntax.workspace = true
@ -46,11 +47,11 @@ cli = ["clap"]
# - code (Deja Vu Sans Mono) # - code (Deja Vu Sans Mono)
# and additionally New Computer Modern for text # and additionally New Computer Modern for text
# into the binary. # into the binary.
embed-fonts = ["tinymist-world/fonts"] embed-fonts = ["tinymist-project/fonts"]
# Disable the default content hint. # Disable the default content hint.
# This requires modifying typst. # This requires modifying typst.
no-content-hint = ["tinymist-world/no-content-hint"] no-content-hint = ["tinymist-project/no-content-hint"]
[lints] [lints]
workspace = true workspace = true

View file

@ -14,8 +14,8 @@ use anyhow::Context as ContextTrait;
use comemo::Track; use comemo::Track;
use criterion::Criterion; use criterion::Criterion;
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use tinymist_world::reflexo_typst::path::unix_slash; use tinymist_project::LspWorld;
use tinymist_world::LspWorld; use tinymist_std::path::unix_slash;
use typst::engine::{Engine, Route, Sink, Traced}; use typst::engine::{Engine, Route, Sink, Traced};
use typst::foundations::{Context, Func, Value}; use typst::foundations::{Context, Func, Value};
use typst::introspection::Introspector; use typst::introspection::Introspector;

View file

@ -2,7 +2,7 @@
use anyhow::Context; use anyhow::Context;
use clap::Parser; use clap::Parser;
use tinymist_world::CompileOnceArgs; use tinymist_project::{CompileOnceArgs, WorldProvider};
/// Common arguments of crityp benchmark. /// Common arguments of crityp benchmark.
#[derive(Debug, Clone, Parser, Default)] #[derive(Debug, Clone, Parser, Default)]

View file

@ -17,12 +17,15 @@ crate-type = ["cdylib", "rlib"]
[features] [features]
default = ["web"] # "no-content-hint" default = ["web"] # "no-content-hint"
# todo: bootstrap me on web
# , "tinymist-world/web"
web = ["wasm-bindgen"] web = ["wasm-bindgen"]
# no-content-hint = ["reflexo-typst/no-content-hint"] # no-content-hint = ["reflexo-typst/no-content-hint"]
[dependencies] [dependencies]
wasm-bindgen = { version = "0.2.92", optional = true } wasm-bindgen = { version = "0.2.92", optional = true }
# tinymist-world.workspace = true
[build-dependencies] [build-dependencies]
anyhow.workspace = true anyhow.workspace = true

View file

@ -0,0 +1,43 @@
[package]
name = "tinymist-project"
description = "Project model of typst for tinymist."
categories = ["compilers"]
keywords = ["language", "typst"]
authors.workspace = true
version.workspace = true
license.workspace = true
edition.workspace = true
homepage.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
anyhow.workspace = true
chrono.workspace = true
clap.workspace = true
comemo.workspace = true
dirs.workspace = true
ecow.workspace = true
log.workspace = true
parking_lot.workspace = true
pathdiff.workspace = true
tokio.workspace = true
rayon.workspace = true
semver.workspace = true
serde.workspace = true
serde_json.workspace = true
tinymist-world = { workspace = true, features = ["system"] }
tinymist-fs.workspace = true
tinymist-std.workspace = true
toml.workspace = true
typst.workspace = true
typst-assets.workspace = true
notify.workspace = true
[features]
fonts = ["typst-assets/fonts"]
# "reflexo-typst/no-content-hint"
no-content-hint = []
[lints]
workspace = true

View file

@ -7,14 +7,14 @@ use std::{
sync::{Arc, Mutex}, sync::{Arc, Mutex},
}; };
use system::SystemFontSearcher; use tinymist_world::font::system::SystemFontSearcher;
use typst::text::{Font, FontBook, FontInfo}; use typst::text::{Font, FontBook, FontInfo};
use typst::utils::LazyHash; use typst::utils::LazyHash;
use reflexo_typst::debug_loc::DataSource; use crate::world::vfs::Bytes;
use reflexo_typst::Bytes; use tinymist_std::debug_loc::DataSource;
pub use reflexo_typst::font::*; pub use crate::world::base::font::*;
#[derive(Debug)] #[derive(Debug)]
/// The default FontResolver implementation. /// The default FontResolver implementation.

View file

@ -8,3 +8,8 @@ mod model;
pub use model::*; pub use model::*;
mod args; mod args;
pub use args::*; pub use args::*;
mod watch;
pub use watch::*;
pub mod world;
pub use world::*;
pub mod font;

View file

@ -1,7 +1,8 @@
use std::{path::Path, sync::Arc}; use std::{path::Path, sync::Arc};
use reflexo_typst::{path::unix_slash, EntryReader, TypstFileId}; use tinymist_std::path::unix_slash;
use typst::diag::EcoString; use tinymist_world::EntryReader;
use typst::{diag::EcoString, syntax::FileId};
use super::model::{Id, ProjectInput, ProjectMaterial, ProjectRoute, ProjectTask, ResourcePath}; use super::model::{Id, ProjectInput, ProjectMaterial, ProjectRoute, ProjectTask, ResourcePath};
use crate::LspWorld; use crate::LspWorld;
@ -80,7 +81,7 @@ impl ProjectLockUpdater {
self.updates.push(LockUpdate::Task(task)); self.updates.push(LockUpdate::Task(task));
} }
pub fn update_materials(&mut self, doc_id: Id, ids: Vec<TypstFileId>) { pub fn update_materials(&mut self, doc_id: Id, ids: Vec<FileId>) {
let mut files = ids let mut files = ids
.into_iter() .into_iter()
.map(ResourcePath::from_file_id) .map(ResourcePath::from_file_id)
@ -103,7 +104,7 @@ impl ProjectLockUpdater {
pub fn commit(self) { pub fn commit(self) {
let err = super::LockFile::update(&self.root, |l| { let err = super::LockFile::update(&self.root, |l| {
let root: EcoString = unix_slash(&self.root).into(); let root: EcoString = unix_slash(&self.root).into();
let root_hash = reflexo_typst::hash::hash128(&root); let root_hash = tinymist_std::hash::hash128(&root);
for update in self.updates { for update in self.updates {
match update { match update {
LockUpdate::Input(input) => { LockUpdate::Input(input) => {
@ -116,7 +117,7 @@ impl ProjectLockUpdater {
mat.root = root.clone(); mat.root = root.clone();
let cache_dir = dirs::cache_dir(); let cache_dir = dirs::cache_dir();
if let Some(cache_dir) = cache_dir { if let Some(cache_dir) = cache_dir {
let id = reflexo_typst::hash::hash128(&mat.id); let id = tinymist_std::hash::hash128(&mat.id);
let lower4096 = root_hash & 0xfff; let lower4096 = root_hash & 0xfff;
let upper4096 = root_hash >> 12; let upper4096 = root_hash >> 12;

View file

@ -4,7 +4,9 @@ use std::{cmp::Ordering, path::Path, str::FromStr};
use anyhow::{bail, Context}; use anyhow::{bail, Context};
use clap::ValueHint; use clap::ValueHint;
use reflexo_typst::{path::unix_slash, typst::diag::EcoString}; use tinymist_std::path::unix_slash;
use typst::diag::EcoString;
use typst::syntax::FileId;
pub use anyhow::Result; pub use anyhow::Result;
@ -339,7 +341,7 @@ impl ResourcePath {
ResourcePath("file".into(), rel.to_string()) ResourcePath("file".into(), rel.to_string())
} }
pub fn from_file_id(id: reflexo_typst::typst::TypstFileId) -> Self { pub fn from_file_id(id: FileId) -> Self {
let package = id.package(); let package = id.package();
match package { match package {
Some(package) => ResourcePath( Some(package) => ResourcePath(

View file

@ -0,0 +1,597 @@
//! upstream <https://github.com/rust-lang/rust-analyzer/tree/master/crates/vfs-notify>
//!
//! An implementation of `watch_deps` using `notify` crate.
//!
//! The file watching bits here are untested and quite probably buggy. For this
//! reason, by default we don't watch files and rely on editor's file watching
//! capabilities.
//!
//! Hopefully, one day a reliable file watching/walking crate appears on
//! crates.io, and we can reduce this to trivial glue code.
use std::collections::HashMap;
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
use tokio::sync::mpsc;
use typst::diag::{EcoString, FileError, FileResult};
use crate::vfs::{
notify::{FileChangeSet, FileSnapshot, FilesystemEvent, NotifyMessage, UpstreamUpdateEvent},
system::SystemAccessModel,
AccessModel, Bytes,
};
use tinymist_std::ImmutPath;
type WatcherPair = (RecommendedWatcher, mpsc::UnboundedReceiver<NotifyEvent>);
type NotifyEvent = notify::Result<notify::Event>;
type FileEntry = (/* key */ ImmutPath, /* value */ FileSnapshot);
type NotifyFilePair = FileResult<(
/* mtime */ tinymist_std::time::Time,
/* content */ Bytes,
)>;
/// The state of a watched file.
///
/// It is used to determine some dirty editors' implementation.
#[derive(Debug)]
enum WatchState {
/// The file is stable, which means we believe that it keeps synchronized
/// as expected.
Stable,
/// The file is empty or removed, but there is a chance that the file is not
/// stable. So we need to recheck the file after a while.
EmptyOrRemoval {
recheck_at: usize,
payload: NotifyFilePair,
},
}
/// By default, the state is stable.
impl Default for WatchState {
fn default() -> Self {
Self::Stable
}
}
/// The data entry of a watched file.
#[derive(Debug)]
struct WatchedEntry {
/// The lifetime of the entry.
///
/// The entry will be removed if the entry is too old.
// todo: generalize lifetime
lifetime: usize,
/// A flag for whether it is really watching.
watching: bool,
/// A flag for watch update.
seen: bool,
/// The state of the entry.
state: WatchState,
/// Previous content of the file.
prev: Option<NotifyFilePair>,
/// Previous metadata of the file.
prev_meta: FileResult<std::fs::Metadata>,
}
/// Self produced event that check whether the file is stable after a while.
#[derive(Debug)]
struct UndeterminedNotifyEvent {
/// The time when the event is produced.
at_realtime: tinymist_std::time::Instant,
/// The logical tick when the event is produced.
at_logical_tick: usize,
/// The path of the file.
path: ImmutPath,
}
// Drop order is significant.
/// The actor that watches files.
/// It is used to watch files and send events to the consumers
#[derive(Debug)]
pub struct NotifyActor {
/// The access model of the actor.
/// We concrete the access model to `SystemAccessModel` for now.
inner: SystemAccessModel,
/// The lifetime of the watched files.
lifetime: usize,
/// The logical tick of the actor.
logical_tick: usize,
/// Output of the actor.
/// See [`FilesystemEvent`] for more information.
sender: mpsc::UnboundedSender<FilesystemEvent>,
/// Internal channel for recheck events.
undetermined_send: mpsc::UnboundedSender<UndeterminedNotifyEvent>,
undetermined_recv: mpsc::UnboundedReceiver<UndeterminedNotifyEvent>,
/// The hold entries for watching, one entry for per file.
watched_entries: HashMap<ImmutPath, WatchedEntry>,
/// The builtin watcher object.
watcher: Option<WatcherPair>,
}
impl NotifyActor {
/// Create a new actor.
fn new(sender: mpsc::UnboundedSender<FilesystemEvent>) -> NotifyActor {
let (undetermined_send, undetermined_recv) = mpsc::unbounded_channel();
let (watcher_sender, watcher_receiver) = mpsc::unbounded_channel();
let watcher = log_notify_error(
RecommendedWatcher::new(
move |event| {
let res = watcher_sender.send(event);
if let Err(err) = res {
log::warn!("error to send event: {err}");
}
},
Config::default(),
),
"failed to create watcher",
);
NotifyActor {
inner: SystemAccessModel,
// we start from 1 to distinguish from 0 (default value)
lifetime: 1,
logical_tick: 1,
sender,
undetermined_send,
undetermined_recv,
watched_entries: HashMap::new(),
watcher: watcher.map(|it| (it, watcher_receiver)),
}
}
/// Send a filesystem event to remove.
fn send(&mut self, msg: FilesystemEvent) {
log_send_error("fs_event", self.sender.send(msg));
}
/// Get the notify event from the watcher.
async fn get_notify_event(watcher: &mut Option<WatcherPair>) -> Option<NotifyEvent> {
match watcher {
Some((_, watcher_receiver)) => watcher_receiver.recv().await,
None => None,
}
}
/// Main loop of the actor.
async fn run(mut self, mut inbox: mpsc::UnboundedReceiver<NotifyMessage>) {
/// The event of the actor.
#[derive(Debug)]
enum ActorEvent {
/// Recheck the notify event.
ReCheck(UndeterminedNotifyEvent),
/// external message to change notifier's state
Message(NotifyMessage),
/// notify event from builtin watcher
NotifyEvent(NotifyEvent),
}
'event_loop: loop {
// Get the event from the inbox or the watcher.
let event = tokio::select! {
Some(it) = inbox.recv() => Some(ActorEvent::Message(it)),
Some(it) = Self::get_notify_event(&mut self.watcher) => Some(ActorEvent::NotifyEvent(it)),
Some(it) = self.undetermined_recv.recv() => Some(ActorEvent::ReCheck(it)),
};
// Failed to get the event.
let Some(event) = event else {
log::info!("failed to get event, exiting...");
return;
};
// Increase the logical tick per event.
self.logical_tick += 1;
// log::info!("vfs-notify event {event:?}");
// function entries to handle some event
match event {
ActorEvent::Message(NotifyMessage::Settle) => {
log::info!("NotifyActor: settle event received");
break 'event_loop;
}
ActorEvent::Message(NotifyMessage::UpstreamUpdate(event)) => {
self.invalidate_upstream(event);
}
ActorEvent::Message(NotifyMessage::SyncDependency(paths)) => {
if let Some(changeset) = self.update_watches(&paths) {
self.send(FilesystemEvent::Update(changeset));
}
}
ActorEvent::NotifyEvent(event) => {
// log::info!("notify event {event:?}");
if let Some(event) = log_notify_error(event, "failed to notify") {
self.notify_event(event);
}
}
ActorEvent::ReCheck(event) => {
self.recheck_notify_event(event).await;
}
}
}
log::info!("NotifyActor: exited");
}
/// Update the watches of corresponding invalidation
fn invalidate_upstream(&mut self, event: UpstreamUpdateEvent) {
// Update watches of invalidated files.
let changeset = self.update_watches(&event.invalidates).unwrap_or_default();
// Send the event to the consumer.
self.send(FilesystemEvent::UpstreamUpdate {
changeset,
upstream_event: Some(event),
});
}
/// Update the watches of corresponding files.
fn update_watches(&mut self, paths: &[ImmutPath]) -> Option<FileChangeSet> {
// Increase the lifetime per external message.
self.lifetime += 1;
let mut changeset = FileChangeSet::default();
// Mark the old entries as unseen.
for path in self.watched_entries.values_mut() {
path.seen = false;
}
// Update watched entries.
//
// Also check whether the file is updated since there is a window
// between unwatch the file and watch the file again.
for path in paths.iter() {
let mut contained = false;
// Update or insert the entry with the new lifetime.
let entry = self
.watched_entries
.entry(path.clone())
.and_modify(|watch_entry| {
contained = true;
watch_entry.lifetime = self.lifetime;
watch_entry.seen = true;
})
.or_insert_with(|| WatchedEntry {
lifetime: self.lifetime,
watching: false,
seen: true,
state: WatchState::Stable,
prev: None,
prev_meta: Err(FileError::Other(Some(EcoString::from("_not-init_")))),
});
// Update in-memory metadata for now.
let meta = path.metadata().map_err(|e| FileError::from_io(e, path));
if let Some((watcher, _)) = &mut self.watcher {
// Case1. meta = Err(..) We cannot get the metadata successfully, so we
// are okay to ignore this file for watching.
//
// Case2. meta = Ok(..) Watch the file if it's not watched.
if meta
.as_ref()
.is_ok_and(|meta| !meta.is_dir() && (!contained || !entry.watching))
{
log::debug!("watching {path:?}");
entry.watching = log_notify_error(
watcher.watch(path.as_ref(), RecursiveMode::NonRecursive),
"failed to watch",
)
.is_some();
}
changeset.may_insert(self.notify_entry_update(path.clone(), Some(meta)));
} else {
let watched = meta.and_then(|meta| {
let content = self.inner.content(path)?;
Ok((meta.modified().unwrap(), content))
});
changeset.inserts.push((path.clone(), watched.into()));
}
}
// Remove old entries.
// Note: since we have increased the lifetime, it is safe to remove the
// old entries after updating the watched entries.
self.watched_entries.retain(|path, entry| {
if !entry.seen && entry.watching {
log::debug!("unwatch {path:?}");
if let Some(watcher) = &mut self.watcher {
log_notify_error(watcher.0.unwatch(path), "failed to unwatch");
entry.watching = false;
}
}
let fresh = self.lifetime - entry.lifetime < 30;
if !fresh {
changeset.removes.push(path.clone());
}
fresh
});
(!changeset.is_empty()).then_some(changeset)
}
/// Notify the event from the builtin watcher.
fn notify_event(&mut self, event: notify::Event) {
// Account file updates.
let mut changeset = FileChangeSet::default();
for path in event.paths.iter() {
// todo: remove this clone: path.into()
changeset.may_insert(self.notify_entry_update(path.as_path().into(), None));
}
// Workaround for notify-rs' implicit unwatch on remove/rename
// (triggered by some editors when saving files) with the
// inotify backend. By keeping track of the potentially
// unwatched files, we can allow those we still depend on to be
// watched again later on.
if matches!(
event.kind,
notify::EventKind::Remove(notify::event::RemoveKind::File)
| notify::EventKind::Modify(notify::event::ModifyKind::Name(
notify::event::RenameMode::From
))
) {
for path in &event.paths {
let Some(entry) = self.watched_entries.get_mut(path.as_path()) else {
continue;
};
if !entry.watching {
continue;
}
// Remove affected path from the watched map to restart
// watching on it later again.
if let Some(watcher) = &mut self.watcher {
log_notify_error(watcher.0.unwatch(path), "failed to unwatch");
}
entry.watching = false;
}
}
// Send file updates.
if !changeset.is_empty() {
self.send(FilesystemEvent::Update(changeset));
}
}
/// Notify any update of the file entry
fn notify_entry_update(
&mut self,
path: ImmutPath,
meta: Option<FileResult<std::fs::Metadata>>,
) -> Option<FileEntry> {
let mut meta =
meta.unwrap_or_else(|| path.metadata().map_err(|e| FileError::from_io(e, &path)));
// The following code in rust-analyzer is commented out
// todo: check whether we need this
// if meta.file_type().is_dir() && self
// .watched_entries.iter().any(|entry| entry.contains_dir(&path))
// {
// self.watch(path);
// return None;
// }
// Find entry and continue
let entry = self.watched_entries.get_mut(&path)?;
std::mem::swap(&mut entry.prev_meta, &mut meta);
let prev_meta = meta;
let next_meta = &entry.prev_meta;
let meta = match (prev_meta, next_meta) {
(Err(prev), Err(next)) => {
if prev != *next {
return Some((path.clone(), FileSnapshot::from(Err(next.clone()))));
}
return None;
}
// todo: check correctness
(Ok(..), Err(next)) => {
// Invalidate the entry content
entry.prev = None;
return Some((path.clone(), FileSnapshot::from(Err(next.clone()))));
}
(_, Ok(meta)) => meta,
};
if !meta.file_type().is_file() {
return None;
}
// Check meta, path, and content
// Get meta, real path and ignore errors
let mtime = meta.modified().ok()?;
let mut file = self.inner.content(&path).map(|it| (mtime, it));
// Check state in fast path: compare state, return None on not sending
// the file change
match (&entry.prev, &mut file) {
// update the content of the entry in the following cases:
// + Case 1: previous content is clear
// + Case 2: previous content is not clear but some error, and the
// current content is ok
(None, ..) | (Some(Err(..)), Ok(..)) => {}
// Meet some error currently
(Some(..), Err(err)) => match &mut entry.state {
// If the file is stable, check whether the editor is removing
// or truncating the file. They are possibly flushing the file
// but not finished yet.
WatchState::Stable => {
if matches!(err, FileError::NotFound(..) | FileError::Other(..)) {
entry.state = WatchState::EmptyOrRemoval {
recheck_at: self.logical_tick,
payload: file.clone(),
};
entry.prev = Some(file);
let event = UndeterminedNotifyEvent {
at_realtime: tinymist_std::time::Instant::now(),
at_logical_tick: self.logical_tick,
path: path.clone(),
};
log_send_error("recheck", self.undetermined_send.send(event));
return None;
}
// Otherwise, we push the error to the consumer.
}
// Very complicated case of check error sequence, so we simplify
// a bit, we regard any subsequent error as the same error.
WatchState::EmptyOrRemoval { payload, .. } => {
// update payload
*payload = file;
return None;
}
},
// Compare content for transitional the state
(Some(Ok((prev_tick, prev_content))), Ok((next_tick, next_content))) => {
// So far it is accurately no change for the file, skip it
if prev_content == next_content {
return None;
}
match entry.state {
// If the file is stable, check whether the editor is
// removing or truncating the file. They are possibly
// flushing the file but not finished yet.
WatchState::Stable => {
if next_content.is_empty() {
entry.state = WatchState::EmptyOrRemoval {
recheck_at: self.logical_tick,
payload: file.clone(),
};
entry.prev = Some(file);
let event = UndeterminedNotifyEvent {
at_realtime: tinymist_std::time::Instant::now(),
at_logical_tick: self.logical_tick,
path,
};
log_send_error("recheck", self.undetermined_send.send(event));
return None;
}
}
// Still empty
WatchState::EmptyOrRemoval { .. } if next_content.is_empty() => return None,
// Otherwise, we push the diff to the consumer.
WatchState::EmptyOrRemoval { .. } => {}
}
// We have found a change, however, we need to check whether the
// mtime is changed. Generally, the mtime should be changed.
// However, It is common that editor (VSCode) to change the
// mtime after writing
//
// this condition should be never happen, but we still check it
//
// There will be cases that user change content of a file and
// then also modify the mtime of the file, so we need to check
// `next_tick == prev_tick`: Whether mtime is changed.
// `matches!(entry.state, WatchState::Fresh)`: Whether the file
// is fresh. We have not submit the file to the compiler, so
// that is ok.
if next_tick == prev_tick && matches!(entry.state, WatchState::Stable) {
// this is necessary to invalidate our mtime-based cache
*next_tick = prev_tick
.checked_add(std::time::Duration::from_micros(1))
.unwrap();
log::warn!("same content but mtime is different...: {:?} content: prev:{:?} v.s. curr:{:?}", path, prev_content, next_content);
};
}
};
// Send the update to the consumer
// Update the entry according to the state
entry.state = WatchState::Stable;
entry.prev = Some(file.clone());
// Slow path: trigger the file change for consumer
Some((path, file.into()))
}
/// Recheck the notify event after a while.
async fn recheck_notify_event(&mut self, event: UndeterminedNotifyEvent) -> Option<()> {
let now = tinymist_std::time::Instant::now();
log::debug!("recheck event {event:?} at {now:?}");
// The async scheduler is not accurate, so we need to ensure a window here
let reserved = now - event.at_realtime;
if reserved < std::time::Duration::from_millis(50) {
let send = self.undetermined_send.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(50) - reserved).await;
log_send_error("reschedule", send.send(event));
});
return None;
}
// Check whether the entry is still valid
let entry = self.watched_entries.get_mut(&event.path)?;
// Check the state of the entry
match std::mem::take(&mut entry.state) {
// If the entry is stable, we do nothing
WatchState::Stable => {}
// If the entry is not stable, and no other event is produced after
// this event, we send the event to the consumer.
WatchState::EmptyOrRemoval {
recheck_at,
payload,
} => {
if recheck_at == event.at_logical_tick {
log::debug!("notify event real happened {event:?}, state: {:?}", payload);
// Send the underlying change to the consumer
let mut changeset = FileChangeSet::default();
changeset.inserts.push((event.path, payload.into()));
self.send(FilesystemEvent::Update(changeset));
}
}
};
Some(())
}
}
#[inline]
fn log_notify_error<T>(res: notify::Result<T>, reason: &'static str) -> Option<T> {
res.map_err(|err| log::warn!("{reason}: notify error: {err}"))
.ok()
}
#[inline]
fn log_send_error<T>(chan: &'static str, res: Result<(), mpsc::error::SendError<T>>) -> bool {
res.map_err(|err| log::warn!("NotifyActor: send to {chan} error: {err}"))
.is_ok()
}
pub async fn watch_deps(
inbox: mpsc::UnboundedReceiver<NotifyMessage>,
mut interrupted_by_events: impl FnMut(FilesystemEvent),
) {
// Setup file watching.
let (tx, mut rx) = mpsc::unbounded_channel();
let actor = NotifyActor::new(tx);
// Watch messages to notify
tokio::spawn(actor.run(inbox));
// Handle events.
log::debug!("start watching files...");
while let Some(event) = rx.recv().await {
interrupted_by_events(event);
}
log::debug!("stop watching files...");
}

View file

@ -0,0 +1,167 @@
//! World implementation of typst for tinymist.
pub use tinymist_std::error::prelude;
pub use tinymist_world as base;
pub use tinymist_world::args::*;
pub use tinymist_world::config::CompileFontOpts;
pub use tinymist_world::vfs;
pub use tinymist_world::{entry::*, EntryOpts, EntryState};
pub use tinymist_world::{font, package, CompilerUniverse, CompilerWorld, Revising, TaskInputs};
use std::path::Path;
use std::{borrow::Cow, sync::Arc};
use ::typst::utils::LazyHash;
use anyhow::Context;
use tinymist_std::error::prelude::*;
use tinymist_std::ImmutPath;
use tinymist_world::font::system::SystemFontSearcher;
use tinymist_world::package::http::HttpRegistry;
use tinymist_world::vfs::{system::SystemAccessModel, Vfs};
use tinymist_world::CompilerFeat;
use typst::foundations::{Dict, Str, Value};
use crate::font::TinymistFontResolver;
/// Compiler feature for LSP universe and worlds without typst.ts to implement
/// more for tinymist. type trait of [`CompilerUniverse`].
#[derive(Debug, Clone, Copy)]
pub struct SystemCompilerFeatExtend;
impl CompilerFeat for SystemCompilerFeatExtend {
/// Uses [`TinymistFontResolver`] directly.
type FontResolver = TinymistFontResolver;
/// It accesses a physical file system.
type AccessModel = SystemAccessModel;
/// It performs native HTTP requests for fetching package data.
type Registry = HttpRegistry;
}
/// The compiler universe in system environment.
pub type TypstSystemUniverseExtend = CompilerUniverse<SystemCompilerFeatExtend>;
/// The compiler world in system environment.
pub type TypstSystemWorldExtend = CompilerWorld<SystemCompilerFeatExtend>;
pub trait WorldProvider {
/// Get the entry options from the arguments.
fn entry(&self) -> anyhow::Result<EntryOpts>;
/// Get a universe instance from the given arguments.
fn resolve(&self) -> anyhow::Result<LspUniverse>;
}
impl WorldProvider for CompileOnceArgs {
fn resolve(&self) -> anyhow::Result<LspUniverse> {
let entry = self.entry()?.try_into()?;
let inputs = self
.inputs
.iter()
.map(|(k, v)| (Str::from(k.as_str()), Value::Str(Str::from(v.as_str()))))
.collect();
let fonts = LspUniverseBuilder::resolve_fonts(self.font.clone())?;
let package = LspUniverseBuilder::resolve_package(
self.cert.as_deref().map(From::from),
Some(&self.package),
);
LspUniverseBuilder::build(
entry,
Arc::new(LazyHash::new(inputs)),
Arc::new(fonts),
package,
)
.context("failed to create universe")
}
fn entry(&self) -> anyhow::Result<EntryOpts> {
let input = self.input.as_ref().context("entry file must be provided")?;
let input = Path::new(&input);
let entry = if input.is_absolute() {
input.to_owned()
} else {
std::env::current_dir().unwrap().join(input)
};
let root = if let Some(root) = &self.root {
if root.is_absolute() {
root.clone()
} else {
std::env::current_dir().unwrap().join(root)
}
} else {
std::env::current_dir().unwrap()
};
if !entry.starts_with(&root) {
log::error!("entry file must be in the root directory");
std::process::exit(1);
}
let relative_entry = match entry.strip_prefix(&root) {
Ok(relative_entry) => relative_entry,
Err(_) => {
log::error!("entry path must be inside the root: {}", entry.display());
std::process::exit(1);
}
};
Ok(EntryOpts::new_rooted(
root.clone(),
Some(relative_entry.to_owned()),
))
}
}
/// Compiler feature for LSP universe and worlds.
pub type LspCompilerFeat = SystemCompilerFeatExtend;
/// LSP universe that spawns LSP worlds.
pub type LspUniverse = TypstSystemUniverseExtend;
/// LSP world.
pub type LspWorld = TypstSystemWorldExtend;
/// Immutable prehashed reference to dictionary.
pub type ImmutDict = Arc<LazyHash<Dict>>;
/// Builder for LSP universe.
pub struct LspUniverseBuilder;
impl LspUniverseBuilder {
/// Create [`LspUniverse`] with the given options.
/// See [`LspCompilerFeat`] for instantiation details.
pub fn build(
entry: EntryState,
inputs: ImmutDict,
font_resolver: Arc<TinymistFontResolver>,
package_registry: HttpRegistry,
) -> ZResult<LspUniverse> {
Ok(LspUniverse::new_raw(
entry,
Some(inputs),
Vfs::new(SystemAccessModel {}),
package_registry,
font_resolver,
))
}
/// Resolve fonts from given options.
pub fn resolve_fonts(args: CompileFontArgs) -> ZResult<TinymistFontResolver> {
let mut searcher = SystemFontSearcher::new();
searcher.resolve_opts(CompileFontOpts {
font_profile_cache_path: Default::default(),
font_paths: args.font_paths,
no_system_fonts: args.ignore_system_fonts,
with_embedded_fonts: typst_assets::fonts().map(Cow::Borrowed).collect(),
})?;
Ok(searcher.into())
}
/// Resolve package registry from given options.
pub fn resolve_package(
cert_path: Option<ImmutPath>,
args: Option<&CompilePackageArgs>,
) -> HttpRegistry {
HttpRegistry::new(
cert_path,
args.and_then(|args| Some(args.package_path.clone()?.into())),
args.and_then(|args| Some(args.package_cache_path.clone()?.into())),
)
}
}

View file

@ -13,9 +13,7 @@ rust-version.workspace = true
[features] [features]
default = ["no-content-hint"] default = ["no-content-hint"]
no-content-hint = []
no-content-hint = ["reflexo-typst/no-content-hint"]
[dependencies] [dependencies]
@ -45,9 +43,7 @@ rayon.workspace = true
typst.workspace = true typst.workspace = true
reflexo.workspace = true
typst-shim.workspace = true typst-shim.workspace = true
reflexo-typst.workspace = true
lsp-types.workspace = true lsp-types.workspace = true
if_chain.workspace = true if_chain.workspace = true
@ -64,8 +60,10 @@ triomphe.workspace = true
base64.workspace = true base64.workspace = true
typlite.workspace = true typlite.workspace = true
tinymist-world.workspace = true tinymist-world.workspace = true
tinymist-project.workspace = true
tinymist-analysis.workspace = true tinymist-analysis.workspace = true
tinymist-derive.workspace = true tinymist-derive.workspace = true
tinymist-std.workspace = true
[dev-dependencies] [dev-dependencies]
once_cell.workspace = true once_cell.workspace = true
@ -73,7 +71,6 @@ insta.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
typst-assets = { workspace = true, features = ["fonts"] } typst-assets = { workspace = true, features = ["fonts"] }
reflexo-typst = { workspace = true, features = ["no-content-hint"] }
sha2 = { version = "0.10" } sha2 = { version = "0.10" }
hex = { version = "0.4" } hex = { version = "0.4" }

View file

@ -40,9 +40,10 @@ pub use global::*;
use ecow::eco_format; use ecow::eco_format;
use lsp_types::Url; use lsp_types::Url;
use reflexo_typst::{EntryReader, TypstFileId}; use tinymist_world::EntryReader;
use typst::diag::{FileError, FileResult}; use typst::diag::{FileError, FileResult};
use typst::foundations::{Func, Value}; use typst::foundations::{Func, Value};
use typst::syntax::FileId;
use crate::path_to_url; use crate::path_to_url;
@ -63,17 +64,17 @@ impl ToFunc for Value {
/// Extension trait for `typst::World`. /// Extension trait for `typst::World`.
pub trait LspWorldExt { pub trait LspWorldExt {
/// Get file's id by its path /// Get file's id by its path
fn file_id_by_path(&self, path: &Path) -> FileResult<TypstFileId>; fn file_id_by_path(&self, path: &Path) -> FileResult<FileId>;
/// Get the source of a file by file path. /// Get the source of a file by file path.
fn source_by_path(&self, path: &Path) -> FileResult<Source>; fn source_by_path(&self, path: &Path) -> FileResult<Source>;
/// Resolve the uri for a file id. /// Resolve the uri for a file id.
fn uri_for_id(&self, fid: TypstFileId) -> FileResult<Url>; fn uri_for_id(&self, fid: FileId) -> FileResult<Url>;
} }
impl LspWorldExt for tinymist_world::LspWorld { impl LspWorldExt for tinymist_project::LspWorld {
fn file_id_by_path(&self, path: &Path) -> FileResult<TypstFileId> { fn file_id_by_path(&self, path: &Path) -> FileResult<FileId> {
// todo: source in packages // todo: source in packages
let root = self.workspace_root().ok_or_else(|| { let root = self.workspace_root().ok_or_else(|| {
let reason = eco_format!("workspace root not found"); let reason = eco_format!("workspace root not found");
@ -84,7 +85,7 @@ impl LspWorldExt for tinymist_world::LspWorld {
FileError::Other(Some(reason)) FileError::Other(Some(reason))
})?; })?;
Ok(TypstFileId::new(None, VirtualPath::new(relative_path))) Ok(FileId::new(None, VirtualPath::new(relative_path)))
} }
fn source_by_path(&self, path: &Path) -> FileResult<Source> { fn source_by_path(&self, path: &Path) -> FileResult<Source> {
@ -92,7 +93,7 @@ impl LspWorldExt for tinymist_world::LspWorld {
self.source(self.file_id_by_path(path)?) self.source(self.file_id_by_path(path)?)
} }
fn uri_for_id(&self, fid: TypstFileId) -> Result<Url, FileError> { fn uri_for_id(&self, fid: FileId) -> Result<Url, FileError> {
self.path_for_id(fid).and_then(|path| { self.path_for_id(fid).and_then(|path| {
path_to_url(&path) path_to_url(&path)
.map_err(|err| FileError::Other(Some(eco_format!("convert to url: {err:?}")))) .map_err(|err| FileError::Other(Some(eco_format!("convert to url: {err:?}"))))
@ -131,7 +132,7 @@ mod matcher_tests {
#[cfg(test)] #[cfg(test)]
mod expr_tests { mod expr_tests {
use reflexo::path::unix_slash; use tinymist_std::path::unix_slash;
use typst::syntax::Source; use typst::syntax::Source;
use crate::syntax::{Expr, RefExpr}; use crate::syntax::{Expr, RefExpr};
@ -241,8 +242,9 @@ mod expr_tests {
#[cfg(test)] #[cfg(test)]
mod module_tests { mod module_tests {
use reflexo::path::unix_slash;
use serde_json::json; use serde_json::json;
use tinymist_std::path::unix_slash;
use typst::syntax::FileId;
use crate::prelude::*; use crate::prelude::*;
use crate::syntax::module::*; use crate::syntax::module::*;
@ -251,7 +253,7 @@ mod module_tests {
#[test] #[test]
fn test() { fn test() {
snapshot_testing("modules", &|ctx, _| { snapshot_testing("modules", &|ctx, _| {
fn ids(ids: EcoVec<TypstFileId>) -> Vec<String> { fn ids(ids: EcoVec<FileId>) -> Vec<String> {
let mut ids: Vec<String> = ids let mut ids: Vec<String> = ids
.into_iter() .into_iter()
.map(|id| unix_slash(id.vpath().as_rooted_path())) .map(|id| unix_slash(id.vpath().as_rooted_path()))

View file

@ -8,11 +8,11 @@ use ecow::{eco_format, EcoString};
use if_chain::if_chain; use if_chain::if_chain;
use lsp_types::InsertTextFormat; use lsp_types::InsertTextFormat;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use reflexo::path::unix_slash;
use regex::{Captures, Regex}; use regex::{Captures, Regex};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tinymist_derive::BindTyCtx; use tinymist_derive::BindTyCtx;
use tinymist_world::LspWorld; use tinymist_project::LspWorld;
use tinymist_std::path::unix_slash;
use typst::foundations::{ use typst::foundations::{
fields_on, format_str, repr, AutoValue, Func, Label, NoneValue, Repr, Scope, StyleChain, Type, fields_on, format_str, repr, AutoValue, Func, Label, NoneValue, Repr, Scope, StyleChain, Type,
Value, Value,

View file

@ -7,11 +7,11 @@ use comemo::{Track, Tracked};
use lsp_types::Url; use lsp_types::Url;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use parking_lot::Mutex; use parking_lot::Mutex;
use reflexo::debug_loc::DataSource;
use reflexo::hash::{hash128, FxDashMap};
use reflexo_typst::{EntryReader, WorldDeps};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use tinymist_world::{LspWorld, DETACHED_ENTRY}; use tinymist_project::LspWorld;
use tinymist_std::debug_loc::DataSource;
use tinymist_std::hash::{hash128, FxDashMap};
use tinymist_world::{EntryReader, WorldDeps, DETACHED_ENTRY};
use typst::diag::{eco_format, At, FileError, FileResult, SourceResult, StrResult}; use typst::diag::{eco_format, At, FileError, FileResult, SourceResult, StrResult};
use typst::engine::{Route, Sink, Traced}; use typst::engine::{Route, Sink, Traced};
use typst::eval::Eval; use typst::eval::Eval;
@ -380,7 +380,7 @@ impl LocalContext {
/// Get depended paths of a compilation. /// Get depended paths of a compilation.
/// Note: must be called after compilation. /// Note: must be called after compilation.
pub(crate) fn depended_paths(&self) -> EcoVec<reflexo::ImmutPath> { pub(crate) fn depended_paths(&self) -> EcoVec<tinymist_std::ImmutPath> {
let mut deps = EcoVec::new(); let mut deps = EcoVec::new();
self.world.iter_dependencies(&mut |path| { self.world.iter_dependencies(&mut |path| {
deps.push(path); deps.push(path);

View file

@ -3,7 +3,7 @@
use std::str::FromStr; use std::str::FromStr;
use lsp_types::Url; use lsp_types::Url;
use reflexo_typst::package::PackageSpec; use tinymist_world::package::PackageSpec;
use super::prelude::*; use super::prelude::*;

View file

@ -11,8 +11,8 @@ use hashbrown::HashMap;
use lsp_types::SemanticToken; use lsp_types::SemanticToken;
use lsp_types::{SemanticTokenModifier, SemanticTokenType}; use lsp_types::{SemanticTokenModifier, SemanticTokenType};
use parking_lot::Mutex; use parking_lot::Mutex;
use reflexo::ImmutPath;
use strum::EnumIter; use strum::EnumIter;
use tinymist_std::ImmutPath;
use typst::syntax::{ast, LinkedNode, Source, SyntaxKind}; use typst::syntax::{ast, LinkedNode, Source, SyntaxKind};
use crate::{ use crate::{

View file

@ -6,8 +6,8 @@ use std::{
}; };
use parking_lot::Mutex; use parking_lot::Mutex;
use reflexo::hash::FxDashMap; use tinymist_std::hash::FxDashMap;
use reflexo_typst::TypstFileId; use typst::syntax::FileId;
use super::Analysis; use super::Analysis;
@ -64,7 +64,7 @@ impl QueryStatGuard {
/// Statistics about the analyzers /// Statistics about the analyzers
#[derive(Default)] #[derive(Default)]
pub struct AnalysisStats { pub struct AnalysisStats {
pub(crate) query_stats: FxDashMap<TypstFileId, FxDashMap<&'static str, QueryStatBucket>>, pub(crate) query_stats: FxDashMap<FileId, FxDashMap<&'static str, QueryStatBucket>>,
} }
impl AnalysisStats { impl AnalysisStats {

View file

@ -1,4 +1,4 @@
use reflexo::TakeAs; use tinymist_std::TakeAs;
use super::*; use super::*;
use crate::syntax::DocString; use crate::syntax::DocString;

View file

@ -1,5 +1,5 @@
use reflexo_typst::ShadowApi;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tinymist_world::ShadowApi;
use typst::foundations::{Bytes, IntoValue, StyleChain}; use typst::foundations::{Bytes, IntoValue, StyleChain};
use typst_shim::syntax::LinkedNodeExt; use typst_shim::syntax::LinkedNodeExt;

View file

@ -1,5 +1,5 @@
use reflexo_typst::EntryReader; use tinymist_project::LspWorld;
use tinymist_world::LspWorld; use tinymist_world::EntryReader;
use typst::syntax::Span; use typst::syntax::Span;
use crate::{prelude::*, LspWorldExt}; use crate::{prelude::*, LspWorldExt};

View file

@ -2,8 +2,7 @@ use std::sync::{Arc, LazyLock};
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use parking_lot::Mutex; use parking_lot::Mutex;
use tinymist_world::base::{EntryState, ShadowApi}; use tinymist_world::{EntryState, ShadowApi, TaskInputs};
use tinymist_world::TaskInputs;
use typlite::scopes::Scopes; use typlite::scopes::Scopes;
use typlite::value::Value; use typlite::value::Value;
use typlite::TypliteFeat; use typlite::TypliteFeat;

View file

@ -6,7 +6,7 @@ mod module;
mod package; mod package;
mod tidy; mod tidy;
use reflexo::path::unix_slash; use tinymist_std::path::unix_slash;
use typst::syntax::FileId; use typst::syntax::FileId;
pub(crate) use convert::convert_docs; pub(crate) use convert::convert_docs;

View file

@ -348,7 +348,7 @@ fn remove_list_annotations(s: &str) -> String {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use reflexo_typst::package::{PackageRegistry, PackageSpec}; use tinymist_world::package::{PackageRegistry, PackageSpec};
use super::{package_docs, PackageInfo}; use super::{package_docs, PackageInfo};
use crate::tests::*; use crate::tests::*;

View file

@ -1,8 +1,8 @@
use std::sync::Arc; use std::sync::Arc;
use std::{collections::HashMap, path::PathBuf}; use std::{collections::HashMap, path::PathBuf};
use reflexo::debug_loc::DataSource;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tinymist_std::debug_loc::DataSource;
use typst::text::{Font, FontStretch, FontStyle, FontWeight}; use typst::text::{Font, FontStretch, FontStyle, FontWeight};
use typst::{ use typst::{
layout::{Frame, FrameItem}, layout::{Frame, FrameItem},

View file

@ -1,6 +1,7 @@
use anyhow::bail; use anyhow::bail;
use reflexo_typst::{EntryState, ImmutPath, TypstFileId}; use tinymist_std::ImmutPath;
use typst::syntax::VirtualPath; use tinymist_world::EntryState;
use typst::syntax::{FileId, VirtualPath};
/// Entry resolver /// Entry resolver
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
@ -59,7 +60,7 @@ impl EntryResolver {
(Some(entry), Some(root)) => match entry.strip_prefix(&root) { (Some(entry), Some(root)) => match entry.strip_prefix(&root) {
Ok(stripped) => Some(EntryState::new_rooted( Ok(stripped) => Some(EntryState::new_rooted(
root, root,
Some(TypstFileId::new(None, VirtualPath::new(stripped))), Some(FileId::new(None, VirtualPath::new(stripped))),
)), )),
Err(err) => { Err(err) => {
log::info!("Entry is not in root directory: err {err:?}: entry: {entry:?}, root: {root:?}"); log::info!("Entry is not in root directory: err {err:?}: entry: {entry:?}, root: {root:?}");
@ -126,7 +127,7 @@ mod entry_tests {
assert_eq!(entry.root(), Some(ImmutPath::from(root_path))); assert_eq!(entry.root(), Some(ImmutPath::from(root_path)));
assert_eq!( assert_eq!(
entry.main(), entry.main(),
Some(TypstFileId::new(None, VirtualPath::new("main.typ"))) Some(FileId::new(None, VirtualPath::new("main.typ")))
); );
} }
@ -151,7 +152,7 @@ mod entry_tests {
assert_eq!(entry.root(), Some(ImmutPath::from(root_path))); assert_eq!(entry.root(), Some(ImmutPath::from(root_path)));
assert_eq!( assert_eq!(
entry.main(), entry.main(),
Some(TypstFileId::new(None, VirtualPath::new("main.typ"))) Some(FileId::new(None, VirtualPath::new("main.typ")))
); );
} }
@ -165,7 +166,7 @@ mod entry_tests {
assert_eq!(entry.root(), Some(ImmutPath::from(root2_path))); assert_eq!(entry.root(), Some(ImmutPath::from(root2_path)));
assert_eq!( assert_eq!(
entry.main(), entry.main(),
Some(TypstFileId::new(None, VirtualPath::new("main.typ"))) Some(FileId::new(None, VirtualPath::new("main.typ")))
); );
} }
} }

View file

@ -2,7 +2,7 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use reflexo::path::PathClean; use tinymist_std::path::PathClean;
use typst::syntax::Source; use typst::syntax::Source;
use crate::prelude::*; use crate::prelude::*;

View file

@ -4,14 +4,15 @@ use std::collections::HashSet;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::OnceLock; use std::sync::OnceLock;
use ecow::{eco_format, eco_vec, EcoVec};
use parking_lot::Mutex; use parking_lot::Mutex;
use reflexo_typst::typst::prelude::*; // use reflexo_typst::typst::prelude::*;
use reflexo_typst::{package::PackageSpec, TypstFileId};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tinymist_world::package::http::HttpRegistry; use tinymist_world::package::http::HttpRegistry;
use tinymist_world::package::PackageSpec;
use typst::diag::{EcoString, StrResult}; use typst::diag::{EcoString, StrResult};
use typst::syntax::package::PackageManifest; use typst::syntax::package::PackageManifest;
use typst::syntax::VirtualPath; use typst::syntax::{FileId, VirtualPath};
use typst::World; use typst::World;
use crate::LocalContext; use crate::LocalContext;
@ -41,8 +42,8 @@ impl From<(PathBuf, PackageSpec)> for PackageInfo {
} }
/// Parses the manifest of the package located at `package_path`. /// Parses the manifest of the package located at `package_path`.
pub fn get_manifest_id(spec: &PackageInfo) -> StrResult<TypstFileId> { pub fn get_manifest_id(spec: &PackageInfo) -> StrResult<FileId> {
Ok(TypstFileId::new( Ok(FileId::new(
Some(PackageSpec { Some(PackageSpec {
namespace: spec.namespace.clone(), namespace: spec.namespace.clone(),
name: spec.name.clone(), name: spec.name.clone(),
@ -53,7 +54,7 @@ pub fn get_manifest_id(spec: &PackageInfo) -> StrResult<TypstFileId> {
} }
/// Parses the manifest of the package located at `package_path`. /// Parses the manifest of the package located at `package_path`.
pub fn get_manifest(world: &dyn World, toml_id: TypstFileId) -> StrResult<PackageManifest> { pub fn get_manifest(world: &dyn World, toml_id: FileId) -> StrResult<PackageManifest> {
let toml_data = world let toml_data = world
.file(toml_id) .file(toml_id)
.map_err(|err| eco_format!("failed to read package manifest ({})", err))?; .map_err(|err| eco_format!("failed to read package manifest ({})", err))?;

View file

@ -16,8 +16,8 @@ pub use lsp_types::{
SemanticTokens, SemanticTokensDelta, SemanticTokensFullDeltaResult, SemanticTokensResult, SemanticTokens, SemanticTokensDelta, SemanticTokensFullDeltaResult, SemanticTokensResult,
SignatureHelp, SignatureInformation, SymbolInformation, TextEdit, Url, WorkspaceEdit, SignatureHelp, SignatureInformation, SymbolInformation, TextEdit, Url, WorkspaceEdit,
}; };
pub use reflexo::vector::ir::DefId;
pub use serde_json::Value as JsonValue; pub use serde_json::Value as JsonValue;
pub use tinymist_std::DefId;
pub use typst::diag::{EcoString, Tracepoint}; pub use typst::diag::{EcoString, Tracepoint};
pub use typst::foundations::Value; pub use typst::foundations::Value;
pub use typst::syntax::ast::{self, AstNode}; pub use typst::syntax::ast::{self, AstNode};

View file

@ -177,7 +177,7 @@ impl ReferencesWorker<'_> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use reflexo::path::unix_slash; use tinymist_std::path::unix_slash;
use super::*; use super::*;
use crate::syntax::find_module_level_docs; use crate::syntax::find_module_level_docs;

View file

@ -2,8 +2,8 @@ use lsp_types::{
DocumentChangeOperation, DocumentChanges, OneOf, OptionalVersionedTextDocumentIdentifier, DocumentChangeOperation, DocumentChanges, OneOf, OptionalVersionedTextDocumentIdentifier,
RenameFile, TextDocumentEdit, RenameFile, TextDocumentEdit,
}; };
use reflexo::path::{unix_slash, PathClean};
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
use tinymist_std::path::{unix_slash, PathClean};
use typst::{ use typst::{
foundations::{Repr, Str}, foundations::{Repr, Str},
syntax::Span, syntax::Span,

View file

@ -1,9 +1,9 @@
use core::fmt; use core::fmt;
use std::{collections::BTreeMap, ops::Range}; use std::{collections::BTreeMap, ops::Range};
use reflexo_typst::package::PackageSpec;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tinymist_derive::DeclEnum; use tinymist_derive::DeclEnum;
use tinymist_world::package::PackageSpec;
use typst::{ use typst::{
foundations::{Element, Func, Module, Type, Value}, foundations::{Element, Func, Module, Type, Value},
syntax::{Span, SyntaxNode}, syntax::{Span, SyntaxNode},

View file

@ -1,16 +1,16 @@
use std::ops::DerefMut; use std::ops::DerefMut;
use parking_lot::Mutex; use parking_lot::Mutex;
use reflexo::hash::hash128;
use reflexo_typst::LazyHash;
use rpds::RedBlackTreeMapSync; use rpds::RedBlackTreeMapSync;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use std::ops::Deref; use std::ops::Deref;
use tinymist_analysis::import::resolve_id_by_path; use tinymist_analysis::import::resolve_id_by_path;
use tinymist_std::hash::hash128;
use typst::{ use typst::{
foundations::{Element, NativeElement, Value}, foundations::{Element, NativeElement, Value},
model::{EmphElem, EnumElem, HeadingElem, ListElem, StrongElem, TermsElem}, model::{EmphElem, EnumElem, HeadingElem, ListElem, StrongElem, TermsElem},
syntax::{Span, SyntaxNode}, syntax::{Span, SyntaxNode},
utils::LazyHash,
}; };
use crate::{ use crate::{

View file

@ -1,7 +1,7 @@
use std::str::FromStr; use std::str::FromStr;
use reflexo_typst::package::PackageSpec;
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
use tinymist_world::package::PackageSpec;
use crate::{adt::interner::Interned, prelude::*}; use crate::{adt::interner::Interned, prelude::*};

View file

@ -56,8 +56,8 @@
//! ^ SurroundingSyntax::Regular //! ^ SurroundingSyntax::Regular
//! ``` //! ```
use reflexo_typst::debug_loc::SourceSpanOffset;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tinymist_std::debug_loc::SourceSpanOffset;
use typst::syntax::Span; use typst::syntax::Span;
use crate::prelude::*; use crate::prelude::*;

View file

@ -9,12 +9,12 @@ use std::{
}; };
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use reflexo_typst::package::PackageSpec;
use reflexo_typst::world::EntryState;
use reflexo_typst::{Compiler, EntryManager, EntryReader, ShadowApi};
use serde_json::{ser::PrettyFormatter, Serializer, Value}; use serde_json::{ser::PrettyFormatter, Serializer, Value};
use tinymist_world::CompileFontArgs; use tinymist_project::CompileFontArgs;
use tinymist_world::package::PackageSpec;
use tinymist_world::EntryState;
use tinymist_world::TaskInputs; use tinymist_world::TaskInputs;
use tinymist_world::{EntryManager, EntryReader, ShadowApi};
use typst::foundations::Bytes; use typst::foundations::Bytes;
use typst::syntax::ast::{self, AstNode}; use typst::syntax::ast::{self, AstNode};
use typst::syntax::{FileId as TypstFileId, LinkedNode, Source, SyntaxKind, VirtualPath}; use typst::syntax::{FileId as TypstFileId, LinkedNode, Source, SyntaxKind, VirtualPath};
@ -22,7 +22,7 @@ use typst::syntax::{FileId as TypstFileId, LinkedNode, Source, SyntaxKind, Virtu
pub use insta::assert_snapshot; pub use insta::assert_snapshot;
pub use serde::Serialize; pub use serde::Serialize;
pub use serde_json::json; pub use serde_json::json;
pub use tinymist_world::{LspUniverse, LspUniverseBuilder}; pub use tinymist_project::{LspUniverse, LspUniverseBuilder};
use typst::World; use typst::World;
use typst_shim::syntax::LinkedNodeExt; use typst_shim::syntax::LinkedNodeExt;
@ -187,8 +187,6 @@ pub fn run_with_sources<T>(source: &str, f: impl FnOnce(&mut LspUniverse, PathBu
} }
verse.mutate_entry(EntryState::new_detached()).unwrap(); verse.mutate_entry(EntryState::new_detached()).unwrap();
let world = verse.snapshot();
let _ = std::marker::PhantomData.compile(&world, &mut Default::default());
let pw = last_pw.unwrap(); let pw = last_pw.unwrap();
verse verse

View file

@ -6,6 +6,7 @@ use once_cell::sync::Lazy;
use regex::RegexSet; use regex::RegexSet;
use strum::{EnumIter, IntoEnumIterator}; use strum::{EnumIter, IntoEnumIterator};
use typst::foundations::CastInfo; use typst::foundations::CastInfo;
use typst::syntax::FileId;
use typst::{ use typst::{
foundations::{AutoValue, Content, Func, NoneValue, ParamInfo, Type, Value}, foundations::{AutoValue, Content, Func, NoneValue, ParamInfo, Type, Value},
layout::Length, layout::Length,
@ -185,10 +186,10 @@ impl fmt::Debug for PackageId {
} }
} }
impl TryFrom<TypstFileId> for PackageId { impl TryFrom<FileId> for PackageId {
type Error = (); type Error = ();
fn try_from(value: TypstFileId) -> Result<Self, Self::Error> { fn try_from(value: FileId) -> Result<Self, Self::Error> {
let Some(spec) = value.package() else { let Some(spec) = value.package() else {
return Err(()); return Err(());
}; };

View file

@ -11,12 +11,11 @@ use std::{
use ecow::EcoString; use ecow::EcoString;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use parking_lot::{Mutex, RwLock}; use parking_lot::{Mutex, RwLock};
use reflexo_typst::TypstFileId;
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::{FxHashMap, FxHashSet};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use typst::{ use typst::{
foundations::{Content, Element, ParamInfo, Type, Value}, foundations::{Content, Element, ParamInfo, Type, Value},
syntax::{ast, Span, SyntaxKind, SyntaxNode}, syntax::{ast, FileId, Span, SyntaxKind, SyntaxNode},
}; };
use super::{BoundPred, PackageId}; use super::{BoundPred, PackageId};
@ -1169,7 +1168,7 @@ pub struct TypeInfo {
/// Whether the typing is valid /// Whether the typing is valid
pub valid: bool, pub valid: bool,
/// The belonging file id /// The belonging file id
pub fid: Option<TypstFileId>, pub fid: Option<FileId>,
/// The revision used /// The revision used
pub revision: usize, pub revision: usize,
/// The exported types /// The exported types
@ -1284,7 +1283,7 @@ impl TyCtxMut for TypeInfo {
Ty::Any Ty::Any
} }
fn check_module_item(&mut self, _module: TypstFileId, _key: &StrRef) -> Option<Ty> { fn check_module_item(&mut self, _module: FileId, _key: &StrRef) -> Option<Ty> {
None None
} }
} }

View file

@ -1,5 +1,5 @@
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use reflexo::hash::hash128; use tinymist_std::hash::hash128;
use typst::foundations::Repr; use typst::foundations::Repr;
use crate::{ use crate::{

View file

@ -1,4 +1,4 @@
use reflexo_typst::TypstFileId; use typst::syntax::FileId;
use typst::{ use typst::{
foundations::{Dict, Module, Scope, Type}, foundations::{Dict, Module, Scope, Type},
syntax::Span, syntax::Span,
@ -27,7 +27,7 @@ pub enum Iface<'a> {
at: &'a Ty, at: &'a Ty,
}, },
Module { Module {
val: TypstFileId, val: FileId,
at: &'a Ty, at: &'a Ty,
}, },
ModuleVal { ModuleVal {

View file

@ -19,10 +19,10 @@ pub(crate) use builtin::*;
pub use def::*; pub use def::*;
pub(crate) use iface::*; pub(crate) use iface::*;
pub(crate) use mutate::*; pub(crate) use mutate::*;
use reflexo_typst::TypstFileId;
pub(crate) use select::*; pub(crate) use select::*;
pub(crate) use sig::*; pub(crate) use sig::*;
use typst::foundations::{self, Func, Module, Value}; use typst::foundations::{self, Func, Module, Value};
use typst::syntax::FileId;
/// A type context. /// A type context.
pub trait TyCtx { pub trait TyCtx {
@ -83,7 +83,7 @@ pub trait TyCtxMut: TyCtx {
ty ty
} }
/// Check a module item. /// Check a module item.
fn check_module_item(&mut self, module: TypstFileId, key: &StrRef) -> Option<Ty>; fn check_module_item(&mut self, module: FileId, key: &StrRef) -> Option<Ty>;
} }
#[cfg(test)] #[cfg(test)]

View file

@ -1,7 +1,7 @@
pub use std::collections::{HashMap, HashSet}; pub use std::collections::{HashMap, HashSet};
pub use reflexo::vector::ir::DefId;
pub use rustc_hash::{FxHashMap, FxHashSet}; pub use rustc_hash::{FxHashMap, FxHashSet};
pub use tinymist_std::DefId;
pub use typst::foundations::Value; pub use typst::foundations::Value;
pub use super::builtin::*; pub use super::builtin::*;

View file

@ -0,0 +1,60 @@
[package]
name = "tinymist-std"
description = "Additional functions wrapping Rust's std library."
authors.workspace = true
version.workspace = true
license.workspace = true
edition.workspace = true
homepage.workspace = true
repository.workspace = true
[dependencies]
comemo.workspace = true
ecow.workspace = true
parking_lot.workspace = true
web-time.workspace = true
wasm-bindgen = { workspace = true, optional = true }
js-sys = { workspace = true, optional = true }
bitvec = { version = "1" }
dashmap = { version = "5" }
# tiny-skia-path.workspace = true
path-clean.workspace = true
base64.workspace = true
fxhash.workspace = true
rustc-hash.workspace = true
siphasher.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_repr = "0.1"
serde_json.workspace = true
serde_with.workspace = true
rkyv = { workspace = true, optional = true }
typst = { workspace = true, optional = true }
typst-shim = { workspace = true, optional = true }
[dev-dependencies]
hex.workspace = true
[features]
default = ["full"]
full = ["web", "rkyv", "typst"]
typst = ["dep:typst", "dep:typst-shim"]
rkyv = ["dep:rkyv", "rkyv/alloc", "rkyv/archive_le"]
rkyv-validation = ["dep:rkyv", "rkyv/validation"]
# flat-vector = ["rkyv", "rkyv-validation"]
__web = ["dep:wasm-bindgen", "dep:js-sys"]
web = ["__web"]
system = []
bi-hash = []
item-dashmap = []
[lints]
workspace = true

View file

@ -0,0 +1,5 @@
# reflexo
A portable format to show (typst) document in web browser.
See [Typst.ts](https://github.com/Myriad-Dreamin/typst.ts)

View file

@ -0,0 +1,111 @@
use std::{collections::HashMap, num::NonZeroU32};
use crate::hash::Fingerprint;
/// A global upper bound on the shard size.
/// If there are too many shards, the memory overhead is unacceptable.
const MAX_SHARD_SIZE: u32 = 512;
/// Return a read-only default shard size.
fn default_shard_size() -> NonZeroU32 {
static ITEM_SHARD_SIZE: std::sync::OnceLock<NonZeroU32> = std::sync::OnceLock::new();
/// By testing, we found that the optimal shard size is 2 * number of
/// threads.
fn determine_default_shard_size() -> NonZeroU32 {
// This detection is from rayon.
let thread_cnt = {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
};
// A valid shard size is a power of two.
let size = (thread_cnt.next_power_of_two() * 2) as u32;
// Perform early non-zero check to avoid panics.
NonZeroU32::new(size.min(MAX_SHARD_SIZE)).unwrap()
}
*ITEM_SHARD_SIZE.get_or_init(determine_default_shard_size)
}
type FMapBase<V> = parking_lot::RwLock<HashMap<Fingerprint, V>>;
/// A map that shards items by their fingerprint.
///
/// It is fast since a fingerprint could split items into different shards
/// efficiently.
///
/// Note: If a fingerprint is calculated from a hash function, it is not
/// guaranteed that the fingerprint is evenly distributed. Thus, in that case,
/// the performance of this map is not guaranteed.
pub struct FingerprintMap<V> {
mask: u32,
shards: Vec<parking_lot::RwLock<HashMap<Fingerprint, V>>>,
}
impl<V> Default for FingerprintMap<V> {
fn default() -> Self {
Self::new(default_shard_size())
}
}
impl<V> FingerprintMap<V> {
/// Create a new `FingerprintMap` with the given shard size.
pub fn new(shard_size: NonZeroU32) -> Self {
let shard_size = shard_size.get().next_power_of_two();
let shard_size = shard_size.min(MAX_SHARD_SIZE);
assert!(
shard_size.is_power_of_two(),
"shard size must be a power of two"
);
assert!(shard_size > 0, "shard size must be greater than zero");
Self {
mask: shard_size - 1,
shards: (0..shard_size)
.map(|_| parking_lot::RwLock::new(HashMap::new()))
.collect(),
}
}
/// Iterate over all items in the map.
pub fn into_items(self) -> impl Iterator<Item = (Fingerprint, V)> {
self.shards
.into_iter()
.flat_map(|shard| shard.into_inner().into_iter())
}
pub fn shard(&self, fg: Fingerprint) -> &FMapBase<V> {
let shards = &self.shards;
let route_idx = (fg.lower32() & self.mask) as usize;
// check that the route index is within the bounds of the shards
debug_assert!(route_idx < shards.len());
// SAFETY: `fg` is a valid index into `shards`, as shards size is never changed
// and mask is always a power of two.
unsafe { shards.get_unchecked(route_idx) }
}
/// Useful for parallel iteration
pub fn as_mut_slice(&mut self) -> &mut [FMapBase<V>] {
&mut self.shards
}
pub fn contains_key(&self, fg: &Fingerprint) -> bool {
self.shard(*fg).read().contains_key(fg)
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_default_shard_size() {
let size = super::default_shard_size().get();
eprintln!("size = {size}");
assert!(size > 0);
assert_eq!(size & (size - 1), 0);
}
}

View file

@ -0,0 +1,5 @@
pub mod fmap;
pub use fmap::FingerprintMap;
// todo: remove it if we could find a better alternative
pub use dashmap::DashMap as CHashMap;

View file

@ -0,0 +1,30 @@
#[derive(Debug)]
pub enum CowMut<'a, T> {
Owned(T),
Borrowed(&'a mut T),
}
impl<T> std::ops::Deref for CowMut<'_, T> {
type Target = T;
fn deref(&self) -> &T {
match self {
CowMut::Owned(it) => it,
CowMut::Borrowed(it) => it,
}
}
}
impl<T> std::ops::DerefMut for CowMut<'_, T> {
fn deref_mut(&mut self) -> &mut T {
match self {
CowMut::Owned(it) => it,
CowMut::Borrowed(it) => it,
}
}
}
impl<T: Default> Default for CowMut<'_, T> {
fn default() -> Self {
CowMut::Owned(T::default())
}
}

View file

@ -0,0 +1,61 @@
//todo: move to core/src/hash.rs
use std::{
hash::{Hash, Hasher},
ops::Deref,
};
use crate::hash::item_hash128;
pub trait StaticHash128 {
fn get_hash(&self) -> u128;
}
impl Hash for dyn StaticHash128 {
#[inline]
fn hash<H: Hasher>(&self, state: &mut H) {
state.write_u128(self.get_hash());
}
}
pub struct HashedTrait<T: ?Sized> {
hash: u128,
t: Box<T>,
}
impl<T: ?Sized> HashedTrait<T> {
pub fn new(hash: u128, t: Box<T>) -> Self {
Self { hash, t }
}
}
impl<T: ?Sized> Deref for HashedTrait<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.t
}
}
impl<T> Hash for HashedTrait<T> {
#[inline]
fn hash<H: Hasher>(&self, state: &mut H) {
state.write_u128(self.hash);
}
}
impl<T: Hash + Default + 'static> Default for HashedTrait<T> {
fn default() -> Self {
let t = T::default();
Self {
hash: item_hash128(&t),
t: Box::new(t),
}
}
}
impl<T: ?Sized> StaticHash128 for HashedTrait<T> {
fn get_hash(&self) -> u128 {
self.hash
}
}

View file

@ -0,0 +1,32 @@
use std::borrow::{Borrow, Cow};
use serde::{Deserializer, Serializer};
use serde_with::{
base64::{Base64, Standard},
formats::Padded,
};
use serde_with::{DeserializeAs, SerializeAs};
pub struct AsCowBytes;
type StdBase64 = Base64<Standard, Padded>;
impl<'b> SerializeAs<Cow<'b, [u8]>> for AsCowBytes {
fn serialize_as<S>(source: &Cow<'b, [u8]>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let t: &[u8] = source.borrow();
StdBase64::serialize_as(&t, serializer)
}
}
impl<'b, 'de> DeserializeAs<'de, Cow<'b, [u8]>> for AsCowBytes {
fn deserialize_as<D>(deserializer: D) -> Result<Cow<'b, [u8]>, D::Error>
where
D: Deserializer<'de>,
{
let buf: Vec<u8> = StdBase64::deserialize_as(deserializer)?;
Ok(Cow::Owned(buf))
}
}

View file

@ -0,0 +1,22 @@
mod takable;
use std::{path::Path, sync::Arc};
pub use takable::*;
mod hash;
pub use hash::*;
pub mod cow_mut;
mod query;
pub use query::*;
mod read;
pub use read::*;
mod marker;
pub use marker::*;
pub type ImmutStr = Arc<str>;
pub type ImmutBytes = Arc<[u8]>;
pub type ImmutPath = Arc<Path>;

View file

@ -0,0 +1,83 @@
use core::fmt;
use std::sync::OnceLock;
use parking_lot::Mutex;
/// Represent the result of an immutable query reference.
/// The compute function should be pure enough.
///
/// [`compute`]: Self::compute
/// [`compute_ref`]: Self::compute_ref
pub struct QueryRef<Res, Err, QueryContext = ()> {
ctx: Mutex<Option<QueryContext>>,
/// `None` means no value has been computed yet.
cell: OnceLock<Result<Res, Err>>,
}
impl<T, E, QC> QueryRef<T, E, QC> {
pub fn with_value(value: T) -> Self {
let cell = OnceLock::new();
cell.get_or_init(|| Ok(value));
Self {
ctx: Mutex::new(None),
cell,
}
}
pub fn with_context(ctx: QC) -> Self {
Self {
ctx: Mutex::new(Some(ctx)),
cell: OnceLock::new(),
}
}
}
impl<T, E: Clone, QC> QueryRef<T, E, QC> {
/// Compute and return a checked reference guard.
#[inline]
pub fn compute<F: FnOnce() -> Result<T, E>>(&self, f: F) -> Result<&T, E> {
self.compute_with_context(|_| f())
}
/// Compute with context and return a checked reference guard.
#[inline]
pub fn compute_with_context<F: FnOnce(QC) -> Result<T, E>>(&self, f: F) -> Result<&T, E> {
let result = self.cell.get_or_init(|| f(self.ctx.lock().take().unwrap()));
result.as_ref().map_err(Clone::clone)
}
/// Gets the reference to the (maybe uninitialized) result.
///
/// Returns `None` if the cell is empty, or being initialized. This
/// method never blocks.
///
/// It is possible not hot, so that it is non-inlined
pub fn get_uninitialized(&self) -> Option<&Result<T, E>> {
self.cell.get()
}
}
impl<T, E> Default for QueryRef<T, E> {
fn default() -> Self {
QueryRef {
ctx: Mutex::new(Some(())),
cell: OnceLock::new(),
}
}
}
impl<T, E, QC> fmt::Debug for QueryRef<T, E, QC>
where
T: fmt::Debug,
E: fmt::Debug,
QC: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let ctx = self.ctx.lock();
let res = self.cell.get();
f.debug_struct("QueryRef")
.field("context", &ctx)
.field("result", &res)
.finish()
}
}

View file

@ -0,0 +1,3 @@
pub trait ReadAllOnce {
fn read_all(self, buf: &mut Vec<u8>) -> std::io::Result<usize>;
}

View file

@ -0,0 +1,17 @@
use std::sync::Arc;
/// Trait for values being taken.
pub trait TakeAs<T> {
/// Takes the inner value if there is exactly one strong reference and
/// clones it otherwise.
fn take(self) -> T;
}
impl<T: Clone> TakeAs<T> for Arc<T> {
fn take(self) -> T {
match Arc::try_unwrap(self) {
Ok(v) => v,
Err(rc) => (*rc).clone(),
}
}
}

View file

@ -0,0 +1,216 @@
use core::fmt;
use serde::{Deserialize, Serialize};
/// A serializable physical position in a document.
///
/// Note that it uses [`f32`] instead of [`f64`] as same as
/// `TypstPosition` for the coordinates to improve both performance
/// of serialization and calculation. It does sacrifice the floating
/// precision, but it is enough in our use cases.
///
/// Also see `TypstPosition`.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct DocumentPosition {
/// The page, starting at 1.
pub page_no: usize,
/// The exact x-coordinate on the page (from the left, as usual).
pub x: f32,
/// The exact y-coordinate on the page (from the top, as usual).
pub y: f32,
}
// impl From<TypstPosition> for DocumentPosition {
// fn from(position: TypstPosition) -> Self {
// Self {
// page_no: position.page.into(),
// x: position.point.x.to_pt() as f32,
// y: position.point.y.to_pt() as f32,
// }
// }
// }
/// Raw representation of a source span.
pub type RawSourceSpan = u64;
/// A resolved source (text) location.
///
/// See [`CharPosition`] for the definition of the position inside a file.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileLocation {
pub filepath: String,
}
/// A char position represented in form of line and column.
/// The position is encoded in Utf-8 or Utf-16, and the encoding is
/// determined by usage.
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq, Hash)]
pub struct CharPosition {
/// The line number, starting at 0.
pub line: usize,
/// The column number, starting at 0.
pub column: usize,
}
impl fmt::Display for CharPosition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.line, self.column)
}
}
impl From<Option<(usize, usize)>> for CharPosition {
fn from(loc: Option<(usize, usize)>) -> Self {
let (start, end) = loc.unwrap_or_default();
CharPosition {
line: start,
column: end,
}
}
}
/// A resolved source (text) location.
///
/// See [`CharPosition`] for the definition of the position inside a file.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SourceLocation {
pub filepath: String,
pub pos: CharPosition,
}
impl SourceLocation {
pub fn from_flat(
flat: FlatSourceLocation,
i: &impl std::ops::Index<usize, Output = FileLocation>,
) -> Self {
Self {
filepath: i[flat.filepath as usize].filepath.clone(),
pos: flat.pos,
}
}
}
/// A flat resolved source (text) location.
///
/// See [`CharPosition`] for the definition of the position inside a file.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FlatSourceLocation {
pub filepath: u32,
pub pos: CharPosition,
}
// /// A resolved file range.
// ///
// /// See [`CharPosition`] for the definition of the position inside a file.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CharRange {
pub start: CharPosition,
pub end: CharPosition,
}
impl fmt::Display for CharRange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.start == self.end {
write!(f, "{}", self.start)
} else {
write!(f, "{}-{}", self.start, self.end)
}
}
}
// /// A resolved source (text) range.
// ///
// /// See [`CharPosition`] for the definition of the position inside a file.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SourceRange {
pub path: String,
pub range: CharRange,
}
#[cfg(feature = "typst")]
mod typst_ext {
pub use typst::layout::Position as TypstPosition;
/// Unevaluated source span.
/// The raw source span is unsafe to serialize and deserialize.
/// Because the real source location is only known during liveness of
/// the compiled document.
pub type SourceSpan = typst::syntax::Span;
/// Unevaluated source span with offset.
///
/// It adds an additional offset relative to the start of the span.
///
/// The offset is usually generated when the location is inside of some
/// text or string content.
#[derive(Debug, Clone, Copy)]
pub struct SourceSpanOffset {
pub span: SourceSpan,
pub offset: usize,
}
/// Lifts a [`SourceSpan`] to [`SourceSpanOffset`].
impl From<SourceSpan> for SourceSpanOffset {
fn from(span: SourceSpan) -> Self {
Self { span, offset: 0 }
}
}
/// Converts a [`SourceSpan`] and an in-text offset to [`SourceSpanOffset`].
impl From<(SourceSpan, u16)> for SourceSpanOffset {
fn from((span, offset): (SourceSpan, u16)) -> Self {
Self {
span,
offset: offset as usize,
}
}
}
}
#[cfg(feature = "typst")]
pub use typst_ext::*;
/// A point on the element tree.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElementPoint {
/// The element kind.
pub kind: u32,
/// The index of the element.
pub index: u32,
/// The fingerprint of the element.
pub fingerprint: String,
}
impl From<(u32, u32, String)> for ElementPoint {
fn from((kind, index, fingerprint): (u32, u32, String)) -> Self {
Self {
kind,
index,
fingerprint,
}
}
}
/// A file system data source.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct FsDataSource {
/// The name of the data source.
pub path: String,
}
/// A in-memory data source.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct MemoryDataSource {
/// The name of the data source.
pub name: String,
}
/// Data source for a document.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(tag = "kind")]
pub enum DataSource {
/// File system data source.
#[serde(rename = "fs")]
Fs(FsDataSource),
/// Memory data source.
#[serde(rename = "memory")]
Memory(MemoryDataSource),
}

View file

@ -0,0 +1,327 @@
use core::fmt;
use ecow::EcoString;
use serde::{Deserialize, Serialize};
use crate::debug_loc::CharRange;
#[derive(serde_repr::Serialize_repr, serde_repr::Deserialize_repr, Debug, Clone)]
#[repr(u8)]
pub enum DiagSeverity {
Error = 1,
Warning = 2,
Information = 3,
Hint = 4,
}
impl fmt::Display for DiagSeverity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DiagSeverity::Error => write!(f, "error"),
DiagSeverity::Warning => write!(f, "warning"),
DiagSeverity::Information => write!(f, "information"),
DiagSeverity::Hint => write!(f, "hint"),
}
}
}
/// <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnostic>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiagMessage {
pub package: String,
pub path: String,
pub message: String,
pub severity: DiagSeverity,
pub range: Option<CharRange>,
// These field could be added to ErrorImpl::arguments
// owner: Option<ImmutStr>,
// source: ImmutStr,
}
impl DiagMessage {}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum ErrKind {
None,
Msg(String),
Diag(DiagMessage),
Inner(Error),
}
pub trait ErrKindExt {
fn to_error_kind(self) -> ErrKind;
}
impl ErrKindExt for ErrKind {
fn to_error_kind(self) -> Self {
self
}
}
impl ErrKindExt for std::io::Error {
fn to_error_kind(self) -> ErrKind {
ErrKind::Msg(self.to_string())
}
}
impl ErrKindExt for String {
fn to_error_kind(self) -> ErrKind {
ErrKind::Msg(self)
}
}
impl ErrKindExt for &str {
fn to_error_kind(self) -> ErrKind {
ErrKind::Msg(self.to_string())
}
}
impl ErrKindExt for &String {
fn to_error_kind(self) -> ErrKind {
ErrKind::Msg(self.to_string())
}
}
impl ErrKindExt for EcoString {
fn to_error_kind(self) -> ErrKind {
ErrKind::Msg(self.to_string())
}
}
impl ErrKindExt for &dyn std::fmt::Display {
fn to_error_kind(self) -> ErrKind {
ErrKind::Msg(self.to_string())
}
}
impl ErrKindExt for serde_json::Error {
fn to_error_kind(self) -> ErrKind {
ErrKind::Msg(self.to_string())
}
}
#[derive(Debug, Clone)]
pub struct ErrorImpl {
loc: &'static str,
kind: ErrKind,
arguments: Box<[(&'static str, String)]>,
}
/// This type represents all possible errors that can occur in typst.ts
#[derive(Debug, Clone)]
pub struct Error {
/// This `Box` allows us to keep the size of `Error` as small as possible. A
/// larger `Error` type was substantially slower due to all the functions
/// that pass around `Result<T, Error>`.
err: Box<ErrorImpl>,
}
impl Error {
pub fn new(loc: &'static str, kind: ErrKind, arguments: Box<[(&'static str, String)]>) -> Self {
Self {
err: Box::new(ErrorImpl {
loc,
kind,
arguments,
}),
}
}
pub fn loc(&self) -> &'static str {
self.err.loc
}
pub fn kind(&self) -> &ErrKind {
&self.err.kind
}
pub fn arguments(&self) -> &[(&'static str, String)] {
&self.err.arguments
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let err = &self.err;
match &err.kind {
ErrKind::Msg(msg) => write!(f, "{}: {} with {:?}", err.loc, msg, err.arguments),
ErrKind::Diag(diag) => {
write!(f, "{}: {} with {:?}", err.loc, diag.message, err.arguments)
}
ErrKind::Inner(e) => write!(f, "{}: {} with {:?}", err.loc, e, err.arguments),
ErrKind::None => write!(f, "{}: with {:?}", err.loc, err.arguments),
}
}
}
impl std::error::Error for Error {}
#[cfg(feature = "web")]
impl ErrKindExt for wasm_bindgen::JsValue {
fn to_error_kind(self) -> ErrKind {
ErrKind::Msg(format!("{self:?}"))
}
}
#[cfg(feature = "web")]
impl From<Error> for wasm_bindgen::JsValue {
fn from(e: Error) -> Self {
js_sys::Error::new(&e.to_string()).into()
}
}
#[cfg(feature = "web")]
impl From<&Error> for wasm_bindgen::JsValue {
fn from(e: &Error) -> Self {
js_sys::Error::new(&e.to_string()).into()
}
}
pub mod prelude {
use super::ErrKindExt;
use crate::Error;
pub type ZResult<T> = Result<T, Error>;
pub trait WithContext<T>: Sized {
fn context(self, loc: &'static str) -> ZResult<T>;
fn with_context<F>(self, loc: &'static str, f: F) -> ZResult<T>
where
F: FnOnce() -> Box<[(&'static str, String)]>;
}
impl<T, E: ErrKindExt> WithContext<T> for Result<T, E> {
fn context(self, loc: &'static str) -> ZResult<T> {
self.map_err(|e| Error::new(loc, e.to_error_kind(), Box::new([])))
}
fn with_context<F>(self, loc: &'static str, f: F) -> ZResult<T>
where
F: FnOnce() -> Box<[(&'static str, String)]>,
{
self.map_err(|e| Error::new(loc, e.to_error_kind(), f()))
}
}
pub fn map_string_err<T: ToString>(loc: &'static str) -> impl Fn(T) -> Error {
move |e| Error::new(loc, e.to_string().to_error_kind(), Box::new([]))
}
pub fn map_into_err<S: ErrKindExt, T: Into<S>>(loc: &'static str) -> impl Fn(T) -> Error {
move |e| Error::new(loc, e.into().to_error_kind(), Box::new([]))
}
pub fn map_err<T: ErrKindExt>(loc: &'static str) -> impl Fn(T) -> Error {
move |e| Error::new(loc, e.to_error_kind(), Box::new([]))
}
pub fn wrap_err(loc: &'static str) -> impl Fn(Error) -> Error {
move |e| Error::new(loc, crate::ErrKind::Inner(e), Box::new([]))
}
pub fn map_string_err_with_args<
T: ToString,
Args: IntoIterator<Item = (&'static str, String)>,
>(
loc: &'static str,
arguments: Args,
) -> impl FnOnce(T) -> Error {
move |e| {
Error::new(
loc,
e.to_string().to_error_kind(),
arguments.into_iter().collect::<Vec<_>>().into_boxed_slice(),
)
}
}
pub fn map_into_err_with_args<
S: ErrKindExt,
T: Into<S>,
Args: IntoIterator<Item = (&'static str, String)>,
>(
loc: &'static str,
arguments: Args,
) -> impl FnOnce(T) -> Error {
move |e| {
Error::new(
loc,
e.into().to_error_kind(),
arguments.into_iter().collect::<Vec<_>>().into_boxed_slice(),
)
}
}
pub fn map_err_with_args<T: ErrKindExt, Args: IntoIterator<Item = (&'static str, String)>>(
loc: &'static str,
arguments: Args,
) -> impl FnOnce(T) -> Error {
move |e| {
Error::new(
loc,
e.to_error_kind(),
arguments.into_iter().collect::<Vec<_>>().into_boxed_slice(),
)
}
}
pub fn wrap_err_with_args<Args: IntoIterator<Item = (&'static str, String)>>(
loc: &'static str,
arguments: Args,
) -> impl FnOnce(Error) -> Error {
move |e| {
Error::new(
loc,
crate::ErrKind::Inner(e),
arguments.into_iter().collect::<Vec<_>>().into_boxed_slice(),
)
}
}
pub fn _error_once(loc: &'static str, args: Box<[(&'static str, String)]>) -> Error {
Error::new(loc, crate::ErrKind::None, args)
}
#[macro_export]
macro_rules! error_once {
($loc:expr, $($arg_key:ident: $arg:expr),+ $(,)?) => {
_error_once($loc, Box::new([$((stringify!($arg_key), $arg.to_string())),+]))
};
($loc:expr $(,)?) => {
_error_once($loc, Box::new([]))
};
}
#[macro_export]
macro_rules! error_once_map {
($loc:expr, $($arg_key:ident: $arg:expr),+ $(,)?) => {
map_err_with_args($loc, [$((stringify!($arg_key), $arg.to_string())),+])
};
($loc:expr $(,)?) => {
map_err($loc)
};
}
#[macro_export]
macro_rules! error_once_map_string {
($loc:expr, $($arg_key:ident: $arg:expr),+ $(,)?) => {
map_string_err_with_args($loc, [$((stringify!($arg_key), $arg.to_string())),+])
};
($loc:expr $(,)?) => {
map_string_err($loc)
};
}
pub use error_once;
pub use error_once_map;
pub use error_once_map_string;
}
#[test]
fn test_send() {
fn is_send<T: Send>() {}
is_send::<Error>();
}

View file

@ -0,0 +1,313 @@
use core::fmt;
use std::{
any::Any,
hash::{Hash, Hasher},
};
use base64::Engine;
use fxhash::FxHasher32;
use siphasher::sip128::{Hasher128, SipHasher13};
#[cfg(feature = "rkyv")]
use rkyv::{Archive, Deserialize as rDeser, Serialize as rSer};
use crate::error::prelude::ZResult;
pub(crate) type FxBuildHasher = std::hash::BuildHasherDefault<FxHasher>;
pub use rustc_hash::{FxHashMap, FxHashSet, FxHasher};
// pub type FxIndexSet<K> = indexmap::IndexSet<K, FxHasher>;
// pub type FxIndexMap<K, V> = indexmap::IndexMap<K, V, FxHasher>;
pub type FxDashMap<K, V> = dashmap::DashMap<K, V, FxBuildHasher>;
/// See <https://github.com/rust-lang/rust/blob/master/compiler/rustc_hir/src/stable_hash_impls.rs#L22>
/// The fingerprint conflicts should be very rare and should be handled by the
/// compiler.
///
/// > That being said, given a high quality hash function, the collision
/// > probabilities in question are very small. For example, for a big crate
/// > like `rustc_middle` (with ~50000 `LocalDefId`s as of the time of writing)
/// > there is a probability of roughly 1 in 14,750,000,000 of a crate-internal
/// > collision occurring. For a big crate graph with 1000 crates in it, there
/// > is a probability of 1 in 36,890,000,000,000 of a `StableCrateId`
/// > collision.
#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "rkyv", derive(Archive, rDeser, rSer))]
#[cfg_attr(feature = "rkyv-validation", archive(check_bytes))]
pub struct Fingerprint {
lo: u64,
hi: u64,
}
impl fmt::Debug for Fingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.as_svg_id("fg"))
}
}
impl serde::Serialize for Fingerprint {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.as_svg_id(""))
}
}
impl<'de> serde::Deserialize<'de> for Fingerprint {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = <std::string::String as serde::Deserialize>::deserialize(deserializer)?;
Fingerprint::try_from_str(&s).map_err(serde::de::Error::custom)
}
}
impl Fingerprint {
/// Create a new fingerprint from the given pair of 64-bit integers.
pub fn from_pair(lo: u64, hi: u64) -> Self {
Self { lo, hi }
}
/// Create a new fingerprint from the given 128-bit integer.
pub const fn from_u128(hash: u128) -> Self {
// Self(hash as u64, (hash >> 64) as u64)
Self {
lo: hash as u64,
hi: (hash >> 64) as u64,
}
}
/// Get the fingerprint as a 128-bit integer.
pub fn to_u128(self) -> u128 {
((self.hi as u128) << 64) | self.lo as u128
}
/// Cut the fingerprint into a 32-bit integer.
/// It could be used as a hash value if the fingerprint is calculated from a
/// stable hash function.
pub fn lower32(self) -> u32 {
self.lo as u32
}
/// Creates a new `Fingerprint` from a svg id that **doesn't have prefix**.
pub fn try_from_str(s: &str) -> ZResult<Self> {
let bytes = base64::engine::general_purpose::STANDARD_NO_PAD
.decode(&s.as_bytes()[..11])
.expect("invalid base64 string");
let lo = u64::from_le_bytes(bytes.try_into().unwrap());
let mut bytes = base64::engine::general_purpose::STANDARD_NO_PAD
.decode(&s.as_bytes()[11..])
.expect("invalid base64 string");
bytes.resize(8, 0);
let hi = u64::from_le_bytes(bytes.try_into().unwrap());
Ok(Self::from_pair(lo, hi))
}
/// Create a xml id from the given prefix and the fingerprint of this
/// reference. Note that the entire html document shares namespace for
/// ids.
#[comemo::memoize]
pub fn as_svg_id(self, prefix: &'static str) -> String {
let fingerprint_lo =
base64::engine::general_purpose::STANDARD_NO_PAD.encode(self.lo.to_le_bytes());
if self.hi == 0 {
return [prefix, &fingerprint_lo].join("");
}
// possible the id in the lower 64 bits.
let fingerprint_hi = {
let id = self.hi.to_le_bytes();
// truncate zero
let rev_zero = id.iter().rev().skip_while(|&&b| b == 0).count();
let id = &id[..rev_zero];
base64::engine::general_purpose::STANDARD_NO_PAD.encode(id)
};
[prefix, &fingerprint_lo, &fingerprint_hi].join("")
}
}
/// A fingerprint hasher that extends the [`std::hash::Hasher`] trait.
pub trait FingerprintHasher: std::hash::Hasher {
/// Finish the fingerprint and return the fingerprint and the data.
/// The data is used to resolve the conflict.
fn finish_fingerprint(self) -> (Fingerprint, Vec<u8>);
}
/// A fingerprint hasher that uses the [`SipHasher13`] algorithm.
#[derive(Default)]
pub struct FingerprintSipHasher {
/// The underlying data passed to the hasher.
data: Vec<u8>,
}
pub type FingerprintSipHasherBase = SipHasher13;
impl FingerprintSipHasher {
pub fn fast_hash(&self) -> (u32, &Vec<u8>) {
let mut inner = FxHasher32::default();
self.data.hash(&mut inner);
(inner.finish() as u32, &self.data)
}
}
impl std::hash::Hasher for FingerprintSipHasher {
fn write(&mut self, bytes: &[u8]) {
self.data.extend_from_slice(bytes);
}
fn finish(&self) -> u64 {
let mut inner = FingerprintSipHasherBase::default();
self.data.hash(&mut inner);
inner.finish()
}
}
impl FingerprintHasher for FingerprintSipHasher {
fn finish_fingerprint(self) -> (Fingerprint, Vec<u8>) {
let buffer = self.data.clone();
let mut inner = FingerprintSipHasherBase::default();
buffer.hash(&mut inner);
let hash = inner.finish128();
(
Fingerprint {
lo: hash.h1,
hi: hash.h2,
},
buffer,
)
}
}
/// A fingerprint builder that produces unique fingerprint for each item.
/// It resolves the conflict by checking the underlying data.
/// See [`Fingerprint`] for more information.
#[derive(Default)]
pub struct FingerprintBuilder {
/// The fast conflict checker mapping fingerprints to their underlying data.
#[cfg(feature = "bi-hash")]
fast_conflict_checker: crate::adt::CHashMap<u32, Vec<u8>>,
/// The conflict checker mapping fingerprints to their underlying data.
conflict_checker: crate::adt::CHashMap<Fingerprint, Vec<u8>>,
}
#[cfg(not(feature = "bi-hash"))]
impl FingerprintBuilder {
pub fn resolve_unchecked<T: Hash>(&self, item: &T) -> Fingerprint {
let mut s = FingerprintSipHasher { data: Vec::new() };
item.hash(&mut s);
let (fingerprint, _featured_data) = s.finish_fingerprint();
fingerprint
}
pub fn resolve<T: Hash + 'static>(&self, item: &T) -> Fingerprint {
let mut s = FingerprintSipHasher { data: Vec::new() };
item.type_id().hash(&mut s);
item.hash(&mut s);
let (fingerprint, featured_data) = s.finish_fingerprint();
let Some(prev_featured_data) = self.conflict_checker.get(&fingerprint) else {
self.conflict_checker.insert(fingerprint, featured_data);
return fingerprint;
};
if *prev_featured_data == *featured_data {
return fingerprint;
}
// todo: soft error
panic!("Fingerprint conflict detected!");
}
}
#[cfg(feature = "bi-hash")]
impl FingerprintBuilder {
pub fn resolve_unchecked<T: Hash>(&self, item: &T) -> Fingerprint {
let mut s = FingerprintSipHasher { data: Vec::new() };
item.hash(&mut s);
let (fingerprint, featured_data) = s.fast_hash();
let Some(prev_featured_data) = self.fast_conflict_checker.get(&fingerprint) else {
self.fast_conflict_checker.insert(fingerprint, s.data);
return Fingerprint::from_pair(fingerprint as u64, 0);
};
if *prev_featured_data == *featured_data {
return Fingerprint::from_pair(fingerprint as u64, 0);
}
let (fingerprint, _featured_data) = s.finish_fingerprint();
fingerprint
}
pub fn resolve<T: Hash + 'static>(&self, item: &T) -> Fingerprint {
let mut s = FingerprintSipHasher { data: Vec::new() };
item.type_id().hash(&mut s);
item.hash(&mut s);
let (fingerprint, featured_data) = s.fast_hash();
let Some(prev_featured_data) = self.fast_conflict_checker.get(&fingerprint) else {
self.fast_conflict_checker.insert(fingerprint, s.data);
return Fingerprint::from_pair(fingerprint as u64, 0);
};
if *prev_featured_data == *featured_data {
return Fingerprint::from_pair(fingerprint as u64, 0);
}
let (fingerprint, featured_data) = s.finish_fingerprint();
let Some(prev_featured_data) = self.conflict_checker.get(&fingerprint) else {
self.conflict_checker.insert(fingerprint, featured_data);
return fingerprint;
};
if *prev_featured_data == *featured_data {
return fingerprint;
}
// todo: soft error
panic!("Fingerprint conflict detected!");
}
}
/// This function provides a hash function for items, which also includes a type
/// id as part of the hash. Note: This function is not stable across different
/// versions of typst-ts, so it is preferred to be always used in memory.
/// Currently, this function use [`SipHasher13`] as the underlying hash
/// algorithm.
pub fn item_hash128<T: Hash + 'static>(item: &T) -> u128 {
// Also hash the TypeId because the type might be converted
// through an unsized coercion.
let mut state = SipHasher13::new();
item.type_id().hash(&mut state);
item.hash(&mut state);
state.finish128().as_u128()
}
/// Calculate a 128-bit siphash of a value.
/// Currently, this function use [`SipHasher13`] as the underlying hash
/// algorithm.
#[inline]
pub fn hash128<T: std::hash::Hash>(value: &T) -> u128 {
let mut state = SipHasher13::new();
value.hash(&mut state);
state.finish128().as_u128()
}
/// A convenience function for when you need a quick 64-bit hash.
#[inline]
pub fn hash64<T: Hash + ?Sized>(v: &T) -> u64 {
let mut state = FxHasher::default();
v.hash(&mut state);
state.finish()
}
// todo: rustc hash doesn't have 32-bit hash
pub use fxhash::hash32;
#[test]
fn test_fingerprint() {
let t = Fingerprint::from_pair(0, 1);
assert_eq!(Fingerprint::try_from_str(&t.as_svg_id("")).unwrap(), t);
let t = Fingerprint::from_pair(1, 1);
assert_eq!(Fingerprint::try_from_str(&t.as_svg_id("")).unwrap(), t);
let t = Fingerprint::from_pair(1, 0);
assert_eq!(Fingerprint::try_from_str(&t.as_svg_id("")).unwrap(), t);
let t = Fingerprint::from_pair(0, 0);
assert_eq!(Fingerprint::try_from_str(&t.as_svg_id("")).unwrap(), t);
}

View file

@ -0,0 +1,27 @@
#![allow(missing_docs)]
pub mod adt;
pub mod debug_loc;
pub mod error;
pub mod hash;
pub mod path;
pub mod time;
pub(crate) mod concepts;
pub use concepts::*;
pub use error::{ErrKind, Error};
#[cfg(feature = "typst")]
pub use typst_shim;
#[cfg(feature = "rkyv")]
use rkyv::{Archive, Deserialize as rDeser, Serialize as rSer};
/// The local id of a svg item.
/// This id is only unique within the svg document.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
#[cfg_attr(feature = "rkyv", derive(Archive, rDeser, rSer))]
#[cfg_attr(feature = "rkyv-validation", archive(check_bytes))]
pub struct DefId(pub u64);

View file

@ -0,0 +1,229 @@
use std::path::{Component, Path};
pub use path_clean::PathClean;
/// Get the path cleaned as a unix-style string.
pub fn unix_slash(root: &Path) -> String {
let mut res = String::with_capacity(root.as_os_str().len());
let mut parent_norm = false;
for comp in root.components() {
match comp {
Component::Prefix(p) => {
res.push_str(&p.as_os_str().to_string_lossy());
parent_norm = false;
}
Component::RootDir => {
res.push('/');
parent_norm = false;
}
Component::CurDir => {
parent_norm = false;
}
Component::ParentDir => {
if parent_norm {
res.push('/');
}
res.push_str("..");
parent_norm = true;
}
Component::Normal(p) => {
if parent_norm {
res.push('/');
}
res.push_str(&p.to_string_lossy());
parent_norm = true;
}
}
}
if res.is_empty() {
res.push('.');
}
res
}
/// Get the path cleaned as a platform-style string.
pub use path_clean::clean;
#[cfg(test)]
mod test {
use std::path::{Path, PathBuf};
use super::{clean as inner_path_clean, unix_slash, PathClean};
pub fn clean<P: AsRef<Path>>(path: P) -> String {
unix_slash(&inner_path_clean(path))
}
#[test]
fn test_unix_slash() {
if cfg!(target_os = "windows") {
// windows group
assert_eq!(
unix_slash(std::path::Path::new("C:\\Users\\a\\b\\c")),
"C:/Users/a/b/c"
);
assert_eq!(
unix_slash(std::path::Path::new("C:\\Users\\a\\b\\c\\")),
"C:/Users/a/b/c"
);
assert_eq!(unix_slash(std::path::Path::new("a\\b\\c")), "a/b/c");
assert_eq!(unix_slash(std::path::Path::new("C:\\")), "C:/");
assert_eq!(unix_slash(std::path::Path::new("C:\\\\")), "C:/");
assert_eq!(unix_slash(std::path::Path::new("C:")), "C:");
assert_eq!(unix_slash(std::path::Path::new("C:\\a")), "C:/a");
assert_eq!(unix_slash(std::path::Path::new("C:\\a\\")), "C:/a");
assert_eq!(unix_slash(std::path::Path::new("C:\\a\\b")), "C:/a/b");
assert_eq!(
unix_slash(std::path::Path::new("C:\\Users\\a\\..\\b\\c")),
"C:/Users/a/../b/c"
);
assert_eq!(
unix_slash(std::path::Path::new("C:\\Users\\a\\..\\b\\c\\")),
"C:/Users/a/../b/c"
);
assert_eq!(
unix_slash(std::path::Path::new("C:\\Users\\a\\..\\..")),
"C:/Users/a/../.."
);
assert_eq!(
unix_slash(std::path::Path::new("C:\\Users\\a\\..\\..\\")),
"C:/Users/a/../.."
);
}
// unix group
assert_eq!(unix_slash(std::path::Path::new("/a/b/c")), "/a/b/c");
assert_eq!(unix_slash(std::path::Path::new("/a/b/c/")), "/a/b/c");
assert_eq!(unix_slash(std::path::Path::new("/")), "/");
assert_eq!(unix_slash(std::path::Path::new("//")), "/");
assert_eq!(unix_slash(std::path::Path::new("a")), "a");
assert_eq!(unix_slash(std::path::Path::new("a/")), "a");
assert_eq!(unix_slash(std::path::Path::new("a/b")), "a/b");
assert_eq!(unix_slash(std::path::Path::new("a/b/")), "a/b");
assert_eq!(unix_slash(std::path::Path::new("a/..")), "a/..");
assert_eq!(unix_slash(std::path::Path::new("a/../")), "a/..");
assert_eq!(unix_slash(std::path::Path::new("a/../..")), "a/../..");
assert_eq!(unix_slash(std::path::Path::new("a/../../")), "a/../..");
assert_eq!(unix_slash(std::path::Path::new("a/./b")), "a/b");
assert_eq!(unix_slash(std::path::Path::new("a/./b/")), "a/b");
assert_eq!(unix_slash(std::path::Path::new(".")), ".");
assert_eq!(unix_slash(std::path::Path::new("./")), ".");
assert_eq!(unix_slash(std::path::Path::new("./a")), "a");
assert_eq!(unix_slash(std::path::Path::new("./a/")), "a");
assert_eq!(unix_slash(std::path::Path::new("./a/b")), "a/b");
assert_eq!(unix_slash(std::path::Path::new("./a/b/")), "a/b");
assert_eq!(unix_slash(std::path::Path::new("./a/./b/")), "a/b");
}
#[test]
fn test_path_clean_empty_path_is_current_dir() {
assert_eq!(clean(""), ".");
}
#[test]
fn test_path_clean_clean_paths_dont_change() {
let tests = vec![(".", "."), ("..", ".."), ("/", "/")];
for test in tests {
assert_eq!(clean(test.0), test.1);
}
}
#[test]
fn test_path_clean_replace_multiple_slashes() {
let tests = vec![
("/", "/"),
("//", "/"),
("///", "/"),
(".//", "."),
("//..", "/"),
("..//", ".."),
("/..//", "/"),
("/.//./", "/"),
("././/./", "."),
("path//to///thing", "path/to/thing"),
("/path//to///thing", "/path/to/thing"),
];
for test in tests {
assert_eq!(clean(test.0), test.1);
}
}
#[test]
fn test_path_clean_eliminate_current_dir() {
let tests = vec![
("./", "."),
("/./", "/"),
("./test", "test"),
("./test/./path", "test/path"),
("/test/./path/", "/test/path"),
("test/path/.", "test/path"),
];
for test in tests {
assert_eq!(clean(test.0), test.1);
}
}
#[test]
fn test_path_clean_eliminate_parent_dir() {
let tests = vec![
("/..", "/"),
("/../test", "/test"),
("test/..", "."),
("test/path/..", "test"),
("test/../path", "path"),
("/test/../path", "/path"),
("test/path/../../", "."),
("test/path/../../..", ".."),
("/test/path/../../..", "/"),
("/test/path/../../../..", "/"),
("test/path/../../../..", "../.."),
("test/path/../../another/path", "another/path"),
("test/path/../../another/path/..", "another"),
("../test", "../test"),
("../test/", "../test"),
("../test/path", "../test/path"),
("../test/..", ".."),
];
for test in tests {
assert_eq!(clean(test.0), test.1);
}
}
#[test]
fn test_path_clean_pathbuf_trait() {
assert_eq!(
unix_slash(&PathBuf::from("/test/../path/").clean()),
"/path"
);
}
#[test]
fn test_path_clean_path_trait() {
assert_eq!(unix_slash(&Path::new("/test/../path/").clean()), "/path");
}
#[test]
#[cfg(target_os = "windows")]
fn test_path_clean_windows_paths() {
let tests = vec![
("\\..", "/"),
("\\..\\test", "/test"),
("test\\..", "."),
("test\\path\\..\\..\\..", ".."),
("test\\path/..\\../another\\path", "another/path"), // Mixed
("test\\path\\my/path", "test/path/my/path"), // Mixed 2
("/dir\\../otherDir/test.json", "/otherDir/test.json"), // User example
("c:\\test\\..", "c:/"), // issue #12
("c:/test/..", "c:/"), // issue #12
];
for test in tests {
assert_eq!(clean(test.0), test.1);
}
}
}

View file

@ -0,0 +1,23 @@
pub use std::time::SystemTime as Time;
pub use web_time::Duration;
pub use web_time::Instant;
/// Returns the current system time (UTC+0).
#[cfg(any(feature = "system", feature = "web"))]
pub fn now() -> Time {
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
{
Time::now()
}
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
{
use web_time::web::SystemTimeExt;
web_time::SystemTime::now().to_std()
}
}
/// Returns a dummy time on environments that do not support time.
#[cfg(not(any(feature = "system", feature = "web")))]
pub fn now() -> Time {
Time::UNIX_EPOCH
}

View file

@ -0,0 +1,32 @@
[package]
name = "tinymist-vfs"
description = "Vfs for tinymist."
authors.workspace = true
version.workspace = true
license.workspace = true
edition.workspace = true
homepage.workspace = true
repository.workspace = true
[dependencies]
typst.workspace = true
tinymist-std = { workspace = true, features = ["typst"] }
parking_lot.workspace = true
nohash-hasher.workspace = true
indexmap.workspace = true
log.workspace = true
rpds = "1"
wasm-bindgen = { workspace = true, optional = true }
web-sys = { workspace = true, optional = true, features = ["console"] }
js-sys = { workspace = true, optional = true }
[features]
web = ["wasm-bindgen", "web-sys", "js-sys", "tinymist-std/web"]
browser = ["web"]
system = ["tinymist-std/system"]
[lints]
workspace = true

View file

@ -0,0 +1,5 @@
# reflexo-vfs
Vfs for reflexo.
See [Typst.ts](https://github.com/Myriad-Dreamin/typst.ts)

View file

@ -0,0 +1,101 @@
use std::path::Path;
use tinymist_std::ImmutPath;
use typst::diag::{FileError, FileResult};
use wasm_bindgen::prelude::*;
use crate::{AccessModel, Bytes, Time};
/// Provides proxy access model from typst compiler to some JavaScript
/// implementation.
#[derive(Debug, Clone)]
pub struct ProxyAccessModel {
/// The `this` value when calling the JavaScript functions
pub context: JsValue,
/// The JavaScript function to get the mtime of a file
pub mtime_fn: js_sys::Function,
/// The JavaScript function to check if a path corresponds to a file or a
/// directory
pub is_file_fn: js_sys::Function,
/// The JavaScript function to get the real path of a file
pub real_path_fn: js_sys::Function,
/// The JavaScript function to get the content of a file
pub read_all_fn: js_sys::Function,
}
impl AccessModel for ProxyAccessModel {
fn mtime(&self, src: &Path) -> FileResult<Time> {
self.mtime_fn
.call1(&self.context, &src.to_string_lossy().as_ref().into())
.map(|v| {
let v = v.as_f64().unwrap();
Time::UNIX_EPOCH + std::time::Duration::from_secs_f64(v)
})
.map_err(|e| {
web_sys::console::error_3(
&"typst_ts::compiler::ProxyAccessModel::mtime failure".into(),
&src.to_string_lossy().as_ref().into(),
&e,
);
FileError::AccessDenied
})
}
fn is_file(&self, src: &Path) -> FileResult<bool> {
self.is_file_fn
.call1(&self.context, &src.to_string_lossy().as_ref().into())
.map(|v| v.as_bool().unwrap())
.map_err(|e| {
web_sys::console::error_3(
&"typst_ts::compiler::ProxyAccessModel::is_file failure".into(),
&src.to_string_lossy().as_ref().into(),
&e,
);
FileError::AccessDenied
})
}
fn real_path(&self, src: &Path) -> FileResult<ImmutPath> {
self.real_path_fn
.call1(&self.context, &src.to_string_lossy().as_ref().into())
.map(|v| Path::new(&v.as_string().unwrap()).into())
.map_err(|e| {
web_sys::console::error_3(
&"typst_ts::compiler::ProxyAccessModel::real_path failure".into(),
&src.to_string_lossy().as_ref().into(),
&e,
);
FileError::AccessDenied
})
}
fn content(&self, src: &Path) -> FileResult<Bytes> {
let data = self
.read_all_fn
.call1(&self.context, &src.to_string_lossy().as_ref().into())
.map_err(|e| {
web_sys::console::error_3(
&"typst_ts::compiler::ProxyAccessModel::read_all failure".into(),
&src.to_string_lossy().as_ref().into(),
&e,
);
FileError::AccessDenied
})?;
let data = if let Some(data) = data.dyn_ref::<js_sys::Uint8Array>() {
Bytes::from(data.to_vec())
} else {
return Err(FileError::AccessDenied);
};
Ok(data)
}
}
// todo
/// Safety: `ProxyAccessModel` is only used in the browser environment, and we
/// cannot share data between workers.
unsafe impl Send for ProxyAccessModel {}
/// Safety: `ProxyAccessModel` is only used in the browser environment, and we
/// cannot share data between workers.
unsafe impl Sync for ProxyAccessModel {}

View file

@ -0,0 +1,33 @@
use std::path::Path;
use tinymist_std::ImmutPath;
use typst::diag::{FileError, FileResult};
use super::AccessModel;
use crate::{Bytes, Time};
/// Provides dummy access model.
///
/// Note: we can still perform compilation with dummy access model, since
/// [`super::Vfs`] will make a overlay access model over the provided dummy
/// access model.
#[derive(Default, Debug, Clone, Copy)]
pub struct DummyAccessModel;
impl AccessModel for DummyAccessModel {
fn mtime(&self, _src: &Path) -> FileResult<Time> {
Ok(Time::UNIX_EPOCH)
}
fn is_file(&self, _src: &Path) -> FileResult<bool> {
Ok(true)
}
fn real_path(&self, src: &Path) -> FileResult<ImmutPath> {
Ok(src.into())
}
fn content(&self, _src: &Path) -> FileResult<Bytes> {
Err(FileError::AccessDenied)
}
}

View file

@ -0,0 +1,357 @@
//! upstream of following files <https://github.com/rust-lang/rust-analyzer/tree/master/crates/vfs>
//! ::path_interner.rs -> path_interner.rs
#![allow(missing_docs)]
/// Provides ProxyAccessModel that makes access to JavaScript objects for
/// browser compilation.
#[cfg(feature = "browser")]
pub mod browser;
/// Provides SystemAccessModel that makes access to the local file system for
/// system compilation.
#[cfg(feature = "system")]
pub mod system;
/// Provides dummy access model.
///
/// Note: we can still perform compilation with dummy access model, since
/// [`Vfs`] will make a overlay access model over the provided dummy access
/// model.
pub mod dummy;
/// Provides notify access model which retrieves file system events and changes
/// from some notify backend.
pub mod notify;
/// Provides overlay access model which allows to shadow the underlying access
/// model with memory contents.
pub mod overlay;
/// Provides trace access model which traces the underlying access model.
pub mod trace;
mod utils;
mod path_interner;
pub use typst::foundations::Bytes;
pub use typst::syntax::FileId as TypstFileId;
pub use tinymist_std::time::Time;
pub use tinymist_std::ImmutPath;
pub(crate) use path_interner::PathInterner;
use core::fmt;
use std::{
collections::HashMap,
hash::Hash,
path::Path,
sync::{
atomic::{AtomicU64, Ordering},
Arc,
},
};
use parking_lot::{Mutex, RwLock};
use tinymist_std::path::PathClean;
use typst::diag::{FileError, FileResult};
use self::{
notify::{FilesystemEvent, NotifyAccessModel},
overlay::OverlayAccessModel,
};
/// Handle to a file in [`Vfs`]
///
/// Most functions in typst-ts use this when they need to refer to a file.
#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub struct FileId(pub u32);
/// safe because `FileId` is a new type of `u32`
impl nohash_hasher::IsEnabled for FileId {}
/// A trait for accessing underlying file system.
///
/// This trait is simplified by [`Vfs`] and requires a minimal method set for
/// typst compilation.
pub trait AccessModel {
/// Clear the cache of the access model.
///
/// This is called when the vfs is reset. See [`Vfs`]'s reset method for
/// more information.
fn clear(&mut self) {}
/// Return a mtime corresponding to the path.
///
/// Note: vfs won't touch the file entry if mtime is same between vfs reset
/// lifecycles for performance design.
fn mtime(&self, src: &Path) -> FileResult<Time>;
/// Return whether a path is corresponding to a file.
fn is_file(&self, src: &Path) -> FileResult<bool>;
/// Return the real path before creating a vfs file entry.
///
/// Note: vfs will fetch the file entry once if multiple paths shares a same
/// real path.
fn real_path(&self, src: &Path) -> FileResult<ImmutPath>;
/// Return the content of a file entry.
fn content(&self, src: &Path) -> FileResult<Bytes>;
}
#[derive(Clone)]
pub struct SharedAccessModel<M> {
pub inner: Arc<RwLock<M>>,
}
impl<M> SharedAccessModel<M> {
pub fn new(inner: M) -> Self {
Self {
inner: Arc::new(RwLock::new(inner)),
}
}
}
impl<M> AccessModel for SharedAccessModel<M>
where
M: AccessModel,
{
fn clear(&mut self) {
self.inner.write().clear();
}
fn mtime(&self, src: &Path) -> FileResult<Time> {
self.inner.read().mtime(src)
}
fn is_file(&self, src: &Path) -> FileResult<bool> {
self.inner.read().is_file(src)
}
fn real_path(&self, src: &Path) -> FileResult<ImmutPath> {
self.inner.read().real_path(src)
}
fn content(&self, src: &Path) -> FileResult<Bytes> {
self.inner.read().content(src)
}
}
/// we add notify access model here since notify access model doesn't introduce
/// overheads by our observation
type VfsAccessModel<M> = OverlayAccessModel<NotifyAccessModel<SharedAccessModel<M>>>;
pub trait FsProvider {
/// Arbitrary one of file path corresponding to the given `id`.
fn file_path(&self, id: FileId) -> ImmutPath;
fn mtime(&self, id: FileId) -> FileResult<Time>;
fn read(&self, id: FileId) -> FileResult<Bytes>;
fn is_file(&self, id: FileId) -> FileResult<bool>;
}
#[derive(Default)]
struct PathMapper {
/// The number of lifecycles since the creation of the `Vfs`.
///
/// Note: The lifetime counter is incremented on resetting vfs.
clock: AtomicU64,
/// Map from path to slot index.
///
/// Note: we use a owned [`FileId`] here, which is resultant from
/// [`PathInterner`]
id_cache: RwLock<HashMap<ImmutPath, FileId>>,
/// The path interner for canonical paths.
intern: Mutex<PathInterner<ImmutPath, u64>>,
}
impl PathMapper {
/// Reset the path references.
///
/// It performs a rolling reset, with discard some cache file entry when it
/// is unused in recent 30 lifecycles.
///
/// Note: The lifetime counter is incremented every time this function is
/// called.
pub fn reset(&self) {
self.clock.fetch_add(1, Ordering::SeqCst);
// todo: clean path interner.
// let new_lifetime_cnt = self.lifetime_cnt;
// self.path2slot.get_mut().clear();
// self.path_interner
// .get_mut()
// .retain(|_, lifetime| new_lifetime_cnt - *lifetime <= 30);
}
/// Id of the given path if it exists in the `Vfs` and is not deleted.
pub fn file_id(&self, path: &Path) -> FileId {
let quick_id = self.id_cache.read().get(path).copied();
if let Some(id) = quick_id {
return id;
}
let path: ImmutPath = path.clean().as_path().into();
let mut path_interner = self.intern.lock();
let lifetime_cnt = self.clock.load(Ordering::SeqCst);
let id = path_interner.intern(path.clone(), lifetime_cnt).0;
let mut path2slot = self.id_cache.write();
path2slot.insert(path.clone(), id);
id
}
/// File path corresponding to the given `file_id`.
pub fn file_path(&self, file_id: FileId) -> ImmutPath {
let path_interner = self.intern.lock();
path_interner.lookup(file_id).clone()
}
}
/// Create a new `Vfs` harnessing over the given `access_model` specific for
/// `reflexo_world::CompilerWorld`. With vfs, we can minimize the
/// implementation overhead for [`AccessModel`] trait.
pub struct Vfs<M: AccessModel + Sized> {
paths: Arc<PathMapper>,
// access_model: TraceAccessModel<VfsAccessModel<M>>,
/// The wrapped access model.
access_model: VfsAccessModel<M>,
}
impl<M: AccessModel + Sized> fmt::Debug for Vfs<M> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Vfs").finish()
}
}
impl<M: AccessModel + Clone + Sized> Vfs<M> {
pub fn snapshot(&self) -> Self {
Self {
paths: self.paths.clone(),
access_model: self.access_model.clone(),
}
}
}
impl<M: AccessModel + Sized> Vfs<M> {
/// Create a new `Vfs` with a given `access_model`.
///
/// Retrieving an [`AccessModel`], it will further wrap the access model
/// with [`OverlayAccessModel`] and [`NotifyAccessModel`]. This means that
/// you don't need to implement:
/// + overlay: allowing to shadow the underlying access model with memory
/// contents, which is useful for a limited execution environment and
/// instrumenting or overriding source files or packages.
/// + notify: regards problems of synchronizing with the file system when
/// the vfs is watching the file system.
///
/// See [`AccessModel`] for more information.
pub fn new(access_model: M) -> Self {
let access_model = SharedAccessModel::new(access_model);
let access_model = NotifyAccessModel::new(access_model);
let access_model = OverlayAccessModel::new(access_model);
// If you want to trace the access model, uncomment the following line
// let access_model = TraceAccessModel::new(access_model);
Self {
paths: Default::default(),
access_model,
}
}
/// Reset the source file and path references.
///
/// It performs a rolling reset, with discard some cache file entry when it
/// is unused in recent 30 lifecycles.
///
/// Note: The lifetime counter is incremented every time this function is
/// called.
pub fn reset(&mut self) {
self.paths.reset();
self.access_model.clear();
}
/// Reset the shadowing files in [`OverlayAccessModel`].
///
/// Note: This function is independent from [`Vfs::reset`].
pub fn reset_shadow(&mut self) {
self.access_model.clear_shadow();
}
/// Get paths to all the shadowing files in [`OverlayAccessModel`].
pub fn shadow_paths(&self) -> Vec<Arc<Path>> {
self.access_model.file_paths()
}
/// Add a shadowing file to the [`OverlayAccessModel`].
pub fn map_shadow(&mut self, path: &Path, content: Bytes) -> FileResult<()> {
self.access_model.add_file(path.into(), content);
Ok(())
}
/// Remove a shadowing file from the [`OverlayAccessModel`].
pub fn remove_shadow(&mut self, path: &Path) {
self.access_model.remove_file(path);
}
/// Let the vfs notify the access model with a filesystem event.
///
/// See [`NotifyAccessModel`] for more information.
pub fn notify_fs_event(&mut self, event: FilesystemEvent) {
self.access_model.inner.notify(event);
}
/// Returns the overall memory usage for the stored files.
pub fn memory_usage(&self) -> usize {
0
}
/// Id of the given path if it exists in the `Vfs` and is not deleted.
pub fn file_id(&self, path: &Path) -> FileId {
self.paths.file_id(path)
}
/// Read a file.
pub fn read(&self, path: &Path) -> FileResult<Bytes> {
if self.access_model.is_file(path)? {
self.access_model.content(path)
} else {
Err(FileError::IsDirectory)
}
}
}
impl<M: AccessModel> FsProvider for Vfs<M> {
fn file_path(&self, id: FileId) -> ImmutPath {
self.paths.file_path(id)
}
fn mtime(&self, src: FileId) -> FileResult<Time> {
self.access_model.mtime(&self.file_path(src))
}
fn read(&self, src: FileId) -> FileResult<Bytes> {
self.access_model.content(&self.file_path(src))
}
fn is_file(&self, src: FileId) -> FileResult<bool> {
self.access_model.is_file(&self.file_path(src))
}
}
#[cfg(test)]
mod tests {
fn is_send<T: Send>() {}
fn is_sync<T: Sync>() {}
#[test]
fn test_vfs_send_sync() {
is_send::<super::Vfs<super::dummy::DummyAccessModel>>();
is_sync::<super::Vfs<super::dummy::DummyAccessModel>>();
}
}

View file

@ -0,0 +1,279 @@
use core::fmt;
use std::path::Path;
use rpds::RedBlackTreeMapSync;
use typst::diag::{FileError, FileResult};
use crate::{AccessModel, Bytes, ImmutPath};
/// internal representation of [`NotifyFile`]
#[derive(Debug, Clone)]
struct NotifyFileRepr {
mtime: crate::Time,
content: Bytes,
}
/// A file snapshot that is notified by some external source
///
/// Note: The error is boxed to avoid large stack size
#[derive(Clone)]
pub struct FileSnapshot(Result<NotifyFileRepr, Box<FileError>>);
impl fmt::Debug for FileSnapshot {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0.as_ref() {
Ok(v) => f
.debug_struct("FileSnapshot")
.field("mtime", &v.mtime)
.field(
"content",
&FileContent {
len: v.content.len(),
},
)
.finish(),
Err(e) => f.debug_struct("FileSnapshot").field("error", &e).finish(),
}
}
}
impl FileSnapshot {
/// Access the internal data of the file snapshot
#[inline]
#[track_caller]
fn retrieve<'a, T>(&'a self, f: impl FnOnce(&'a NotifyFileRepr) -> T) -> FileResult<T> {
self.0.as_ref().map(f).map_err(|e| *e.clone())
}
/// mtime of the file
pub fn mtime(&self) -> FileResult<&crate::Time> {
self.retrieve(|e| &e.mtime)
}
/// content of the file
pub fn content(&self) -> FileResult<&Bytes> {
self.retrieve(|e| &e.content)
}
/// Whether the related file is a file
pub fn is_file(&self) -> FileResult<bool> {
self.retrieve(|_| true)
}
}
/// Convenient function to create a [`FileSnapshot`] from tuple
impl From<FileResult<(crate::Time, Bytes)>> for FileSnapshot {
fn from(result: FileResult<(crate::Time, Bytes)>) -> Self {
Self(
result
.map(|(mtime, content)| NotifyFileRepr { mtime, content })
.map_err(Box::new),
)
}
}
/// A set of changes to the filesystem.
///
/// The correct order of applying changes is:
/// 1. Remove files
/// 2. Upsert (Insert or Update) files
#[derive(Debug, Clone, Default)]
pub struct FileChangeSet {
/// Files to remove
pub removes: Vec<ImmutPath>,
/// Files to insert or update
pub inserts: Vec<(ImmutPath, FileSnapshot)>,
}
impl FileChangeSet {
/// Create a new empty changeset
pub fn is_empty(&self) -> bool {
self.inserts.is_empty() && self.removes.is_empty()
}
/// Create a new changeset with removing files
pub fn new_removes(removes: Vec<ImmutPath>) -> Self {
Self {
removes,
inserts: vec![],
}
}
/// Create a new changeset with inserting files
pub fn new_inserts(inserts: Vec<(ImmutPath, FileSnapshot)>) -> Self {
Self {
removes: vec![],
inserts,
}
}
/// Utility function to insert a possible file to insert or update
pub fn may_insert(&mut self, v: Option<(ImmutPath, FileSnapshot)>) {
if let Some(v) = v {
self.inserts.push(v);
}
}
/// Utility function to insert multiple possible files to insert or update
pub fn may_extend(&mut self, v: Option<impl Iterator<Item = (ImmutPath, FileSnapshot)>>) {
if let Some(v) = v {
self.inserts.extend(v);
}
}
}
/// A memory event that is notified by some external source
#[derive(Debug)]
pub enum MemoryEvent {
/// Reset all dependencies and update according to the given changeset
///
/// We have not provided a way to reset all dependencies without updating
/// yet, but you can create a memory event with empty changeset to achieve
/// this:
///
/// ```
/// use tinymist_vfs::notify::{MemoryEvent, FileChangeSet};
/// let event = MemoryEvent::Sync(FileChangeSet::default());
/// ```
Sync(FileChangeSet),
/// Update according to the given changeset
Update(FileChangeSet),
}
/// A upstream update event that is notified by some external source.
///
/// This event is used to notify some file watcher to invalidate some files
/// before applying upstream changes. This is very important to make some atomic
/// changes.
#[derive(Debug)]
pub struct UpstreamUpdateEvent {
/// Associated files that the event causes to invalidate
pub invalidates: Vec<ImmutPath>,
/// Opaque data that is passed to the file watcher
pub opaque: Box<dyn std::any::Any + Send>,
}
/// Aggregated filesystem events from some file watcher
#[derive(Debug)]
pub enum FilesystemEvent {
/// Update file system files according to the given changeset
Update(FileChangeSet),
/// See [`UpstreamUpdateEvent`]
UpstreamUpdate {
/// New changeset produced by invalidation
changeset: FileChangeSet,
/// The upstream event that causes the invalidation
upstream_event: Option<UpstreamUpdateEvent>,
},
}
/// A message that is sent to some file watcher
#[derive(Debug)]
pub enum NotifyMessage {
/// Oettle the watching
Settle,
/// Overrides all dependencies
SyncDependency(Vec<ImmutPath>),
/// upstream invalidation This is very important to make some atomic changes
///
/// Example:
/// ```plain
/// /// Receive memory event
/// let event: MemoryEvent = retrieve();
/// let invalidates = event.invalidates();
///
/// /// Send memory change event to [`NotifyActor`]
/// let event = Box::new(event);
/// self.send(NotifyMessage::UpstreamUpdate{ invalidates, opaque: event });
///
/// /// Wait for [`NotifyActor`] to finish
/// let fs_event = self.fs_notify.block_receive();
/// let event: MemoryEvent = fs_event.opaque.downcast().unwrap();
///
/// /// Apply changes
/// self.lock();
/// update_memory(event);
/// apply_fs_changes(fs_event.changeset);
/// self.unlock();
/// ```
UpstreamUpdate(UpstreamUpdateEvent),
}
/// Provides notify access model which retrieves file system events and changes
/// from some notify backend.
///
/// It simply hold notified filesystem data in memory, but still have a fallback
/// access model, whose the typical underlying access model is
/// [`crate::system::SystemAccessModel`]
#[derive(Debug, Clone)]
pub struct NotifyAccessModel<M> {
files: RedBlackTreeMapSync<ImmutPath, FileSnapshot>,
/// The fallback access model when the file is not notified ever.
pub inner: M,
}
impl<M: AccessModel> NotifyAccessModel<M> {
/// Create a new notify access model
pub fn new(inner: M) -> Self {
Self {
files: RedBlackTreeMapSync::default(),
inner,
}
}
/// Notify the access model with a filesystem event
pub fn notify(&mut self, event: FilesystemEvent) {
match event {
FilesystemEvent::UpstreamUpdate { changeset, .. }
| FilesystemEvent::Update(changeset) => {
for path in changeset.removes {
self.files.remove_mut(&path);
}
for (path, contents) in changeset.inserts {
self.files.insert_mut(path, contents);
}
}
}
}
}
impl<M: AccessModel> AccessModel for NotifyAccessModel<M> {
fn mtime(&self, src: &Path) -> FileResult<crate::Time> {
if let Some(entry) = self.files.get(src) {
return entry.mtime().cloned();
}
self.inner.mtime(src)
}
fn is_file(&self, src: &Path) -> FileResult<bool> {
if let Some(entry) = self.files.get(src) {
return entry.is_file();
}
self.inner.is_file(src)
}
fn real_path(&self, src: &Path) -> FileResult<ImmutPath> {
if self.files.contains_key(src) {
return Ok(src.into());
}
self.inner.real_path(src)
}
fn content(&self, src: &Path) -> FileResult<Bytes> {
if let Some(entry) = self.files.get(src) {
return entry.content().cloned();
}
self.inner.content(src)
}
}
#[derive(Debug)]
#[allow(dead_code)]
struct FileContent {
len: usize,
}

View file

@ -0,0 +1,121 @@
use std::path::Path;
use std::sync::Arc;
use rpds::RedBlackTreeMapSync;
use tinymist_std::ImmutPath;
use typst::diag::FileResult;
use crate::{AccessModel, Bytes, Time};
#[derive(Debug, Clone)]
struct OverlayFileMeta {
mt: Time,
content: Bytes,
}
/// Provides overlay access model which allows to shadow the underlying access
/// model with memory contents.
#[derive(Default, Debug, Clone)]
pub struct OverlayAccessModel<M> {
files: RedBlackTreeMapSync<Arc<Path>, OverlayFileMeta>,
/// The underlying access model
pub inner: M,
}
impl<M: AccessModel> OverlayAccessModel<M> {
/// Create a new [`OverlayAccessModel`] with the given inner access model
pub fn new(inner: M) -> Self {
Self {
files: RedBlackTreeMapSync::default(),
inner,
}
}
/// Get the inner access model
pub fn inner(&self) -> &M {
&self.inner
}
/// Get the mutable reference to the inner access model
pub fn inner_mut(&mut self) -> &mut M {
&mut self.inner
}
/// Clear the shadowed files
pub fn clear_shadow(&mut self) {
self.files = RedBlackTreeMapSync::default();
}
/// Get the shadowed file paths
pub fn file_paths(&self) -> Vec<Arc<Path>> {
self.files.keys().cloned().collect()
}
/// Add a shadow file to the [`OverlayAccessModel`]
pub fn add_file(&mut self, path: Arc<Path>, content: Bytes) {
// we change mt every time, since content almost changes every time
// Note: we can still benefit from cache, since we incrementally parse source
let mt = tinymist_std::time::now();
let meta = OverlayFileMeta { mt, content };
match self.files.get_mut(&path) {
Some(e) => {
if e.mt == meta.mt && e.content != meta.content {
e.mt = meta
.mt
// [`crate::Time`] has a minimum resolution of 1ms
// we negate the time by 1ms so that the time is always
// invalidated
.checked_sub(std::time::Duration::from_millis(1))
.unwrap();
e.content = meta.content.clone();
} else {
*e = meta.clone();
}
}
None => {
self.files.insert_mut(path, meta);
}
}
}
/// Remove a shadow file from the [`OverlayAccessModel`]
pub fn remove_file(&mut self, path: &Path) {
self.files.remove_mut(path);
}
}
impl<M: AccessModel> AccessModel for OverlayAccessModel<M> {
fn mtime(&self, src: &Path) -> FileResult<Time> {
if let Some(meta) = self.files.get(src) {
return Ok(meta.mt);
}
self.inner.mtime(src)
}
fn is_file(&self, src: &Path) -> FileResult<bool> {
if self.files.get(src).is_some() {
return Ok(true);
}
self.inner.is_file(src)
}
fn real_path(&self, src: &Path) -> FileResult<ImmutPath> {
if self.files.get(src).is_some() {
return Ok(src.into());
}
self.inner.real_path(src)
}
fn content(&self, src: &Path) -> FileResult<Bytes> {
if let Some(meta) = self.files.get(src) {
return Ok(meta.content.clone());
}
self.inner.content(src)
}
}

View file

@ -0,0 +1,71 @@
//! Maps paths to compact integer ids. We don't care about clearings paths which
//! no longer exist -- the assumption is total size of paths we ever look at is
//! not too big.
use std::hash::{BuildHasherDefault, Hash};
use indexmap::IndexMap;
use tinymist_std::hash::FxHasher;
use super::FileId;
/// Structure to map between [`VfsPath`] and [`FileId`].
#[derive(Debug)]
pub(crate) struct PathInterner<P, Ext = ()> {
map: IndexMap<P, Ext, BuildHasherDefault<FxHasher>>,
}
impl<P, Ext> Default for PathInterner<P, Ext> {
fn default() -> Self {
Self {
map: IndexMap::default(),
}
}
}
impl<P: Hash + Eq, Ext> PathInterner<P, Ext> {
/// Scan through each value in the set and keep those where the
/// closure `keep` returns `true`.
///
/// The elements are visited in order, and remaining elements keep their
/// order.
///
/// Computes in **O(n)** time (average).
#[allow(dead_code)]
pub fn retain(&mut self, keep: impl FnMut(&P, &mut Ext) -> bool) {
self.map.retain(keep)
}
/// Insert `path` in `self`.
///
/// - If `path` already exists in `self`, returns its associated id;
/// - Else, returns a newly allocated id.
#[inline]
pub(crate) fn intern(&mut self, path: P, ext: Ext) -> (FileId, Option<&mut Ext>) {
let (id, _) = self.map.insert_full(path, ext);
assert!(id < u32::MAX as usize);
(FileId(id as u32), None)
}
/// Returns the path corresponding to `id`.
///
/// # Panics
///
/// Panics if `id` does not exists in `self`.
#[allow(dead_code)]
pub(crate) fn lookup(&self, id: FileId) -> &P {
self.map.get_index(id.0 as usize).unwrap().0
}
}
#[cfg(test)]
mod tests {
use super::PathInterner;
use std::path::PathBuf;
#[test]
fn test_interner_path_buf() {
let mut interner = PathInterner::<PathBuf>::default();
let (id, ..) = interner.intern(PathBuf::from("foo"), ());
assert_eq!(interner.lookup(id), &PathBuf::from("foo"));
}
}

View file

@ -0,0 +1,83 @@
use std::{fs::File, io::Read, path::Path};
use typst::diag::{FileError, FileResult};
use crate::{AccessModel, Bytes, Time};
use tinymist_std::{ImmutPath, ReadAllOnce};
/// Provides SystemAccessModel that makes access to the local file system for
/// system compilation.
#[derive(Debug, Clone, Copy)]
pub struct SystemAccessModel;
impl SystemAccessModel {
fn stat(&self, src: &Path) -> std::io::Result<SystemFileMeta> {
let meta = std::fs::metadata(src)?;
Ok(SystemFileMeta {
mt: meta.modified()?,
is_file: meta.is_file(),
})
}
}
impl AccessModel for SystemAccessModel {
fn mtime(&self, src: &Path) -> FileResult<Time> {
let f = |e| FileError::from_io(e, src);
Ok(self.stat(src).map_err(f)?.mt)
}
fn is_file(&self, src: &Path) -> FileResult<bool> {
let f = |e| FileError::from_io(e, src);
Ok(self.stat(src).map_err(f)?.is_file)
}
fn real_path(&self, src: &Path) -> FileResult<ImmutPath> {
Ok(src.into())
}
fn content(&self, src: &Path) -> FileResult<Bytes> {
let f = |e| FileError::from_io(e, src);
let mut buf = Vec::<u8>::new();
std::fs::File::open(src)
.map_err(f)?
.read_to_end(&mut buf)
.map_err(f)?;
Ok(buf.into())
}
}
/// Lazily opened file entry corresponding to a file in the local file system.
///
/// This is used by font loading instead of the [`SystemAccessModel`].
#[derive(Debug)]
pub struct LazyFile {
path: std::path::PathBuf,
file: Option<std::io::Result<File>>,
}
impl LazyFile {
/// Create a new [`LazyFile`] with the given path.
pub fn new(path: std::path::PathBuf) -> Self {
Self { path, file: None }
}
}
impl ReadAllOnce for LazyFile {
fn read_all(mut self, buf: &mut Vec<u8>) -> std::io::Result<usize> {
let file = self.file.get_or_insert_with(|| File::open(&self.path));
let Ok(ref mut file) = file else {
let err = file.as_ref().unwrap_err();
// todo: clone error or hide error
return Err(std::io::Error::new(err.kind(), err.to_string()));
};
file.read_to_end(buf)
}
}
/// Meta data of a file in the local file system.
#[derive(Debug, Clone, Copy)]
pub struct SystemFileMeta {
mt: std::time::SystemTime,
is_file: bool,
}

View file

@ -0,0 +1,81 @@
use std::{path::Path, sync::atomic::AtomicU64};
use tinymist_std::ImmutPath;
use typst::diag::FileResult;
use crate::{AccessModel, Bytes};
/// Provides trace access model which traces the underlying access model.
///
/// It simply wraps the underlying access model and prints all the access to the
/// stdout or the browser console.
#[derive(Debug)]
pub struct TraceAccessModel<M: AccessModel + Sized> {
pub inner: M,
trace: [AtomicU64; 6],
}
impl<M: AccessModel + Sized> TraceAccessModel<M> {
/// Create a new [`TraceAccessModel`] with the given inner access model
pub fn new(inner: M) -> Self {
Self {
inner,
trace: Default::default(),
}
}
}
impl<M: AccessModel + Sized> AccessModel for TraceAccessModel<M> {
fn clear(&mut self) {
self.inner.clear();
}
fn mtime(&self, src: &Path) -> FileResult<crate::Time> {
let instant = tinymist_std::time::Instant::now();
let res = self.inner.mtime(src);
let elapsed = instant.elapsed();
// self.trace[0] += elapsed.as_nanos() as u64;
self.trace[0].fetch_add(
elapsed.as_nanos() as u64,
std::sync::atomic::Ordering::Relaxed,
);
crate::utils::console_log!("mtime: {:?} {:?} => {:?}", src, elapsed, res);
res
}
fn is_file(&self, src: &Path) -> FileResult<bool> {
let instant = tinymist_std::time::Instant::now();
let res = self.inner.is_file(src);
let elapsed = instant.elapsed();
self.trace[1].fetch_add(
elapsed.as_nanos() as u64,
std::sync::atomic::Ordering::Relaxed,
);
crate::utils::console_log!("is_file: {:?} {:?}", src, elapsed);
res
}
fn real_path(&self, src: &Path) -> FileResult<ImmutPath> {
let instant = tinymist_std::time::Instant::now();
let res = self.inner.real_path(src);
let elapsed = instant.elapsed();
self.trace[2].fetch_add(
elapsed.as_nanos() as u64,
std::sync::atomic::Ordering::Relaxed,
);
crate::utils::console_log!("real_path: {:?} {:?}", src, elapsed);
res
}
fn content(&self, src: &Path) -> FileResult<Bytes> {
let instant = tinymist_std::time::Instant::now();
let res = self.inner.content(src);
let elapsed = instant.elapsed();
self.trace[3].fetch_add(
elapsed.as_nanos() as u64,
std::sync::atomic::Ordering::Relaxed,
);
crate::utils::console_log!("read_all: {:?} {:?}", src, elapsed);
res
}
}

View file

@ -0,0 +1,23 @@
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
#[allow(unused_macros)]
macro_rules! console_log {
($($arg:tt)*) => {
#[cfg(feature = "web")]
web_sys::console::info_1(&format!(
$($arg)*
).into());
}
}
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
#[allow(unused_macros)]
macro_rules! console_log {
($($arg:tt)*) => {
eprintln!(
$($arg)*
);
}
}
#[allow(unused_imports)]
pub(crate) use console_log;

View file

@ -1,6 +1,6 @@
[package] [package]
name = "tinymist-world" name = "tinymist-world"
description = "World implementation of typst for tinymist." description = "Typst's World implementation for tinymist."
categories = ["compilers"] categories = ["compilers"]
keywords = ["language", "typst"] keywords = ["language", "typst"]
authors.workspace = true authors.workspace = true
@ -12,11 +12,18 @@ repository.workspace = true
rust-version.workspace = true rust-version.workspace = true
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
chrono.workspace = true chrono.workspace = true
clap.workspace = true clap.workspace = true
codespan-reporting.workspace = true
comemo.workspace = true comemo.workspace = true
dirs.workspace = true dirs = { workspace = true, optional = true }
ecow.workspace = true
flate2.workspace = true
fontdb = { workspace = true, optional = true }
hex.workspace = true
js-sys = { workspace = true, optional = true }
log.workspace = true log.workspace = true
parking_lot.workspace = true parking_lot.workspace = true
pathdiff.workspace = true pathdiff.workspace = true
@ -27,15 +34,57 @@ reflexo-typst-shim = { workspace = true, features = ["nightly"] }
semver.workspace = true semver.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
serde_with.workspace = true
serde-wasm-bindgen = { workspace = true, optional = true }
sha2.workspace = true
strum.workspace = true
tar.workspace = true
tinymist-fs.workspace = true tinymist-fs.workspace = true
tinymist-std.workspace = true
tinymist-vfs.workspace = true
typst.workspace = true typst.workspace = true
toml.workspace = true toml.workspace = true
typst-assets.workspace = true typst-assets.workspace = true
wasm-bindgen = { workspace = true, optional = true }
web-sys = { workspace = true, optional = true, features = ["console"] }
reqwest.workspace = true
[features] [features]
fonts = ["typst-assets/fonts"]
no-content-hint = ["reflexo-typst/no-content-hint"] default = []
lazy-fontdb = []
browser-embedded-fonts = ["fonts"]
web = [
"wasm-bindgen",
"web-sys",
"js-sys",
"serde-wasm-bindgen",
"tinymist-std/web",
"tinymist-vfs/web",
]
browser = ["web"]
system = [
"dep:dirs",
"dep:fontdb",
"tinymist-std/system",
"tinymist-vfs/system",
]
# todo: remove me
fonts = []
no-content-hint = []
[lints] [lints]
workspace = true workspace = true
# ====
# [dependencies]
# [features]
# fonts = ["typst-assets/fonts"]
# no-content-hint = ["reflexo-typst/no-content-hint"]
# [lints]
# workspace = true

View file

@ -0,0 +1,5 @@
# reflexo-world
Typst's World implementation for reflexo.
See [Typst.ts](https://github.com/Myriad-Dreamin/typst.ts)

View file

@ -0,0 +1,113 @@
use std::path::PathBuf;
use chrono::{DateTime, Utc};
use clap::{builder::ValueParser, ArgAction, Parser};
use serde::{Deserialize, Serialize};
const ENV_PATH_SEP: char = if cfg!(windows) { ';' } else { ':' };
/// The font arguments for the compiler.
#[derive(Debug, Clone, Default, Parser, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompileFontArgs {
/// Font paths
#[clap(
long = "font-path",
value_name = "DIR",
action = clap::ArgAction::Append,
env = "TYPST_FONT_PATHS",
value_delimiter = ENV_PATH_SEP
)]
pub font_paths: Vec<PathBuf>,
/// Ensures system fonts won't be searched, unless explicitly included via
/// `--font-path`
#[clap(long, default_value = "false")]
pub ignore_system_fonts: bool,
}
/// Arguments related to where packages are stored in the system.
#[derive(Debug, Clone, Parser, Default, PartialEq, Eq)]
pub struct CompilePackageArgs {
/// Custom path to local packages, defaults to system-dependent location
#[clap(long = "package-path", env = "TYPST_PACKAGE_PATH", value_name = "DIR")]
pub package_path: Option<PathBuf>,
/// Custom path to package cache, defaults to system-dependent location
#[clap(
long = "package-cache-path",
env = "TYPST_PACKAGE_CACHE_PATH",
value_name = "DIR"
)]
pub package_cache_path: Option<PathBuf>,
}
/// Common arguments of compile, watch, and query.
#[derive(Debug, Clone, Parser, Default)]
pub struct CompileOnceArgs {
/// Path to input Typst file
#[clap(value_name = "INPUT")]
pub input: Option<String>,
/// Configures the project root (for absolute paths)
#[clap(long = "root", value_name = "DIR")]
pub root: Option<PathBuf>,
/// Add a string key-value pair visible through `sys.inputs`
#[clap(
long = "input",
value_name = "key=value",
action = ArgAction::Append,
value_parser = ValueParser::new(parse_input_pair),
)]
pub inputs: Vec<(String, String)>,
/// Font related arguments.
#[clap(flatten)]
pub font: CompileFontArgs,
/// Package related arguments.
#[clap(flatten)]
pub package: CompilePackageArgs,
/// The document's creation date formatted as a UNIX timestamp.
///
/// For more information, see <https://reproducible-builds.org/specs/source-date-epoch/>.
#[clap(
long = "creation-timestamp",
env = "SOURCE_DATE_EPOCH",
value_name = "UNIX_TIMESTAMP",
value_parser = parse_source_date_epoch,
hide(true),
)]
pub creation_timestamp: Option<DateTime<Utc>>,
/// Path to CA certificate file for network access, especially for
/// downloading typst packages.
#[clap(long = "cert", env = "TYPST_CERT", value_name = "CERT_PATH")]
pub cert: Option<PathBuf>,
}
/// Parses key/value pairs split by the first equal sign.
///
/// This function will return an error if the argument contains no equals sign
/// or contains the key (before the equals sign) is empty.
fn parse_input_pair(raw: &str) -> Result<(String, String), String> {
let (key, val) = raw
.split_once('=')
.ok_or("input must be a key and a value separated by an equal sign")?;
let key = key.trim().to_owned();
if key.is_empty() {
return Err("the key was missing or empty".to_owned());
}
let val = val.trim().to_owned();
Ok((key, val))
}
/// Parses a UNIX timestamp according to <https://reproducible-builds.org/specs/source-date-epoch/>
pub fn parse_source_date_epoch(raw: &str) -> Result<DateTime<Utc>, String> {
let timestamp: i64 = raw
.parse()
.map_err(|err| format!("timestamp must be decimal integer ({err})"))?;
DateTime::from_timestamp(timestamp, 0).ok_or_else(|| "timestamp out of range".to_string())
}

View file

@ -0,0 +1,52 @@
use std::{path::PathBuf, sync::Arc};
use tinymist_vfs::browser::ProxyAccessModel;
use typst::foundations::Dict as TypstDict;
use typst::utils::LazyHash;
use crate::entry::EntryState;
use crate::font::FontResolverImpl;
use crate::package::browser::ProxyRegistry;
/// A world that provides access to the browser.
/// It is under development.
pub type TypstBrowserUniverse = crate::world::CompilerUniverse<BrowserCompilerFeat>;
pub type TypstBrowserWorld = crate::world::CompilerWorld<BrowserCompilerFeat>;
#[derive(Debug, Clone, Copy)]
pub struct BrowserCompilerFeat;
impl crate::CompilerFeat for BrowserCompilerFeat {
/// Uses [`FontResolverImpl`] directly.
type FontResolver = FontResolverImpl;
type AccessModel = ProxyAccessModel;
type Registry = ProxyRegistry;
}
// todo
/// Safety: `ProxyRegistry` is only used in the browser environment, and we
/// cannot share data between workers.
unsafe impl Send for ProxyRegistry {}
/// Safety: `ProxyRegistry` is only used in the browser environment, and we
/// cannot share data between workers.
unsafe impl Sync for ProxyRegistry {}
impl TypstBrowserUniverse {
pub fn new(
root_dir: PathBuf,
inputs: Option<Arc<LazyHash<TypstDict>>>,
access_model: ProxyAccessModel,
registry: ProxyRegistry,
font_resolver: FontResolverImpl,
) -> Self {
let vfs = tinymist_vfs::Vfs::new(access_model);
Self::new_raw(
EntryState::new_rooted(root_dir.into(), None),
inputs,
vfs,
registry,
Arc::new(font_resolver),
)
}
}

View file

@ -0,0 +1,68 @@
use std::borrow::Cow;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use tinymist_std::AsCowBytes;
use typst::foundations::Dict;
use crate::EntryOpts;
#[serde_as]
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct CompileOpts {
/// Path to entry
pub entry: EntryOpts,
/// Additional input arguments to compile the entry file.
pub inputs: Dict,
/// 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<PathBuf>,
/// Exclude system font paths
#[serde(rename = "noSystemFonts")]
pub no_system_fonts: bool,
/// Include embedded fonts
#[serde(rename = "withEmbeddedFonts")]
#[serde_as(as = "Vec<AsCowBytes>")]
pub with_embedded_fonts: Vec<Cow<'static, [u8]>>,
}
#[serde_as]
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
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<PathBuf>,
/// Exclude system font paths
#[serde(rename = "noSystemFonts")]
pub no_system_fonts: bool,
/// Include embedded fonts
#[serde(rename = "withEmbeddedFonts")]
#[serde_as(as = "Vec<AsCowBytes>")]
pub with_embedded_fonts: Vec<Cow<'static, [u8]>>,
}
impl From<CompileOpts> for CompileFontOpts {
fn from(opts: CompileOpts) -> Self {
Self {
font_profile_cache_path: opts.font_profile_cache_path,
font_paths: opts.font_paths,
no_system_fonts: opts.no_system_fonts,
with_embedded_fonts: opts.with_embedded_fonts,
}
}
}

View file

@ -0,0 +1,219 @@
use std::path::{Path, PathBuf};
use std::sync::{Arc, LazyLock};
use serde::{Deserialize, Serialize};
use tinymist_std::{error::prelude::*, ImmutPath};
use typst::diag::SourceResult;
use typst::syntax::{FileId, VirtualPath};
pub trait EntryReader {
fn entry_state(&self) -> EntryState;
fn workspace_root(&self) -> Option<Arc<Path>> {
self.entry_state().root().clone()
}
fn main_id(&self) -> Option<FileId> {
self.entry_state().main()
}
}
pub trait EntryManager: EntryReader {
fn reset(&mut self) -> SourceResult<()> {
Ok(())
}
fn mutate_entry(&mut self, state: EntryState) -> SourceResult<EntryState>;
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, Default)]
pub struct EntryState {
/// The differences is that: if the entry is rooted, the workspace root is
/// the parent of the entry file and cannot be used by workspace functions
/// like [`EntryState::try_select_path_in_workspace`].
rooted: bool,
/// Path to the root directory of compilation.
/// The world forbids direct access to files outside this directory.
root: Option<ImmutPath>,
/// Identifier of the main file in the workspace
main: Option<FileId>,
}
pub static DETACHED_ENTRY: LazyLock<FileId> =
LazyLock::new(|| FileId::new(None, VirtualPath::new(Path::new("/__detached.typ"))));
pub static MEMORY_MAIN_ENTRY: LazyLock<FileId> =
LazyLock::new(|| FileId::new(None, VirtualPath::new(Path::new("/__main__.typ"))));
impl EntryState {
/// Create an entry state with no workspace root and no main file.
pub fn new_detached() -> Self {
Self {
rooted: false,
root: None,
main: None,
}
}
/// Create an entry state with a workspace root and no main file.
pub fn new_workspace(root: ImmutPath) -> Self {
Self::new_rooted(root, None)
}
/// Create an entry state with a workspace root and an optional main file.
pub fn new_rooted(root: ImmutPath, main: Option<FileId>) -> Self {
Self {
rooted: true,
root: Some(root),
main,
}
}
/// Create an entry state with only a main file given.
pub fn new_rootless(entry: ImmutPath) -> Option<Self> {
Some(Self {
rooted: false,
root: entry.parent().map(From::from),
main: Some(FileId::new(None, VirtualPath::new(entry.file_name()?))),
})
}
pub fn main(&self) -> Option<FileId> {
self.main
}
pub fn root(&self) -> Option<ImmutPath> {
self.root.clone()
}
pub fn workspace_root(&self) -> Option<ImmutPath> {
self.rooted.then(|| self.root.clone()).flatten()
}
pub fn select_in_workspace(&self, id: FileId) -> EntryState {
Self {
rooted: self.rooted,
root: self.root.clone(),
main: Some(id),
}
}
pub fn try_select_path_in_workspace(
&self,
p: &Path,
allow_rootless: bool,
) -> ZResult<Option<EntryState>> {
Ok(match self.workspace_root() {
Some(root) => match p.strip_prefix(&root) {
Ok(p) => Some(EntryState::new_rooted(
root.clone(),
Some(FileId::new(None, VirtualPath::new(p))),
)),
Err(e) => {
return Err(
error_once!("entry file is not in workspace", err: e, entry: p.display(), root: root.display()),
)
}
},
None if allow_rootless => EntryState::new_rootless(p.into()),
None => None,
})
}
pub fn is_detached(&self) -> bool {
self.root.is_none() && self.main.is_none()
}
pub fn is_inactive(&self) -> bool {
self.main.is_none()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum EntryOpts {
Workspace {
/// Path to the root directory of compilation.
/// The world forbids direct access to files outside this directory.
root: PathBuf,
/// Relative path to the main file in the workspace.
entry: Option<PathBuf>,
},
RootlessEntry {
/// Path to the entry file of compilation.
entry: PathBuf,
/// Parent directory of the entry file.
root: Option<PathBuf>,
},
Detached,
}
impl Default for EntryOpts {
fn default() -> Self {
Self::Detached
}
}
impl EntryOpts {
pub fn new_detached() -> Self {
Self::Detached
}
pub fn new_workspace(root: PathBuf) -> Self {
Self::Workspace { root, entry: None }
}
pub fn new_rooted(root: PathBuf, entry: Option<PathBuf>) -> Self {
Self::Workspace { root, entry }
}
pub fn new_rootless(entry: PathBuf) -> Option<Self> {
if entry.is_relative() {
return None;
}
Some(Self::RootlessEntry {
entry: entry.clone(),
root: entry.parent().map(From::from),
})
}
}
impl TryFrom<EntryOpts> for EntryState {
type Error = tinymist_std::Error;
fn try_from(value: EntryOpts) -> Result<Self, Self::Error> {
match value {
EntryOpts::Workspace { root, entry } => Ok(EntryState::new_rooted(
root.as_path().into(),
entry.map(|e| FileId::new(None, VirtualPath::new(e))),
)),
EntryOpts::RootlessEntry { entry, root } => {
if entry.is_relative() {
return Err(error_once!("entry path must be absolute", path: entry.display()));
}
// todo: is there path that has no parent?
let root = root
.as_deref()
.or_else(|| entry.parent())
.ok_or_else(|| error_once!("a root must be determined for EntryOpts::PreparedEntry", path: entry.display()))?;
let relative_entry = match entry.strip_prefix(root) {
Ok(e) => e,
Err(_) => {
return Err(
error_once!("entry path must be inside the root", path: entry.display()),
)
}
};
Ok(EntryState {
rooted: false,
root: Some(root.into()),
main: Some(FileId::new(None, VirtualPath::new(relative_entry))),
})
}
EntryOpts::Detached => Ok(EntryState::new_detached()),
}
}
}

View file

@ -0,0 +1,26 @@
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use typst::text::FontInfo;
#[derive(Serialize, Deserialize)]
#[serde(tag = "t", content = "v")]
pub enum CacheCondition {
Sha256(String),
}
#[derive(Serialize, Deserialize)]
pub struct FontInfoCache {
pub info: Vec<FontInfo>,
pub conditions: Vec<CacheCondition>,
}
impl FontInfoCache {
pub fn from_data(buffer: &[u8]) -> Self {
let hash = hex::encode(Sha256::digest(buffer));
FontInfoCache {
info: FontInfo::iter(buffer).collect(),
conditions: vec![CacheCondition::Sha256(hash)],
}
}
}

View file

@ -0,0 +1,83 @@
/// Trim style naming from a family name and fix bad names.
#[allow(dead_code)]
pub fn typst_typographic_family(mut family: &str) -> &str {
// Separators between names, modifiers and styles.
const SEPARATORS: [char; 3] = [' ', '-', '_'];
// Modifiers that can appear in combination with suffixes.
const MODIFIERS: &[&str] = &[
"extra", "ext", "ex", "x", "semi", "sem", "sm", "demi", "dem", "ultra",
];
// Style suffixes.
#[rustfmt::skip]
const SUFFIXES: &[&str] = &[
"normal", "italic", "oblique", "slanted",
"thin", "th", "hairline", "light", "lt", "regular", "medium", "med",
"md", "bold", "bd", "demi", "extb", "black", "blk", "bk", "heavy",
"narrow", "condensed", "cond", "cn", "cd", "compressed", "expanded", "exp"
];
let mut extra = [].as_slice();
let newcm = family.starts_with("NewCM") || family.starts_with("NewComputerModern");
if newcm {
extra = &["book"];
}
// Trim spacing and weird leading dots in Apple fonts.
family = family.trim().trim_start_matches('.');
// Lowercase the string so that the suffixes match case-insensitively.
let lower = family.to_ascii_lowercase();
let mut len = usize::MAX;
let mut trimmed = lower.as_str();
// Trim style suffixes repeatedly.
while trimmed.len() < len {
len = trimmed.len();
// Find style suffix.
let mut t = trimmed;
let mut shortened = false;
while let Some(s) = SUFFIXES.iter().chain(extra).find_map(|s| t.strip_suffix(s)) {
shortened = true;
t = s;
}
if !shortened {
break;
}
// Strip optional separator.
if let Some(s) = t.strip_suffix(SEPARATORS) {
trimmed = s;
t = s;
}
// Also allow an extra modifier, but apply it only if it is separated it
// from the text before it (to prevent false positives).
if let Some(t) = MODIFIERS.iter().find_map(|s| t.strip_suffix(s)) {
if let Some(stripped) = t.strip_suffix(SEPARATORS) {
trimmed = stripped;
}
}
}
// Apply style suffix trimming.
family = &family[..len];
if newcm {
family = family.trim_end_matches("10");
}
// Fix bad names.
match family {
"Noto Sans Symbols2" => "Noto Sans Symbols 2",
"NewComputerModern" => "New Computer Modern",
"NewComputerModernMono" => "New Computer Modern Mono",
"NewComputerModernSans" => "New Computer Modern Sans",
"NewComputerModernMath" => "New Computer Modern Math",
"NewCMUncial" | "NewComputerModernUncial" => "New Computer Modern Uncial",
other => other,
}
}

View file

@ -0,0 +1,43 @@
use tinymist_std::ReadAllOnce;
use typst::text::Font;
use crate::Bytes;
/// A FontLoader would help load a font from somewhere.
pub trait FontLoader {
fn load(&mut self) -> Option<Font>;
}
/// Load font from a buffer.
pub struct BufferFontLoader {
pub buffer: Option<Bytes>,
pub index: u32,
}
impl FontLoader for BufferFontLoader {
fn load(&mut self) -> Option<Font> {
Font::new(self.buffer.take().unwrap(), self.index)
}
}
pub struct LazyBufferFontLoader<R> {
pub read: Option<R>,
pub index: u32,
}
impl<R: ReadAllOnce + Sized> LazyBufferFontLoader<R> {
pub fn new(read: R, index: u32) -> Self {
Self {
read: Some(read),
index,
}
}
}
impl<R: ReadAllOnce + Sized> FontLoader for LazyBufferFontLoader<R> {
fn load(&mut self) -> Option<Font> {
let mut buf = vec![];
self.read.take().unwrap().read_all(&mut buf).ok()?;
Font::new(buf.into(), self.index)
}
}

View file

@ -0,0 +1,25 @@
#[cfg(feature = "system")]
pub mod system;
#[cfg(feature = "web")]
pub mod web;
pub mod cache;
pub(crate) mod info;
pub mod pure;
pub(crate) mod profile;
pub use profile::*;
pub(crate) mod loader;
pub use loader::*;
pub(crate) mod slot;
pub use slot::*;
pub(crate) mod resolver;
pub use resolver::*;
pub(crate) mod partial_book;
pub use partial_book::*;

View file

@ -0,0 +1,52 @@
use core::fmt;
use typst::text::{FontFlags, FontInfo, FontVariant};
use crate::font::FontSlot;
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct FontInfoKey {
pub family: String,
pub variant: FontVariant,
pub flags: FontFlags,
}
impl From<&FontInfo> for FontInfoKey {
fn from(info: &FontInfo) -> Self {
Self {
family: info.family.clone(),
variant: info.variant,
flags: info.flags,
}
}
}
#[derive(Debug, Default)]
pub struct PartialFontBook {
pub revision: usize,
pub partial_hit: bool,
pub changes: Vec<(Option<usize>, FontInfo, FontSlot)>,
}
impl PartialFontBook {
pub fn push(&mut self, change: (Option<usize>, FontInfo, FontSlot)) {
self.partial_hit = true;
self.changes.push(change);
}
}
impl fmt::Display for PartialFontBook {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for (idx, info, slot) in &self.changes {
writeln!(
f,
"{:?}: {} -> {:?}\n",
idx,
info.family,
slot.get_uninitialized()
)?;
}
Ok(())
}
}

View file

@ -0,0 +1,138 @@
use serde::{Deserialize, Serialize};
use sha2::Digest;
use std::{collections::HashMap, time::SystemTime};
use typst::text::{Coverage, FontInfo};
type FontMetaDict = HashMap<String, String>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FontInfoItem {
/// customized profile data
pub meta: FontMetaDict,
/// The informatioin of the font
pub info: FontInfo,
}
impl FontInfoItem {
pub fn new(info: FontInfo) -> Self {
Self {
meta: Default::default(),
info,
}
}
pub fn index(&self) -> Option<u32> {
self.meta.get("index").and_then(|v| v.parse::<u32>().ok())
}
pub fn set_index(&mut self, v: u32) {
self.meta.insert("index".to_owned(), v.to_string());
}
pub fn coverage_hash(&self) -> Option<&String> {
self.meta.get("coverage_hash")
}
pub fn set_coverage_hash(&mut self, v: String) {
self.meta.insert("coverage_hash".to_owned(), v);
}
pub fn meta(&self) -> &FontMetaDict {
&self.meta
}
pub fn info(&self) -> &FontInfo {
&self.info
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FontProfileItem {
/// The hash of the file
pub hash: String,
/// customized profile data
pub meta: FontMetaDict,
/// The informatioin of the font
pub info: Vec<FontInfoItem>,
}
fn to_micro_lossy(t: SystemTime) -> u128 {
t.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_micros()
}
impl FontProfileItem {
pub fn new(kind: &str, hash: String) -> Self {
let mut meta: FontMetaDict = Default::default();
meta.insert("kind".to_owned(), kind.to_string());
Self {
hash,
meta,
info: Default::default(),
}
}
pub fn path(&self) -> Option<&String> {
self.meta.get("path")
}
pub fn mtime(&self) -> Option<SystemTime> {
self.meta.get("mtime").and_then(|v| {
let v = v.parse::<u64>().ok();
v.map(|v| SystemTime::UNIX_EPOCH + std::time::Duration::from_micros(v))
})
}
pub fn mtime_is_exact(&self, t: SystemTime) -> bool {
self.mtime()
.map(|s| {
let s = to_micro_lossy(s);
let t = to_micro_lossy(t);
s == t
})
.unwrap_or_default()
}
pub fn set_path(&mut self, v: String) {
self.meta.insert("path".to_owned(), v);
}
pub fn set_mtime(&mut self, v: SystemTime) {
self.meta
.insert("mtime".to_owned(), to_micro_lossy(v).to_string());
}
pub fn hash(&self) -> &str {
&self.hash
}
pub fn meta(&self) -> &FontMetaDict {
&self.meta
}
pub fn info(&self) -> &[FontInfoItem] {
&self.info
}
pub fn add_info(&mut self, info: FontInfoItem) {
self.info.push(info);
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct FontProfile {
pub version: String,
pub build_info: String,
pub items: Vec<FontProfileItem>,
}
pub fn get_font_coverage_hash(coverage: &Coverage) -> String {
let mut coverage_hash = sha2::Sha256::new();
coverage
.iter()
.for_each(|c| coverage_hash.update(c.to_le_bytes()));
let coverage_hash = coverage_hash.finalize();
format!("sha256:{coverage_hash:x}")
}

View file

@ -0,0 +1,56 @@
use tinymist_std::debug_loc::{DataSource, MemoryDataSource};
use typst::foundations::Bytes;
use typst::text::{FontBook, FontInfo};
use crate::font::{BufferFontLoader, FontResolverImpl, FontSlot};
/// memory font builder.
#[derive(Debug)]
pub struct MemoryFontBuilder {
pub book: FontBook,
pub fonts: Vec<FontSlot>,
}
impl Default for MemoryFontBuilder {
fn default() -> Self {
Self::new()
}
}
impl From<MemoryFontBuilder> for FontResolverImpl {
fn from(searcher: MemoryFontBuilder) -> Self {
FontResolverImpl::new(
Vec::new(),
searcher.book,
Default::default(),
searcher.fonts,
Default::default(),
)
}
}
impl MemoryFontBuilder {
/// Create a new, empty system searcher.
pub fn new() -> Self {
Self {
book: FontBook::new(),
fonts: vec![],
}
}
/// Add an in-memory font.
pub fn add_memory_font(&mut self, data: Bytes) {
for (index, info) in FontInfo::iter(&data).enumerate() {
self.book.push(info.clone());
self.fonts.push(
FontSlot::new_boxed(BufferFontLoader {
buffer: Some(data.clone()),
index: index as u32,
})
.describe(DataSource::Memory(MemoryDataSource {
name: "<memory>".to_owned(),
})),
);
}
}
}

View file

@ -0,0 +1,201 @@
use core::fmt;
use std::{
collections::HashMap,
path::PathBuf,
sync::{Arc, Mutex},
};
use tinymist_std::debug_loc::DataSource;
use typst::text::{Font, FontBook, FontInfo};
use typst::utils::LazyHash;
use super::{BufferFontLoader, FontProfile, FontSlot, PartialFontBook};
use crate::Bytes;
/// A FontResolver can resolve a font by index.
/// It also reuse FontBook for font-related query.
/// The index is the index of the font in the `FontBook.infos`.
pub trait FontResolver {
fn font_book(&self) -> &LazyHash<FontBook>;
fn font(&self, idx: usize) -> Option<Font>;
fn default_get_by_info(&self, info: &FontInfo) -> Option<Font> {
// todo: font alternative
let mut alternative_text = 'c';
if let Some(codepoint) = info.coverage.iter().next() {
alternative_text = std::char::from_u32(codepoint).unwrap();
};
let idx = self
.font_book()
.select_fallback(Some(info), info.variant, &alternative_text.to_string())
.unwrap();
self.font(idx)
}
fn get_by_info(&self, info: &FontInfo) -> Option<Font> {
self.default_get_by_info(info)
}
}
#[derive(Debug)]
/// The default FontResolver implementation.
pub struct FontResolverImpl {
font_paths: Vec<PathBuf>,
book: LazyHash<FontBook>,
partial_book: Arc<Mutex<PartialFontBook>>,
fonts: Vec<FontSlot>,
profile: FontProfile,
}
impl FontResolverImpl {
pub fn new(
font_paths: Vec<PathBuf>,
book: FontBook,
partial_book: Arc<Mutex<PartialFontBook>>,
fonts: Vec<FontSlot>,
profile: FontProfile,
) -> Self {
Self {
font_paths,
book: LazyHash::new(book),
partial_book,
fonts,
profile,
}
}
pub fn len(&self) -> usize {
self.fonts.len()
}
pub fn is_empty(&self) -> bool {
self.fonts.is_empty()
}
pub fn profile(&self) -> &FontProfile {
&self.profile
}
pub fn font_paths(&self) -> &[PathBuf] {
&self.font_paths
}
pub fn partial_resolved(&self) -> bool {
self.partial_book.lock().unwrap().partial_hit
}
pub fn loaded_fonts(&self) -> impl Iterator<Item = (usize, Font)> + '_ {
let slots_with_index = self.fonts.iter().enumerate();
slots_with_index.flat_map(|(idx, slot)| {
let maybe_font = slot.get_uninitialized().flatten();
maybe_font.map(|font| (idx, font))
})
}
pub fn describe_font(&self, font: &Font) -> Option<Arc<DataSource>> {
let f = Some(Some(font.clone()));
for slot in &self.fonts {
if slot.get_uninitialized() == f {
return slot.description.clone();
}
}
None
}
pub fn modify_font_data(&mut self, idx: usize, buffer: Bytes) {
let mut font_book = self.partial_book.lock().unwrap();
for (i, info) in FontInfo::iter(buffer.as_slice()).enumerate() {
let buffer = buffer.clone();
let modify_idx = if i > 0 { None } else { Some(idx) };
font_book.push((
modify_idx,
info,
FontSlot::new(Box::new(BufferFontLoader {
buffer: Some(buffer),
index: i as u32,
})),
));
}
}
pub fn append_font(&mut self, info: FontInfo, slot: FontSlot) {
let mut font_book = self.partial_book.lock().unwrap();
font_book.push((None, info, slot));
}
pub fn rebuild(&mut self) {
let mut partial_book = self.partial_book.lock().unwrap();
if !partial_book.partial_hit {
return;
}
partial_book.revision += 1;
let mut book = FontBook::default();
let mut font_changes = HashMap::new();
let mut new_fonts = vec![];
for (idx, info, slot) in partial_book.changes.drain(..) {
if let Some(idx) = idx {
font_changes.insert(idx, (info, slot));
} else {
new_fonts.push((info, slot));
}
}
partial_book.changes.clear();
partial_book.partial_hit = false;
let mut font_slots = Vec::new();
font_slots.append(&mut self.fonts);
self.fonts.clear();
for (i, slot_ref) in font_slots.iter_mut().enumerate() {
let (info, slot) = if let Some((_, v)) = font_changes.remove_entry(&i) {
v
} else {
book.push(self.book.info(i).unwrap().clone());
continue;
};
book.push(info);
*slot_ref = slot;
}
for (info, slot) in new_fonts.drain(..) {
book.push(info);
font_slots.push(slot);
}
self.book = LazyHash::new(book);
self.fonts = font_slots;
}
pub fn add_glyph_packs(&mut self) {
todo!()
}
}
impl FontResolver for FontResolverImpl {
fn font_book(&self) -> &LazyHash<FontBook> {
&self.book
}
fn font(&self, idx: usize) -> Option<Font> {
self.fonts[idx].get_or_init()
}
fn get_by_info(&self, info: &FontInfo) -> Option<Font> {
FontResolver::default_get_by_info(self, info)
}
}
impl fmt::Display for FontResolverImpl {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for (idx, slot) in self.fonts.iter().enumerate() {
writeln!(f, "{:?} -> {:?}", idx, slot.get_uninitialized())?;
}
Ok(())
}
}

View file

@ -0,0 +1,68 @@
use core::fmt;
use std::sync::Arc;
use tinymist_std::debug_loc::DataSource;
use tinymist_std::QueryRef;
use typst::text::Font;
use crate::font::FontLoader;
type FontSlotInner = QueryRef<Option<Font>, (), Box<dyn FontLoader + Send>>;
/// Lazy Font Reference, load as needed.
pub struct FontSlot {
inner: FontSlotInner,
pub description: Option<Arc<DataSource>>,
}
impl FontSlot {
pub fn with_value(f: Option<Font>) -> Self {
Self {
inner: FontSlotInner::with_value(f),
description: None,
}
}
pub fn new(f: Box<dyn FontLoader + Send>) -> Self {
Self {
inner: FontSlotInner::with_context(f),
description: None,
}
}
pub fn new_boxed<F: FontLoader + Send + 'static>(f: F) -> Self {
Self::new(Box::new(f))
}
pub fn describe(self, desc: DataSource) -> Self {
Self {
inner: self.inner,
description: Some(Arc::new(desc)),
}
}
/// Gets the reference to the font load result (possible uninitialized).
///
/// Returns `None` if the cell is empty, or being initialized. This
/// method never blocks.
pub fn get_uninitialized(&self) -> Option<Option<Font>> {
self.inner
.get_uninitialized()
.cloned()
.map(|e| e.ok().flatten())
}
/// Gets or make the font load result.
pub fn get_or_init(&self) -> Option<Font> {
let res = self.inner.compute_with_context(|mut c| Ok(c.load()));
res.unwrap().clone()
}
}
impl fmt::Debug for FontSlot {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_tuple("FontSlot")
.field(&self.get_uninitialized())
.finish()
}
}

View file

@ -0,0 +1,302 @@
use std::{
borrow::Cow,
collections::HashMap,
fs::File,
path::{Path, PathBuf},
sync::{Arc, Mutex},
};
use fontdb::Database;
use sha2::{Digest, Sha256};
use tinymist_std::debug_loc::{DataSource, MemoryDataSource};
use tinymist_std::error::prelude::*;
use tinymist_vfs::system::LazyFile;
use typst::{
diag::{FileError, FileResult},
foundations::Bytes,
text::{FontBook, FontInfo},
};
use super::{
BufferFontLoader, FontProfile, FontProfileItem, FontResolverImpl, FontSlot,
LazyBufferFontLoader, PartialFontBook,
};
use crate::{build_info, config::CompileFontOpts};
#[derive(Debug, Default)]
struct FontProfileRebuilder {
path_items: HashMap<PathBuf, FontProfileItem>,
pub profile: FontProfile,
can_profile: bool,
}
impl FontProfileRebuilder {
/// Index the fonts in the file at the given path.
#[allow(dead_code)]
fn search_file(&mut self, path: impl AsRef<Path>) -> Option<&FontProfileItem> {
let path = path.as_ref().canonicalize().unwrap();
if let Some(item) = self.path_items.get(&path) {
return Some(item);
}
if let Ok(mut file) = File::open(&path) {
let hash = if self.can_profile {
let mut hasher = Sha256::new();
let _bytes_written = std::io::copy(&mut file, &mut hasher).unwrap();
let hash = hasher.finalize();
format!("sha256:{}", hex::encode(hash))
} else {
"".to_owned()
};
let mut profile_item = FontProfileItem::new("path", hash);
profile_item.set_path(path.to_str().unwrap().to_owned());
profile_item.set_mtime(file.metadata().unwrap().modified().unwrap());
// eprintln!("searched font: {:?}", path);
// if let Ok(mmap) = unsafe { Mmap::map(&file) } {
// for (i, info) in FontInfo::iter(&mmap).enumerate() {
// let coverage_hash = get_font_coverage_hash(&info.coverage);
// let mut ff = FontInfoItem::new(info);
// ff.set_coverage_hash(coverage_hash);
// if i != 0 {
// ff.set_index(i as u32);
// }
// profile_item.add_info(ff);
// }
// }
self.profile.items.push(profile_item);
return self.profile.items.last();
}
None
}
}
/// Searches for fonts.
#[derive(Debug)]
pub struct SystemFontSearcher {
db: Database,
pub book: FontBook,
pub fonts: Vec<FontSlot>,
pub font_paths: Vec<PathBuf>,
profile_rebuilder: FontProfileRebuilder,
}
impl SystemFontSearcher {
/// Create a new, empty system searcher.
pub fn new() -> Self {
let mut profile_rebuilder = FontProfileRebuilder::default();
"v1beta".clone_into(&mut profile_rebuilder.profile.version);
profile_rebuilder.profile.build_info = build_info::VERSION.to_string();
let db = Database::new();
Self {
font_paths: vec![],
db,
book: FontBook::new(),
fonts: vec![],
profile_rebuilder,
}
}
/// Resolve fonts from given options.
pub fn resolve_opts(&mut self, opts: CompileFontOpts) -> ZResult<()> {
if opts
.font_profile_cache_path
.to_str()
.map(|e| !e.is_empty())
.unwrap_or_default()
{
self.set_can_profile(true);
}
// Note: the order of adding fonts is important.
// See: https://github.com/typst/typst/blob/9c7f31870b4e1bf37df79ebbe1df9a56df83d878/src/font/book.rs#L151-L154
// Source1: add the fonts specified by the user.
for path in opts.font_paths {
if path.is_dir() {
self.search_dir(&path);
} else {
let _ = self.search_file(&path);
}
}
// Source2: add the fonts from system paths.
if !opts.no_system_fonts {
self.search_system();
}
// flush source1 and source2 before adding source3
self.flush();
// Source3: add the fonts in memory.
for font_data in opts.with_embedded_fonts {
self.add_memory_font(match font_data {
Cow::Borrowed(data) => Bytes::from_static(data),
Cow::Owned(data) => Bytes::from(data),
});
}
Ok(())
}
pub fn set_can_profile(&mut self, can_profile: bool) {
self.profile_rebuilder.can_profile = can_profile;
}
pub fn add_profile_by_path(&mut self, profile_path: &Path) {
// let begin = std::time::Instant::now();
// profile_path is in format of json.gz
let profile_file = File::open(profile_path).unwrap();
let profile_gunzip = flate2::read::GzDecoder::new(profile_file);
let profile: FontProfile = serde_json::from_reader(profile_gunzip).unwrap();
if self.profile_rebuilder.profile.version != profile.version
|| self.profile_rebuilder.profile.build_info != profile.build_info
{
return;
}
for item in profile.items {
let path = match item.path() {
Some(path) => path,
None => continue,
};
let path = PathBuf::from(path);
if let Ok(m) = std::fs::metadata(&path) {
let modified = m.modified().ok();
if !modified.map(|m| item.mtime_is_exact(m)).unwrap_or_default() {
continue;
}
}
self.profile_rebuilder.path_items.insert(path, item.clone());
self.profile_rebuilder.profile.items.push(item);
}
// let end = std::time::Instant::now();
// eprintln!("profile_rebuilder init took {:?}", end - begin);
}
pub fn flush(&mut self) {
use fontdb::Source;
use tinymist_std::debug_loc::FsDataSource;
for face in self.db.faces() {
let path = match &face.source {
Source::File(path) | Source::SharedFile(path, _) => path,
// We never add binary sources to the database, so there
// shouln't be any.
Source::Binary(_) => unreachable!(),
};
let info = self
.db
.with_face_data(face.id, FontInfo::new)
.expect("database must contain this font");
// eprintln!("searched font: {idx} {:?}", path);
if let Some(info) = info {
self.book.push(info);
self.fonts.push(
FontSlot::new_boxed(LazyBufferFontLoader::new(
LazyFile::new(path.clone()),
face.index,
))
.describe(DataSource::Fs(FsDataSource {
path: path.to_str().unwrap_or_default().to_owned(),
})),
);
}
}
self.db = Database::new();
}
/// Add an in-memory font.
pub fn add_memory_font(&mut self, data: Bytes) {
if !self.db.is_empty() {
panic!("dirty font search state, please flush the searcher before adding memory fonts");
}
for (index, info) in FontInfo::iter(&data).enumerate() {
self.book.push(info.clone());
self.fonts.push(
FontSlot::new_boxed(BufferFontLoader {
buffer: Some(data.clone()),
index: index as u32,
})
.describe(DataSource::Memory(MemoryDataSource {
name: "<memory>".to_owned(),
})),
);
}
}
pub fn search_system(&mut self) {
self.db.load_system_fonts();
}
fn record_path(&mut self, path: &Path) {
self.font_paths.push(if !path.is_relative() {
path.to_owned()
} else {
let current_dir = std::env::current_dir();
match current_dir {
Ok(current_dir) => current_dir.join(path),
Err(_) => path.to_owned(),
}
});
}
/// Search for all fonts in a directory recursively.
pub fn search_dir(&mut self, path: impl AsRef<Path>) {
self.record_path(path.as_ref());
self.db.load_fonts_dir(path);
}
/// Index the fonts in the file at the given path.
pub fn search_file(&mut self, path: impl AsRef<Path>) -> FileResult<()> {
self.record_path(path.as_ref());
self.db
.load_font_file(path.as_ref())
.map_err(|e| FileError::from_io(e, path.as_ref()))
}
}
impl Default for SystemFontSearcher {
fn default() -> Self {
Self::new()
}
}
impl From<SystemFontSearcher> for FontResolverImpl {
fn from(searcher: SystemFontSearcher) -> Self {
// let profile_item = match
// self.profile_rebuilder.search_file(path.as_ref()) {
// Some(profile_item) => profile_item,
// None => return,
// };
// for info in profile_item.info.iter() {
// self.book.push(info.info.clone());
// self.fonts
// .push(FontSlot::new_boxed(LazyBufferFontLoader::new(
// LazyFile::new(path.as_ref().to_owned()),
// info.index().unwrap_or_default(),
// )));
// }
FontResolverImpl::new(
searcher.font_paths,
searcher.book,
Arc::new(Mutex::new(PartialFontBook::default())),
searcher.fonts,
searcher.profile_rebuilder.profile,
)
}
}

View file

@ -0,0 +1,477 @@
use std::sync::{Arc, Mutex};
use js_sys::ArrayBuffer;
use tinymist_std::error::prelude::*;
use typst::foundations::Bytes;
use typst::text::{
Coverage, Font, FontBook, FontFlags, FontInfo, FontStretch, FontStyle, FontVariant, FontWeight,
};
use wasm_bindgen::prelude::*;
use super::{
BufferFontLoader, FontLoader, FontProfile, FontResolverImpl, FontSlot, PartialFontBook,
};
use crate::font::cache::FontInfoCache;
use crate::font::info::typst_typographic_family;
/// Destructures a JS `[key, value]` pair into a tuple of [`Deserializer`]s.
pub(crate) fn convert_pair(pair: JsValue) -> (JsValue, JsValue) {
let pair = pair.unchecked_into::<js_sys::Array>();
(pair.get(0), pair.get(1))
}
struct FontBuilder {}
fn font_family_web_to_typst(family: &str, full_name: &str) -> ZResult<String> {
let mut family = family;
if family.starts_with("Noto")
|| family.starts_with("NewCM")
|| family.starts_with("NewComputerModern")
{
family = full_name;
}
if family.is_empty() {
return Err(error_once!("font_family_web_to_typst.empty_family"));
}
Ok(typst_typographic_family(family).to_string())
}
struct WebFontInfo {
family: String,
full_name: String,
postscript_name: String,
style: String,
}
fn infer_info_from_web_font(
WebFontInfo {
family,
full_name,
postscript_name,
style,
}: WebFontInfo,
) -> ZResult<FontInfo> {
let family = font_family_web_to_typst(&family, &full_name)?;
let mut full = full_name;
full.make_ascii_lowercase();
let mut postscript = postscript_name;
postscript.make_ascii_lowercase();
let mut style = style;
style.make_ascii_lowercase();
let search_scopes = [style.as_str(), postscript.as_str(), full.as_str()];
let variant = {
// Some fonts miss the relevant bits for italic or oblique, so
// we also try to infer that from the full name.
let italic = full.contains("italic");
let oblique = full.contains("oblique") || full.contains("slanted");
let style = match (italic, oblique) {
(false, false) => FontStyle::Normal,
(true, _) => FontStyle::Italic,
(_, true) => FontStyle::Oblique,
};
let weight = {
let mut weight = None;
let mut secondary_weight = None;
'searchLoop: for &search_style in &[
"thin",
"extralight",
"extra light",
"extra-light",
"light",
"regular",
"medium",
"semibold",
"semi bold",
"semi-bold",
"bold",
"extrabold",
"extra bold",
"extra-bold",
"black",
] {
for (idx, &search_scope) in search_scopes.iter().enumerate() {
if search_scope.contains(search_style) {
let guess_weight = match search_style {
"thin" => Some(FontWeight::THIN),
"extralight" => Some(FontWeight::EXTRALIGHT),
"extra light" => Some(FontWeight::EXTRALIGHT),
"extra-light" => Some(FontWeight::EXTRALIGHT),
"light" => Some(FontWeight::LIGHT),
"regular" => Some(FontWeight::REGULAR),
"medium" => Some(FontWeight::MEDIUM),
"semibold" => Some(FontWeight::SEMIBOLD),
"semi bold" => Some(FontWeight::SEMIBOLD),
"semi-bold" => Some(FontWeight::SEMIBOLD),
"bold" => Some(FontWeight::BOLD),
"extrabold" => Some(FontWeight::EXTRABOLD),
"extra bold" => Some(FontWeight::EXTRABOLD),
"extra-bold" => Some(FontWeight::EXTRABOLD),
"black" => Some(FontWeight::BLACK),
_ => unreachable!(),
};
if let Some(guess_weight) = guess_weight {
if idx == 0 {
weight = Some(guess_weight);
break 'searchLoop;
} else {
secondary_weight = Some(guess_weight);
}
}
}
}
}
weight.unwrap_or(secondary_weight.unwrap_or(FontWeight::REGULAR))
};
let stretch = {
let mut stretch = None;
'searchLoop: for &search_style in &[
"ultracondensed",
"ultra_condensed",
"ultra-condensed",
"extracondensed",
"extra_condensed",
"extra-condensed",
"condensed",
"semicondensed",
"semi_condensed",
"semi-condensed",
"normal",
"semiexpanded",
"semi_expanded",
"semi-expanded",
"expanded",
"extraexpanded",
"extra_expanded",
"extra-expanded",
"ultraexpanded",
"ultra_expanded",
"ultra-expanded",
] {
for (idx, &search_scope) in search_scopes.iter().enumerate() {
if search_scope.contains(search_style) {
let guess_stretch = match search_style {
"ultracondensed" => Some(FontStretch::ULTRA_CONDENSED),
"ultra_condensed" => Some(FontStretch::ULTRA_CONDENSED),
"ultra-condensed" => Some(FontStretch::ULTRA_CONDENSED),
"extracondensed" => Some(FontStretch::EXTRA_CONDENSED),
"extra_condensed" => Some(FontStretch::EXTRA_CONDENSED),
"extra-condensed" => Some(FontStretch::EXTRA_CONDENSED),
"condensed" => Some(FontStretch::CONDENSED),
"semicondensed" => Some(FontStretch::SEMI_CONDENSED),
"semi_condensed" => Some(FontStretch::SEMI_CONDENSED),
"semi-condensed" => Some(FontStretch::SEMI_CONDENSED),
"normal" => Some(FontStretch::NORMAL),
"semiexpanded" => Some(FontStretch::SEMI_EXPANDED),
"semi_expanded" => Some(FontStretch::SEMI_EXPANDED),
"semi-expanded" => Some(FontStretch::SEMI_EXPANDED),
"expanded" => Some(FontStretch::EXPANDED),
"extraexpanded" => Some(FontStretch::EXTRA_EXPANDED),
"extra_expanded" => Some(FontStretch::EXTRA_EXPANDED),
"extra-expanded" => Some(FontStretch::EXTRA_EXPANDED),
"ultraexpanded" => Some(FontStretch::ULTRA_EXPANDED),
"ultra_expanded" => Some(FontStretch::ULTRA_EXPANDED),
"ultra-expanded" => Some(FontStretch::ULTRA_EXPANDED),
_ => None,
};
if let Some(guess_stretch) = guess_stretch {
if idx == 0 {
stretch = Some(guess_stretch);
break 'searchLoop;
}
}
}
}
}
stretch.unwrap_or(FontStretch::NORMAL)
};
FontVariant {
style,
weight,
stretch,
}
};
let flags = {
// guess mono and serif
let mut flags = FontFlags::empty();
for search_scope in search_scopes {
if search_scope.contains("mono") {
flags |= FontFlags::MONOSPACE;
} else if search_scope.contains("serif") {
flags |= FontFlags::SERIF;
}
}
flags
};
let coverage = Coverage::from_vec(vec![0, 4294967295]);
Ok(FontInfo {
family,
variant,
flags,
coverage,
})
}
impl FontBuilder {
// fn to_f64(&self, field: &str, val: &JsValue) -> Result<f64, JsValue> {
// Ok(val
// .as_f64()
// .ok_or_else(|| JsValue::from_str(&format!("expected f64 for {}, got
// {:?}", field, val))) .unwrap())
// }
fn to_string(&self, field: &str, val: &JsValue) -> ZResult<String> {
Ok(val
.as_string()
.ok_or_else(|| JsValue::from_str(&format!("expected string for {field}, got {val:?}")))
.unwrap())
}
fn font_web_to_typst(
&self,
val: &JsValue,
) -> ZResult<(JsValue, js_sys::Function, Vec<typst::text::FontInfo>)> {
let mut postscript_name = String::new();
let mut family = String::new();
let mut full_name = String::new();
let mut style = String::new();
let mut font_ref = None;
let mut font_blob_loader = None;
let mut font_cache: Option<FontInfoCache> = None;
for (k, v) in
js_sys::Object::entries(val.dyn_ref().ok_or_else(
|| error_once!("WebFontToTypstFont.entries", val: format!("{:?}", val)),
)?)
.iter()
.map(convert_pair)
{
let k = self.to_string("web_font.key", &k)?;
match k.as_str() {
"postscriptName" => {
postscript_name = self.to_string("web_font.postscriptName", &v)?;
}
"family" => {
family = self.to_string("web_font.family", &v)?;
}
"fullName" => {
full_name = self.to_string("web_font.fullName", &v)?;
}
"style" => {
style = self.to_string("web_font.style", &v)?;
}
"ref" => {
font_ref = Some(v);
}
"info" => {
// a previous calculated font info
font_cache = serde_wasm_bindgen::from_value(v).ok();
}
"blob" => {
font_blob_loader = Some(v.clone().dyn_into().map_err(error_once_map!(
"web_font.blob_builder",
v: format!("{:?}", v)
))?);
}
_ => panic!("unknown key for {}: {}", "web_font", k),
}
}
let font_info = match font_cache {
Some(font_cache) => Some(
// todo cache invalidatio: font_cache.conditions.iter()
font_cache.info,
),
None => None,
};
let font_info: Vec<FontInfo> = match font_info {
Some(font_info) => font_info,
None => {
vec![infer_info_from_web_font(WebFontInfo {
family: family.clone(),
full_name,
postscript_name,
style,
})?]
}
};
Ok((
font_ref.ok_or_else(|| error_once!("WebFontToTypstFont.NoFontRef", family: family))?,
font_blob_loader.ok_or_else(
|| error_once!("WebFontToTypstFont.NoFontBlobLoader", family: family),
)?,
font_info,
))
}
}
#[derive(Clone, Debug)]
pub struct WebFont {
pub info: FontInfo,
pub context: JsValue,
pub blob: js_sys::Function,
pub index: u32,
}
impl WebFont {
pub fn load(&self) -> Option<ArrayBuffer> {
self.blob
.call1(&self.context, &self.index.into())
.unwrap()
.dyn_into::<ArrayBuffer>()
.ok()
}
}
/// Safety: `WebFont` is only used in the browser environment, and we
/// cannot share data between workers.
unsafe impl Send for WebFont {}
#[derive(Debug)]
pub struct WebFontLoader {
font: WebFont,
index: u32,
}
impl WebFontLoader {
pub fn new(font: WebFont, index: u32) -> Self {
Self { font, index }
}
}
impl FontLoader for WebFontLoader {
fn load(&mut self) -> Option<Font> {
let font = &self.font;
web_sys::console::log_3(
&"dyn init".into(),
&font.context,
&format!("{:?}", font.info).into(),
);
// let blob = pollster::block_on(JsFuture::from(blob.array_buffer())).unwrap();
let blob = font.load()?;
let blob = Bytes::from(js_sys::Uint8Array::new(&blob).to_vec());
Font::new(blob, self.index)
}
}
/// Searches for fonts.
pub struct BrowserFontSearcher {
pub book: FontBook,
pub fonts: Vec<FontSlot>,
pub profile: FontProfile,
pub partial_book: Arc<Mutex<PartialFontBook>>,
}
impl BrowserFontSearcher {
/// Create a new, empty system searcher.
pub fn new() -> Self {
let profile = FontProfile {
version: "v1beta".to_owned(),
..Default::default()
};
let mut searcher = Self {
book: FontBook::new(),
fonts: vec![],
profile,
partial_book: Arc::new(Mutex::new(PartialFontBook::default())),
};
if cfg!(feature = "browser-embedded-fonts") {
searcher.add_embedded();
}
searcher
}
/// Add fonts that are embedded in the binary.
pub fn add_embedded(&mut self) {
for font_data in typst_assets::fonts() {
let buffer = Bytes::from_static(font_data);
for font in Font::iter(buffer) {
self.book.push(font.info().clone());
self.fonts.push(FontSlot::with_value(Some(font)));
}
}
}
pub async fn add_web_fonts(&mut self, fonts: js_sys::Array) -> ZResult<()> {
let font_builder = FontBuilder {};
for v in fonts.iter() {
let (font_ref, font_blob_loader, font_info) = font_builder.font_web_to_typst(&v)?;
for (i, info) in font_info.into_iter().enumerate() {
self.book.push(info.clone());
let index = self.fonts.len();
self.fonts.push(FontSlot::new(Box::new(WebFontLoader {
font: WebFont {
info,
context: font_ref.clone(),
blob: font_blob_loader.clone(),
index: index as u32,
},
index: i as u32,
})))
}
}
Ok(())
}
pub fn add_font_data(&mut self, buffer: Bytes) {
for (i, info) in FontInfo::iter(buffer.as_slice()).enumerate() {
self.book.push(info);
let buffer = buffer.clone();
self.fonts.push(FontSlot::new(Box::new(BufferFontLoader {
buffer: Some(buffer),
index: i as u32,
})))
}
}
pub async fn add_glyph_pack(&mut self) -> ZResult<()> {
Err(error_once!(
"BrowserFontSearcher.add_glyph_pack is not implemented"
))
}
}
impl Default for BrowserFontSearcher {
fn default() -> Self {
Self::new()
}
}
impl From<BrowserFontSearcher> for FontResolverImpl {
fn from(value: BrowserFontSearcher) -> Self {
FontResolverImpl::new(
vec![],
value.book,
value.partial_book,
value.fonts,
value.profile,
)
}
}

View file

@ -1,273 +1,163 @@
//! World implementation of typst for tinymist. //! World implementation of typst for tinymist.
use font::TinymistFontResolver; #![allow(missing_docs)]
pub use reflexo_typst;
pub use reflexo_typst::config::CompileFontOpts;
pub use reflexo_typst::error::prelude;
pub use reflexo_typst::world as base;
pub use reflexo_typst::world::{package, CompilerUniverse, CompilerWorld, Revising, TaskInputs};
pub use reflexo_typst::{entry::*, vfs, EntryOpts, EntryState};
use std::path::Path; pub mod args;
use std::{borrow::Cow, path::PathBuf, sync::Arc};
use ::typst::utils::LazyHash; pub mod source;
use anyhow::Context;
use chrono::{DateTime, Utc}; pub mod config;
use clap::{builder::ValueParser, ArgAction, Parser};
use reflexo_typst::error::prelude::*; pub mod entry;
use reflexo_typst::font::system::SystemFontSearcher; pub use entry::*;
use reflexo_typst::foundations::{Str, Value};
use reflexo_typst::package::http::HttpRegistry; pub mod world;
use reflexo_typst::vfs::{system::SystemAccessModel, Vfs}; pub use world::*;
use reflexo_typst::{CompilerFeat, ImmutPath, TypstDict};
use serde::{Deserialize, Serialize};
pub mod font; pub mod font;
pub mod project; pub mod package;
pub mod parser;
const ENV_PATH_SEP: char = if cfg!(windows) { ';' } else { ':' }; pub use tinymist_vfs as vfs;
/// Compiler feature for LSP universe and worlds without typst.ts to implement /// Run the compiler in the system environment.
/// more for tinymist. type trait of [`CompilerUniverse`]. #[cfg(feature = "system")]
#[derive(Debug, Clone, Copy)] pub mod system;
pub struct SystemCompilerFeatExtend; #[cfg(feature = "system")]
pub use system::{SystemCompilerFeat, TypstSystemUniverse, TypstSystemWorld};
impl CompilerFeat for SystemCompilerFeatExtend { /// Run the compiler in the browser environment.
/// Uses [`TinymistFontResolver`] directly. #[cfg(feature = "browser")]
type FontResolver = TinymistFontResolver; pub(crate) mod browser;
/// It accesses a physical file system. #[cfg(feature = "browser")]
type AccessModel = SystemAccessModel; pub use browser::{BrowserCompilerFeat, TypstBrowserUniverse, TypstBrowserWorld};
/// It performs native HTTP requests for fetching package data.
type Registry = HttpRegistry;
}
/// The compiler universe in system environment. use std::{
pub type TypstSystemUniverseExtend = CompilerUniverse<SystemCompilerFeatExtend>; path::{Path, PathBuf},
/// The compiler world in system environment. sync::Arc,
pub type TypstSystemWorldExtend = CompilerWorld<SystemCompilerFeatExtend>; };
/// The font arguments for the compiler. use ecow::EcoVec;
#[derive(Debug, Clone, Default, Parser, PartialEq, Eq, Serialize, Deserialize)] use tinymist_std::ImmutPath;
#[serde(rename_all = "camelCase")] use tinymist_vfs::AccessModel as VfsAccessModel;
pub struct CompileFontArgs { use typst::{
/// Font paths diag::{At, FileResult, SourceResult},
#[clap( foundations::Bytes,
long = "font-path", syntax::FileId,
value_name = "DIR", syntax::Span,
action = clap::ArgAction::Append, };
env = "TYPST_FONT_PATHS",
value_delimiter = ENV_PATH_SEP
)]
pub font_paths: Vec<PathBuf>,
/// Ensures system fonts won't be searched, unless explicitly included via use font::FontResolver;
/// `--font-path` use package::PackageRegistry;
#[clap(long, default_value = "false")]
pub ignore_system_fonts: bool,
}
/// Arguments related to where packages are stored in the system. /// Latest version of the shadow api, which is in beta.
#[derive(Debug, Clone, Parser, Default, PartialEq, Eq)] pub trait ShadowApi {
pub struct CompilePackageArgs { fn _shadow_map_id(&self, _file_id: FileId) -> FileResult<PathBuf> {
/// Custom path to local packages, defaults to system-dependent location unimplemented!()
#[clap(long = "package-path", env = "TYPST_PACKAGE_PATH", value_name = "DIR")]
pub package_path: Option<PathBuf>,
/// Custom path to package cache, defaults to system-dependent location
#[clap(
long = "package-cache-path",
env = "TYPST_PACKAGE_CACHE_PATH",
value_name = "DIR"
)]
pub package_cache_path: Option<PathBuf>,
}
/// Common arguments of compile, watch, and query.
#[derive(Debug, Clone, Parser, Default)]
pub struct CompileOnceArgs {
/// Path to input Typst file
#[clap(value_name = "INPUT")]
pub input: Option<String>,
/// Configures the project root (for absolute paths)
#[clap(long = "root", value_name = "DIR")]
pub root: Option<PathBuf>,
/// Add a string key-value pair visible through `sys.inputs`
#[clap(
long = "input",
value_name = "key=value",
action = ArgAction::Append,
value_parser = ValueParser::new(parse_input_pair),
)]
pub inputs: Vec<(String, String)>,
/// Font related arguments.
#[clap(flatten)]
pub font: CompileFontArgs,
/// Package related arguments.
#[clap(flatten)]
pub package: CompilePackageArgs,
/// The document's creation date formatted as a UNIX timestamp.
///
/// For more information, see <https://reproducible-builds.org/specs/source-date-epoch/>.
#[clap(
long = "creation-timestamp",
env = "SOURCE_DATE_EPOCH",
value_name = "UNIX_TIMESTAMP",
value_parser = parse_source_date_epoch,
hide(true),
)]
pub creation_timestamp: Option<DateTime<Utc>>,
/// Path to CA certificate file for network access, especially for
/// downloading typst packages.
#[clap(long = "cert", env = "TYPST_CERT", value_name = "CERT_PATH")]
pub cert: Option<PathBuf>,
}
impl CompileOnceArgs {
/// Get a universe instance from the given arguments.
pub fn resolve(&self) -> anyhow::Result<LspUniverse> {
let entry = self.entry()?.try_into()?;
let inputs = self
.inputs
.iter()
.map(|(k, v)| (Str::from(k.as_str()), Value::Str(Str::from(v.as_str()))))
.collect();
let fonts = LspUniverseBuilder::resolve_fonts(self.font.clone())?;
let package = LspUniverseBuilder::resolve_package(
self.cert.as_deref().map(From::from),
Some(&self.package),
);
LspUniverseBuilder::build(
entry,
Arc::new(LazyHash::new(inputs)),
Arc::new(fonts),
package,
)
.context("failed to create universe")
} }
/// Get the entry options from the arguments. /// Get the shadow files.
pub fn entry(&self) -> anyhow::Result<EntryOpts> { fn shadow_paths(&self) -> Vec<Arc<Path>>;
let input = self.input.as_ref().context("entry file must be provided")?;
let input = Path::new(&input);
let entry = if input.is_absolute() {
input.to_owned()
} else {
std::env::current_dir().unwrap().join(input)
};
let root = if let Some(root) = &self.root { /// Reset the shadow files.
if root.is_absolute() { fn reset_shadow(&mut self) {
root.clone() for path in self.shadow_paths() {
} else { self.unmap_shadow(&path).unwrap();
std::env::current_dir().unwrap().join(root)
} }
} else {
std::env::current_dir().unwrap()
};
if !entry.starts_with(&root) {
log::error!("entry file must be in the root directory");
std::process::exit(1);
} }
let relative_entry = match entry.strip_prefix(&root) { /// Add a shadow file to the driver.
Ok(relative_entry) => relative_entry, fn map_shadow(&mut self, path: &Path, content: Bytes) -> FileResult<()>;
Err(_) => {
log::error!("entry path must be inside the root: {}", entry.display());
std::process::exit(1);
}
};
Ok(EntryOpts::new_rooted( /// Add a shadow file to the driver.
root.clone(), fn unmap_shadow(&mut self, path: &Path) -> FileResult<()>;
Some(relative_entry.to_owned()),
)) /// Add a shadow file to the driver by file id.
/// Note: to enable this function, `ShadowApi` must implement
/// `_shadow_map_id`.
fn map_shadow_by_id(&mut self, file_id: FileId, content: Bytes) -> FileResult<()> {
let file_path = self._shadow_map_id(file_id)?;
self.map_shadow(&file_path, content)
}
/// Add a shadow file to the driver by file id.
/// Note: to enable this function, `ShadowApi` must implement
/// `_shadow_map_id`.
fn unmap_shadow_by_id(&mut self, file_id: FileId) -> FileResult<()> {
let file_path = self._shadow_map_id(file_id)?;
self.unmap_shadow(&file_path)
} }
} }
/// Compiler feature for LSP universe and worlds. pub trait ShadowApiExt {
pub type LspCompilerFeat = SystemCompilerFeatExtend; /// Wrap the driver with a given shadow file and run the inner function.
/// LSP universe that spawns LSP worlds. fn with_shadow_file<T>(
pub type LspUniverse = TypstSystemUniverseExtend; &mut self,
/// LSP world. file_path: &Path,
pub type LspWorld = TypstSystemWorldExtend; content: Bytes,
/// Immutable prehashed reference to dictionary. f: impl FnOnce(&mut Self) -> SourceResult<T>,
pub type ImmutDict = Arc<LazyHash<TypstDict>>; ) -> SourceResult<T>;
/// Builder for LSP universe. /// Wrap the driver with a given shadow file and run the inner function by
pub struct LspUniverseBuilder; /// file id.
/// Note: to enable this function, `ShadowApi` must implement
/// `_shadow_map_id`.
fn with_shadow_file_by_id<T>(
&mut self,
file_id: FileId,
content: Bytes,
f: impl FnOnce(&mut Self) -> SourceResult<T>,
) -> SourceResult<T>;
}
impl LspUniverseBuilder { impl<C: ShadowApi> ShadowApiExt for C {
/// Create [`LspUniverse`] with the given options. /// Wrap the driver with a given shadow file and run the inner function.
/// See [`LspCompilerFeat`] for instantiation details. fn with_shadow_file<T>(
pub fn build( &mut self,
entry: EntryState, file_path: &Path,
inputs: ImmutDict, content: Bytes,
font_resolver: Arc<TinymistFontResolver>, f: impl FnOnce(&mut Self) -> SourceResult<T>,
package_registry: HttpRegistry, ) -> SourceResult<T> {
) -> ZResult<LspUniverse> { self.map_shadow(file_path, content).at(Span::detached())?;
Ok(LspUniverse::new_raw( let res: Result<T, EcoVec<typst::diag::SourceDiagnostic>> = f(self);
entry, self.unmap_shadow(file_path).at(Span::detached())?;
Some(inputs), res
Vfs::new(SystemAccessModel {}),
package_registry,
font_resolver,
))
} }
/// Resolve fonts from given options. /// Wrap the driver with a given shadow file and run the inner function by
pub fn resolve_fonts(args: CompileFontArgs) -> ZResult<TinymistFontResolver> { /// file id.
let mut searcher = SystemFontSearcher::new(); /// Note: to enable this function, `ShadowApi` must implement
searcher.resolve_opts(CompileFontOpts { /// `_shadow_map_id`.
font_profile_cache_path: Default::default(), fn with_shadow_file_by_id<T>(
font_paths: args.font_paths, &mut self,
no_system_fonts: args.ignore_system_fonts, file_id: FileId,
with_embedded_fonts: typst_assets::fonts().map(Cow::Borrowed).collect(), content: Bytes,
})?; f: impl FnOnce(&mut Self) -> SourceResult<T>,
Ok(searcher.into()) ) -> SourceResult<T> {
} let file_path = self._shadow_map_id(file_id).at(Span::detached())?;
self.with_shadow_file(&file_path, content, f)
/// Resolve package registry from given options.
pub fn resolve_package(
cert_path: Option<ImmutPath>,
args: Option<&CompilePackageArgs>,
) -> HttpRegistry {
HttpRegistry::new(
cert_path,
args.and_then(|args| Some(args.package_path.clone()?.into())),
args.and_then(|args| Some(args.package_cache_path.clone()?.into())),
)
} }
} }
/// Parses key/value pairs split by the first equal sign. /// Latest version of the world dependencies api, which is in beta.
/// pub trait WorldDeps {
/// This function will return an error if the argument contains no equals sign fn iter_dependencies(&self, f: &mut dyn FnMut(ImmutPath));
/// or contains the key (before the equals sign) is empty.
fn parse_input_pair(raw: &str) -> Result<(String, String), String> {
let (key, val) = raw
.split_once('=')
.ok_or("input must be a key and a value separated by an equal sign")?;
let key = key.trim().to_owned();
if key.is_empty() {
return Err("the key was missing or empty".to_owned());
}
let val = val.trim().to_owned();
Ok((key, val))
} }
/// Parses a UNIX timestamp according to <https://reproducible-builds.org/specs/source-date-epoch/> type CodespanResult<T> = Result<T, CodespanError>;
pub fn parse_source_date_epoch(raw: &str) -> Result<DateTime<Utc>, String> { type CodespanError = codespan_reporting::files::Error;
let timestamp: i64 = raw
.parse() /// type trait interface of [`CompilerWorld`].
.map_err(|err| format!("timestamp must be decimal integer ({err})"))?; pub trait CompilerFeat {
DateTime::from_timestamp(timestamp, 0).ok_or_else(|| "timestamp out of range".to_string()) /// Specify the font resolver for typst compiler.
type FontResolver: FontResolver + Send + Sync + Sized;
/// Specify the access model for VFS.
type AccessModel: VfsAccessModel + Clone + Send + Sync + Sized;
/// Specify the package registry.
type Registry: PackageRegistry + Send + Sync + Sized;
}
pub mod build_info {
/// The version of the reflexo-world crate.
pub static VERSION: &str = env!("CARGO_PKG_VERSION");
} }

View file

@ -0,0 +1,111 @@
use std::{io::Read, path::Path};
use js_sys::Uint8Array;
use typst::diag::{eco_format, EcoString};
use wasm_bindgen::{prelude::*, JsValue};
use super::{PackageError, PackageRegistry, PackageSpec};
#[wasm_bindgen]
#[derive(Debug, Clone)]
pub struct ProxyContext {
context: JsValue,
}
#[wasm_bindgen]
impl ProxyContext {
#[wasm_bindgen(constructor)]
pub fn new(context: JsValue) -> Self {
Self { context }
}
#[wasm_bindgen(getter)]
pub fn context(&self) -> JsValue {
self.context.clone()
}
pub fn untar(&self, data: &[u8], cb: js_sys::Function) -> Result<(), JsValue> {
let cb = move |key: String, value: &[u8], mtime: u64| -> Result<(), JsValue> {
let key = JsValue::from_str(&key);
let value = Uint8Array::from(value);
let mtime = JsValue::from_f64(mtime as f64);
cb.call3(&self.context, &key, &value, &mtime).map(|_| ())
};
let decompressed = flate2::read::GzDecoder::new(data);
let mut reader = tar::Archive::new(decompressed);
let entries = reader.entries();
let entries = entries.map_err(|err| {
let t = PackageError::MalformedArchive(Some(eco_format!("{err}")));
JsValue::from_str(&format!("{t:?}"))
})?;
let mut buf = Vec::with_capacity(1024);
for entry in entries {
// Read single entry
let mut entry = entry.map_err(|e| format!("{e:?}"))?;
let header = entry.header();
let is_file = header.entry_type().is_file();
if !is_file {
continue;
}
let mtime = header.mtime().unwrap_or(0);
let path = header.path().map_err(|e| format!("{e:?}"))?;
let path = path.to_string_lossy().as_ref().to_owned();
let size = header.size().map_err(|e| format!("{e:?}"))?;
buf.clear();
buf.reserve(size as usize);
entry.read_to_end(&mut buf).map_err(|e| format!("{e:?}"))?;
cb(path, &buf, mtime)?
}
Ok(())
}
}
#[derive(Debug)]
pub struct ProxyRegistry {
pub context: ProxyContext,
pub real_resolve_fn: js_sys::Function,
}
impl PackageRegistry for ProxyRegistry {
fn resolve(&self, spec: &PackageSpec) -> Result<std::sync::Arc<Path>, PackageError> {
// prepare js_spec
let js_spec = js_sys::Object::new();
js_sys::Reflect::set(&js_spec, &"name".into(), &spec.name.to_string().into()).unwrap();
js_sys::Reflect::set(
&js_spec,
&"namespace".into(),
&spec.namespace.to_string().into(),
)
.unwrap();
js_sys::Reflect::set(
&js_spec,
&"version".into(),
&spec.version.to_string().into(),
)
.unwrap();
self.real_resolve_fn
.call1(&self.context.clone().into(), &js_spec)
.map_err(|e| PackageError::Other(Some(eco_format!("{:?}", e))))
.and_then(|v| {
if v.is_undefined() {
Err(PackageError::NotFound(spec.clone()))
} else {
Ok(Path::new(&v.as_string().unwrap()).into())
}
})
}
// todo: provide package list for browser
fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] {
&[]
}
}

View file

@ -0,0 +1,12 @@
use std::{path::Path, sync::Arc};
use super::{PackageError, PackageRegistry, PackageSpec};
#[derive(Default, Debug)]
pub struct DummyRegistry;
impl PackageRegistry for DummyRegistry {
fn resolve(&self, spec: &PackageSpec) -> Result<Arc<Path>, PackageError> {
Err(PackageError::NotFound(spec.clone()))
}
}

View file

@ -0,0 +1,337 @@
//! Https registry for tinymist.
use std::path::Path;
use std::sync::{Arc, OnceLock};
use parking_lot::Mutex;
use reqwest::blocking::Response;
use reqwest::Certificate;
use tinymist_std::ImmutPath;
use typst::diag::{eco_format, EcoString, PackageResult, StrResult};
use typst::syntax::package::{PackageVersion, VersionlessPackageSpec};
use crate::package::{DummyNotifier, Notifier, PackageError, PackageRegistry, PackageSpec};
/// The http package registry for typst.ts.
pub struct HttpRegistry {
/// The path at which local packages (`@local` packages) are stored.
package_path: Option<ImmutPath>,
/// The path at which non-local packages (`@preview` packages) should be
/// stored when downloaded.
package_cache_path: Option<ImmutPath>,
/// lazily initialized package storage.
storage: OnceLock<PackageStorage>,
/// The path to the certificate file to use for HTTPS requests.
cert_path: Option<ImmutPath>,
/// The notifier to use for progress updates.
notifier: Arc<Mutex<dyn Notifier + Send>>,
// package_dir_cache: RwLock<HashMap<PackageSpec, Result<ImmutPath, PackageError>>>,
}
impl Default for HttpRegistry {
fn default() -> Self {
Self {
notifier: Arc::new(Mutex::<DummyNotifier>::default()),
cert_path: None,
package_path: None,
package_cache_path: None,
storage: OnceLock::new(),
// package_dir_cache: RwLock::new(HashMap::new()),
}
}
}
impl std::ops::Deref for HttpRegistry {
type Target = PackageStorage;
fn deref(&self) -> &Self::Target {
self.storage()
}
}
impl HttpRegistry {
/// Create a new registry.
pub fn new(
cert_path: Option<ImmutPath>,
package_path: Option<ImmutPath>,
package_cache_path: Option<ImmutPath>,
) -> Self {
Self {
cert_path,
package_path,
package_cache_path,
..Default::default()
}
}
/// Get `typst-kit` implementing package storage
pub fn storage(&self) -> &PackageStorage {
self.storage.get_or_init(|| {
PackageStorage::new(
self.package_cache_path
.clone()
.or_else(|| Some(dirs::cache_dir()?.join(DEFAULT_PACKAGES_SUBDIR).into())),
self.package_path
.clone()
.or_else(|| Some(dirs::data_dir()?.join(DEFAULT_PACKAGES_SUBDIR).into())),
self.cert_path.clone(),
self.notifier.clone(),
)
})
}
/// Get local path option
pub fn local_path(&self) -> Option<ImmutPath> {
self.storage().package_path().cloned()
}
/// Get data & cache dir
pub fn paths(&self) -> Vec<ImmutPath> {
let data_dir = self.storage().package_path().cloned();
let cache_dir = self.storage().package_cache_path().cloned();
data_dir.into_iter().chain(cache_dir).collect::<Vec<_>>()
}
/// Set list of packages for testing.
pub fn test_package_list(&self, f: impl FnOnce() -> Vec<(PackageSpec, Option<EcoString>)>) {
self.storage().index.get_or_init(f);
}
}
impl PackageRegistry for HttpRegistry {
fn resolve(&self, spec: &PackageSpec) -> Result<ImmutPath, PackageError> {
self.storage().prepare_package(spec)
}
fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] {
self.storage().download_index()
}
}
/// The default Typst registry.
pub const DEFAULT_REGISTRY: &str = "https://packages.typst.org";
/// The default packages sub directory within the package and package cache
/// paths.
pub const DEFAULT_PACKAGES_SUBDIR: &str = "typst/packages";
/// Holds information about where packages should be stored and downloads them
/// on demand, if possible.
pub struct PackageStorage {
/// The path at which non-local packages should be stored when downloaded.
package_cache_path: Option<ImmutPath>,
/// The path at which local packages are stored.
package_path: Option<ImmutPath>,
/// The downloader used for fetching the index and packages.
cert_path: Option<ImmutPath>,
/// The cached index of the preview namespace.
index: OnceLock<Vec<(PackageSpec, Option<EcoString>)>>,
notifier: Arc<Mutex<dyn Notifier + Send>>,
}
impl PackageStorage {
/// Creates a new package storage for the given package paths.
/// It doesn't fallback directories, thus you can disable the related
/// storage by passing `None`.
pub fn new(
package_cache_path: Option<ImmutPath>,
package_path: Option<ImmutPath>,
cert_path: Option<ImmutPath>,
notifier: Arc<Mutex<dyn Notifier + Send>>,
) -> Self {
Self {
package_cache_path,
package_path,
cert_path,
notifier,
index: OnceLock::new(),
}
}
/// Returns the path at which non-local packages should be stored when
/// downloaded.
pub fn package_cache_path(&self) -> Option<&ImmutPath> {
self.package_cache_path.as_ref()
}
/// Returns the path at which local packages are stored.
pub fn package_path(&self) -> Option<&ImmutPath> {
self.package_path.as_ref()
}
/// Make a package available in the on-disk cache.
pub fn prepare_package(&self, spec: &PackageSpec) -> PackageResult<ImmutPath> {
let subdir = format!("{}/{}/{}", spec.namespace, spec.name, spec.version);
if let Some(packages_dir) = &self.package_path {
let dir = packages_dir.join(&subdir);
if dir.exists() {
return Ok(dir.into());
}
}
if let Some(cache_dir) = &self.package_cache_path {
let dir = cache_dir.join(&subdir);
if dir.exists() {
return Ok(dir.into());
}
// Download from network if it doesn't exist yet.
if spec.namespace == "preview" {
self.download_package(spec, &dir)?;
if dir.exists() {
return Ok(dir.into());
}
}
}
Err(PackageError::NotFound(spec.clone()))
}
/// Try to determine the latest version of a package.
pub fn determine_latest_version(
&self,
spec: &VersionlessPackageSpec,
) -> StrResult<PackageVersion> {
if spec.namespace == "preview" {
// For `@preview`, download the package index and find the latest
// version.
self.download_index()
.iter()
.filter(|(package, _)| package.name == spec.name)
.map(|(package, _)| package.version)
.max()
.ok_or_else(|| eco_format!("failed to find package {spec}"))
} else {
// For other namespaces, search locally. We only search in the data
// directory and not the cache directory, because the latter is not
// intended for storage of local packages.
let subdir = format!("{}/{}", spec.namespace, spec.name);
self.package_path
.iter()
.flat_map(|dir| std::fs::read_dir(dir.join(&subdir)).ok())
.flatten()
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.filter_map(|path| path.file_name()?.to_string_lossy().parse().ok())
.max()
.ok_or_else(|| eco_format!("please specify the desired version"))
}
}
/// Get the cached package index without network access.
pub fn cached_index(&self) -> Option<&[(PackageSpec, Option<EcoString>)]> {
self.index.get().map(Vec::as_slice)
}
/// Download the package index. The result of this is cached for efficiency.
pub fn download_index(&self) -> &[(PackageSpec, Option<EcoString>)] {
self.index.get_or_init(|| {
let url = format!("{DEFAULT_REGISTRY}/preview/index.json");
threaded_http(&url, self.cert_path.as_deref(), |resp| {
let reader = match resp.and_then(|r| r.error_for_status()) {
Ok(response) => response,
Err(err) => {
// todo: silent error
log::error!("Failed to fetch package index: {err} from {url}");
return vec![];
}
};
#[derive(serde::Deserialize)]
struct RemotePackageIndex {
name: EcoString,
version: PackageVersion,
description: Option<EcoString>,
}
let indices: Vec<RemotePackageIndex> = match serde_json::from_reader(reader) {
Ok(index) => index,
Err(err) => {
log::error!("Failed to parse package index: {err} from {url}");
return vec![];
}
};
indices
.into_iter()
.map(|index| {
(
PackageSpec {
namespace: "preview".into(),
name: index.name,
version: index.version,
},
index.description,
)
})
.collect::<Vec<_>>()
})
.unwrap_or_default()
})
}
/// Download a package over the network.
///
/// # Panics
/// Panics if the package spec namespace isn't `preview`.
pub fn download_package(&self, spec: &PackageSpec, package_dir: &Path) -> PackageResult<()> {
assert_eq!(spec.namespace, "preview");
let url = format!(
"{DEFAULT_REGISTRY}/preview/{}-{}.tar.gz",
spec.name, spec.version
);
self.notifier.lock().downloading(spec);
threaded_http(&url, self.cert_path.as_deref(), |resp| {
let reader = match resp.and_then(|r| r.error_for_status()) {
Ok(response) => response,
Err(err) if matches!(err.status().map(|s| s.as_u16()), Some(404)) => {
return Err(PackageError::NotFound(spec.clone()))
}
Err(err) => return Err(PackageError::NetworkFailed(Some(eco_format!("{err}")))),
};
let decompressed = flate2::read::GzDecoder::new(reader);
tar::Archive::new(decompressed)
.unpack(package_dir)
.map_err(|err| {
std::fs::remove_dir_all(package_dir).ok();
PackageError::MalformedArchive(Some(eco_format!("{err}")))
})
})
.ok_or_else(|| PackageError::Other(Some(eco_format!("cannot spawn http thread"))))?
}
}
fn threaded_http<T: Send + Sync>(
url: &str,
cert_path: Option<&Path>,
f: impl FnOnce(Result<Response, reqwest::Error>) -> T + Send + Sync,
) -> Option<T> {
std::thread::scope(|s| {
s.spawn(move || {
let client_builder = reqwest::blocking::Client::builder();
let client = if let Some(cert_path) = cert_path {
let cert = std::fs::read(cert_path)
.ok()
.and_then(|buf| Certificate::from_pem(&buf).ok());
if let Some(cert) = cert {
client_builder.add_root_certificate(cert).build().unwrap()
} else {
client_builder.build().unwrap()
}
} else {
client_builder.build().unwrap()
};
f(client.get(url).send())
})
.join()
.ok()
})
}

View file

@ -0,0 +1,37 @@
impl Notifier for DummyNotifier {}
use std::{path::Path, sync::Arc};
use ecow::EcoString;
pub use typst::diag::PackageError;
pub use typst::syntax::package::PackageSpec;
pub mod dummy;
#[cfg(feature = "browser")]
pub mod browser;
#[cfg(feature = "system")]
pub mod http;
pub trait PackageRegistry {
fn reset(&mut self) {}
fn resolve(&self, spec: &PackageSpec) -> Result<Arc<Path>, PackageError>;
/// A list of all available packages and optionally descriptions for them.
///
/// This function is optional to implement. It enhances the user experience
/// by enabling autocompletion for packages. Details about packages from the
/// `@preview` namespace are available from
/// `https://packages.typst.org/preview/index.json`.
fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] {
&[]
}
}
pub trait Notifier {
fn downloading(&self, _spec: &PackageSpec) {}
}
#[derive(Debug, Default, Clone, Copy, Hash)]
pub struct DummyNotifier;

View file

@ -0,0 +1,8 @@
mod modifier_set;
mod semantic_tokens;
mod typst_tokens;
pub use semantic_tokens::{
get_semantic_tokens_full, get_semantic_tokens_legend, OffsetEncoding, SemanticToken,
SemanticTokensLegend,
};

View file

@ -0,0 +1,33 @@
use std::ops;
use super::typst_tokens::Modifier;
#[derive(Default, Clone, Copy)]
pub struct ModifierSet(u32);
impl ModifierSet {
pub fn empty() -> Self {
Self::default()
}
pub fn new(modifiers: &[Modifier]) -> Self {
let bits = modifiers
.iter()
.copied()
.map(Modifier::bitmask)
.fold(0, |bits, mask| bits | mask);
Self(bits)
}
pub fn bitset(self) -> u32 {
self.0
}
}
impl ops::BitOr for ModifierSet {
type Output = Self;
fn bitor(self, rhs: Self) -> Self::Output {
Self(self.0 | rhs.0)
}
}

View file

@ -0,0 +1,237 @@
//! From <https://github.com/nvarner/typst-lsp/blob/cc7bad9bd9764bfea783f2fab415cb3061fd8bff/src/server/semantic_tokens/mod.rs>
use strum::IntoEnumIterator;
use typst::syntax::{ast, LinkedNode, Source, SyntaxKind};
use super::modifier_set::ModifierSet;
use super::typst_tokens::{Modifier, TokenType};
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
pub struct SemanticTokensLegend {
#[serde(rename = "tokenTypes")]
pub token_types: Vec<String>,
#[serde(rename = "tokenModifiers")]
pub token_modifiers: Vec<String>,
}
pub fn get_semantic_tokens_legend() -> SemanticTokensLegend {
SemanticTokensLegend {
token_types: TokenType::iter()
.map(|e| {
let e: &'static str = e.into();
e.to_owned()
})
.collect(),
token_modifiers: Modifier::iter()
.map(|e| {
let e: &'static str = e.into();
e.to_owned()
})
.collect(),
}
}
#[derive(Debug, Clone, Copy)]
pub enum OffsetEncoding {
Utf8,
Utf16,
}
pub fn get_semantic_tokens_full(source: &Source, encoding: OffsetEncoding) -> Vec<SemanticToken> {
let root = LinkedNode::new(source.root());
let mut full = tokenize_tree(&root, ModifierSet::empty());
let mut init = (0, 0);
for token in full.iter_mut() {
// resolve offset to position
let offset = ((token.delta_line as u64) << 32) | token.delta_start_character as u64;
let position = (match encoding {
OffsetEncoding::Utf8 => offset_to_position_utf8,
OffsetEncoding::Utf16 => offset_to_position_utf16,
})(offset as usize, source);
token.delta_line = position.0;
token.delta_start_character = position.1;
let next = (token.delta_line, token.delta_start_character);
token.delta_line -= init.0;
if token.delta_line == 0 {
token.delta_start_character -= init.1;
}
init = next;
}
full
}
fn tokenize_single_node(node: &LinkedNode, modifiers: ModifierSet) -> Option<SemanticToken> {
let is_leaf = node.children().next().is_none();
token_from_node(node)
.or_else(|| is_leaf.then_some(TokenType::Text))
.map(|token_type| SemanticToken::new(token_type, modifiers, node))
}
/// Tokenize a node and its children
fn tokenize_tree(root: &LinkedNode<'_>, parent_modifiers: ModifierSet) -> Vec<SemanticToken> {
let modifiers = parent_modifiers | modifiers_from_node(root);
let token = tokenize_single_node(root, modifiers).into_iter();
let children = root
.children()
.flat_map(move |child| tokenize_tree(&child, modifiers));
token.chain(children).collect()
}
#[derive(Debug, Clone, Copy)]
pub struct SemanticToken {
pub delta_line: u32,
pub delta_start_character: u32,
pub length: u32,
pub token_type: u32,
pub token_modifiers: u32,
}
impl SemanticToken {
fn new(token_type: TokenType, modifiers: ModifierSet, node: &LinkedNode) -> Self {
let source = node.get().clone().into_text();
let raw_position = node.offset() as u64;
let raw_position = ((raw_position >> 32) as u32, raw_position as u32);
Self {
token_type: token_type as u32,
token_modifiers: modifiers.bitset(),
delta_line: raw_position.0,
delta_start_character: raw_position.1,
length: source.chars().map(char::len_utf16).sum::<usize>() as u32,
}
}
}
/// Determines the [`Modifier`]s to be applied to a node and all its children.
///
/// Note that this does not recurse up, so calling it on a child node may not
/// return a modifier that should be applied to it due to a parent.
fn modifiers_from_node(node: &LinkedNode) -> ModifierSet {
match node.kind() {
SyntaxKind::Emph => ModifierSet::new(&[Modifier::Emph]),
SyntaxKind::Strong => ModifierSet::new(&[Modifier::Strong]),
SyntaxKind::Math | SyntaxKind::Equation => ModifierSet::new(&[Modifier::Math]),
_ => ModifierSet::empty(),
}
}
/// Determines the best [`TokenType`] for an entire node and its children, if
/// any. If there is no single `TokenType`, or none better than `Text`, returns
/// `None`.
///
/// In tokenization, returning `Some` stops recursion, while returning `None`
/// continues and attempts to tokenize each of `node`'s children. If there are
/// no children, `Text` is taken as the default.
fn token_from_node(node: &LinkedNode) -> Option<TokenType> {
use SyntaxKind::*;
match node.kind() {
Star if node.parent_kind() == Some(Strong) => Some(TokenType::Punctuation),
Star if node.parent_kind() == Some(ModuleImport) => Some(TokenType::Operator),
Underscore if node.parent_kind() == Some(Emph) => Some(TokenType::Punctuation),
Underscore if node.parent_kind() == Some(MathAttach) => Some(TokenType::Operator),
MathIdent | Ident => Some(token_from_ident(node)),
Hash => token_from_hashtag(node),
LeftBrace | RightBrace | LeftBracket | RightBracket | LeftParen | RightParen | Comma
| Semicolon | Colon => Some(TokenType::Punctuation),
Linebreak | Escape | Shorthand => Some(TokenType::Escape),
Link => Some(TokenType::Link),
Raw => Some(TokenType::Raw),
Label => Some(TokenType::Label),
RefMarker => Some(TokenType::Ref),
Heading | HeadingMarker => Some(TokenType::Heading),
ListMarker | EnumMarker | TermMarker => Some(TokenType::ListMarker),
MathAlignPoint | Plus | Minus | Slash | Hat | Dot | Eq | EqEq | ExclEq | Lt | LtEq | Gt
| GtEq | PlusEq | HyphEq | StarEq | SlashEq | Dots | Arrow | Not | And | Or => {
Some(TokenType::Operator)
}
Dollar => Some(TokenType::Delimiter),
None | Auto | Let | Show | If | Else | For | In | While | Break | Continue | Return
| Import | Include | As | Set => Some(TokenType::Keyword),
Bool => Some(TokenType::Bool),
Int | Float | Numeric => Some(TokenType::Number),
Str => Some(TokenType::String),
LineComment | BlockComment => Some(TokenType::Comment),
Error => Some(TokenType::Error),
// Disambiguate from `SyntaxKind::None`
_ => Option::None,
}
}
// TODO: differentiate also using tokens in scope, not just context
fn is_function_ident(ident: &LinkedNode) -> bool {
let Some(next) = ident.next_leaf() else {
return false;
};
let function_call = matches!(next.kind(), SyntaxKind::LeftParen)
&& matches!(
next.parent_kind(),
Some(SyntaxKind::Args | SyntaxKind::Params)
);
let function_content = matches!(next.kind(), SyntaxKind::LeftBracket)
&& matches!(next.parent_kind(), Some(SyntaxKind::ContentBlock));
function_call || function_content
}
fn token_from_ident(ident: &LinkedNode) -> TokenType {
if is_function_ident(ident) {
TokenType::Function
} else {
TokenType::Interpolated
}
}
fn get_expr_following_hashtag<'a>(hashtag: &LinkedNode<'a>) -> Option<LinkedNode<'a>> {
hashtag
.next_sibling()
.filter(|next| next.cast::<ast::Expr>().is_some_and(|expr| expr.hash()))
.and_then(|node| node.leftmost_leaf())
}
fn token_from_hashtag(hashtag: &LinkedNode) -> Option<TokenType> {
get_expr_following_hashtag(hashtag)
.as_ref()
.and_then(token_from_node)
}
fn offset_to_position_utf8(typst_offset: usize, typst_source: &Source) -> (u32, u32) {
let line_index = typst_source.byte_to_line(typst_offset).unwrap();
let column_index = typst_source.byte_to_column(typst_offset).unwrap();
(line_index as u32, column_index as u32)
}
fn offset_to_position_utf16(typst_offset: usize, typst_source: &Source) -> (u32, u32) {
let line_index = typst_source.byte_to_line(typst_offset).unwrap();
let lsp_line = line_index as u32;
// See the implementation of `lsp_to_typst::position_to_offset` for discussion
// relevant to this function.
// TODO: Typst's `Source` could easily provide an implementation of the method
// we need here. Submit a PR to `typst` to add it, then update
// this if/when merged.
let utf16_offset = typst_source.byte_to_utf16(typst_offset).unwrap();
let byte_line_offset = typst_source.line_to_byte(line_index).unwrap();
let utf16_line_offset = typst_source.byte_to_utf16(byte_line_offset).unwrap();
let utf16_column_offset = utf16_offset - utf16_line_offset;
let lsp_column = utf16_column_offset;
(lsp_line, lsp_column as u32)
}

View file

@ -0,0 +1,113 @@
//! Types for tokens used for Typst syntax
use strum::EnumIter;
/// Very similar to [`typst_ide::Tag`], but with convenience traits, and
/// extensible because we want to further customize highlighting
#[derive(Debug, Clone, Copy, EnumIter)]
#[repr(u32)]
pub enum TokenType {
// Standard LSP types
Comment,
String,
Keyword,
Operator,
Number,
Function,
Decorator,
// Custom types
Bool,
Punctuation,
Escape,
Link,
Raw,
Label,
Ref,
Heading,
ListMarker,
ListTerm,
Delimiter,
Interpolated,
Error,
/// Any text in markup without a more specific token type, possible styled.
///
/// We perform styling (like bold and italics) via modifiers. That means
/// everything that should receive styling needs to be a token so we can
/// apply a modifier to it. This token type is mostly for that, since
/// text should usually not be specially styled.
Text,
}
impl From<TokenType> for &'static str {
fn from(token_type: TokenType) -> Self {
use TokenType::*;
match token_type {
Comment => "comment",
String => "string",
Keyword => "keyword",
Operator => "operator",
Number => "number",
Function => "function",
Decorator => "decorator",
Bool => "bool",
Punctuation => "punctuation",
Escape => "escape",
Link => "link",
Raw => "raw",
Label => "label",
Ref => "ref",
Heading => "heading",
ListMarker => "marker",
ListTerm => "term",
Delimiter => "delim",
Interpolated => "pol",
Error => "error",
Text => "text",
}
}
}
#[derive(Debug, Clone, Copy, EnumIter)]
#[repr(u8)]
pub enum Modifier {
Strong,
Emph,
Math,
}
impl Modifier {
pub fn index(self) -> u8 {
self as u8
}
pub fn bitmask(self) -> u32 {
0b1 << self.index()
}
}
impl From<Modifier> for &'static str {
fn from(modifier: Modifier) -> Self {
use Modifier::*;
match modifier {
Strong => "strong",
Emph => "emph",
Math => "math",
}
}
}
#[cfg(test)]
mod test {
use strum::IntoEnumIterator;
use super::*;
#[test]
fn ensure_not_too_many_modifiers() {
// Because modifiers are encoded in a 32 bit bitmask, we can't have more than 32
// modifiers
assert!(Modifier::iter().len() <= 32);
}
}

View file

@ -0,0 +1,256 @@
// use std::sync::Arc;
use core::fmt;
use std::{num::NonZeroUsize, sync::Arc};
use parking_lot::{Mutex, RwLock};
use tinymist_std::hash::FxHashMap;
use tinymist_std::{ImmutPath, QueryRef};
use tinymist_vfs::{Bytes, FileId, FsProvider, TypstFileId};
use typst::{
diag::{FileError, FileResult},
syntax::Source,
};
/// incrementally query a value from a self holding state
type IncrQueryRef<S, E> = QueryRef<S, E, Option<S>>;
type FileQuery<T> = QueryRef<T, FileError>;
type IncrFileQuery<T> = IncrQueryRef<T, FileError>;
pub trait Revised {
fn last_accessed_rev(&self) -> NonZeroUsize;
}
pub struct SharedState<T> {
pub committed_revision: Option<usize>,
// todo: fine-grained lock
/// The cache entries for each paths
cache_entries: FxHashMap<TypstFileId, T>,
}
impl<T> fmt::Debug for SharedState<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SharedState")
.field("committed_revision", &self.committed_revision)
.finish()
}
}
impl<T> Default for SharedState<T> {
fn default() -> Self {
SharedState {
committed_revision: None,
cache_entries: FxHashMap::default(),
}
}
}
impl<T: Revised> SharedState<T> {
fn gc(&mut self) {
let committed = self.committed_revision.unwrap_or(0);
self.cache_entries
.retain(|_, v| committed.saturating_sub(v.last_accessed_rev().get()) <= 30);
}
}
pub struct SourceCache {
last_accessed_rev: NonZeroUsize,
fid: FileId,
source: IncrFileQuery<Source>,
buffer: FileQuery<Bytes>,
}
impl fmt::Debug for SourceCache {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SourceCache").finish()
}
}
impl Revised for SourceCache {
fn last_accessed_rev(&self) -> NonZeroUsize {
self.last_accessed_rev
}
}
pub struct SourceState {
pub revision: NonZeroUsize,
pub slots: Arc<Mutex<FxHashMap<TypstFileId, SourceCache>>>,
}
impl SourceState {
pub fn commit_impl(self, state: &mut SharedState<SourceCache>) {
log::debug!("drop source db revision {}", self.revision);
if let Ok(slots) = Arc::try_unwrap(self.slots) {
// todo: utilize the committed revision is not zero
if state
.committed_revision
.is_some_and(|committed| committed >= self.revision.get())
{
return;
}
log::debug!("committing source db revision {}", self.revision);
state.committed_revision = Some(self.revision.get());
state.cache_entries = slots.into_inner();
state.gc();
}
}
}
#[derive(Clone)]
pub struct SourceDb {
pub revision: NonZeroUsize,
pub shared: Arc<RwLock<SharedState<SourceCache>>>,
/// The slots for all the files during a single lifecycle.
pub slots: Arc<Mutex<FxHashMap<TypstFileId, SourceCache>>>,
/// Whether to reparse the file when it is changed.
/// Default to `true`.
pub do_reparse: bool,
}
impl fmt::Debug for SourceDb {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SourceDb").finish()
}
}
impl SourceDb {
pub fn take_state(&mut self) -> SourceState {
SourceState {
revision: self.revision,
slots: std::mem::take(&mut self.slots),
}
}
/// Set the `do_reparse` flag that indicates whether to reparsing the file
/// instead of creating a new [`Source`] when the file is changed.
/// Default to `true`.
///
/// You usually want to set this flag to `true` for better performance.
/// However, one could disable this flag for debugging purpose.
pub fn set_do_reparse(&mut self, do_reparse: bool) {
self.do_reparse = do_reparse;
}
/// Returns the overall memory usage for the stored files.
pub fn memory_usage(&self) -> usize {
let mut w = self.slots.lock().len() * core::mem::size_of::<SourceCache>();
w += self
.slots
.lock()
.iter()
.map(|(_, slot)| {
slot.source
.get_uninitialized()
.and_then(|e| e.as_ref().ok())
.map_or(16, |e| e.text().len() * 8)
+ slot
.buffer
.get_uninitialized()
.and_then(|e| e.as_ref().ok())
.map_or(16, |e| e.len())
})
.sum::<usize>();
w
}
/// Get all the files that are currently in the VFS.
///
/// This is typically corresponds to the file dependencies of a single
/// compilation.
///
/// When you don't reset the vfs for each compilation, this function will
/// still return remaining files from the previous compilation.
pub fn iter_dependencies_dyn<'a>(
&'a self,
p: &'a impl FsProvider,
f: &mut dyn FnMut(ImmutPath),
) {
for slot in self.slots.lock().iter() {
f(p.file_path(slot.1.fid));
}
}
/// Get file content by path.
pub fn file(&self, id: TypstFileId, fid: FileId, p: &impl FsProvider) -> FileResult<Bytes> {
self.slot(id, fid, |slot| slot.buffer.compute(|| p.read(fid)).cloned())
}
/// Get source content by path and assign the source with a given typst
/// global file id.
///
/// See `Vfs::resolve_with_f` for more information.
pub fn source(&self, id: TypstFileId, fid: FileId, p: &impl FsProvider) -> FileResult<Source> {
self.slot(id, fid, |slot| {
slot.source
.compute_with_context(|prev| {
let content = p.read(fid)?;
let next = from_utf8_or_bom(&content)?.to_owned();
// otherwise reparse the source
match prev {
Some(mut source) if self.do_reparse => {
source.replace(&next);
Ok(source)
}
// Return a new source if we don't have a reparse feature or no prev
_ => Ok(Source::new(id, next)),
}
})
.cloned()
})
}
/// Insert a new slot into the vfs.
fn slot<T>(&self, id: TypstFileId, fid: FileId, f: impl FnOnce(&SourceCache) -> T) -> T {
let mut slots = self.slots.lock();
f(slots.entry(id).or_insert_with(|| {
let state = self.shared.read();
let cache_entry = state.cache_entries.get(&id);
cache_entry
.map(|e| SourceCache {
last_accessed_rev: self.revision.max(e.last_accessed_rev),
fid,
source: IncrFileQuery::with_context(
e.source
.get_uninitialized()
.cloned()
.transpose()
.ok()
.flatten(),
),
buffer: FileQuery::default(),
})
.unwrap_or_else(|| SourceCache {
last_accessed_rev: self.revision,
fid,
source: IncrFileQuery::with_context(None),
buffer: FileQuery::default(),
})
}))
}
}
pub trait MergeCache: Sized {
fn merge(self, _other: Self) -> Self {
self
}
}
pub struct FontDb {}
pub struct PackageDb {}
/// Convert a byte slice to a string, removing UTF-8 BOM if present.
fn from_utf8_or_bom(buf: &[u8]) -> FileResult<&str> {
Ok(std::str::from_utf8(if buf.starts_with(b"\xef\xbb\xbf") {
// remove UTF-8 BOM
&buf[3..]
} else {
// Assume UTF-8
buf
})?)
}

Some files were not shown because too many files have changed in this diff Show more