Skip to main content

solar_interface/source_map/
file.rs

1use crate::{BytePos, CharPos, pos::RelativeBytePos};
2use std::{
3    fmt, io,
4    ops::{Range, RangeInclusive},
5    path::{Path, PathBuf},
6    sync::Arc,
7};
8
9/// Identifies an offset of a multi-byte character in a `SourceFile`.
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub struct MultiByteChar {
12    /// The relative offset of the character in the `SourceFile`.
13    pub pos: RelativeBytePos,
14    /// The number of bytes, `>= 2`.
15    pub bytes: u8,
16}
17
18/// The name of a source file.
19///
20/// This is used as the key in the source map. See
21/// [`SourceMap::get_file`](crate::SourceMap::get_file).
22#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
23pub enum FileName {
24    /// Files from the file system.
25    Real(PathBuf),
26    /// Command line.
27    Stdin,
28    /// Custom sources for explicit parser calls from plugins and drivers.
29    Custom(String),
30}
31
32impl PartialEq<Path> for FileName {
33    fn eq(&self, other: &Path) -> bool {
34        match self {
35            Self::Real(p) => p == other,
36            _ => false,
37        }
38    }
39}
40
41impl PartialEq<&Path> for FileName {
42    fn eq(&self, other: &&Path) -> bool {
43        match self {
44            Self::Real(p) => p == *other,
45            _ => false,
46        }
47    }
48}
49
50impl PartialEq<PathBuf> for FileName {
51    fn eq(&self, other: &PathBuf) -> bool {
52        match self {
53            Self::Real(p) => p == other,
54            _ => false,
55        }
56    }
57}
58
59impl From<PathBuf> for FileName {
60    fn from(p: PathBuf) -> Self {
61        Self::Real(p)
62    }
63}
64
65impl From<&PathBuf> for FileName {
66    fn from(p: &PathBuf) -> Self {
67        Self::Real(p.clone())
68    }
69}
70
71impl From<&Path> for FileName {
72    fn from(p: &Path) -> Self {
73        Self::Real(p.to_path_buf())
74    }
75}
76
77impl From<String> for FileName {
78    fn from(s: String) -> Self {
79        Self::Custom(s)
80    }
81}
82
83impl From<&Self> for FileName {
84    fn from(s: &Self) -> Self {
85        s.clone()
86    }
87}
88
89impl FileName {
90    /// Parses a file name from a string.
91    ///
92    /// See [`SourceMap::parse_file_name`](crate::SourceMap::parse_file_name).
93    pub fn parse(sm: &crate::SourceMap, s: &str) -> Self {
94        sm.parse_file_name(s)
95    }
96
97    /// Creates a new `FileName` from a path.
98    pub fn real(path: impl Into<PathBuf>) -> Self {
99        Self::Real(path.into())
100    }
101
102    /// Creates a new `FileName` from a string.
103    pub fn custom(s: impl Into<String>) -> Self {
104        Self::Custom(s.into())
105    }
106
107    /// Displays the filename.
108    #[inline]
109    pub fn display(&self) -> FileNameDisplay<'_> {
110        let base_path =
111            crate::SessionGlobals::try_with(|g| g.and_then(|g| g.source_map.base_path()));
112        FileNameDisplay { inner: self, base_path }
113    }
114
115    /// Returns the path if the file name is a real file.
116    #[inline]
117    pub fn as_real(&self) -> Option<&Path> {
118        match self {
119            Self::Real(path) => Some(path),
120            _ => None,
121        }
122    }
123}
124
125/// A display wrapper for `FileName`.
126///
127/// Created by [`FileName::display`].
128pub struct FileNameDisplay<'a> {
129    pub(crate) inner: &'a FileName,
130    pub(crate) base_path: Option<PathBuf>,
131}
132
133impl fmt::Display for FileNameDisplay<'_> {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        match self.inner {
136            FileName::Real(path) => {
137                let path = if let Some(base_path) = &self.base_path
138                    && let Ok(rpath) = path.strip_prefix(base_path)
139                {
140                    rpath
141                } else {
142                    path.as_path()
143                };
144                path.display().fmt(f)
145            }
146            FileName::Stdin => f.write_str("<stdin>"),
147            FileName::Custom(s) => write!(f, "<{s}>"),
148        }
149    }
150}
151
152#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
153pub(crate) struct SourceFileId(u64);
154
155impl SourceFileId {
156    pub(crate) fn new(filename: &FileName) -> Self {
157        use std::hash::{Hash, Hasher};
158        let mut hasher = solar_data_structures::map::FxHasher::with_seed(0);
159        filename.hash(&mut hasher);
160        Self(hasher.finish())
161    }
162}
163
164/// Sum of all file lengths is over [`u32::MAX`].
165#[derive(Debug)]
166pub struct OffsetOverflowError(pub(crate) ());
167
168impl fmt::Display for OffsetOverflowError {
169    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170        f.write_str("files larger than 4GiB are not supported")
171    }
172}
173
174impl std::error::Error for OffsetOverflowError {}
175
176impl From<OffsetOverflowError> for io::Error {
177    fn from(e: OffsetOverflowError) -> Self {
178        Self::new(io::ErrorKind::FileTooLarge, e)
179    }
180}
181
182/// A single source in the `SourceMap`.
183#[derive(Clone, derive_more::Debug)]
184#[non_exhaustive]
185pub struct SourceFile {
186    /// The name of the file that the source came from. Source that doesn't
187    /// originate from files has names between angle brackets by convention
188    /// (e.g., `<stdin>`).
189    pub name: FileName,
190    /// The complete source code.
191    #[debug(skip)]
192    pub src: Arc<String>,
193    /// The start position of this source in the `SourceMap`.
194    pub start_pos: BytePos,
195    /// The byte length of this source.
196    pub source_len: RelativeBytePos,
197    /// Locations of lines beginnings in the source code.
198    #[debug(skip)]
199    lines: Vec<RelativeBytePos>,
200    /// Locations of multi-byte characters in the source code.
201    #[debug(skip)]
202    pub multibyte_chars: Vec<MultiByteChar>,
203}
204
205impl PartialEq for SourceFile {
206    fn eq(&self, other: &Self) -> bool {
207        self.start_pos == other.start_pos
208    }
209}
210
211impl Eq for SourceFile {}
212
213impl std::hash::Hash for SourceFile {
214    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
215        self.start_pos.hash(state);
216    }
217}
218
219impl SourceFile {
220    /// Creates a new `SourceFile`. Use the [`SourceMap`](crate::SourceMap) methods instead.
221    pub(crate) fn new(
222        name: FileName,
223        id: SourceFileId,
224        mut src: String,
225    ) -> Result<Self, OffsetOverflowError> {
226        // Compute the file hash before any normalization.
227        // let src_hash = SourceFileHash::new(hash_kind, &src);
228
229        // let normalized_pos = normalize_src(&mut src);
230
231        debug_assert_eq!(id, SourceFileId::new(&name));
232        let source_len = src.len();
233        let source_len = u32::try_from(source_len).map_err(|_| OffsetOverflowError(()))?;
234
235        let (lines, multibyte_chars) = super::analyze::analyze_source_file(&src);
236
237        src.shrink_to_fit();
238        Ok(Self {
239            name,
240            src: Arc::new(src),
241            start_pos: BytePos::from_u32(0),
242            source_len: RelativeBytePos::from_u32(source_len),
243            lines,
244            multibyte_chars,
245        })
246    }
247
248    pub fn lines(&self) -> &[RelativeBytePos] {
249        &self.lines
250    }
251
252    pub fn count_lines(&self) -> usize {
253        self.lines().len()
254    }
255
256    #[inline]
257    pub fn absolute_position(&self, pos: RelativeBytePos) -> BytePos {
258        BytePos::from_u32(pos.to_u32() + self.start_pos.to_u32())
259    }
260
261    #[inline]
262    pub fn relative_position(&self, pos: BytePos) -> RelativeBytePos {
263        RelativeBytePos::from_u32(pos.to_u32() - self.start_pos.to_u32())
264    }
265
266    #[inline]
267    pub fn end_position(&self) -> BytePos {
268        self.absolute_position(self.source_len)
269    }
270
271    /// Finds the line containing the given position. The return value is the
272    /// index into the `lines` array of this `SourceFile`, not the 1-based line
273    /// number. If the source_file is empty or the position is located before the
274    /// first line, `None` is returned.
275    pub fn lookup_line(&self, pos: RelativeBytePos) -> Option<usize> {
276        self.lines().partition_point(|x| x <= &pos).checked_sub(1)
277    }
278
279    pub fn line_bounds(&self, line_index: usize) -> Range<BytePos> {
280        if self.is_empty() {
281            return self.start_pos..self.start_pos;
282        }
283
284        let lines = self.lines();
285        assert!(line_index < lines.len());
286        if line_index == (lines.len() - 1) {
287            self.absolute_position(lines[line_index])..self.end_position()
288        } else {
289            self.absolute_position(lines[line_index])..self.absolute_position(lines[line_index + 1])
290        }
291    }
292
293    /// Returns the relative byte position of the start of the line at the given
294    /// 0-based line index.
295    pub fn line_position(&self, line_number: usize) -> Option<usize> {
296        self.lines().get(line_number).map(|x| x.to_usize())
297    }
298
299    /// Converts a `RelativeBytePos` to a `CharPos` relative to the `SourceFile`.
300    pub(crate) fn bytepos_to_file_charpos(&self, bpos: RelativeBytePos) -> CharPos {
301        // The number of extra bytes due to multibyte chars in the `SourceFile`.
302        let mut total_extra_bytes = 0;
303
304        for mbc in self.multibyte_chars.iter() {
305            if mbc.pos < bpos {
306                // Every character is at least one byte, so we only
307                // count the actual extra bytes.
308                total_extra_bytes += mbc.bytes as u32 - 1;
309                // We should never see a byte position in the middle of a
310                // character.
311                assert!(bpos.to_u32() >= mbc.pos.to_u32() + mbc.bytes as u32);
312            } else {
313                break;
314            }
315        }
316
317        assert!(total_extra_bytes <= bpos.to_u32());
318        CharPos(bpos.to_usize() - total_extra_bytes as usize)
319    }
320
321    /// Looks up the file's (1-based) line number and (0-based `CharPos`) column offset, for a
322    /// given `RelativeBytePos`.
323    fn lookup_file_pos(&self, pos: RelativeBytePos) -> (usize, CharPos) {
324        let chpos = self.bytepos_to_file_charpos(pos);
325        match self.lookup_line(pos) {
326            Some(a) => {
327                let line = a + 1; // Line numbers start at 1
328                let linebpos = self.lines()[a];
329                let linechpos = self.bytepos_to_file_charpos(linebpos);
330                let col = chpos - linechpos;
331                assert!(chpos >= linechpos);
332                (line, col)
333            }
334            None => (0, chpos),
335        }
336    }
337
338    /// Looks up the file's (1-based) line number, (0-based `CharPos`) column offset, and (0-based)
339    /// column offset when displayed, for a given `BytePos`.
340    pub fn lookup_file_pos_with_col_display(&self, pos: BytePos) -> (usize, CharPos, usize) {
341        let pos = self.relative_position(pos);
342        let (line, col_or_chpos) = self.lookup_file_pos(pos);
343        if line > 0 {
344            let Some(code) = self.get_line(line - 1) else {
345                // If we don't have the code available, it is ok as a fallback to return the bytepos
346                // instead of the "display" column, which is only used to properly show underlines
347                // in the terminal.
348                // FIXME: we'll want better handling of this in the future for the sake of tools
349                // that want to use the display col instead of byte offsets to modify code, but
350                // that is a problem for another day, the previous code was already incorrect for
351                // both displaying *and* third party tools using the json output naïvely.
352                debug!("couldn't find line {line} in {:?}", self.name);
353                return (line, col_or_chpos, col_or_chpos.0);
354            };
355            let display_col = code.chars().take(col_or_chpos.0).map(char_width).sum();
356            (line, col_or_chpos, display_col)
357        } else {
358            // This is never meant to happen?
359            (0, col_or_chpos, col_or_chpos.0)
360        }
361    }
362
363    /// Gets a line from the list of pre-computed line-beginnings.
364    /// The line number here is 0-based.
365    pub fn get_line(&self, line_number: usize) -> Option<&str> {
366        fn get_until_newline(src: &str, begin: usize) -> &str {
367            // We can't use `lines.get(line_number+1)` because we might
368            // be parsing when we call this function and thus the current
369            // line is the last one we have line info for.
370            let slice = &src[begin..];
371            match slice.find('\n') {
372                Some(e) => &slice[..e],
373                None => slice,
374            }
375        }
376
377        let start = self.lines().get(line_number)?.to_usize();
378        Some(get_until_newline(&self.src, start))
379    }
380
381    /// Gets a slice of the source text between two lines, including the
382    /// terminator of the second line (if any).
383    pub fn get_lines(&self, range: RangeInclusive<usize>) -> Option<&str> {
384        fn get_until_newline(src: &str, start: usize, end: usize) -> &str {
385            match src[end..].find('\n') {
386                Some(e) => &src[start..end + e + 1],
387                None => &src[start..],
388            }
389        }
390
391        let (start, end) = range.into_inner();
392        let lines = self.lines();
393        let start = lines.get(start)?.to_usize();
394        let end = lines.get(end)?.to_usize();
395        Some(get_until_newline(&self.src, start, end))
396    }
397
398    /// Returns whether or not the file contains the given `SourceMap` byte
399    /// position. The position one past the end of the file is considered to be
400    /// contained by the file. This implies that files for which `is_empty`
401    /// returns true still contain one byte position according to this function.
402    #[inline]
403    pub fn contains(&self, byte_pos: BytePos) -> bool {
404        byte_pos >= self.start_pos && byte_pos <= self.end_position()
405    }
406
407    #[inline]
408    pub fn is_empty(&self) -> bool {
409        self.source_len.to_u32() == 0
410    }
411
412    /// Calculates the original byte position relative to the start of the file
413    /// based on the given byte position.
414    pub fn original_relative_byte_pos(&self, pos: BytePos) -> RelativeBytePos {
415        let pos = self.relative_position(pos);
416        RelativeBytePos::from_u32(pos.0)
417    }
418}
419
420pub fn char_width(ch: char) -> usize {
421    match ch {
422        '\t' => 4,
423        _ => unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1),
424    }
425}