Skip to main content

fff_query_parser/
location.rs

1//! Location parsing for file:line:col patterns
2//!
3//! Parses various location formats like:
4//! - `file:12` - Line number
5//! - `file:12:4` - Line and column
6//! - `file:12-114` - Line range
7//! - `file:12:4-20` - Column range on same line
8//! - `file:12:4-14:20` - Position range
9//! - `file(12)` - Visual Studio style line
10//! - `file(12,4)` - Visual Studio style line and column
11
12#[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 there are more than 2 parts it's not the range treat as normal query
26    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
36/// Parse "line-line" format
37fn 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
49/// Parse "line:col-col" format (column range on same line)
50fn 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
66/// Parse "line:col-line:col" format (position range)
67fn 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
84/// Try to parse range patterns (contains '-')
85fn 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    // Try position range (line:col-line:col)
93    if start_part.contains(':') && end_part.contains(':') {
94        return parse_position_range(start_part, end_part);
95    }
96
97    // Try column range (line:col-col)
98    if start_part.contains(':') {
99        return parse_column_range(start_part, end_part);
100    }
101
102    // Try simple line range (line-line)
103    parse_simple_range(location)
104}
105
106/// Try to parse position patterns (contains ':' but not '-')
107fn 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
119/// Parses various location formats like file:12, file:12:4, file:12-114
120fn 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
157/// Parse location from the end of a query string.
158///
159/// Returns the query without the location suffix, and the parsed location if found.
160///
161/// # Examples
162/// ```
163/// use fff_query_parser::location::{parse_location, Location};
164///
165/// let (query, loc) = parse_location("file:12");
166/// assert_eq!(query, "file");
167/// assert_eq!(loc, Some(Location::Line(12)));
168///
169/// let (query, loc) = parse_location("search term");
170/// assert_eq!(query, "search term");
171/// assert_eq!(loc, None);
172/// ```
173pub fn parse_location(query: &str) -> (&str, Option<Location>) {
174    // simply ignore the last semicolon even if there are no additional location info
175    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}