tinymist_vfs/
path_mapper.rs1use 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::path::PathClean;
13use tinymist_std::ImmutPath;
14use typst::diag::{eco_format, EcoString, FileError, FileResult};
15use typst::syntax::package::{PackageSpec, PackageVersion};
16use typst::syntax::VirtualPath;
17
18use super::TypstFileId;
19
20#[derive(Debug)]
21pub enum PathResolution {
22 Resolved(PathBuf),
23 Rootless(Cow<'static, VirtualPath>),
24}
25
26impl PathResolution {
27 pub fn to_err(self) -> FileResult<PathBuf> {
28 match self {
29 PathResolution::Resolved(path) => Ok(path),
30 PathResolution::Rootless(_) => Err(FileError::AccessDenied),
31 }
32 }
33
34 pub fn as_path(&self) -> &Path {
35 match self {
36 PathResolution::Resolved(path) => path.as_path(),
37 PathResolution::Rootless(path) => path.as_rooted_path(),
38 }
39 }
40
41 pub fn join(&self, path: &str) -> FileResult<PathResolution> {
42 match self {
43 PathResolution::Resolved(root) => Ok(PathResolution::Resolved(root.join(path))),
44 PathResolution::Rootless(root) => {
45 Ok(PathResolution::Rootless(Cow::Owned(root.join(path))))
46 }
47 }
48 }
49}
50
51pub trait RootResolver {
52 fn path_for_id(&self, file_id: TypstFileId) -> FileResult<PathResolution> {
53 use WorkspaceResolution::*;
54 let root = match WorkspaceResolver::resolve(file_id)? {
55 Workspace(id) => id.path().clone(),
56 Package => {
57 self.resolve_package_root(file_id.package().expect("not a file in package"))?
58 }
59 UntitledRooted(..) | Rootless => {
60 return Ok(PathResolution::Rootless(Cow::Borrowed(file_id.vpath())))
61 }
62 };
63
64 file_id
65 .vpath()
66 .resolve(&root)
67 .map(PathResolution::Resolved)
68 .ok_or_else(|| FileError::AccessDenied)
69 }
70
71 fn resolve_root(&self, file_id: TypstFileId) -> FileResult<Option<ImmutPath>> {
72 use WorkspaceResolution::*;
73 match WorkspaceResolver::resolve(file_id)? {
74 Workspace(id) | UntitledRooted(id) => Ok(Some(id.path().clone())),
75 Rootless => Ok(None),
76 Package => self
77 .resolve_package_root(file_id.package().expect("not a file in package"))
78 .map(Some),
79 }
80 }
81
82 fn resolve_package_root(&self, pkg: &PackageSpec) -> FileResult<ImmutPath>;
83}
84
85#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
86pub struct WorkspaceId(u16);
87
88const NO_VERSION: PackageVersion = PackageVersion {
89 major: 0,
90 minor: 0,
91 patch: 0,
92};
93
94const UNTITLED_ROOT: PackageVersion = PackageVersion {
95 major: 0,
96 minor: 0,
97 patch: 1,
98};
99
100impl WorkspaceId {
101 fn package(&self) -> PackageSpec {
102 PackageSpec {
103 namespace: WorkspaceResolver::WORKSPACE_NS.clone(),
104 name: eco_format!("p{}", self.0),
105 version: NO_VERSION,
106 }
107 }
108
109 fn untitled_root(&self) -> PackageSpec {
110 PackageSpec {
111 namespace: WorkspaceResolver::WORKSPACE_NS.clone(),
112 name: eco_format!("p{}", self.0),
113 version: UNTITLED_ROOT,
114 }
115 }
116
117 pub fn path(&self) -> ImmutPath {
118 let interner = INTERNER.read();
119 interner
120 .from_id
121 .get(self.0 as usize)
122 .expect("invalid workspace id")
123 .clone()
124 }
125
126 fn from_package_name(name: &str) -> Option<WorkspaceId> {
127 if !name.starts_with("p") {
128 return None;
129 }
130
131 let num = name[1..].parse().ok()?;
132 Some(WorkspaceId(num))
133 }
134}
135
136static INTERNER: LazyLock<RwLock<Interner>> = LazyLock::new(|| {
138 RwLock::new(Interner {
139 to_id: HashMap::new(),
140 from_id: Vec::new(),
141 })
142});
143
144pub enum WorkspaceResolution {
145 Workspace(WorkspaceId),
146 UntitledRooted(WorkspaceId),
147 Rootless,
148 Package,
149}
150
151struct Interner {
153 to_id: HashMap<ImmutPath, WorkspaceId>,
154 from_id: Vec<ImmutPath>,
155}
156
157#[derive(Default)]
158pub struct WorkspaceResolver {}
159
160impl WorkspaceResolver {
161 pub const WORKSPACE_NS: EcoString = EcoString::inline("ws");
162
163 pub fn is_workspace_file(fid: TypstFileId) -> bool {
164 fid.package()
165 .is_some_and(|p| p.namespace == WorkspaceResolver::WORKSPACE_NS)
166 }
167
168 pub fn is_package_file(fid: TypstFileId) -> bool {
169 fid.package()
170 .is_some_and(|p| p.namespace != WorkspaceResolver::WORKSPACE_NS)
171 }
172
173 pub fn workspace_id(root: &ImmutPath) -> WorkspaceId {
175 let mut interner = INTERNER.write();
181 if let Some(&id) = interner.to_id.get(root) {
182 return id;
183 }
184
185 let root = ImmutPath::from(root.clean());
186
187 let num = interner.from_id.len().try_into().expect("out of file ids");
191 let id = WorkspaceId(num);
192 interner.to_id.insert(root.clone(), id);
193 interner.from_id.push(root.clone());
194 id
195 }
196
197 pub fn rootless_file(path: VirtualPath) -> TypstFileId {
199 TypstFileId::new(None, path)
200 }
201
202 pub fn file_with_parent_root(path: &Path) -> Option<TypstFileId> {
204 if !path.is_absolute() {
205 return None;
206 }
207 let parent = path.parent()?;
208 let parent = ImmutPath::from(parent);
209 let path = VirtualPath::new(path.file_name()?);
210 Some(Self::workspace_file(Some(&parent), path))
211 }
212
213 pub fn workspace_file(root: Option<&ImmutPath>, path: VirtualPath) -> TypstFileId {
217 let workspace = root.map(Self::workspace_id);
218 TypstFileId::new(workspace.as_ref().map(WorkspaceId::package), path)
219 }
220
221 pub fn rooted_untitled(root: Option<&ImmutPath>, path: VirtualPath) -> TypstFileId {
225 let workspace = root.map(Self::workspace_id);
226 TypstFileId::new(workspace.as_ref().map(WorkspaceId::untitled_root), path)
227 }
228
229 pub fn resolve(fid: TypstFileId) -> FileResult<WorkspaceResolution> {
231 let Some(package) = fid.package() else {
232 return Ok(WorkspaceResolution::Rootless);
233 };
234
235 match package.namespace.as_str() {
236 "ws" => {
237 let id = WorkspaceId::from_package_name(&package.name).ok_or_else(|| {
238 FileError::Other(Some(eco_format!("bad workspace id: {fid:?}")))
239 })?;
240
241 Ok(if package.version == UNTITLED_ROOT {
242 WorkspaceResolution::UntitledRooted(id)
243 } else {
244 WorkspaceResolution::Workspace(id)
245 })
246 }
247 _ => Ok(WorkspaceResolution::Package),
248 }
249 }
250
251 pub fn display(id: Option<TypstFileId>) -> Resolving {
253 Resolving { id }
254 }
255}
256
257pub struct Resolving {
258 id: Option<TypstFileId>,
259}
260
261impl fmt::Debug for Resolving {
262 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263 use WorkspaceResolution::*;
264 let Some(id) = self.id else {
265 return write!(f, "None");
266 };
267
268 let path = match WorkspaceResolver::resolve(id) {
269 Ok(Workspace(workspace)) => id.vpath().resolve(&workspace.path()),
270 Ok(UntitledRooted(..)) => Some(id.vpath().as_rootless_path().to_owned()),
271 Ok(Rootless | Package) | Err(_) => None,
272 };
273
274 if let Some(path) = path {
275 write!(f, "{}", path.display())
276 } else {
277 write!(f, "{:?}", self.id)
278 }
279 }
280}
281
282#[cfg(test)]
283mod tests {
284
285 #[test]
286 fn test_interner_untitled() {}
287}