1use std::path::PathBuf;
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11#[serde(rename_all = "kebab-case")]
12#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
13#[non_exhaustive]
14pub struct Span {
15 pub start: u32,
17 pub end: u32,
19 pub start_line: u32,
21 pub start_col: u32,
23 pub end_line: u32,
25 pub end_col: u32,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
31#[serde(rename_all = "kebab-case")]
32#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
33#[non_exhaustive]
34pub struct Location {
35 #[serde(serialize_with = "crate::display::serialize_path")]
37 pub path: PathBuf,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub span: Option<Span>,
41}
42
43impl Location {
44 #[must_use]
46 pub fn new(path: impl Into<PathBuf>, span: Option<Span>) -> Self {
47 Self {
48 path: path.into(),
49 span,
50 }
51 }
52
53 #[must_use]
55 pub fn file(path: impl Into<PathBuf>) -> Self {
56 Self::new(path, None)
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
62#[serde(rename_all = "kebab-case")]
63#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
64#[non_exhaustive]
65pub struct Related {
66 pub message: String,
68 pub location: Location,
70}
71
72impl Related {
73 #[must_use]
75 pub fn new(message: impl Into<String>, location: Location) -> Self {
76 Self {
77 message: message.into(),
78 location,
79 }
80 }
81}
82
83impl Span {
84 #[must_use]
86 pub const fn new(start: u32, end: u32, start_line: u32, start_col: u32, end_line: u32, end_col: u32) -> Self {
87 Self {
88 start,
89 end,
90 start_line,
91 start_col,
92 end_line,
93 end_col,
94 }
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99struct WideChar {
100 offset: u32,
101 utf8_len: u32,
102 utf16_len: u32,
103}
104
105impl WideChar {
106 const fn end(self) -> u32 {
107 self.offset + self.utf8_len
108 }
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct LineIndex {
114 line_starts: Vec<u32>,
115 wide_chars: Vec<WideChar>,
116}
117
118fn to_u32(n: usize) -> u32 {
119 u32::try_from(n).unwrap_or(u32::MAX)
120}
121
122impl LineIndex {
123 #[must_use]
125 pub fn new(source: &str) -> Self {
126 let line_starts = std::iter::once(0)
127 .chain(memchr::memchr_iter(b'\n', source.as_bytes()).map(|i| to_u32(i + 1)))
128 .collect();
129 let wide_chars = if source.is_ascii() {
132 Vec::new()
133 } else {
134 source
135 .char_indices()
136 .filter(|&(_, c)| c.len_utf8() > 1)
137 .map(|(i, c)| WideChar {
138 offset: to_u32(i),
139 utf8_len: to_u32(c.len_utf8()),
140 utf16_len: to_u32(c.len_utf16()),
141 })
142 .collect()
143 };
144 Self {
145 line_starts,
146 wide_chars,
147 }
148 }
149
150 #[must_use]
152 pub fn line_col(&self, offset: u32) -> (u32, u32) {
153 let (line, start) = self.line_of(offset);
154 let skipped = self.skipped_bytes(start, offset, |w| w.utf8_len - 1);
155 (line, offset - start - skipped + 1)
156 }
157
158 #[must_use]
160 pub fn span(&self, start: u32, end: u32) -> Span {
161 let (start_line, start_col) = self.line_col(start);
162 let (end_line, end_col) = self.line_col(end);
163 Span {
164 start,
165 end,
166 start_line,
167 start_col,
168 end_line,
169 end_col,
170 }
171 }
172
173 #[must_use]
175 pub fn utf16_col(&self, offset: u32) -> u32 {
176 let (_, start) = self.line_of(offset);
177 let skipped = self.skipped_bytes(start, offset, |w| w.utf8_len - w.utf16_len);
178 offset - start - skipped + 1
179 }
180
181 fn line_of(&self, offset: u32) -> (u32, u32) {
182 let line = self.line_starts.partition_point(|&start| start <= offset);
183 let start = line
184 .checked_sub(1)
185 .and_then(|i| self.line_starts.get(i))
186 .copied()
187 .unwrap_or(0);
188 (to_u32(line.max(1)), start)
189 }
190
191 fn skipped_bytes(&self, line_start: u32, offset: u32, weight: impl Fn(WideChar) -> u32) -> u32 {
192 let lo = self.wide_chars.partition_point(|w| w.offset < line_start);
193 let hi = self.wide_chars.partition_point(|w| w.end() <= offset).max(lo);
194 self.wide_chars
195 .get(lo..hi)
196 .unwrap_or_default()
197 .iter()
198 .copied()
199 .map(weight)
200 .sum()
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn constructors_fill_every_field() {
210 let span = Span::new(1, 4, 1, 2, 1, 5);
211 assert_eq!(
212 span,
213 Span {
214 start: 1,
215 end: 4,
216 start_line: 1,
217 start_col: 2,
218 end_line: 1,
219 end_col: 5
220 }
221 );
222 let related = Related::new("why", Location::new("a.json", Some(span)));
223 assert_eq!(related.message, "why");
224 assert_eq!(related.location.span, Some(span));
225 }
226
227 #[test]
228 fn non_ascii_and_ascii_text_index_alike_up_to_the_wide_characters() {
229 let ascii = LineIndex::new("ab\ncd");
230 assert_eq!(ascii.utf16_col(4), 2);
231 let wide = LineIndex::new("é\nab");
232 assert_eq!(wide.line_col(3), (2, 1));
233 assert_eq!(wide.utf16_col(2), 2);
234 }
235
236 #[test]
237 fn ascii_single_line() {
238 let index = LineIndex::new("hello");
239 assert_eq!(index.line_col(0), (1, 1));
240 assert_eq!(index.line_col(4), (1, 5));
241 assert_eq!(index.line_col(5), (1, 6));
242 }
243
244 #[test]
245 fn multi_line_and_line_ends() {
246 let index = LineIndex::new("ab\ncd\n\nef");
247 let cases = [
248 (0, (1, 1)),
249 (2, (1, 3)),
250 (3, (2, 1)),
251 (5, (2, 3)),
252 (6, (3, 1)),
253 (7, (4, 1)),
254 (9, (4, 3)),
255 ];
256 for (offset, expected) in cases {
257 assert_eq!(index.line_col(offset), expected, "offset {offset}");
258 }
259 }
260
261 #[test]
262 fn crlf_keeps_carriage_return_on_previous_line() {
263 let index = LineIndex::new("ab\r\ncd");
264 assert_eq!(index.line_col(2), (1, 3));
265 assert_eq!(index.line_col(3), (1, 4));
266 assert_eq!(index.line_col(4), (2, 1));
267 assert_eq!(index.line_col(6), (2, 3));
268 }
269
270 #[test]
271 fn multibyte_columns_count_scalar_values() {
272 let index = LineIndex::new("h\u{e9}llo\nw\u{e9}\u{e9}");
273 assert_eq!(index.line_col(1), (1, 2));
274 assert_eq!(index.line_col(3), (1, 3));
275 assert_eq!(index.line_col(6), (1, 6));
276 assert_eq!(index.line_col(7), (2, 1));
277 assert_eq!(index.line_col(10), (2, 3));
278 }
279
280 #[test]
281 fn astral_chars_are_one_column_but_two_utf16_units() {
282 let index = LineIndex::new("a\u{1F600}b\n\u{1F600}c");
283 assert_eq!(index.line_col(5), (1, 3));
284 assert_eq!(index.utf16_col(5), 4);
285 assert_eq!(index.line_col(6), (1, 4));
286 assert_eq!(index.utf16_col(6), 5);
287 assert_eq!(index.line_col(11), (2, 2));
288 assert_eq!(index.utf16_col(11), 3);
289 }
290
291 #[test]
292 fn utf16_col_matches_scalar_col_for_bmp_text() {
293 let index = LineIndex::new("h\u{e9}llo");
294 for offset in [0, 1, 3, 4, 6] {
295 assert_eq!(index.utf16_col(offset), index.line_col(offset).1, "offset {offset}");
296 }
297 }
298
299 #[test]
300 fn span_combines_both_ends() {
301 let index = LineIndex::new("ab\ncd");
302 assert_eq!(
303 index.span(1, 4),
304 Span {
305 start: 1,
306 end: 4,
307 start_line: 1,
308 start_col: 2,
309 end_line: 2,
310 end_col: 2
311 }
312 );
313 }
314
315 #[test]
316 fn empty_source_and_location_serde() {
317 let index = LineIndex::new("");
318 assert_eq!(index.line_col(0), (1, 1));
319 let location = Location {
320 path: "a/package.json".into(),
321 span: None,
322 };
323 assert_eq!(
324 serde_json::to_value(&location).unwrap_or_default(),
325 serde_json::json!({ "path": "a/package.json" })
326 );
327 }
328
329 #[test]
330 fn location_constructors_set_path_and_span() {
331 let whole = Location::file("package.json");
332 assert_eq!(
333 whole,
334 Location {
335 path: "package.json".into(),
336 span: None
337 }
338 );
339 assert_eq!(Location::new("a", whole.span).path, PathBuf::from("a"));
340 }
341}