compose_syntax/
file.rs

1use std::collections::HashMap;
2use std::fmt;
3use std::fmt::{Debug, Display, Formatter};
4use std::num::NonZeroU16;
5use std::path::PathBuf;
6use std::sync::{LazyLock, RwLock};
7
8static INTERNER: LazyLock<RwLock<FileInterner>> = LazyLock::new(|| {
9    RwLock::new(FileInterner {
10        from_id: Vec::new(),
11        to_id: HashMap::new(),
12    })
13});
14
15/// Resets the interner for file ids.
16///
17/// # Warning
18///
19/// This will break existing file ids and cause any reading of paths from invalidated file ids to panic!
20/// Only use this if you know you will not use any of the existing file ids.
21pub fn reset_interner() {
22    *INTERNER.write().unwrap() = FileInterner {
23        from_id: Vec::new(),
24        to_id: HashMap::new(),
25    }
26}
27
28struct FileInterner {
29    from_id: Vec<FileRef>,
30    to_id: HashMap<FileRef, FileId>,
31}
32
33type FileRef = &'static VirtualPath;
34
35/// An absolute path in the virtual file system of a project
36#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
37pub struct VirtualPath(pub PathBuf);
38
39impl<T> From<T> for VirtualPath
40where
41    T: Into<PathBuf>,
42{
43    fn from(value: T) -> Self {
44        VirtualPath::new(value)   
45    }
46}
47
48impl VirtualPath {
49    /// Create a new virtual path.
50    pub fn new(path: impl Into<PathBuf>) -> Self {
51        let path = path.into();
52        Self(path.clone().canonicalize().unwrap_or(path))
53    }
54
55    pub fn display(&self) -> String {
56        self.0.display().to_string()
57    }
58}
59
60impl Debug for VirtualPath {
61    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
62        Display::fmt(&self.0.display(), f)
63    }
64}
65
66/// Identifier for a file,
67///
68/// Globally interned and thus cheap to copy, compare, and hash.
69#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
70pub struct FileId(NonZeroU16);
71
72impl FileId {
73    #[track_caller]
74    pub fn new(path: impl Into<VirtualPath>) -> Self {
75        // Check if the file is already in the interner.
76        let path = path.into();
77        let interner = INTERNER.read().unwrap();
78        if let Some(&id) = interner.to_id.get(&path) {
79            return id;
80        }
81
82        let num = u16::try_from(interner.from_id.len() + 1)
83            .and_then(NonZeroU16::try_from)
84            .expect("out of file ids");
85        drop(interner);
86
87        let id = FileId(num);
88        let leaked = Box::leak(Box::new(path));
89        
90        let mut interner = INTERNER.write().unwrap();
91        interner.to_id.insert(leaked, id);
92        interner.from_id.push(leaked);
93        id
94    }
95
96    /// Create a fake file id for a virtual path.
97    ///
98    /// Always returns a new file id, no matter if the file is already in the interner.
99    #[track_caller]
100    pub fn fake(path: impl Into<VirtualPath>) -> Self {
101        let mut interner = INTERNER.write().unwrap();
102        let num = u16::try_from(interner.from_id.len() + 1)
103            .and_then(NonZeroU16::try_from)
104            .expect("out of file ids");
105
106        let id = FileId(num);
107        let leaked = Box::leak(Box::new(path.into()));
108        interner.from_id.push(leaked);
109        id
110    }
111
112    #[track_caller]
113    pub fn path(&self) -> &'static VirtualPath {
114        self.try_path().expect("file id not interned")
115    }
116
117    pub fn try_path(&self) -> Option<&'static VirtualPath> {
118        let interner = INTERNER.try_read().ok()?;
119        let id = usize::from(self.0.get() - 1);
120
121        interner.from_id.get(id).copied()
122    }
123
124    /// Extract the raw underlying number
125    pub(crate) const fn into_raw(self) -> NonZeroU16 {
126        self.0
127    }
128    /// Create a FileId from a raw NonZeroU16
129    pub(crate) const fn from_raw(raw: NonZeroU16) -> Self {
130        Self(raw)
131    }
132}
133
134impl Debug for FileId {
135    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
136        let path = self
137            .try_path()
138            .map(|p| format!("{:?}", p))
139            .unwrap_or_else(|| String::from("not interned"));
140        write!(f, "FileId({id}, {path})", id = self.0.get())
141    }
142}