mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-11-23 12:46:43 +00:00
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:
parent
a9437b2772
commit
6180e343e0
122 changed files with 7829 additions and 439 deletions
140
Cargo.lock
generated
140
Cargo.lock
generated
|
|
@ -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"
|
|
||||||
|
|
|
||||||
47
Cargo.toml
47
Cargo.toml
|
|
@ -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/" }
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
43
crates/tinymist-project/Cargo.toml
Normal file
43
crates/tinymist-project/Cargo.toml
Normal 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
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
@ -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(
|
||||||
597
crates/tinymist-project/src/watch.rs
Normal file
597
crates/tinymist-project/src/watch.rs
Normal 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...");
|
||||||
|
}
|
||||||
167
crates/tinymist-project/src/world.rs
Normal file
167
crates/tinymist-project/src/world.rs
Normal 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())),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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" }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()))
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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::*;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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::{
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use reflexo::TakeAs;
|
use tinymist_std::TakeAs;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::syntax::DocString;
|
use crate::syntax::DocString;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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::*;
|
||||||
|
|
|
||||||
|
|
@ -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},
|
||||||
|
|
|
||||||
|
|
@ -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")))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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::*;
|
||||||
|
|
|
||||||
|
|
@ -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))?;
|
||||||
|
|
|
||||||
|
|
@ -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};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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},
|
||||||
|
|
|
||||||
|
|
@ -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::{
|
||||||
|
|
|
||||||
|
|
@ -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::*};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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::*;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(());
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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::{
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
||||||
|
|
|
||||||
|
|
@ -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::*;
|
||||||
|
|
|
||||||
60
crates/tinymist-std/Cargo.toml
Normal file
60
crates/tinymist-std/Cargo.toml
Normal 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
|
||||||
5
crates/tinymist-std/README.md
Normal file
5
crates/tinymist-std/README.md
Normal 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)
|
||||||
111
crates/tinymist-std/src/adt/fmap.rs
Normal file
111
crates/tinymist-std/src/adt/fmap.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
5
crates/tinymist-std/src/adt/mod.rs
Normal file
5
crates/tinymist-std/src/adt/mod.rs
Normal 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;
|
||||||
30
crates/tinymist-std/src/concepts/cow_mut.rs
Normal file
30
crates/tinymist-std/src/concepts/cow_mut.rs
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
61
crates/tinymist-std/src/concepts/hash.rs
Normal file
61
crates/tinymist-std/src/concepts/hash.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
32
crates/tinymist-std/src/concepts/marker.rs
Normal file
32
crates/tinymist-std/src/concepts/marker.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
22
crates/tinymist-std/src/concepts/mod.rs
Normal file
22
crates/tinymist-std/src/concepts/mod.rs
Normal 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>;
|
||||||
83
crates/tinymist-std/src/concepts/query.rs
Normal file
83
crates/tinymist-std/src/concepts/query.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
3
crates/tinymist-std/src/concepts/read.rs
Normal file
3
crates/tinymist-std/src/concepts/read.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub trait ReadAllOnce {
|
||||||
|
fn read_all(self, buf: &mut Vec<u8>) -> std::io::Result<usize>;
|
||||||
|
}
|
||||||
17
crates/tinymist-std/src/concepts/takable.rs
Normal file
17
crates/tinymist-std/src/concepts/takable.rs
Normal 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
216
crates/tinymist-std/src/debug_loc.rs
Normal file
216
crates/tinymist-std/src/debug_loc.rs
Normal 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),
|
||||||
|
}
|
||||||
327
crates/tinymist-std/src/error.rs
Normal file
327
crates/tinymist-std/src/error.rs
Normal 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>();
|
||||||
|
}
|
||||||
313
crates/tinymist-std/src/hash.rs
Normal file
313
crates/tinymist-std/src/hash.rs
Normal 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);
|
||||||
|
}
|
||||||
27
crates/tinymist-std/src/lib.rs
Normal file
27
crates/tinymist-std/src/lib.rs
Normal 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);
|
||||||
229
crates/tinymist-std/src/path.rs
Normal file
229
crates/tinymist-std/src/path.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
crates/tinymist-std/src/time.rs
Normal file
23
crates/tinymist-std/src/time.rs
Normal 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
|
||||||
|
}
|
||||||
32
crates/tinymist-vfs/Cargo.toml
Normal file
32
crates/tinymist-vfs/Cargo.toml
Normal 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
|
||||||
5
crates/tinymist-vfs/README.md
Normal file
5
crates/tinymist-vfs/README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# reflexo-vfs
|
||||||
|
|
||||||
|
Vfs for reflexo.
|
||||||
|
|
||||||
|
See [Typst.ts](https://github.com/Myriad-Dreamin/typst.ts)
|
||||||
101
crates/tinymist-vfs/src/browser.rs
Normal file
101
crates/tinymist-vfs/src/browser.rs
Normal 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 {}
|
||||||
33
crates/tinymist-vfs/src/dummy.rs
Normal file
33
crates/tinymist-vfs/src/dummy.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
357
crates/tinymist-vfs/src/lib.rs
Normal file
357
crates/tinymist-vfs/src/lib.rs
Normal 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>>();
|
||||||
|
}
|
||||||
|
}
|
||||||
279
crates/tinymist-vfs/src/notify.rs
Normal file
279
crates/tinymist-vfs/src/notify.rs
Normal 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,
|
||||||
|
}
|
||||||
121
crates/tinymist-vfs/src/overlay.rs
Normal file
121
crates/tinymist-vfs/src/overlay.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
71
crates/tinymist-vfs/src/path_interner.rs
Normal file
71
crates/tinymist-vfs/src/path_interner.rs
Normal 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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
83
crates/tinymist-vfs/src/system.rs
Normal file
83
crates/tinymist-vfs/src/system.rs
Normal 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,
|
||||||
|
}
|
||||||
81
crates/tinymist-vfs/src/trace.rs
Normal file
81
crates/tinymist-vfs/src/trace.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
23
crates/tinymist-vfs/src/utils.rs
Normal file
23
crates/tinymist-vfs/src/utils.rs
Normal 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;
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
5
crates/tinymist-world/README.md
Normal file
5
crates/tinymist-world/README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# reflexo-world
|
||||||
|
|
||||||
|
Typst's World implementation for reflexo.
|
||||||
|
|
||||||
|
See [Typst.ts](https://github.com/Myriad-Dreamin/typst.ts)
|
||||||
113
crates/tinymist-world/src/args.rs
Normal file
113
crates/tinymist-world/src/args.rs
Normal 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())
|
||||||
|
}
|
||||||
52
crates/tinymist-world/src/browser.rs
Normal file
52
crates/tinymist-world/src/browser.rs
Normal 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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
68
crates/tinymist-world/src/config.rs
Normal file
68
crates/tinymist-world/src/config.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
219
crates/tinymist-world/src/entry.rs
Normal file
219
crates/tinymist-world/src/entry.rs
Normal 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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
crates/tinymist-world/src/font/cache.rs
Normal file
26
crates/tinymist-world/src/font/cache.rs
Normal 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)],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
83
crates/tinymist-world/src/font/info.rs
Normal file
83
crates/tinymist-world/src/font/info.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
43
crates/tinymist-world/src/font/loader.rs
Normal file
43
crates/tinymist-world/src/font/loader.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
25
crates/tinymist-world/src/font/mod.rs
Normal file
25
crates/tinymist-world/src/font/mod.rs
Normal 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::*;
|
||||||
52
crates/tinymist-world/src/font/partial_book.rs
Normal file
52
crates/tinymist-world/src/font/partial_book.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
138
crates/tinymist-world/src/font/profile.rs
Normal file
138
crates/tinymist-world/src/font/profile.rs
Normal 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}")
|
||||||
|
}
|
||||||
56
crates/tinymist-world/src/font/pure.rs
Normal file
56
crates/tinymist-world/src/font/pure.rs
Normal 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(),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
201
crates/tinymist-world/src/font/resolver.rs
Normal file
201
crates/tinymist-world/src/font/resolver.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
68
crates/tinymist-world/src/font/slot.rs
Normal file
68
crates/tinymist-world/src/font/slot.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
302
crates/tinymist-world/src/font/system.rs
Normal file
302
crates/tinymist-world/src/font/system.rs
Normal 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
477
crates/tinymist-world/src/font/web/mod.rs
Normal file
477
crates/tinymist-world/src/font/web/mod.rs
Normal 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
111
crates/tinymist-world/src/package/browser.rs
Normal file
111
crates/tinymist-world/src/package/browser.rs
Normal 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>)] {
|
||||||
|
&[]
|
||||||
|
}
|
||||||
|
}
|
||||||
12
crates/tinymist-world/src/package/dummy.rs
Normal file
12
crates/tinymist-world/src/package/dummy.rs
Normal 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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
337
crates/tinymist-world/src/package/http.rs
Normal file
337
crates/tinymist-world/src/package/http.rs
Normal 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()
|
||||||
|
})
|
||||||
|
}
|
||||||
37
crates/tinymist-world/src/package/mod.rs
Normal file
37
crates/tinymist-world/src/package/mod.rs
Normal 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;
|
||||||
8
crates/tinymist-world/src/parser/mod.rs
Normal file
8
crates/tinymist-world/src/parser/mod.rs
Normal 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,
|
||||||
|
};
|
||||||
33
crates/tinymist-world/src/parser/modifier_set.rs
Normal file
33
crates/tinymist-world/src/parser/modifier_set.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
237
crates/tinymist-world/src/parser/semantic_tokens.rs
Normal file
237
crates/tinymist-world/src/parser/semantic_tokens.rs
Normal 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)
|
||||||
|
}
|
||||||
113
crates/tinymist-world/src/parser/typst_tokens.rs
Normal file
113
crates/tinymist-world/src/parser/typst_tokens.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
256
crates/tinymist-world/src/source.rs
Normal file
256
crates/tinymist-world/src/source.rs
Normal 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
Loading…
Add table
Add a link
Reference in a new issue