1use core::fmt;
6use std::borrow::Cow;
7use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9use std::sync::LazyLock;
10
11use parking_lot::RwLock;
12use tinymist_std::ImmutPath;
13use tinymist_std::path::PathClean;
14use tinymist_std::typst_shim::syntax::{RootedPathExt, VirtualPathExt};
15use typst::diag::{EcoString, FileError, FileResult, eco_format};
16use typst::syntax::package::{PackageSpec, PackageVersion};
17use typst::syntax::{FileId, RootedPath, VirtualPath, VirtualRoot};
18
19#[derive(Debug)]
21pub enum PathResolution {
22 Resolved(PathBuf),
24 Rootless(Cow<'static, VirtualPath>),
26}
27
28impl PathResolution {
29 pub fn to_err(self) -> FileResult<PathBuf> {
31 match self {
32 PathResolution::Resolved(path) => Ok(path),
33 PathResolution::Rootless(_) => Err(FileError::AccessDenied),
34 }
35 }
36
37 pub fn as_path(&self) -> &Path {
39 match self {
40 PathResolution::Resolved(path) => path.as_path(),
41 PathResolution::Rootless(path) => path.as_ref().as_rooted_path_compat(),
42 }
43 }
44
45 pub fn join(&self, path: &str) -> FileResult<PathResolution> {
47 match self {
48 PathResolution::Resolved(root) => Ok(PathResolution::Resolved(root.join(path))),
49 PathResolution::Rootless(root) => Ok(PathResolution::Rootless(Cow::Owned(
50 root.join(path).map_err(|_| FileError::AccessDenied)?,
51 ))),
52 }
53 }
54
55 pub fn resolve_to(&self, path: &VirtualPath) -> Option<PathResolution> {
57 match self {
58 PathResolution::Resolved(root) => {
59 Some(PathResolution::Resolved(path.realize(root).ok()?))
60 }
61 PathResolution::Rootless(root) => Some(PathResolution::Rootless(Cow::Owned(
62 root.as_ref().join(path.get_without_slash()).ok()?,
63 ))),
64 }
65 }
66}
67
68pub trait RootResolver {
70 fn path_for_id(&self, file_id: FileId) -> FileResult<PathResolution> {
72 use WorkspaceResolution::*;
73 let root = match WorkspaceResolver::resolve(file_id)? {
74 Workspace(id) => id.path().clone(),
75 Package => {
76 self.resolve_package_root(file_id.package_compat().expect("not a file in package"))?
77 }
78 UntitledRooted(..) | Rootless => {
79 return Ok(PathResolution::Rootless(Cow::Owned(
80 file_id.vpath().clone(),
81 )));
82 }
83 };
84
85 Ok(PathResolution::Resolved(file_id.vpath().realize(&root)?))
86 }
87
88 fn resolve_root(&self, file_id: FileId) -> FileResult<Option<ImmutPath>> {
90 use WorkspaceResolution::*;
91 match WorkspaceResolver::resolve(file_id)? {
92 Workspace(id) | UntitledRooted(id) => Ok(Some(id.path().clone())),
93 Rootless => Ok(None),
94 Package => self
95 .resolve_package_root(file_id.package_compat().expect("not a file in package"))
96 .map(Some),
97 }
98 }
99
100 fn resolve_package_root(&self, pkg: &PackageSpec) -> FileResult<ImmutPath>;
102}
103
104#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
106pub struct WorkspaceId(u16);
107
108const NO_VERSION: PackageVersion = PackageVersion {
109 major: 0,
110 minor: 0,
111 patch: 0,
112};
113
114const UNTITLED_ROOT: PackageVersion = PackageVersion {
115 major: 0,
116 minor: 0,
117 patch: 1,
118};
119
120impl WorkspaceId {
121 fn package(&self) -> PackageSpec {
122 PackageSpec {
123 namespace: WorkspaceResolver::WORKSPACE_NS.clone(),
124 name: eco_format!("p{}", self.0),
125 version: NO_VERSION,
126 }
127 }
128
129 fn untitled_root(&self) -> PackageSpec {
130 PackageSpec {
131 namespace: WorkspaceResolver::WORKSPACE_NS.clone(),
132 name: eco_format!("p{}", self.0),
133 version: UNTITLED_ROOT,
134 }
135 }
136
137 pub fn path(&self) -> ImmutPath {
139 let interner = INTERNER.read();
140 interner
141 .from_id
142 .get(self.0 as usize)
143 .expect("invalid workspace id")
144 .clone()
145 }
146
147 fn from_package_name(name: &str) -> Option<WorkspaceId> {
148 if !name.starts_with("p") {
149 return None;
150 }
151
152 let num = name[1..].parse().ok()?;
153 Some(WorkspaceId(num))
154 }
155}
156
157static INTERNER: LazyLock<RwLock<Interner>> = LazyLock::new(|| {
159 RwLock::new(Interner {
160 to_id: HashMap::new(),
161 from_id: Vec::new(),
162 })
163});
164
165pub enum WorkspaceResolution {
167 Workspace(WorkspaceId),
169 UntitledRooted(WorkspaceId),
171 Rootless,
173 Package,
175}
176
177struct Interner {
179 to_id: HashMap<ImmutPath, WorkspaceId>,
180 from_id: Vec<ImmutPath>,
181}
182
183#[derive(Default)]
185pub struct WorkspaceResolver {}
186
187impl WorkspaceResolver {
188 pub const WORKSPACE_NS: EcoString = EcoString::inline("ws");
190
191 pub fn is_workspace_file(fid: FileId) -> bool {
193 matches!(fid.root(), VirtualRoot::Package(pkg) if pkg.namespace == WorkspaceResolver::WORKSPACE_NS)
194 }
195
196 pub fn is_package_file(fid: FileId) -> bool {
198 matches!(fid.root(), VirtualRoot::Package(pkg) if pkg.namespace != WorkspaceResolver::WORKSPACE_NS)
199 }
200
201 pub fn workspace_id(root: &ImmutPath) -> WorkspaceId {
203 let mut interner = INTERNER.write();
209 if let Some(&id) = interner.to_id.get(root) {
210 return id;
211 }
212
213 let root = ImmutPath::from(root.clean());
214
215 let num = interner.from_id.len().try_into().expect("out of file ids");
219 let id = WorkspaceId(num);
220 interner.to_id.insert(root.clone(), id);
221 interner.from_id.push(root.clone());
222 id
223 }
224
225 pub fn rootless_file(path: VirtualPath) -> FileId {
227 FileId::unique(RootedPath::new(VirtualRoot::Project, path))
228 }
229
230 pub fn file_with_parent_root(path: &Path) -> Option<FileId> {
232 if !path.is_absolute() {
233 return None;
234 }
235 let parent = path.parent()?;
236 let parent = ImmutPath::from(parent);
237 let path = VirtualPath::new(path.file_name()?.to_str()?).ok()?;
238 Some(Self::workspace_file(Some(&parent), path))
239 }
240
241 pub fn workspace_file(root: Option<&ImmutPath>, path: VirtualPath) -> FileId {
245 match root {
246 Some(root) => {
247 let workspace = Self::workspace_id(root);
248 RootedPath::new(VirtualRoot::Package(workspace.package()), path).intern()
249 }
250 None => FileId::unique(RootedPath::new(VirtualRoot::Project, path)),
251 }
252 }
253
254 pub fn rooted_untitled(root: Option<&ImmutPath>, path: VirtualPath) -> FileId {
258 match root {
259 Some(root) => {
260 let workspace = Self::workspace_id(root);
261 FileId::unique(RootedPath::new(
262 VirtualRoot::Package(workspace.untitled_root()),
263 path,
264 ))
265 }
266 None => FileId::unique(RootedPath::new(VirtualRoot::Project, path)),
267 }
268 }
269
270 pub fn resolve(fid: FileId) -> FileResult<WorkspaceResolution> {
272 match fid.root() {
273 VirtualRoot::Project => Ok(WorkspaceResolution::Rootless),
274 VirtualRoot::Package(package)
275 if package.namespace == WorkspaceResolver::WORKSPACE_NS =>
276 {
277 let id = WorkspaceId::from_package_name(&package.name).ok_or_else(|| {
278 FileError::Other(Some(eco_format!("bad workspace id: {fid:?}")))
279 })?;
280
281 Ok(if package.version == UNTITLED_ROOT {
282 WorkspaceResolution::UntitledRooted(id)
283 } else {
284 WorkspaceResolution::Workspace(id)
285 })
286 }
287 VirtualRoot::Package(_) => Ok(WorkspaceResolution::Package),
288 }
289 }
290
291 pub fn display(id: Option<FileId>) -> Resolving {
293 Resolving { id }
294 }
295}
296
297pub struct Resolving {
299 id: Option<FileId>,
300}
301
302impl fmt::Debug for Resolving {
303 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304 use WorkspaceResolution::*;
305 let Some(id) = self.id else {
306 return write!(f, "unresolved-path");
307 };
308
309 let path = match WorkspaceResolver::resolve(id) {
310 Ok(Workspace(workspace)) => id.vpath().realize(&workspace.path()).ok(),
311 Ok(UntitledRooted(..)) => Some(id.vpath().as_rootless_path_compat().to_owned()),
312 Ok(Rootless | Package) | Err(_) => None,
313 };
314
315 if let Some(path) = path {
316 write!(f, "{}", path.display())
317 } else {
318 write!(f, "{:?}", self.id)
319 }
320 }
321}
322
323impl fmt::Display for Resolving {
324 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
325 use WorkspaceResolution::*;
326 let Some(id) = self.id else {
327 return write!(f, "unresolved-path");
328 };
329
330 let path = match WorkspaceResolver::resolve(id) {
331 Ok(Workspace(workspace)) => id.vpath().realize(&workspace.path()).ok(),
332 Ok(UntitledRooted(..)) => Some(Path::new(id.vpath().get_without_slash()).to_owned()),
333 Ok(Rootless | Package) | Err(_) => None,
334 };
335
336 if let Some(path) = path {
337 write!(f, "{}", path.display())
338 } else {
339 match id.root() {
340 VirtualRoot::Package(pkg) => {
341 write!(f, "{pkg}{}", id.vpath().as_rooted_path_compat().display())
342 }
343 _ => write!(f, "{}", id.vpath().as_rooted_path_compat().display()),
344 }
345 }
346 }
347}
348
349#[cfg(test)]
350mod tests {
351
352 #[test]
353 fn test_interner_untitled() {}
354}