Skip to main content

nova_diagnostics/
files.rs

1//! Source file database — maps `FileId` to source text.
2
3use std::collections::HashMap;
4
5/// An opaque identifier for a source file in the `FileDb`.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
7pub struct FileId(u32);
8
9impl FileId {
10    /// A sentinel `FileId` used in tests and synthesized spans.
11    pub const DUMMY: Self = Self(u32::MAX);
12}
13
14/// Database of source files, keyed by `FileId`.
15#[derive(Debug, Default)]
16pub struct FileDb {
17    files: Vec<SourceFile>,
18    by_name: HashMap<String, FileId>,
19}
20
21#[derive(Debug)]
22struct SourceFile {
23    name: String,
24    source: String,
25    /// Byte offsets of the start of each line (for fast line/column lookup).
26    line_starts: Vec<u32>,
27}
28
29impl SourceFile {
30    fn new(name: String, source: String) -> Self {
31        let line_starts = std::iter::once(0)
32            .chain(source.match_indices('\n').map(|(i, _)| (i + 1) as u32))
33            .collect();
34        Self {
35            name,
36            source,
37            line_starts,
38        }
39    }
40}
41
42impl FileDb {
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    /// Add a file and return its `FileId`.
48    pub fn add(&mut self, name: impl Into<String>, source: impl Into<String>) -> FileId {
49        let name = name.into();
50        let source = source.into();
51        let id = FileId(self.files.len() as u32);
52        self.by_name.insert(name.clone(), id);
53        self.files.push(SourceFile::new(name, source));
54        id
55    }
56
57    pub fn get_source(&self, id: FileId) -> Option<&str> {
58        self.files.get(id.0 as usize).map(|f| f.source.as_str())
59    }
60
61    pub fn get_name(&self, id: FileId) -> Option<&str> {
62        self.files.get(id.0 as usize).map(|f| f.name.as_str())
63    }
64
65    /// The file added under `name`, the latest if several were.
66    pub fn id_of(&self, name: &str) -> Option<FileId> {
67        self.by_name.get(name).copied()
68    }
69
70    /// Returns (1-based line, 1-based column) for a byte offset.
71    pub fn location(&self, id: FileId, offset: u32) -> Option<(usize, usize)> {
72        let file = self.files.get(id.0 as usize)?;
73        let line_idx = file
74            .line_starts
75            .partition_point(|&start| start <= offset)
76            .saturating_sub(1);
77        let line_start = file.line_starts[line_idx] as usize;
78        let col = offset as usize - line_start + 1;
79        Some((line_idx + 1, col))
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn a_file_is_found_by_its_name() {
89        let mut db = FileDb::new();
90        let a = db.add("<std/core>", "a");
91        let b = db.add("b.nova", "b");
92        assert_eq!(db.id_of("<std/core>"), Some(a));
93        assert_eq!(db.id_of("b.nova"), Some(b));
94        assert_eq!(db.id_of("c.nova"), None);
95    }
96}