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 entry.strip_prefix(&root) {
129 Ok(stripped) => Some(EntryState::new_rooted(
130 root,
131 Some(VirtualPath::new(stripped)),
132 )),
133 Err(err) => {
134 log::info!(
135 "Entry is not in root directory: err {err:?}: entry: {entry:?}, root: {root:?}"
136 );
137 EntryState::new_rooted_by_parent(entry)
138 }
139 },
140 (Some(entry), None) => EntryState::new_rooted_by_parent(entry),
141 (None, Some(root)) => Some(EntryState::new_workspace(root)),
142 (None, None) => None,
143 };
144
145 entry.unwrap_or_else(|| match self.root(None) {
146 Some(root) => EntryState::new_workspace(root),
147 None => EntryState::new_detached(),
148 })
149 }
150
151 pub fn resolve_lock(&self, entry: &EntryState) -> Option<ImmutPath> {
153 match self.project_resolution {
154 ProjectResolutionKind::LockDatabase if entry.is_in_package() => {
155 log::info!("ProjectResolver: no lock for package: {entry:?}");
156 None
157 }
158 ProjectResolutionKind::LockDatabase => {
159 let root = entry.workspace_root();
160 log::info!("ProjectResolver: lock for {entry:?} at {root:?}");
161
162 root
163 }
164 ProjectResolutionKind::SingleFile => None,
165 }
166 }
167
168 pub fn resolve_default(&self) -> Option<ImmutPath> {
170 let entry = self.entry.as_ref();
171 if let Some(entry) = entry
173 && entry.is_relative()
174 {
175 let root = self.root(None)?;
176 return Some(root.join(entry).as_path().into());
177 }
178 entry.cloned()
179 }
180
181 pub fn validate(&self) -> Result<()> {
183 if let Some(root) = &self.root_path
184 && !root.is_absolute()
185 {
186 tinymist_l10n::bail!(
187 "tinymist-project.validate-error.root-path-not-absolute",
188 "rootPath or typstExtraArgs.root must be an absolute path: {root:?}",
189 root = root.debug_l10n()
190 );
191 }
192
193 Ok(())
194 }
195}
196
197#[cfg(test)]
198#[cfg(any(windows, unix, target_os = "macos"))]
199mod entry_tests {
200 use tinymist_world::vfs::WorkspaceResolver;
201
202 use super::*;
203 use std::path::Path;
204
205 const ROOT: &str = if cfg!(windows) {
206 "C:\\dummy-root"
207 } else {
208 "/dummy-root"
209 };
210 const ROOT2: &str = if cfg!(windows) {
211 "C:\\dummy-root2"
212 } else {
213 "/dummy-root2"
214 };
215
216 #[test]
217 fn test_entry_resolution() {
218 let root_path = Path::new(ROOT);
219
220 let entry = EntryResolver {
221 root_path: Some(ImmutPath::from(root_path)),
222 ..Default::default()
223 };
224
225 let entry = entry.resolve(Some(root_path.join("main.typ").into()));
226
227 assert_eq!(entry.root(), Some(ImmutPath::from(root_path)));
228 assert_eq!(
229 entry.main(),
230 Some(WorkspaceResolver::workspace_file(
231 entry.root().as_ref(),
232 VirtualPath::new("main.typ")
233 ))
234 );
235 }
236
237 #[test]
238 fn test_entry_resolution_multi_root() {
239 let root_path = Path::new(ROOT);
240 let root2_path = Path::new(ROOT2);
241
242 let entry = EntryResolver {
243 root_path: Some(ImmutPath::from(root_path)),
244 roots: vec![ImmutPath::from(root_path), ImmutPath::from(root2_path)],
245 ..Default::default()
246 };
247
248 {
249 let entry = entry.resolve(Some(root_path.join("main.typ").into()));
250
251 assert_eq!(entry.root(), Some(ImmutPath::from(root_path)));
252 assert_eq!(
253 entry.main(),
254 Some(WorkspaceResolver::workspace_file(
255 entry.root().as_ref(),
256 VirtualPath::new("main.typ")
257 ))
258 );
259 }
260
261 {
262 let entry = entry.resolve(Some(root2_path.join("main.typ").into()));
263
264 assert_eq!(entry.root(), Some(ImmutPath::from(root2_path)));
265 assert_eq!(
266 entry.main(),
267 Some(WorkspaceResolver::workspace_file(
268 entry.root().as_ref(),
269 VirtualPath::new("main.typ")
270 ))
271 );
272 }
273 }
274
275 #[test]
276 fn test_entry_resolution_default_multi_root() {
277 let root_path = Path::new(ROOT);
278 let root2_path = Path::new(ROOT2);
279
280 let mut entry = EntryResolver {
281 root_path: Some(ImmutPath::from(root_path)),
282 roots: vec![ImmutPath::from(root_path), ImmutPath::from(root2_path)],
283 ..Default::default()
284 };
285
286 {
287 entry.entry = Some(root_path.join("main.typ").into());
288
289 let default_entry = entry.resolve_default();
290
291 assert_eq!(default_entry, entry.entry);
292 }
293
294 {
295 entry.entry = Some(Path::new("main.typ").into());
296
297 let default_entry = entry.resolve_default();
298
299 assert_eq!(default_entry, Some(root_path.join("main.typ").into()));
300 }
301 }
302}