1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
use crate::{offset_calculator::OffsetCalculator, to_console_string::ToConsoleString, util};
use anyhow::Result;
use std::{
    cell::RefCell,
    collections::HashMap,
    fs::read_to_string,
    ops::Deref,
    path::{Path, PathBuf},
    rc::Rc,
};

thread_local! {
    static ROOT: RefCell<Option<Rc<PathBuf>>> = RefCell::new(None);
    static SOURCE_FILES: RefCell<HashMap<PathBuf, SourceFile>> = RefCell::new(HashMap::new());
}

#[derive(Clone, Eq, PartialEq)]
pub struct SourceFile {
    inner: Rc<Inner>,
}

struct Inner {
    root: Rc<PathBuf>,
    path: PathBuf,
    contents: &'static str,
    offset_calculator: RefCell<OffsetCalculator<'static>>,
}

impl Eq for Inner {}

impl PartialEq for Inner {
    fn eq(&self, other: &Self) -> bool {
        self.root.eq(&other.root) && self.path.eq(&other.path)
    }
}

impl SourceFile {
    pub fn new(root: Rc<PathBuf>, path: PathBuf) -> Result<Self> {
        ROOT.with(|root_prev| {
            let mut root_prev = root_prev.borrow_mut();

            if let Some(root_prev) = root_prev.as_ref() {
                assert_eq!(*root_prev, root);
            } else {
                assert!(root.is_absolute());
                *root_prev = Some(root.clone());
            }
        });

        assert!(path.starts_with(&*root));

        SOURCE_FILES.with(|source_files| {
            let mut source_files = source_files.borrow_mut();

            if let Some(source_file) = source_files.get(&path) {
                Ok(source_file.clone())
            } else {
                let contents = read_to_string(&path)?;
                let leaked = Box::leak(contents.into_boxed_str());
                let source_file = Self {
                    inner: Rc::new(Inner {
                        root,
                        path: path.clone(),
                        contents: leaked,
                        offset_calculator: RefCell::new(OffsetCalculator::new(leaked)),
                    }),
                };
                source_files.insert(path, source_file.clone());
                Ok(source_file)
            }
        })
    }

    fn relative_path(&self) -> &Path {
        #[allow(clippy::unwrap_used)]
        util::strip_prefix(&self.inner.path, &self.inner.root).unwrap()
    }

    // smoelius: Leaking the file contents is a hack.
    #[must_use]
    pub fn contents(&self) -> &'static str {
        self.inner.contents
    }

    #[must_use]
    pub fn offset_calculator(&self) -> &RefCell<OffsetCalculator<'static>> {
        &self.inner.offset_calculator
    }
}

impl std::fmt::Debug for SourceFile {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        <_ as std::fmt::Debug>::fmt(&self.inner.path, f)
    }
}

/// Gives the path relative to the project root
impl std::fmt::Display for SourceFile {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.relative_path().to_string_lossy())
    }
}

impl ToConsoleString for SourceFile {
    fn to_console_string(&self) -> String {
        util::strip_current_dir(self).to_string_lossy().to_string()
    }
}

impl Ord for SourceFile {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        self.relative_path().cmp(other.relative_path())
    }
}

impl PartialOrd for SourceFile {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

impl AsRef<PathBuf> for SourceFile {
    fn as_ref(&self) -> &PathBuf {
        &self.inner.path
    }
}

impl Deref for SourceFile {
    type Target = Path;
    fn deref(&self) -> &Self::Target {
        #[allow(clippy::explicit_deref_methods)]
        self.inner.path.deref()
    }
}