1use php_ast::Span;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub struct LineCol {
24 pub line: u32,
26 pub col: u32,
28}
29
30impl LineCol {
31 pub fn to_one_based(self) -> (u32, u32) {
33 (self.line + 1, self.col + 1)
34 }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
39pub struct LineColSpan {
40 pub start: LineCol,
42 pub end: LineCol,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct SourceMap {
52 line_starts: Vec<u32>,
54 len: u32,
56}
57
58impl SourceMap {
59 pub fn new(source: &str) -> Self {
61 let mut line_starts = Vec::with_capacity(source.len() / 40 + 1);
62 line_starts.push(0u32);
63 for pos in memchr::memchr_iter(b'\n', source.as_bytes()) {
64 line_starts.push((pos + 1) as u32);
65 }
66 Self {
67 line_starts,
68 len: source.len() as u32,
69 }
70 }
71
72 pub fn empty() -> Self {
76 Self {
77 line_starts: vec![0u32],
78 len: u32::MAX,
79 }
80 }
81
82 pub fn line_count(&self) -> usize {
84 self.line_starts.len()
85 }
86
87 pub fn line_start(&self, line: u32) -> Option<u32> {
90 self.line_starts.get(line as usize).copied()
91 }
92
93 pub fn offset_to_line_col(&self, offset: u32) -> LineCol {
98 let line = match self.line_starts.binary_search(&offset) {
99 Ok(exact) => exact,
100 Err(after) => after - 1,
101 };
102 let col = offset - self.line_starts[line];
103 LineCol {
104 line: line as u32,
105 col,
106 }
107 }
108
109 pub fn span_to_line_col(&self, span: Span) -> LineColSpan {
111 LineColSpan {
112 start: self.offset_to_line_col(span.start),
113 end: self.offset_to_line_col(span.end),
114 }
115 }
116
117 pub fn line_col_to_offset(&self, lc: LineCol) -> Option<u32> {
119 let line = lc.line as usize;
120 let start = *self.line_starts.get(line)?;
121 let end = match self.line_starts.get(line + 1) {
122 Some(next_start) => next_start - 1,
123 None => self.len,
124 };
125 start.checked_add(lc.col).filter(|&offset| offset <= end)
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn empty_source() {
135 let map = SourceMap::new("");
136 assert_eq!(map.line_count(), 1);
137 assert_eq!(map.offset_to_line_col(0), LineCol { line: 0, col: 0 });
138 }
139
140 #[test]
141 fn single_line_no_newline() {
142 let map = SourceMap::new("<?php echo 1;");
143 assert_eq!(map.line_count(), 1);
144 assert_eq!(map.offset_to_line_col(0), LineCol { line: 0, col: 0 });
145 assert_eq!(map.offset_to_line_col(6), LineCol { line: 0, col: 6 });
146 }
147
148 #[test]
149 fn multiple_lines() {
150 let src = "<?php\necho 'hi';\nreturn;\n";
151 let map = SourceMap::new(src);
152 assert_eq!(map.line_count(), 4); assert_eq!(map.offset_to_line_col(0), LineCol { line: 0, col: 0 });
156 assert_eq!(map.offset_to_line_col(6), LineCol { line: 1, col: 0 });
158 assert_eq!(map.offset_to_line_col(6), LineCol { line: 1, col: 0 });
160 assert_eq!(map.offset_to_line_col(17), LineCol { line: 2, col: 0 });
162 }
163
164 #[test]
165 fn span_conversion() {
166 let src = "<?php\necho 'hi';\n";
167 let map = SourceMap::new(src);
168 let span = Span::new(6, 10); let lc = map.span_to_line_col(span);
170 assert_eq!(lc.start, LineCol { line: 1, col: 0 });
171 assert_eq!(lc.end, LineCol { line: 1, col: 4 });
172 }
173
174 #[test]
175 fn round_trip() {
176 let src = "<?php\necho 'hi';\nreturn;\n";
177 let map = SourceMap::new(src);
178 let lc = LineCol { line: 1, col: 5 };
179 let offset = map.line_col_to_offset(lc).unwrap();
180 assert_eq!(map.offset_to_line_col(offset), lc);
181 }
182
183 #[test]
184 fn column_past_line_end_is_rejected() {
185 let map = SourceMap::new("ab\ncd");
186 assert_eq!(map.line_col_to_offset(LineCol { line: 0, col: 2 }), Some(2));
187 assert_eq!(map.line_col_to_offset(LineCol { line: 0, col: 3 }), None);
188 assert_eq!(map.line_col_to_offset(LineCol { line: 1, col: 2 }), Some(5));
189 assert_eq!(map.line_col_to_offset(LineCol { line: 1, col: 3 }), None);
190 assert_eq!(
191 map.line_col_to_offset(LineCol {
192 line: 1,
193 col: u32::MAX
194 }),
195 None
196 );
197 }
198
199 #[test]
200 fn empty_map_accepts_any_column() {
201 let map = SourceMap::empty();
202 assert_eq!(
203 map.line_col_to_offset(LineCol { line: 0, col: 42 }),
204 Some(42)
205 );
206 assert_eq!(map.line_col_to_offset(LineCol { line: 1, col: 0 }), None);
207 }
208
209 #[test]
210 fn one_based() {
211 let lc = LineCol { line: 0, col: 0 };
212 assert_eq!(lc.to_one_based(), (1, 1));
213 let lc = LineCol { line: 2, col: 5 };
214 assert_eq!(lc.to_one_based(), (3, 6));
215 }
216
217 #[test]
218 fn line_start_lookup() {
219 let src = "aaa\nbbb\nccc";
220 let map = SourceMap::new(src);
221 assert_eq!(map.line_start(0), Some(0));
222 assert_eq!(map.line_start(1), Some(4));
223 assert_eq!(map.line_start(2), Some(8));
224 assert_eq!(map.line_start(3), None);
225 }
226
227 #[test]
228 fn crlf_treated_as_two_bytes() {
229 let src = "a\r\nb";
231 let map = SourceMap::new(src);
232 assert_eq!(map.line_count(), 2);
233 assert_eq!(map.offset_to_line_col(3), LineCol { line: 1, col: 0 });
235 }
236}