1use std::{
4 path::{Path, PathBuf},
5 sync::{Arc, LazyLock},
6};
7
8use tinymist_project::{
9 CompileFontArgs, DynAccessModel, EntryManager, EntryState, ExportTarget, LspUniverse,
10 LspUniverseBuilder, base::ShadowApi, font::FontResolverImpl, vfs::system::SystemAccessModel,
11};
12use typst::{foundations::Bytes, syntax::VirtualPath};
13
14pub use insta::{Settings, assert_debug_snapshot, assert_snapshot, glob, with_settings};
15
16#[macro_export]
18macro_rules! snapshot_testing {
19 ($name:expr, $f:expr) => {
20 let name = $name;
21 let name = if name.is_empty() { "playground" } else { name };
22 let mut settings = $crate::Settings::new();
23 settings.set_prepend_module_to_snapshot(false);
24 settings.set_snapshot_path(format!("fixtures/{name}/snaps"));
25 settings.bind(|| {
26 let glob_path = format!("fixtures/{name}/*.typ");
27 $crate::glob!(&glob_path, |path| {
28 let contents = std::fs::read_to_string(path).unwrap();
29 #[cfg(windows)]
30 let contents = contents.replace("\r\n", "\n");
31
32 $crate::run_with_sources(&contents, $f);
33 });
34 });
35 };
36}
37
38pub fn run_with_sources<T>(source: &str, f: impl FnOnce(&mut LspUniverse, PathBuf) -> T) -> T {
44 static FONT_RESOLVER: LazyLock<Arc<FontResolverImpl>> = LazyLock::new(|| {
45 Arc::new(
46 LspUniverseBuilder::resolve_fonts(CompileFontArgs {
47 ignore_system_fonts: true,
48 ..Default::default()
49 })
50 .unwrap(),
51 )
52 });
53
54 let root = if cfg!(windows) {
55 PathBuf::from("C:\\dummy-root")
56 } else {
57 PathBuf::from("/dummy-root")
58 };
59 let mut verse = LspUniverseBuilder::build(
60 EntryState::new_rooted(root.as_path().into(), None),
61 ExportTarget::Paged,
62 Default::default(),
63 Default::default(),
64 LspUniverseBuilder::resolve_package(None, None),
65 FONT_RESOLVER.clone(),
66 None,
67 DynAccessModel(Arc::new(SystemAccessModel {})),
68 );
69 let sources = source.split("-----");
70
71 let mut last_pw = None;
72 for (idx, source) in sources.enumerate() {
73 let mut source = source.trim_start();
75 let mut path = None;
76
77 if source.starts_with("//") {
78 let first_line = source.lines().next().unwrap();
79 let content = first_line.trim_start_matches("/").trim();
80
81 if let Some(path_attr) = content.strip_prefix("path:") {
82 source = source.strip_prefix(first_line).unwrap().trim();
83 path = Some(path_attr.trim().to_owned())
84 }
85 };
86
87 let path = path.unwrap_or_else(|| format!("/s{idx}.typ"));
88 let path = path.strip_prefix("/").unwrap_or(path.as_str());
89
90 let pw = root.join(Path::new(&path));
91 verse
92 .map_shadow(&pw, Bytes::from_string(source.to_owned()))
93 .unwrap();
94 last_pw = Some(pw);
95 }
96
97 let pw = last_pw.unwrap();
98 verse
99 .mutate_entry(EntryState::new_rooted(
100 root.as_path().into(),
101 Some(VirtualPath::new(pw.strip_prefix(root).unwrap())),
102 ))
103 .unwrap();
104 f(&mut verse, pw)
105}