1use std::{
9 path::{Path, PathBuf},
10 sync::{Arc, LazyLock},
11};
12
13use tinymist_vfs::{
14 RootResolver, Vfs,
15 mock::{MockChange, MockPathAccess, MockWorkspace},
16};
17use typst::{
18 Features,
19 diag::FileResult,
20 foundations::{Bytes, Dict},
21 syntax::VirtualPath,
22 utils::LazyHash,
23};
24
25use crate::{
26 CompilerFeat, CompilerUniverse, CompilerWorld, EntryState,
27 font::{FontResolverImpl, memory::MemoryFontSearcher},
28 package::{RegistryPathMapper, registry::DummyRegistry},
29};
30
31#[derive(Debug, Clone, Copy)]
33pub struct MockCompilerFeat;
34
35impl CompilerFeat for MockCompilerFeat {
36 type FontResolver = FontResolverImpl;
37 type AccessModel = MockPathAccess;
38 type Registry = DummyRegistry;
39}
40
41pub type MockUniverse = CompilerUniverse<MockCompilerFeat>;
43
44pub type MockWorld = CompilerWorld<MockCompilerFeat>;
46
47pub trait MockWorkspaceWorldExt {
49 fn entry_state(&self, entry: impl AsRef<Path>) -> FileResult<EntryState>;
51
52 fn world(&self, entry: impl Into<PathBuf>) -> MockWorldBuilder;
54}
55
56impl MockWorkspaceWorldExt for MockWorkspace {
57 fn entry_state(&self, entry: impl AsRef<Path>) -> FileResult<EntryState> {
58 Ok(EntryState::new_rooted(
59 self.root_path(),
60 Some(VirtualPath::new(
61 self.path(entry)
62 .strip_prefix(self.root())
63 .map_err(|_| typst::diag::FileError::AccessDenied)?,
64 )),
65 ))
66 }
67
68 fn world(&self, entry: impl Into<PathBuf>) -> MockWorldBuilder {
69 MockWorldBuilder::new(self.clone(), entry)
70 }
71}
72
73pub trait MockWorldChangeExt {
75 fn apply_to_universe<F>(&self, universe: &mut CompilerUniverse<F>)
77 where
78 F: CompilerFeat;
79}
80
81impl MockWorldChangeExt for MockChange {
82 fn apply_to_universe<F>(&self, universe: &mut CompilerUniverse<F>)
83 where
84 F: CompilerFeat,
85 {
86 universe.increment_revision(|universe| {
87 universe.vfs().notify_fs_changes(self.changeset().clone());
88 });
89 }
90}
91
92#[derive(Debug, Clone)]
94pub struct MockWorldBuilder {
95 workspace: MockWorkspace,
96 entry: PathBuf,
97 features: Features,
98 inputs: Option<Arc<LazyHash<Dict>>>,
99 font_resolver: Option<Arc<FontResolverImpl>>,
100 creation_timestamp: Option<i64>,
101}
102
103impl MockWorldBuilder {
104 pub fn new(workspace: MockWorkspace, entry: impl Into<PathBuf>) -> Self {
106 Self {
107 workspace,
108 entry: entry.into(),
109 features: Features::default(),
110 inputs: None,
111 font_resolver: None,
112 creation_timestamp: None,
113 }
114 }
115
116 pub fn with_features(mut self, features: Features) -> Self {
118 self.features = features;
119 self
120 }
121
122 pub fn with_inputs(mut self, inputs: Dict) -> Self {
124 self.inputs = Some(Arc::new(LazyHash::new(inputs)));
125 self
126 }
127
128 pub fn with_lazy_inputs(mut self, inputs: Arc<LazyHash<Dict>>) -> Self {
130 self.inputs = Some(inputs);
131 self
132 }
133
134 pub fn with_font_resolver(mut self, resolver: Arc<FontResolverImpl>) -> Self {
136 self.font_resolver = Some(resolver);
137 self
138 }
139
140 pub fn with_creation_timestamp(mut self, timestamp: Option<i64>) -> Self {
142 self.creation_timestamp = timestamp;
143 self
144 }
145
146 pub fn build_universe(&self) -> FileResult<MockUniverse> {
148 let registry = Arc::new(DummyRegistry);
149 let resolver: Arc<dyn RootResolver + Send + Sync> =
150 Arc::new(RegistryPathMapper::new(registry.clone()));
151
152 Ok(CompilerUniverse::new_raw(
153 self.workspace.entry_state(&self.entry)?,
154 self.features.clone(),
155 self.inputs.clone(),
156 Vfs::new(resolver, self.workspace.access_model()),
157 registry,
158 self.font_resolver
159 .clone()
160 .unwrap_or_else(embedded_font_resolver),
161 self.creation_timestamp,
162 ))
163 }
164
165 pub fn build_world(&self) -> FileResult<MockWorld> {
167 Ok(self.build_universe()?.snapshot())
168 }
169}
170
171pub fn embedded_font_resolver() -> Arc<FontResolverImpl> {
173 static FONT_RESOLVER: LazyLock<Arc<FontResolverImpl>> = LazyLock::new(|| {
174 let mut searcher = MemoryFontSearcher::new();
175 for font in typst_assets::fonts() {
176 searcher.add_memory_font(Bytes::new(font));
177 }
178 Arc::new(searcher.build())
179 });
180
181 FONT_RESOLVER.clone()
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn mock_world_reads_follow_up_updates() {
190 let workspace = MockWorkspace::default_builder()
191 .file("main.typ", "#import \"content.typ\": value\n#value")
192 .file("content.typ", "#let value = [before]")
193 .build();
194 let mut universe = workspace.world("main.typ").build_universe().unwrap();
195 let content_path = workspace.path("content.typ");
196
197 assert_eq!(
198 universe
199 .snapshot()
200 .source_by_path(&content_path)
201 .unwrap()
202 .text(),
203 "#let value = [before]"
204 );
205
206 workspace
207 .update_source("content.typ", "#let value = [after]")
208 .apply_to_universe(&mut universe);
209
210 assert_eq!(
211 universe
212 .snapshot()
213 .source_by_path(&content_path)
214 .unwrap()
215 .text(),
216 "#let value = [after]"
217 );
218 }
219
220 #[test]
221 fn mock_world_handles_rename_remove_sequence() {
222 let workspace = MockWorkspace::default_builder()
223 .file("main.typ", "#import \"content.typ\": value\n#value")
224 .file("content.typ", "#let value = [before]")
225 .build();
226 let mut universe = workspace.world("main.typ").build_universe().unwrap();
227 let content_path = workspace.path("content.typ");
228 let renamed_path = workspace.path("renamed.typ");
229 let main_path = workspace.path("main.typ");
230
231 workspace
232 .rename("content.typ", "renamed.typ")
233 .unwrap()
234 .apply_to_universe(&mut universe);
235 workspace
236 .update_source("main.typ", "#import \"renamed.typ\": value\n#value")
237 .apply_to_universe(&mut universe);
238
239 let world = universe.snapshot();
240 assert!(world.source_by_path(&content_path).is_err());
241 assert_eq!(
242 world.source_by_path(&renamed_path).unwrap().text(),
243 "#let value = [before]"
244 );
245
246 workspace
247 .remove("renamed.typ")
248 .unwrap()
249 .apply_to_universe(&mut universe);
250 workspace
251 .update_source("main.typ", "#let value = [inline]\n#value")
252 .apply_to_universe(&mut universe);
253
254 let world = universe.snapshot();
255 assert!(world.source_by_path(&renamed_path).is_err());
256 assert_eq!(
257 world.source_by_path(&main_path).unwrap().text(),
258 "#let value = [inline]\n#value"
259 );
260 }
261}