typst_syntax/
file.rs

1//! File and package management.
2
3use std::fmt::{self, Debug, Formatter};
4use std::num::NonZeroU16;
5use std::sync::{LazyLock, RwLock};
6
7use rustc_hash::FxHashMap;
8
9use crate::VirtualPath;
10use crate::package::PackageSpec;
11
12/// The global package-path interner.
13static INTERNER: LazyLock<RwLock<Interner>> = LazyLock::new(|| {
14    RwLock::new(Interner { to_id: FxHashMap::default(), from_id: Vec::new() })
15});
16
17/// A package-path interner.
18struct Interner {
19    to_id: FxHashMap<Pair, FileId>,
20    from_id: Vec<Pair>,
21}
22
23/// An interned pair of a package specification and a path.
24type Pair = &'static (Option<PackageSpec>, VirtualPath);
25
26/// Identifies a file in a project or package.
27///
28/// This type is globally interned and thus cheap to copy, compare, and hash.
29#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
30pub struct FileId(NonZeroU16);
31
32impl FileId {
33    /// Create a new interned file specification.
34    ///
35    /// The path must start with a `/` or this function will panic.
36    /// Note that the path is normalized before interning.
37    #[track_caller]
38    pub fn new(package: Option<PackageSpec>, path: VirtualPath) -> Self {
39        // Try to find an existing entry that we can reuse.
40        //
41        // We could check with just a read lock, but if the pair is not yet
42        // present, we would then need to recheck after acquiring a write lock,
43        // which is probably not worth it.
44        let pair = (package, path);
45        let mut interner = INTERNER.write().unwrap();
46        if let Some(&id) = interner.to_id.get(&pair) {
47            return id;
48        }
49
50        // Create a new entry forever by leaking the pair. We can't leak more
51        // than 2^16 pair (and typically will leak a lot less), so its not a
52        // big deal.
53        let num = u16::try_from(interner.from_id.len() + 1)
54            .and_then(NonZeroU16::try_from)
55            .expect("out of file ids");
56
57        let id = FileId(num);
58        let leaked = Box::leak(Box::new(pair));
59        interner.to_id.insert(leaked, id);
60        interner.from_id.push(leaked);
61        id
62    }
63
64    /// Create a new unique ("fake") file specification, which is not
65    /// accessible by path.
66    ///
67    /// Caution: the ID returned by this method is the *only* identifier of the
68    /// file, constructing a file ID with a path will *not* reuse the ID even
69    /// if the path is the same. This method should only be used for generating
70    /// "virtual" file ids such as content read from stdin.
71    #[track_caller]
72    pub fn new_fake(path: VirtualPath) -> Self {
73        let mut interner = INTERNER.write().unwrap();
74        let num = u16::try_from(interner.from_id.len() + 1)
75            .and_then(NonZeroU16::try_from)
76            .expect("out of file ids");
77
78        let id = FileId(num);
79        let leaked = Box::leak(Box::new((None, path)));
80        interner.from_id.push(leaked);
81        id
82    }
83
84    /// The package the file resides in, if any.
85    pub fn package(&self) -> Option<&'static PackageSpec> {
86        self.pair().0.as_ref()
87    }
88
89    /// The absolute and normalized path to the file _within_ the project or
90    /// package.
91    pub fn vpath(&self) -> &'static VirtualPath {
92        &self.pair().1
93    }
94
95    /// Resolve a file location relative to this file.
96    pub fn join(self, path: &str) -> Self {
97        Self::new(self.package().cloned(), self.vpath().join(path))
98    }
99
100    /// The same file location, but with a different extension.
101    pub fn with_extension(&self, extension: &str) -> Self {
102        Self::new(self.package().cloned(), self.vpath().with_extension(extension))
103    }
104
105    /// Construct from a raw number.
106    ///
107    /// Should only be used with numbers retrieved via
108    /// [`into_raw`](Self::into_raw). Misuse may results in panics, but no
109    /// unsafety.
110    pub const fn from_raw(v: NonZeroU16) -> Self {
111        Self(v)
112    }
113
114    /// Extract the raw underlying number.
115    pub const fn into_raw(self) -> NonZeroU16 {
116        self.0
117    }
118
119    /// Get the static pair.
120    fn pair(&self) -> Pair {
121        INTERNER.read().unwrap().from_id[usize::from(self.0.get() - 1)]
122    }
123}
124
125impl Debug for FileId {
126    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
127        let vpath = self.vpath();
128        match self.package() {
129            Some(package) => write!(f, "{package:?}{vpath:?}"),
130            None => write!(f, "{vpath:?}"),
131        }
132    }
133}