diff --git a/src-tauri/src/assets.rs b/src-tauri/src/assets.rs index cb4764c..94c4eca 100644 --- a/src-tauri/src/assets.rs +++ b/src-tauri/src/assets.rs @@ -245,7 +245,7 @@ pub fn asset_files(app: &AppHandle, store: &Store) -> HashMap> { if name.starts_with('.') { continue; } - if let Ok(data) = std::fs::read(entry.path()) { + if let Some(data) = crate::workspace::read_file_cached(&entry.path()) { files.insert(name, data); } } diff --git a/src-tauri/src/workspace.rs b/src-tauri/src/workspace.rs index 0185fb1..fa5918e 100644 --- a/src-tauri/src/workspace.rs +++ b/src-tauri/src/workspace.rs @@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::collections::HashMap; use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex, OnceLock}; +use std::time::SystemTime; use tauri::{AppHandle, Manager}; use crate::db::Store; @@ -25,6 +27,34 @@ pub fn content_hash(bytes: &[u8]) -> String { format!("{:x}", Sha256::digest(bytes)) } +pub fn read_file_cached(path: &Path) -> Option> { + type Cache = Mutex>)>>; + static CACHE: OnceLock = OnceLock::new(); + + let metadata = std::fs::metadata(path).ok()?; + let modified = metadata.modified().ok()?; + let size = metadata.len(); + let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new())); + + if let Ok(map) = cache.lock() { + if let Some((stamp, cached_size, bytes)) = map.get(path) { + if *stamp == modified && *cached_size == size { + return Some(bytes.as_ref().clone()); + } + } + } + + let bytes = std::fs::read(path).ok()?; + if let Ok(mut map) = cache.lock() { + map.insert( + path.to_path_buf(), + (modified, size, Arc::new(bytes.clone())), + ); + } + + Some(bytes) +} + #[derive(Serialize, Deserialize, Clone)] pub struct Settings { pub workspace_root: String, @@ -128,8 +158,6 @@ pub struct BrowseEntry { pub last_synced_at: Option, pub child_count: usize, pub cloud_linked: bool, - /// "synced" when nothing changed since the last sync, "pending" when local - /// edits are waiting to go up, or None when the entry is not linked. pub sync_state: Option, } @@ -161,8 +189,6 @@ fn newest_change(dir: &Path) -> Option> { newest } -/// Compares when the entry last changed on disk against when it was last -/// synced. Metadata only, so browsing stays cheap. pub fn sync_state_of(path: &Path, synced_at: Option<&str>) -> Option { sync_state_for(modified_time(path), synced_at) } @@ -257,8 +283,6 @@ pub fn browse(app: &AppHandle, store: &Store, relative: &str) -> Result Result>, St let mut map = HashMap::new(); for relative in collect_files(project_dir)? { let full = project_file_path(project_dir, &relative)?; - let bytes = std::fs::read(&full).map_err(|e| e.to_string())?; - map.insert(relative, bytes); + if let Some(bytes) = read_file_cached(&full) { + map.insert(relative, bytes); + } } Ok(map) } @@ -455,9 +480,6 @@ pub struct FileEntry { pub size: u64, } -/// Lists a project's contents for the editor tree. Unlike `collect_files`, -/// which feeds sync and only cares about file contents, this includes -/// directories so an empty folder is still visible after it is created. pub fn list_files(project_dir: &Path) -> Result, String> { let mut entries = Vec::new(); diff --git a/src-tauri/src/world.rs b/src-tauri/src/world.rs index 50a4421..7b211d9 100644 --- a/src-tauri/src/world.rs +++ b/src-tauri/src/world.rs @@ -1,5 +1,6 @@ use chrono::Datelike; use std::collections::HashMap; +use std::sync::{Mutex, OnceLock}; use typst::diag::{FileError, FileResult}; use typst::foundations::{Bytes, Datetime, Duration}; @@ -23,6 +24,37 @@ fn normalize_path(path: &str) -> String { path.trim_start_matches('/').replace('\\', "/") } +fn bundled_fonts() -> &'static Vec { + static FONTS: OnceLock> = OnceLock::new(); + FONTS.get_or_init(|| { + let mut fonts = Vec::new(); + for data in typst_assets::fonts() { + fonts.extend(Font::iter(Bytes::new(data))); + } + fonts + }) +} + +fn custom_fonts(name: &str, data: &[u8]) -> Vec { + static CACHE: OnceLock>>> = OnceLock::new(); + let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new())); + let key = (name.to_string(), data.len()); + + if let Ok(map) = cache.lock() { + if let Some(fonts) = map.get(&key) { + return fonts.clone(); + } + } + + let fonts: Vec = Font::iter(Bytes::new(data.to_vec())).collect(); + + if let Ok(mut map) = cache.lock() { + map.insert(key, fonts.clone()); + } + + fonts +} + impl ProjectWorld { pub fn new(entrypoint: String, files: HashMap>, enable_html: bool) -> Self { let main = FileId::new(RootedPath::new( @@ -33,16 +65,7 @@ impl ProjectWorld { let downloader = SystemDownloader::new("TypstDesktop (typst-kit)"); let packages = SystemPackages::new(downloader); - let mut book = FontBook::new(); - let mut fonts = Vec::new(); - - for data in typst_assets::fonts() { - let buffer = Bytes::new(data); - for font in Font::iter(buffer) { - book.push(font.info().clone()); - fonts.push(font); - } - } + let mut fonts = bundled_fonts().clone(); for (name, data) in &files { let lower = name.to_lowercase(); @@ -50,13 +73,15 @@ impl ProjectWorld { .iter() .any(|ext| lower.ends_with(ext)) { - for font in Font::iter(Bytes::new(data.clone())) { - book.push(font.info().clone()); - fonts.push(font); - } + fonts.extend(custom_fonts(name, data)); } } + let mut book = FontBook::new(); + for font in &fonts { + book.push(font.info().clone()); + } + let library = if enable_html { Library::builder() .with_features([typst::Feature::Html].into_iter().collect())