typst_syntax/
file.rs

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