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