tinymist_project/
entry.rs1use std::sync::Arc;
2
3use serde::{Deserialize, Serialize};
4use tinymist_l10n::DebugL10n;
5use tinymist_std::ImmutPath;
6use tinymist_std::error::prelude::*;
7use tinymist_std::hash::FxDashMap;
8use tinymist_world::EntryState;
9use typst::syntax::VirtualPath;
10
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
13#[serde(rename_all = "camelCase")]
14pub enum ProjectResolutionKind {
15 #[default]
19 SingleFile,
20 LockDatabase,
27}
28
29#[derive(Debug, Default, Clone)]
31pub struct EntryResolver {
32 pub project_resolution: ProjectResolutionKind,
34 pub root_path: Option<ImmutPath>,
36 pub roots: Vec<ImmutPath>,
38 pub entry: Option<ImmutPath>,
40 pub typst_toml_cache: Arc<FxDashMap<ImmutPath, Option<ImmutPath>>>,
42}
43
44impl EntryResolver {
45 pub fn root(&self, entry: Option<&ImmutPath>) -> Option<ImmutPath> {
47 if let Some(root) = &self.root_path {
48 return Some(root.clone());
49 }
50
51 if let Some(entry) = entry {
52 for root in self.roots.iter() {
53 if entry.starts_with(root) {
54 return Some(root.clone());
55 }
56 }
57
58 if !self.roots.is_empty() {
59 log::warn!("entry is not in any set root directory");
60 }
61
62 let typst_toml_cache = &self.typst_toml_cache;
63
64 match typst_toml_cache.get(entry).map(|r| r.clone()) {
65 Some(None) => return None,
72 Some(Some(cached)) => {
73 let cached = cached.clone();
74 if cached.join("typst.toml").exists() {
75 return Some(cached.clone());
76 }
77 typst_toml_cache.remove(entry);
78 }
79 None => {}
80 };
81
82 for ancestor in entry.ancestors() {
85 let typst_toml = ancestor.join("typst.toml");
86 if typst_toml.exists() {
87 let ancestor: ImmutPath = ancestor.into();
88 typst_toml_cache.insert(entry.clone(), Some(ancestor.clone()));
89 return Some(ancestor);
90 }
91 }
92 typst_toml_cache.insert(entry.clone(), None);
93
94 if let Some(parent) = entry.parent() {
95 return Some(parent.into());
96 }
97 }
98
99 if !self.roots.is_empty() {
100 return Some(self.roots[0].clone());
101 }
102
103 None
104 }
105
106 pub fn resolve(&self, entry: Option<ImmutPath>) -> EntryState {
108 let root_dir = self.root(entry.as_ref());
109 self.resolve_with_root(root_dir, entry)
110 }
111
112 pub fn resolve_with_root(
114 &self,
115 root_dir: Option<ImmutPath>,
116 entry: Option<ImmutPath>,
117 ) -> EntryState {
118 let entry = match (entry, root_dir) {
124 (Some(entry), Some(root)) => match VirtualPath::virtualize(&root, &entry) {
129 Ok(vpath) => Some(EntryState::new_rooted(root, Some(vpath))),
130 Err(err) => {
131 log::info!(
132 "Entry is not in root directory: err {err:?}: entry: {entry:?}, root: {root:?}"
133 );
134 EntryState::new_rooted_by_parent(entry)
135 }
136 },
137 (Some(entry), None) => EntryState::new_rooted_by_parent(entry),
138 (None, Some(root)) => Some(EntryState::new_workspace(root)),
139 (None, None) => None,
140 };
141
142 entry.unwrap_or_else(|| match self.root(None) {
143 Some(root) => EntryState::new_workspace(root),
144 None => EntryState::new_detached(),
145 })
146 }
147
148 pub fn resolve_lock(&self, entry: &EntryState) -> Option<ImmutPath> {
150 match self.project_resolution {
151 ProjectResolutionKind::LockDatabase if entry.is_in_package() => {
152 log::info!("ProjectResolver: no lock for package: {entry:?}");
153 None
154 }
155 ProjectResolutionKind::LockDatabase => {
156 let root = entry.workspace_root();
157 log::info!("ProjectResolver: lock for {entry:?} at {root:?}");
158
159 root
160 }
161 ProjectResolutionKind::SingleFile => None,
162 }
163 }
164
165 pub fn resolve_default(&self) -> Option<ImmutPath> {
167 let entry = self.entry.as_ref();
168 if let Some(entry) = entry
170 && entry.is_relative()
171 {
172 let root = self.root(None)?;
173 return Some(root.join(entry).as_path().into());
174 }
175 entry.cloned()
176 }
177
178 pub fn validate(&self) -> Result<()> {
180 if let Some(root) = &self.root_path
181 && !root.is_absolute()
182 {
183 tinymist_l10n::bail!(
184 "tinymist-project.validate-error.root-path-not-absolute",
185 "rootPath or typstExtraArgs.root must be an absolute path: {root:?}",
186 root = root.debug_l10n()
187 );
188 }
189
190 Ok(())
191 }
192}
193
194#[cfg(test)]
195#[cfg(any(windows, unix, target_os = "macos"))]
196mod entry_tests {
197 use tinymist_world::vfs::WorkspaceResolver;
198
199 use super::*;
200 use std::path::Path;
201
202 const ROOT: &str = if cfg!(windows) {
203 "C:\\dummy-root"
204 } else {
205 "/dummy-root"
206 };
207 const ROOT2: &str = if cfg!(windows) {
208 "C:\\dummy-root2"
209 } else {
210 "/dummy-root2"
211 };
212
213 #[test]
214 fn test_entry_resolution() {
215 let root_path = Path::new(ROOT);
216
217 let entry = EntryResolver {
218 root_path: Some(ImmutPath::from(root_path)),
219 ..Default::default()
220 };
221
222 let entry = entry.resolve(Some(root_path.join("main.typ").into()));
223
224 assert_eq!(entry.root(), Some(ImmutPath::from(root_path)));
225 assert_eq!(
226 entry.main(),
227 Some(WorkspaceResolver::workspace_file(
228 entry.root().as_ref(),
229 VirtualPath::new("main.typ").unwrap()
230 ))
231 );
232 }
233
234 #[test]
235 fn test_entry_resolution_multi_root() {
236 let root_path = Path::new(ROOT);
237 let root2_path = Path::new(ROOT2);
238
239 let entry = EntryResolver {
240 root_path: Some(ImmutPath::from(root_path)),
241 roots: vec![ImmutPath::from(root_path), ImmutPath::from(root2_path)],
242 ..Default::default()
243 };
244
245 {
246 let entry = entry.resolve(Some(root_path.join("main.typ").into()));
247
248 assert_eq!(entry.root(), Some(ImmutPath::from(root_path)));
249 assert_eq!(
250 entry.main(),
251 Some(WorkspaceResolver::workspace_file(
252 entry.root().as_ref(),
253 VirtualPath::new("main.typ").unwrap()
254 ))
255 );
256 }
257
258 {
259 let entry = entry.resolve(Some(root2_path.join("main.typ").into()));
260
261 assert_eq!(entry.root(), Some(ImmutPath::from(root2_path)));
262 assert_eq!(
263 entry.main(),
264 Some(WorkspaceResolver::workspace_file(
265 entry.root().as_ref(),
266 VirtualPath::new("main.typ").unwrap()
267 ))
268 );
269 }
270 }
271
272 #[test]
273 fn test_entry_resolution_default_multi_root() {
274 let root_path = Path::new(ROOT);
275 let root2_path = Path::new(ROOT2);
276
277 let mut entry = EntryResolver {
278 root_path: Some(ImmutPath::from(root_path)),
279 roots: vec![ImmutPath::from(root_path), ImmutPath::from(root2_path)],
280 ..Default::default()
281 };
282
283 {
284 entry.entry = Some(root_path.join("main.typ").into());
285
286 let default_entry = entry.resolve_default();
287
288 assert_eq!(default_entry, entry.entry);
289 }
290
291 {
292 entry.entry = Some(Path::new("main.typ").into());
293
294 let default_entry = entry.resolve_default();
295
296 assert_eq!(default_entry, Some(root_path.join("main.typ").into()));
297 }
298 }
299}