1#[derive(Debug, Eq, PartialEq, Copy, Clone)]
13pub enum Location {
14 Line(i32),
15 Range { start: (i32, i32), end: (i32, i32) },
16 Position { line: i32, col: i32 },
17}
18
19fn parse_number_pair(location: &str, split_char: char) -> Option<(i32, i32)> {
20 let mut iter = location.split(split_char);
21
22 let start_str = iter.next()?;
23 let end_str = iter.next()?;
24
25 if iter.next().is_some() {
27 return None;
28 }
29
30 let start = start_str.parse::<i32>().ok()?;
31 let end = end_str.parse::<i32>().ok()?;
32
33 Some((start, end))
34}
35
36fn parse_simple_range(location: &str) -> Option<Location> {
38 let (start, end) = parse_number_pair(location, '-')?;
39 if end < start {
40 return Some(Location::Line(start));
41 }
42
43 Some(Location::Range {
44 start: (start, 0),
45 end: (end, 0),
46 })
47}
48
49fn parse_column_range(start_part: &str, end_part: &str) -> Option<Location> {
51 let (line_str, start_col_str) = start_part.split_once(':')?;
52 let line = line_str.parse::<i32>().ok()?;
53 let start_col = start_col_str.parse::<i32>().ok()?;
54 let end_col = end_part.parse::<i32>().ok()?;
55
56 if end_col < start_col {
57 return Some(Location::Line(line));
58 }
59
60 Some(Location::Range {
61 start: (line, start_col),
62 end: (line, end_col),
63 })
64}
65
66fn parse_position_range(start_part: &str, end_part: &str) -> Option<Location> {
68 let (start_line, start_col) = parse_number_pair(start_part, ':')?;
69 let (end_line, end_col) = parse_number_pair(end_part, ':')?;
70
71 if end_line < start_line || (end_line == start_line && end_col < start_col) {
72 return Some(Location::Position {
73 line: start_line,
74 col: start_col,
75 });
76 }
77
78 Some(Location::Range {
79 start: (start_line, start_col),
80 end: (end_line, end_col),
81 })
82}
83
84fn try_parse_column_range(location: &str) -> Option<Location> {
86 if !location.contains('-') {
87 return None;
88 }
89
90 let (start_part, end_part) = location.split_once('-')?;
91
92 if start_part.contains(':') && end_part.contains(':') {
94 return parse_position_range(start_part, end_part);
95 }
96
97 if start_part.contains(':') {
99 return parse_column_range(start_part, end_part);
100 }
101
102 parse_simple_range(location)
104}
105
106fn try_parse_column_position(location: &str) -> Option<Location> {
108 if !location.contains(':') {
109 return None;
110 }
111
112 let (line_str, col_str) = location.split_once(':')?;
113 let line = line_str.parse::<i32>().ok()?;
114 let col = col_str.parse::<i32>().ok()?;
115
116 Some(Location::Position { line, col })
117}
118
119fn parse_column_location(query: &str) -> Option<(&str, Location)> {
121 let mut from = 0;
124 while let Some(offset) = query[from..].find(':') {
125 let at = from + offset;
126 let location_part = &query[at + 1..];
127
128 if let Some(range_location) = try_parse_column_range(location_part) {
129 return Some((&query[..at], range_location));
130 }
131
132 if let Some(position_location) = try_parse_column_position(location_part) {
133 return Some((&query[..at], position_location));
134 }
135
136 if let Ok(line_location) = location_part.parse::<i32>() {
137 return Some((&query[..at], Location::Line(line_location)));
138 }
139
140 from = at + 1;
141 }
142
143 None
144}
145
146fn parse_vstudio_location(query: &str) -> Option<(&str, Location)> {
147 if !query.ends_with(')') {
148 return None;
149 }
150
151 let (file_path, location_with_paren) = query.rsplit_once('(')?;
152 let location = location_with_paren.trim_end_matches(')');
153
154 if let Ok(line) = location.parse::<i32>() {
155 return Some((file_path, Location::Line(line)));
156 }
157
158 if let Some((line, col)) = parse_number_pair(location, ',') {
159 return Some((file_path, Location::Position { line, col }));
160 }
161
162 None
163}
164
165pub fn parse_location(query: &str) -> (&str, Option<Location>) {
182 let query = query.trim_end_matches([':', '-', '(']);
184 if let Some((path, location)) = parse_column_location(query) {
185 return (path, Some(location));
186 }
187
188 if let Some((path, location)) = parse_vstudio_location(query) {
189 return (path, Some(location));
190 }
191
192 (query, None)
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn test_location_parsing() {
201 assert_eq!(
202 parse_location("new_file:12"),
203 ("new_file", Some(Location::Line(12)))
204 );
205 assert_eq!(parse_location("new_file:12ab"), ("new_file:12ab", None));
206
207 assert_eq!(parse_location("something"), ("something", None));
208 assert_eq!(
209 parse_location("file:12:4"),
210 ("file", Some(Location::Position { line: 12, col: 4 }))
211 );
212
213 assert_eq!(
214 parse_location("file:12-114"),
215 (
216 "file",
217 Some(Location::Range {
218 start: (12, 0),
219 end: (114, 0)
220 })
221 )
222 );
223
224 assert_eq!(
225 parse_location("file:12:4-20"),
226 (
227 "file",
228 Some(Location::Range {
229 start: (12, 4),
230 end: (12, 20)
231 })
232 )
233 );
234
235 assert_eq!(
236 parse_location("file:100:4-14:20"),
237 ("file", Some(Location::Position { line: 100, col: 4 }))
238 );
239
240 assert_eq!(
241 parse_location("file:12:4-14:20"),
242 (
243 "file",
244 Some(Location::Range {
245 start: (12, 4),
246 end: (14, 20)
247 })
248 )
249 );
250 }
251
252 #[test]
253 fn test_vstudio_parsing() {
254 assert_eq!(
255 parse_location("file(12)"),
256 ("file", Some(Location::Line(12)))
257 );
258 assert_eq!(
259 parse_location("file(12,4)"),
260 ("file", Some(Location::Position { line: 12, col: 4 }))
261 );
262 }
263
264 #[test]
265 fn trimes_end_character() {
266 assert_eq!(
267 parse_location("file:12-"),
268 ("file", Some(Location::Line(12)))
269 );
270 assert_eq!(parse_location("file:-"), ("file", None));
271 assert_eq!(parse_location("file("), ("file", None));
272 }
273
274 #[test]
275 fn parses_location_after_a_windows_drive_letter() {
276 assert_eq!(
277 parse_location(r"C:\Users\me\file.rs:12"),
278 (r"C:\Users\me\file.rs", Some(Location::Line(12)))
279 );
280 assert_eq!(
281 parse_location(r"C:\src\main.rs:12:4"),
282 (
283 r"C:\src\main.rs",
284 Some(Location::Position { line: 12, col: 4 })
285 )
286 );
287
288 assert_eq!(
290 parse_location(r"C:\Users\me\file.rs"),
291 (r"C:\Users\me\file.rs", None)
292 );
293 assert_eq!(parse_location("foo:bar"), ("foo:bar", None));
294 }
295}