1use std::{
8 collections::BTreeMap,
9 path::{Path, PathBuf},
10 sync::{Arc, RwLock},
11};
12
13use typst::{
14 diag::{FileError, FileResult},
15 foundations::Bytes,
16 syntax::VirtualPath,
17};
18
19use crate::{
20 FileChangeSet, FileId, FileSnapshot, FilesystemEvent, ImmutPath, MemoryEvent, PathAccessModel,
21 RootResolver, Vfs, WorkspaceResolver,
22};
23
24type SharedFiles = Arc<RwLock<BTreeMap<PathBuf, FileSnapshot>>>;
25
26#[derive(Debug, Clone)]
32pub struct MockPathAccess {
33 files: SharedFiles,
34}
35
36impl PathAccessModel for MockPathAccess {
37 fn content(&self, src: &Path) -> FileResult<Bytes> {
38 self.files
39 .read()
40 .expect("mock workspace lock poisoned")
41 .get(src)
42 .ok_or_else(|| FileError::NotFound(src.into()))
43 .and_then(|snapshot| snapshot.content().cloned())
44 }
45}
46
47#[derive(Debug, Default)]
49pub struct MockRootResolver;
50
51impl RootResolver for MockRootResolver {
52 fn resolve_package_root(
53 &self,
54 _pkg: &typst::syntax::package::PackageSpec,
55 ) -> FileResult<ImmutPath> {
56 Err(FileError::AccessDenied)
57 }
58}
59
60#[derive(Debug, Clone)]
66pub struct MockWorkspace {
67 root: PathBuf,
68 files: SharedFiles,
69}
70
71impl Default for MockWorkspace {
72 fn default() -> Self {
73 Self::new(default_mock_root())
74 }
75}
76
77impl MockWorkspace {
78 pub fn new(root: impl Into<PathBuf>) -> Self {
80 Self {
81 root: root.into(),
82 files: Arc::default(),
83 }
84 }
85
86 pub fn builder(root: impl Into<PathBuf>) -> MockWorkspaceBuilder {
88 MockWorkspaceBuilder::new(root)
89 }
90
91 pub fn default_builder() -> MockWorkspaceBuilder {
93 MockWorkspaceBuilder::new(default_mock_root())
94 }
95
96 pub fn root(&self) -> &Path {
98 &self.root
99 }
100
101 pub fn root_path(&self) -> ImmutPath {
103 immut_path(self.root.clone())
104 }
105
106 pub fn path(&self, path: impl AsRef<Path>) -> PathBuf {
108 let path = path.as_ref();
109 if path.is_absolute() {
110 path.to_owned()
111 } else {
112 self.root.join(path)
113 }
114 }
115
116 pub fn immut_path(&self, path: impl AsRef<Path>) -> ImmutPath {
118 immut_path(self.path(path))
119 }
120
121 pub fn virtual_path(&self, path: impl AsRef<Path>) -> FileResult<VirtualPath> {
123 let path = self.path(path);
124 let relative = path
125 .strip_prefix(&self.root)
126 .map_err(|_| FileError::AccessDenied)?;
127 Ok(VirtualPath::new(relative))
128 }
129
130 pub fn file_id(&self, path: impl AsRef<Path>) -> FileResult<FileId> {
132 Ok(WorkspaceResolver::workspace_file(
133 Some(&self.root_path()),
134 self.virtual_path(path)?,
135 ))
136 }
137
138 pub fn access_model(&self) -> MockPathAccess {
140 MockPathAccess {
141 files: self.files.clone(),
142 }
143 }
144
145 pub fn vfs(&self) -> Vfs<MockPathAccess> {
147 Vfs::new(Arc::new(MockRootResolver), self.access_model())
148 }
149
150 pub fn read(&self, path: impl AsRef<Path>) -> FileResult<Bytes> {
152 let path = self.path(path);
153 self.files
154 .read()
155 .expect("mock workspace lock poisoned")
156 .get(&path)
157 .ok_or_else(|| FileError::NotFound(path.clone()))
158 .and_then(|snapshot| snapshot.content().cloned())
159 }
160
161 pub fn contains(&self, path: impl AsRef<Path>) -> bool {
163 self.files
164 .read()
165 .expect("mock workspace lock poisoned")
166 .contains_key(&self.path(path))
167 }
168
169 pub fn write_source(&self, path: impl AsRef<Path>, source: impl Into<String>) -> MockChange {
171 self.write_bytes(path, Bytes::from_string(source.into()))
172 }
173
174 pub fn create_source(&self, path: impl AsRef<Path>, source: impl Into<String>) -> MockChange {
176 self.write_source(path, source)
177 }
178
179 pub fn update_source(&self, path: impl AsRef<Path>, source: impl Into<String>) -> MockChange {
181 self.write_source(path, source)
182 }
183
184 pub fn write_bytes(&self, path: impl AsRef<Path>, bytes: Bytes) -> MockChange {
186 let path = self.path(path);
187 let snapshot = snapshot(bytes);
188
189 self.files
190 .write()
191 .expect("mock workspace lock poisoned")
192 .insert(path.clone(), snapshot.clone());
193
194 MockChange::new(FileChangeSet::new_inserts(vec![(
195 immut_path(path),
196 snapshot,
197 )]))
198 }
199
200 pub fn remove(&self, path: impl AsRef<Path>) -> FileResult<MockChange> {
202 let path = self.path(path);
203
204 let removed = self
205 .files
206 .write()
207 .expect("mock workspace lock poisoned")
208 .remove(&path);
209
210 match removed {
211 Some(_) => Ok(MockChange::new(FileChangeSet::new_removes(vec![
212 immut_path(path),
213 ]))),
214 None => Err(FileError::NotFound(path)),
215 }
216 }
217
218 pub fn rename(&self, from: impl AsRef<Path>, to: impl AsRef<Path>) -> FileResult<MockChange> {
220 let from = self.path(from);
221 let to = self.path(to);
222
223 let mut files = self.files.write().expect("mock workspace lock poisoned");
224 let snapshot = files
225 .remove(&from)
226 .ok_or_else(|| FileError::NotFound(from.clone()))?;
227 files.insert(to.clone(), snapshot.clone());
228
229 Ok(MockChange::new(FileChangeSet {
230 removes: vec![immut_path(from)],
231 inserts: vec![(immut_path(to), snapshot)],
232 }))
233 }
234
235 pub fn sync_changeset(&self) -> FileChangeSet {
237 let inserts = self
238 .files
239 .read()
240 .expect("mock workspace lock poisoned")
241 .iter()
242 .map(|(path, snapshot)| (immut_path(path.clone()), snapshot.clone()))
243 .collect();
244
245 FileChangeSet::new_inserts(inserts)
246 }
247
248 pub fn sync_filesystem_event(&self) -> FilesystemEvent {
250 FilesystemEvent::Update(self.sync_changeset(), true)
251 }
252
253 pub fn sync_memory_event(&self) -> MemoryEvent {
255 MemoryEvent::Sync(self.sync_changeset())
256 }
257}
258
259#[derive(Debug)]
261pub struct MockWorkspaceBuilder {
262 workspace: MockWorkspace,
263}
264
265impl MockWorkspaceBuilder {
266 pub fn new(root: impl Into<PathBuf>) -> Self {
268 Self {
269 workspace: MockWorkspace::new(root),
270 }
271 }
272
273 pub fn file(self, path: impl AsRef<Path>, source: impl Into<String>) -> Self {
275 self.workspace.write_source(path, source);
276 self
277 }
278
279 pub fn bytes(self, path: impl AsRef<Path>, bytes: Bytes) -> Self {
281 self.workspace.write_bytes(path, bytes);
282 self
283 }
284
285 pub fn build(self) -> MockWorkspace {
287 self.workspace
288 }
289}
290
291#[derive(Debug, Clone)]
293pub struct MockChange {
294 changeset: FileChangeSet,
295}
296
297impl MockChange {
298 pub fn new(changeset: FileChangeSet) -> Self {
300 Self { changeset }
301 }
302
303 pub fn changeset(&self) -> &FileChangeSet {
305 &self.changeset
306 }
307
308 pub fn into_changeset(self) -> FileChangeSet {
310 self.changeset
311 }
312
313 pub fn filesystem_event(&self, is_sync: bool) -> FilesystemEvent {
315 FilesystemEvent::Update(self.changeset.clone(), is_sync)
316 }
317
318 pub fn into_filesystem_event(self, is_sync: bool) -> FilesystemEvent {
320 FilesystemEvent::Update(self.changeset, is_sync)
321 }
322
323 pub fn memory_event(&self) -> MemoryEvent {
325 MemoryEvent::Update(self.changeset.clone())
326 }
327
328 pub fn memory_sync_event(&self) -> MemoryEvent {
330 MemoryEvent::Sync(self.changeset.clone())
331 }
332
333 pub fn apply_to_vfs<M>(&self, vfs: &mut Vfs<M>)
335 where
336 M: PathAccessModel,
337 {
338 vfs.revise().notify_fs_changes(self.changeset.clone());
339 }
340}
341
342pub fn default_mock_root() -> PathBuf {
344 if cfg!(windows) {
345 PathBuf::from(r"C:\tinymist-mock")
346 } else {
347 PathBuf::from("/tinymist-mock")
348 }
349}
350
351fn snapshot(bytes: Bytes) -> FileSnapshot {
352 Ok(bytes).into()
353}
354
355fn immut_path(path: PathBuf) -> ImmutPath {
356 Arc::from(path.into_boxed_path())
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362
363 #[test]
364 fn mock_workspace_drives_vfs_updates() {
365 let workspace = MockWorkspace::default_builder()
366 .file("main.typ", "#let value = [before]\n#value")
367 .build();
368 let mut vfs = workspace.vfs();
369 let main_id = workspace.file_id("main.typ").unwrap();
370
371 assert_eq!(
372 vfs.source(main_id).unwrap().text(),
373 "#let value = [before]\n#value"
374 );
375
376 workspace
377 .update_source("main.typ", "#let value = [after]\n#value")
378 .apply_to_vfs(&mut vfs);
379
380 assert_eq!(
381 vfs.source(main_id).unwrap().text(),
382 "#let value = [after]\n#value"
383 );
384 }
385}