Skip to main content

moxy_token/source/
file.rs

1use std::cell::RefCell;
2use std::collections::BTreeMap;
3use std::path::PathBuf;
4
5use super::Location;
6use crate::span::fallback::Span;
7
8/// Primarily used to map spans (0 based character index ranges)
9/// to bytes.
10/// Source text with a stable identifier for span resolution.
11#[derive(Debug)]
12pub struct Source {
13    /// the filepath if this source belongs to a file
14    /// on disk.
15    path: Option<PathBuf>,
16
17    /// raw source text
18    text: String,
19
20    /// file-wide lo..hi in proc-macro2 space
21    span: Span,
22
23    /// line start offsets, in char units
24    lines: Vec<usize>,
25
26    /// Cache mapping character indices to UTF-8 byte offsets for efficient span slicing
27    char_to_byte: RefCell<BTreeMap<usize, usize>>,
28}
29
30impl Source {
31    pub(crate) fn new(start: usize, src: impl Into<String>) -> Self {
32        let text = src.into();
33        let mut lines = vec![0];
34        let mut total = 0usize;
35
36        for ch in text.chars() {
37            total += 1;
38
39            if ch == '\n' {
40                lines.push(total);
41            }
42        }
43
44        Self {
45            path: None,
46            text,
47            span: Span::new(start as u32, (start + total) as u32),
48            lines,
49            char_to_byte: RefCell::new(BTreeMap::new()),
50        }
51    }
52
53    pub fn from_file(path: impl Into<PathBuf>) -> std::io::Result<Self> {
54        let path = path.into();
55        let src = std::fs::read_to_string(&path)?;
56        let mut src = Self::new(0, src);
57
58        src.path = Some(path);
59        Ok(src)
60    }
61
62    pub fn path(&self) -> Option<&PathBuf> {
63        self.path.as_ref()
64    }
65
66    pub fn text(&self) -> &str {
67        &self.text
68    }
69
70    pub fn span(&self) -> Span {
71        self.span
72    }
73
74    /// Resolves the given span into a byte index range.
75    pub fn range(&self, span: Span) -> std::ops::Range<usize> {
76        let r = span.byte_range();
77        self.byte(r.start)..self.byte(r.end)
78    }
79
80    /// Gets a sub span of source text from the file.
81    pub fn slice(&self, span: Span) -> String {
82        self.text[self.range(span)].to_owned()
83    }
84
85    /// Resolves a global character index within this file into a 0-based `Location`.
86    pub fn location(&self, i: usize) -> Location {
87        let index = i - self.span.byte_range().start;
88
89        match self.lines.binary_search(&index) {
90            Err(next) => Location::new(index, next - 1, index - self.lines[next - 1]),
91            Ok(line) => Location::new(index, line, 0),
92        }
93    }
94
95    /// Returns the UTF-8 byte index corresponding to a global character index.
96    pub fn byte(&self, i: usize) -> usize {
97        let index = i - self.span.byte_range().start;
98        let mut cache = self.char_to_byte.borrow_mut();
99
100        if let Some(byte_index) = cache.get(&index) {
101            return *byte_index;
102        }
103
104        let (ci, bi) = cache
105            .range(..=index)
106            .next_back()
107            .map(|(&char_index, &byte_index)| (char_index, byte_index))
108            .unwrap_or((0, 0));
109
110        let mut char_index = ci;
111        let mut byte_index = bi;
112
113        for ch in self.text[bi..].chars() {
114            if char_index == index {
115                cache.insert(index, byte_index);
116                return byte_index;
117            }
118
119            char_index += 1;
120            byte_index += ch.len_utf8();
121        }
122
123        cache.insert(index, byte_index);
124        byte_index
125    }
126}
127
128impl Default for Source {
129    fn default() -> Self {
130        Self {
131            path: None,
132            text: String::default(),
133            span: Span::default(),
134            lines: vec![0],
135            char_to_byte: RefCell::new(BTreeMap::default()),
136        }
137    }
138}