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: Vec<usize>,
18}
19
20impl SourceFile {
21 fn new(id: usize, path: PathBuf, source: Arc<str>) -> Self {
23 let line_starts = std::iter::once(0)
24 .chain(source.match_indices('\n').map(|(i, _)| i + 1))
25 .collect();
26
27 Self {
28 id,
29 path,
30 source,
31 line_starts,
32 }
33 }
34
35 #[must_use]
37 pub fn line_col(&self, offset: usize) -> (usize, usize) {
38 let line = self
39 .line_starts
40 .iter()
41 .rposition(|&start| start <= offset)
42 .unwrap_or(0);
43
44 let col = offset - self.line_starts[line];
45
46 (line + 1, col + 1)
47 }
48
49 #[must_use]
51 pub fn span_text(&self, span: &Span) -> &str {
52 &self.source[span.start..span.end.min(self.source.len())]
53 }
54
55 #[must_use]
57 pub fn line(&self, line_num: usize) -> Option<&str> {
58 if line_num == 0 || line_num > self.line_starts.len() {
59 return None;
60 }
61
62 let start = self.line_starts[line_num - 1];
63 let end = if line_num < self.line_starts.len() {
64 self.line_starts[line_num] - 1 } else {
66 self.source.len()
67 };
68
69 Some(&self.source[start..end])
70 }
71
72 #[must_use]
74 pub const fn num_lines(&self) -> usize {
75 self.line_starts.len()
76 }
77
78 #[must_use]
82 pub fn line_start(&self, line_num: usize) -> Option<usize> {
83 if line_num == 0 || line_num > self.line_starts.len() {
84 return None;
85 }
86 Some(self.line_starts[line_num - 1])
87 }
88}
89
90#[derive(Debug, Default)]
92pub struct SourceMap {
93 files: Vec<SourceFile>,
94}
95
96impl SourceMap {
97 #[must_use]
99 pub fn new() -> Self {
100 Self::default()
101 }
102
103 pub fn add_file(&mut self, path: PathBuf, source: Arc<str>) -> usize {
115 let id = self.files.len();
116 assert!(
117 id < rustledger_parser::SYNTHESIZED_FILE_ID as usize,
118 "SourceMap exceeded {} files; file_id {id} collides with SYNTHESIZED_FILE_ID sentinel",
119 rustledger_parser::SYNTHESIZED_FILE_ID,
120 );
121 self.files.push(SourceFile::new(id, path, source));
122 id
123 }
124
125 #[must_use]
127 pub fn get(&self, id: usize) -> Option<&SourceFile> {
128 self.files.get(id)
129 }
130
131 #[must_use]
133 pub fn get_by_path(&self, path: &std::path::Path) -> Option<&SourceFile> {
134 self.files.iter().find(|f| f.path == path)
135 }
136
137 #[must_use]
139 pub fn files(&self) -> &[SourceFile] {
140 &self.files
141 }
142
143 #[must_use]
145 pub fn format_span(&self, file_id: usize, span: &Span) -> String {
146 if let Some(file) = self.get(file_id) {
147 let (line, col) = file.line_col(span.start);
148 format!("{}:{}:{}", file.path.display(), line, col)
149 } else {
150 format!("?:{}..{}", span.start, span.end)
151 }
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn test_line_col() {
161 let source: Arc<str> = "line 1\nline 2\nline 3".into();
162 let file = SourceFile::new(0, PathBuf::from("test.beancount"), source);
163
164 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)); }
169
170 #[test]
171 fn test_get_line() {
172 let source: Arc<str> = "line 1\nline 2\nline 3".into();
173 let file = SourceFile::new(0, PathBuf::from("test.beancount"), source);
174
175 assert_eq!(file.line(1), Some("line 1"));
176 assert_eq!(file.line(2), Some("line 2"));
177 assert_eq!(file.line(3), Some("line 3"));
178 assert_eq!(file.line(0), None);
179 assert_eq!(file.line(4), None);
180 }
181
182 #[test]
183 fn test_line_start() {
184 let source: Arc<str> = "line 1\nline 2\nline 3".into();
185 let file = SourceFile::new(0, PathBuf::from("test.beancount"), source);
186
187 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); }
197
198 #[test]
199 fn test_source_map() {
200 let mut sm = SourceMap::new();
201 let id = sm.add_file(PathBuf::from("test.beancount"), "content".into());
202
203 assert_eq!(id, 0);
204 assert!(sm.get(0).is_some());
205 assert!(sm.get(1).is_none());
206 }
207}