rustledger_loader/
source_map.rs1use rustledger_parser::Span;
4use std::path::PathBuf;
5use std::sync::Arc;
6
7#[derive(Debug, Clone)]
9pub struct SourceFile {
10 pub id: usize,
12 pub path: PathBuf,
14 pub source: Arc<str>,
16 line_starts: std::sync::OnceLock<Vec<usize>>,
25}
26
27impl SourceFile {
28 const fn new(id: usize, path: PathBuf, source: Arc<str>) -> Self {
30 Self {
31 id,
32 path,
33 source,
34 line_starts: std::sync::OnceLock::new(),
35 }
36 }
37
38 fn line_starts(&self) -> &[usize] {
40 self.line_starts.get_or_init(|| {
41 std::iter::once(0)
42 .chain(self.source.match_indices('\n').map(|(i, _)| i + 1))
43 .collect()
44 })
45 }
46
47 #[must_use]
49 pub fn line_col(&self, offset: usize) -> (usize, usize) {
50 let starts = self.line_starts();
57 let line = starts.partition_point(|&start| start <= offset) - 1;
58
59 let col = offset - starts[line];
60
61 (line + 1, col + 1)
62 }
63
64 #[must_use]
66 pub fn span_text(&self, span: &Span) -> &str {
67 &self.source[span.start..span.end.min(self.source.len())]
68 }
69
70 #[must_use]
72 pub fn line(&self, line_num: usize) -> Option<&str> {
73 let starts = self.line_starts();
74 if line_num == 0 || line_num > starts.len() {
75 return None;
76 }
77
78 let start = starts[line_num - 1];
79 let end = if line_num < starts.len() {
80 starts[line_num] - 1 } else {
82 self.source.len()
83 };
84
85 Some(&self.source[start..end])
86 }
87
88 #[must_use]
90 pub fn num_lines(&self) -> usize {
91 self.line_starts().len()
92 }
93
94 #[must_use]
98 pub fn line_start(&self, line_num: usize) -> Option<usize> {
99 let starts = self.line_starts();
100 if line_num == 0 || line_num > starts.len() {
101 return None;
102 }
103 Some(starts[line_num - 1])
104 }
105}
106
107#[derive(Debug, Default)]
109pub struct SourceMap {
110 files: Vec<SourceFile>,
111}
112
113impl SourceMap {
114 #[must_use]
116 pub fn new() -> Self {
117 Self::default()
118 }
119
120 pub fn add_file(&mut self, path: PathBuf, source: Arc<str>) -> usize {
132 let id = self.files.len();
133 assert!(
134 id < rustledger_parser::SYNTHESIZED_FILE_ID as usize,
135 "SourceMap exceeded {} files; file_id {id} collides with SYNTHESIZED_FILE_ID sentinel",
136 rustledger_parser::SYNTHESIZED_FILE_ID,
137 );
138 self.files.push(SourceFile::new(id, path, source));
139 id
140 }
141
142 #[must_use]
144 pub fn get(&self, id: usize) -> Option<&SourceFile> {
145 self.files.get(id)
146 }
147
148 #[must_use]
150 pub fn get_by_path(&self, path: &std::path::Path) -> Option<&SourceFile> {
151 self.files.iter().find(|f| f.path == path)
152 }
153
154 #[must_use]
156 pub fn files(&self) -> &[SourceFile] {
157 &self.files
158 }
159
160 #[must_use]
162 pub fn format_span(&self, file_id: usize, span: &Span) -> String {
163 if let Some(file) = self.get(file_id) {
164 let (line, col) = file.line_col(span.start);
165 format!("{}:{}:{}", file.path.display(), line, col)
166 } else {
167 format!("?:{}..{}", span.start, span.end)
168 }
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn test_line_col() {
178 let source: Arc<str> = "line 1\nline 2\nline 3".into();
179 let file = SourceFile::new(0, PathBuf::from("test.beancount"), source);
180
181 assert_eq!(file.line_col(0), (1, 1)); assert_eq!(file.line_col(5), (1, 6)); assert_eq!(file.line_col(7), (2, 1)); assert_eq!(file.line_col(14), (3, 1)); }
186
187 #[test]
188 fn test_get_line() {
189 let source: Arc<str> = "line 1\nline 2\nline 3".into();
190 let file = SourceFile::new(0, PathBuf::from("test.beancount"), source);
191
192 assert_eq!(file.line(1), Some("line 1"));
193 assert_eq!(file.line(2), Some("line 2"));
194 assert_eq!(file.line(3), Some("line 3"));
195 assert_eq!(file.line(0), None);
196 assert_eq!(file.line(4), None);
197 }
198
199 #[test]
200 fn test_line_start() {
201 let source: Arc<str> = "line 1\nline 2\nline 3".into();
202 let file = SourceFile::new(0, PathBuf::from("test.beancount"), source);
203
204 assert_eq!(file.line_start(1), Some(0)); assert_eq!(file.line_start(2), Some(7)); assert_eq!(file.line_start(3), Some(14)); assert_eq!(file.line_start(0), None); assert_eq!(file.line_start(4), None); assert_eq!(file.line_start(100), None); }
214
215 #[test]
216 fn test_source_map() {
217 let mut sm = SourceMap::new();
218 let id = sm.add_file(PathBuf::from("test.beancount"), "content".into());
219
220 assert_eq!(id, 0);
221 assert!(sm.get(0).is_some());
222 assert!(sm.get(1).is_none());
223 }
224}