1use std::{fmt, fmt::Write as _};
2
3use crate::{
4 common::{
5 arena::{self, SymStr},
6 object::Object,
7 },
8 util::pathname,
9};
10
11#[derive(Copy, Clone, PartialEq, Eq)]
22pub struct Locator {
23 pub source: SymStr,
24 pub from_line: u32,
25 pub to_line: u32,
26 pub from_column: u32,
27 pub to_column: u32,
28}
29
30impl Default for Locator {
31 fn default() -> Self {
32 Locator {
33 source: arena::pin(file!()),
34 from_line: line!(),
35 to_line: line!(),
36 from_column: column!(),
37 to_column: column!(),
38 }
39 }
40}
41
42impl fmt::Debug for Locator {
44 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[...]") }
45}
46
47impl fmt::Display for Locator {
48 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
49 write!(f, "{}", self.get_short_source(""))?;
50 if self.from_line > 0 {
51 write!(f, "; line {}", self.from_line)?;
52 if self.from_column > 0 {
53 write!(f, " col {}", self.from_column)?;
54 }
55 }
56 if self.to_line > 0 {
57 write!(f, " - line {}", self.to_line)?;
58 if self.to_column > 0 {
59 write!(f, " col {}", self.to_column)?;
60 }
61 }
62 Ok(())
63 }
64}
65
66impl Locator {
67 pub fn new<S: AsRef<str>>(
68 source: S,
69 from_line: u32,
70 from_column: u32,
71 to_line: u32,
72 to_column: u32,
73 ) -> Self {
74 Locator {
75 source: arena::pin(source.as_ref()),
76 from_line,
77 to_line,
78 from_column,
79 to_column,
80 }
81 }
82
83 pub fn from_sym(
89 source: SymStr,
90 from_line: u32,
91 from_column: u32,
92 to_line: u32,
93 to_column: u32,
94 ) -> Self {
95 Locator {
96 source,
97 from_line,
98 to_line,
99 from_column,
100 to_column,
101 }
102 }
103
104 pub fn new_range(from: Locator, to: Locator) -> Option<Locator> {
106 if from.source != to.source {
109 return None;
110 }
111 let (to_line, to_column) = if to.is_range() {
113 (to.to_line, to.to_column)
114 } else {
115 (to.from_line, to.from_column)
116 };
117 Some(Locator {
118 source: from.source,
119 from_line: from.from_line,
120 from_column: from.from_column,
121 to_line,
122 to_column,
123 })
124 }
125
126 pub fn is_range(&self) -> bool { self.to_line > 0 || self.to_column > 0 }
127
128 pub fn get_short_source(&self, string_source: &str) -> String {
129 arena::with(self.source, |source| {
130 if source.is_empty() {
131 if string_source.is_empty() {
132 "String".to_string()
133 } else {
134 string_source.to_string()
135 }
136 } else if source.contains(':') {
137 let (base, ext) = pathname::url_split(source);
138 s!("{}.{}", base, ext)
139 } else {
140 let (_path, base, _ext) = pathname::split(source);
141 base
142 }
143 })
144 }
145 pub fn get_source(&self) -> SymStr { self.source }
146
147 pub fn get_from_locator(&self) -> Locator {
148 Locator {
149 source: self.source,
150 from_line: self.from_line,
151 from_column: self.from_column,
152 ..Locator::default()
153 }
154 }
155
156 pub fn get_to_locator(&self) -> Locator {
157 Locator {
158 source: self.source,
159 from_line: self.to_line,
160 from_column: self.to_column,
161 ..Locator::default()
162 }
163 }
164}
165impl Object for Locator {
166 fn stringify(&self) -> String {
167 let mut loc = arena::to_string(self.source);
168 if loc.is_empty() {
169 loc = "Anonymous String".to_string()
170 };
171 let range_from = if self.is_range() { " from" } else { "" };
172 if self.from_line > 0 {
173 write!(loc, ";{} line {}", range_from, self.from_line).ok();
174 if self.from_column > 0 {
175 write!(loc, " col {}", self.from_column).ok();
176 }
177 }
178 if self.to_line > 0 {
179 write!(loc, " to line {}", self.to_line).ok();
180 if self.to_column > 0 {
181 write!(loc, " col {}", self.to_column).ok();
182 }
183 }
184 loc
185 }
186
187 fn get_locator(&self) -> Option<Locator> { Some(*self) }
189}
190
191impl Locator {
192 pub fn to_attribute(&self) -> String {
193 let mut loc = self.get_short_source("anonymous_string") + "#text";
194 if self.is_range() {
195 loc.push_str("range(from='");
196 loc.push_str(&self.from_line.to_string());
198 loc.push(';');
201 loc.push_str(&self.from_column.to_string());
202 loc.push_str(",to='");
204 loc.push_str(&self.to_line.to_string());
206 loc.push(';');
209 loc.push_str(&self.to_column.to_string());
210 } else {
211 loc.push_str("point('");
212 loc.push_str(&self.from_line.to_string());
214 loc.push(';');
217 loc.push_str(&self.from_column.to_string());
218 }
219 loc.push_str(")'");
221 loc
222 }
223
224 pub fn to_sourcepos(&self, tag: u32) -> String {
239 if self.is_range() {
240 format!(
241 "{tag}:{}:{}-{tag}:{}:{}",
242 self.from_line, self.from_column, self.to_line, self.to_column
243 )
244 } else {
245 format!("{tag}:{}:{}", self.from_line, self.from_column)
246 }
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn new_builds_from_parts() {
256 let l = Locator::new("source.tex", 1, 2, 3, 4);
257 assert_eq!(l.from_line, 1);
258 assert_eq!(l.from_column, 2);
259 assert_eq!(l.to_line, 3);
260 assert_eq!(l.to_column, 4);
261 }
262
263 #[test]
264 fn is_range_false_for_point() {
265 let l = Locator::new("source", 1, 1, 0, 0);
267 assert!(!l.is_range());
268 }
269
270 #[test]
271 fn is_range_true_when_any_to_set() {
272 let with_to_line = Locator::new("source", 1, 1, 5, 0);
273 let with_to_col = Locator::new("source", 1, 1, 0, 5);
274 assert!(with_to_line.is_range());
275 assert!(with_to_col.is_range());
276 }
277
278 #[test]
279 fn new_range_requires_same_source() {
280 let a = Locator::new("a.tex", 1, 1, 0, 0);
281 let b = Locator::new("b.tex", 5, 5, 0, 0);
282 assert!(
283 Locator::new_range(a, b).is_none(),
284 "different sources must return None"
285 );
286 }
287
288 #[test]
289 fn new_range_takes_from_and_to() {
290 let a = Locator::new("same.tex", 1, 2, 0, 0);
291 let b = Locator::new("same.tex", 5, 6, 0, 0);
292 let r = Locator::new_range(a, b).unwrap();
293 assert_eq!(r.from_line, 1);
294 assert_eq!(r.from_column, 2);
295 assert_eq!(r.to_line, 5);
298 assert_eq!(r.to_column, 6);
299 }
300
301 #[test]
302 fn new_range_propagates_to_range() {
303 let a = Locator::new("same.tex", 1, 1, 0, 0);
305 let b = Locator::new("same.tex", 5, 5, 10, 20);
306 let r = Locator::new_range(a, b).unwrap();
307 assert_eq!(r.from_line, 1);
308 assert_eq!(r.to_line, 10);
309 assert_eq!(r.to_column, 20);
310 }
311
312 #[test]
313 fn display_includes_source_and_line() {
314 let l = Locator::new("paper.tex", 42, 10, 0, 0);
315 let s = format!("{l}");
316 assert!(s.contains("paper"), "got {s:?}");
317 assert!(s.contains("line 42"), "got {s:?}");
318 }
319
320 #[test]
321 fn stringify_empty_source_is_anonymous_string() {
322 let l = Locator::new("", 1, 1, 0, 0);
323 let s = l.stringify();
324 assert!(s.contains("Anonymous String"), "got {s:?}");
325 }
326
327 #[test]
328 fn get_short_source_fallback_when_empty() {
329 let l = Locator::new("", 0, 0, 0, 0);
330 assert_eq!(l.get_short_source(""), "String");
331 assert_eq!(l.get_short_source("inline"), "inline");
332 }
333
334 #[test]
335 fn get_from_and_to_locators_preserve_source() {
336 let l = Locator::new("paper.tex", 1, 2, 3, 4);
337 let from = l.get_from_locator();
338 let to = l.get_to_locator();
339 assert_eq!(from.source, l.source);
340 assert_eq!(to.source, l.source);
341 assert_eq!(from.from_line, 1);
343 assert_eq!(from.from_column, 2);
344 assert_eq!(to.from_line, 3);
345 assert_eq!(to.from_column, 4);
346 }
347
348 #[test]
349 fn object_get_locator_returns_self() {
350 let l = Locator::new("paper.tex", 1, 2, 3, 4);
351 let got = l.get_locator();
352 assert_eq!(got, Some(l));
353 }
354
355 #[test]
356 fn to_sourcepos_point() {
357 let l = Locator::new("paper.tex", 12, 1, 0, 0);
359 assert_eq!(l.to_sourcepos(0), "0:12:1");
360 }
361
362 #[test]
363 fn to_sourcepos_range() {
364 let l = Locator::new("paper.tex", 12, 1, 12, 240);
365 assert_eq!(l.to_sourcepos(0), "0:12:1-0:12:240");
366 }
367
368 #[test]
369 fn to_sourcepos_tag_is_per_endpoint() {
370 let l = Locator::new("paper.tex", 3, 5, 7, 9);
372 assert_eq!(l.to_sourcepos(2), "2:3:5-2:7:9");
373 }
374}