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 (file_path, location_part) = query.split_once(':')?;
122
123 if let Some(range_location) = try_parse_column_range(location_part) {
124 return Some((file_path, range_location));
125 }
126
127 if let Some(position_location) = try_parse_column_position(location_part) {
128 return Some((file_path, position_location));
129 }
130
131 if let Ok(line_location) = location_part.parse::<i32>() {
132 return Some((file_path, Location::Line(line_location)));
133 }
134
135 None
136}
137
138fn parse_vstudio_location(query: &str) -> Option<(&str, Location)> {
139 if !query.ends_with(')') {
140 return None;
141 }
142
143 let (file_path, location_with_paren) = query.rsplit_once('(')?;
144 let location = location_with_paren.trim_end_matches(')');
145
146 if let Ok(line) = location.parse::<i32>() {
147 return Some((file_path, Location::Line(line)));
148 }
149
150 if let Some((line, col)) = parse_number_pair(location, ',') {
151 return Some((file_path, Location::Position { line, col }));
152 }
153
154 None
155}
156
157pub fn parse_location(query: &str) -> (&str, Option<Location>) {
174 let query = query.trim_end_matches([':', '-', '(']);
176 if let Some((path, location)) = parse_column_location(query) {
177 return (path, Some(location));
178 }
179
180 if let Some((path, location)) = parse_vstudio_location(query) {
181 return (path, Some(location));
182 }
183
184 (query, None)
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[test]
192 fn test_location_parsing() {
193 assert_eq!(
194 parse_location("new_file:12"),
195 ("new_file", Some(Location::Line(12)))
196 );
197 assert_eq!(parse_location("new_file:12ab"), ("new_file:12ab", None));
198
199 assert_eq!(parse_location("something"), ("something", None));
200 assert_eq!(
201 parse_location("file:12:4"),
202 ("file", Some(Location::Position { line: 12, col: 4 }))
203 );
204
205 assert_eq!(
206 parse_location("file:12-114"),
207 (
208 "file",
209 Some(Location::Range {
210 start: (12, 0),
211 end: (114, 0)
212 })
213 )
214 );
215
216 assert_eq!(
217 parse_location("file:12:4-20"),
218 (
219 "file",
220 Some(Location::Range {
221 start: (12, 4),
222 end: (12, 20)
223 })
224 )
225 );
226
227 assert_eq!(
228 parse_location("file:100:4-14:20"),
229 ("file", Some(Location::Position { line: 100, col: 4 }))
230 );
231
232 assert_eq!(
233 parse_location("file:12:4-14:20"),
234 (
235 "file",
236 Some(Location::Range {
237 start: (12, 4),
238 end: (14, 20)
239 })
240 )
241 );
242 }
243
244 #[test]
245 fn test_vstudio_parsing() {
246 assert_eq!(
247 parse_location("file(12)"),
248 ("file", Some(Location::Line(12)))
249 );
250 assert_eq!(
251 parse_location("file(12,4)"),
252 ("file", Some(Location::Position { line: 12, col: 4 }))
253 );
254 }
255
256 #[test]
257 fn trimes_end_character() {
258 assert_eq!(
259 parse_location("file:12-"),
260 ("file", Some(Location::Line(12)))
261 );
262 assert_eq!(parse_location("file:-"), ("file", None));
263 assert_eq!(parse_location("file("), ("file", None));
264 }
265}