1use std::path::{Path, PathBuf};
2
3use regex::Regex;
4
5type OptionalLineRangeSuffix<'a> = (&'a str, Option<(usize, usize)>);
6
7#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct ParsedSelector {
11 pub file_path: PathBuf,
13 pub target: SelectorTarget,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum SelectorTarget {
19 Symbol(SymbolSelector),
20 LineRange {
21 start_line: usize,
22 end_line: usize,
23 },
24 AroundLine {
25 line: usize,
26 context: usize,
27 },
28 Match {
29 pattern: String,
30 around: Option<usize>,
31 },
32 BeforeLine {
33 line: usize,
34 },
35 AfterLine {
36 line: usize,
37 },
38 Enclosing {
39 line: usize,
40 },
41 Outline,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct SymbolSelector {
46 pub kind: SymbolKind,
48 pub name: String,
50 pub line_range: Option<(usize, usize)>,
52}
53
54impl ParsedSelector {
55 pub fn as_symbol(&self) -> Option<&SymbolSelector> {
56 match &self.target {
57 SelectorTarget::Symbol(symbol) => Some(symbol),
58 _ => None,
59 }
60 }
61
62 pub fn kind(&self) -> Option<&SymbolKind> {
64 self.as_symbol().map(|symbol| &symbol.kind)
65 }
66
67 pub fn name(&self) -> Option<&str> {
69 self.as_symbol().map(|symbol| symbol.name.as_str())
70 }
71
72 pub fn line_range(&self) -> Option<(usize, usize)> {
74 self.as_symbol().and_then(|symbol| symbol.line_range)
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum SymbolKind {
80 Function,
81 Struct,
82 Enum,
83 Trait,
84 Impl,
85 Class,
86 Unknown,
88}
89
90impl SymbolKind {
91 fn from_prefix(prefix: &str) -> Self {
93 match prefix {
94 "fn" => SymbolKind::Function,
96 "struct" => SymbolKind::Struct,
97 "enum" => SymbolKind::Enum,
98 "trait" => SymbolKind::Trait,
99 "impl" => SymbolKind::Impl,
100 "const" => SymbolKind::Unknown,
101 "let" => SymbolKind::Unknown,
102 "var" => SymbolKind::Unknown,
103 "func" => SymbolKind::Function,
105 "type" => SymbolKind::Struct,
106 "method" => SymbolKind::Function,
107 "package" => SymbolKind::Unknown,
108 "class" => SymbolKind::Class,
110 "interface" => SymbolKind::Trait,
111 "constructor" => SymbolKind::Function,
112 "def" => SymbolKind::Function,
113 "module" => SymbolKind::Unknown,
114 _ => SymbolKind::Unknown,
115 }
116 }
117
118 pub fn from_ts_node_kind(kind: &str) -> Self {
121 match kind {
122 "function_item" => SymbolKind::Function,
123 "struct_item" => SymbolKind::Struct,
124 "enum_item" => SymbolKind::Enum,
125 "trait_item" => SymbolKind::Trait,
126 "impl_item" => SymbolKind::Impl,
127 "function_definition" => SymbolKind::Function,
129 "class_definition" => SymbolKind::Class,
130 "decorated_definition" => SymbolKind::Function,
131 "function_declaration" => SymbolKind::Function,
133 "class_declaration" => SymbolKind::Class,
134 "interface_declaration" => SymbolKind::Trait,
135 "enum_declaration" => SymbolKind::Enum,
136 "method_definition" => SymbolKind::Function,
137 "arrow_function" => SymbolKind::Function,
138 "variable_declarator" => SymbolKind::Function,
139 "type_alias_declaration" => SymbolKind::Struct,
140 "method_declaration" => SymbolKind::Function,
142 "type_declaration" => SymbolKind::Struct,
143 "type_identifier" => SymbolKind::Struct,
144 "constructor_declaration" => SymbolKind::Function,
146 "field_declaration" => SymbolKind::Unknown,
147 "local_variable_declaration" => SymbolKind::Unknown,
148 "class_specifier" => SymbolKind::Class,
150 "struct_specifier" => SymbolKind::Struct,
151 "singleton_method" => SymbolKind::Function,
153 _ => SymbolKind::Unknown,
154 }
155 }
156}
157
158impl std::fmt::Display for SymbolKind {
159 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160 match self {
161 SymbolKind::Function => write!(f, "fn"),
162 SymbolKind::Struct => write!(f, "struct"),
163 SymbolKind::Enum => write!(f, "enum"),
164 SymbolKind::Trait => write!(f, "trait"),
165 SymbolKind::Impl => write!(f, "impl"),
166 SymbolKind::Class => write!(f, "class"),
167 SymbolKind::Unknown => write!(f, "symbol"),
168 }
169 }
170}
171
172pub fn parse_selector(input: &str) -> Result<ParsedSelector, String> {
182 if !input.contains("::")
183 && let Some((file_part, suffix)) = input.split_once('#')
184 {
185 return parse_hash_selector(file_part, suffix, input);
186 }
187
188 let (file_part, symbol_part) = input.split_once("::").ok_or_else(|| {
190 format!("selector must contain '::' separating file path from symbol: '{input}'")
191 })?;
192
193 let file_path = parse_file_path(file_part)?;
194
195 let symbol_part = symbol_part.trim();
196 if symbol_part.is_empty() {
197 return Err("selector symbol part is empty after '::'".to_string());
198 }
199
200 let (symbol_part, line_range) = parse_line_range_suffix(symbol_part)?;
201
202 let (kind, name) = parse_symbol_expr(symbol_part);
204
205 if name.is_empty() {
206 return Err(format!("selector symbol name is empty: '{symbol_part}'"));
207 }
208
209 Ok(ParsedSelector {
210 file_path,
211 target: SelectorTarget::Symbol(SymbolSelector {
212 kind,
213 name,
214 line_range,
215 }),
216 })
217}
218
219fn parse_file_path(file_part: &str) -> Result<PathBuf, String> {
220 let file_path = PathBuf::from(file_part.trim());
221 if file_path.as_os_str().is_empty() {
222 return Err("selector file path is empty".to_string());
223 }
224 Ok(file_path)
225}
226
227fn parse_hash_selector(
228 file_part: &str,
229 suffix: &str,
230 input: &str,
231) -> Result<ParsedSelector, String> {
232 let file_path = parse_file_path(file_part)?;
233 let target = if let Some(range_expr) = suffix.strip_prefix('L') {
234 let (start_line, end_line) = parse_line_range_expr(range_expr)?;
235 SelectorTarget::LineRange {
236 start_line,
237 end_line,
238 }
239 } else if let Some(expr) = suffix.strip_prefix("around:L") {
240 let (line, context) = parse_around_line_expr(expr)?;
241 SelectorTarget::AroundLine { line, context }
242 } else if let Some(expr) = suffix.strip_prefix("match:/") {
243 parse_match_target(expr)?
244 } else if let Some(expr) = suffix.strip_prefix("before:L") {
245 let line = parse_positive_usize(expr, "before line")?;
246 SelectorTarget::BeforeLine { line }
247 } else if let Some(expr) = suffix.strip_prefix("after:L") {
248 let line = parse_positive_usize(expr, "after line")?;
249 SelectorTarget::AfterLine { line }
250 } else if let Some(expr) = suffix.strip_prefix("enclosing:L") {
251 let line = parse_positive_usize(expr, "enclosing line")?;
252 SelectorTarget::Enclosing { line }
253 } else if suffix == "outline" {
254 SelectorTarget::Outline
255 } else {
256 return Err(format!("unsupported selector suffix in '{input}'"));
257 };
258
259 Ok(ParsedSelector { file_path, target })
260}
261
262fn parse_line_range_suffix(expr: &str) -> Result<OptionalLineRangeSuffix<'_>, String> {
264 let Some((symbol_expr, range_expr)) = expr.rsplit_once("#L") else {
265 return Ok((expr, None));
266 };
267
268 let symbol_expr = symbol_expr.trim_end();
269 if symbol_expr.is_empty() {
270 return Err(format!(
271 "selector symbol name is empty before line range: '{expr}'"
272 ));
273 }
274
275 let (start_str, end_str) = range_expr
276 .split_once("-L")
277 .or_else(|| range_expr.split_once('-'))
278 .ok_or_else(|| format!("bad selector line range '#L{range_expr}'"))?;
279 let start = start_str
280 .parse::<usize>()
281 .map_err(|_| format!("bad selector line range start: '#L{range_expr}'"))?;
282 let end = end_str
283 .parse::<usize>()
284 .map_err(|_| format!("bad selector line range end: '#L{range_expr}'"))?;
285 if start == 0 || end == 0 || start > end {
286 return Err(format!("bad selector line range '#L{range_expr}'"));
287 }
288
289 Ok((symbol_expr, Some((start, end))))
290}
291
292fn parse_line_range_expr(expr: &str) -> Result<(usize, usize), String> {
293 let (start_str, end_str) = expr
294 .split_once("-L")
295 .or_else(|| expr.split_once('-'))
296 .ok_or_else(|| format!("bad selector line range '#L{expr}'"))?;
297 let start = parse_positive_usize(start_str, "line range start")?;
298 let end = parse_positive_usize(end_str, "line range end")?;
299 if start > end {
300 return Err(format!("bad selector line range '#L{expr}'"));
301 }
302 Ok((start, end))
303}
304
305fn parse_around_line_expr(expr: &str) -> Result<(usize, usize), String> {
306 let (line_str, context_str) = expr
307 .split_once('±')
308 .or_else(|| expr.split_once("+-"))
309 .or_else(|| expr.split_once("+/-"))
310 .ok_or_else(|| format!("bad around selector '#around:L{expr}'"))?;
311 Ok((
312 parse_positive_usize(line_str, "around line")?,
313 parse_positive_usize(context_str, "around context")?,
314 ))
315}
316
317fn parse_match_target(expr: &str) -> Result<SelectorTarget, String> {
318 let (pattern, rest) = expr
319 .split_once('/')
320 .ok_or_else(|| "bad match selector; expected #match:/pattern/".to_string())?;
321 if pattern.is_empty() {
322 return Err("match selector pattern is empty".to_string());
323 }
324 Regex::new(pattern).map_err(|e| format!("bad match selector regex: {e}"))?;
325 let around = if rest.is_empty() {
326 None
327 } else if let Some(around_expr) = rest.strip_prefix("#around:") {
328 Some(parse_positive_usize(around_expr, "match around context")?)
329 } else {
330 return Err(format!("unsupported match selector suffix: '{rest}'"));
331 };
332 Ok(SelectorTarget::Match {
333 pattern: pattern.to_string(),
334 around,
335 })
336}
337
338fn parse_positive_usize(input: &str, label: &str) -> Result<usize, String> {
339 let value = input
340 .parse::<usize>()
341 .map_err(|_| format!("bad {label}: '{input}'"))?;
342 if value == 0 {
343 return Err(format!("bad {label}: '{input}'"));
344 }
345 Ok(value)
346}
347
348fn parse_symbol_expr(expr: &str) -> (SymbolKind, String) {
359 let expr = expr.trim();
360
361 let expr = expr.strip_suffix("()").unwrap_or(expr);
363
364 let parts: Vec<&str> = expr.splitn(2, char::is_whitespace).collect();
366 if parts.len() == 2 {
367 let prefix = parts[0];
368 let remainder = parts[1].trim();
369 let kind = SymbolKind::from_prefix(prefix);
370 if !matches!(kind, SymbolKind::Unknown) {
371 if matches!(kind, SymbolKind::Impl)
373 && let Some(trait_name) = remainder.split_whitespace().next()
374 {
375 return (kind, trait_name.to_string());
376 }
377 let name = remainder.strip_suffix("()").unwrap_or(remainder);
379 return (kind, name.to_string());
380 }
381 }
382
383 (SymbolKind::Unknown, expr.to_string())
385}
386
387pub fn resolve_file(
390 selector: &ParsedSelector,
391 project_root: &Path,
392) -> Result<(PathBuf, String), String> {
393 let full_path = project_root.join(&selector.file_path);
394 if !full_path.exists() {
395 return Err(format!("file not found: {}", full_path.display()));
396 }
397 let ext = full_path
398 .extension()
399 .and_then(|e| e.to_str())
400 .unwrap_or("")
401 .to_string();
402 if ext.is_empty() {
403 return Err(format!(
404 "cannot determine language from file: {}",
405 full_path.display()
406 ));
407 }
408 Ok((full_path, ext))
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414
415 #[test]
416 fn parse_fn_selector() {
417 let sel = parse_selector("src/foo.rs::fn authenticate").unwrap();
418 assert_eq!(sel.file_path, PathBuf::from("src/foo.rs"));
419 assert_eq!(sel.kind(), Some(&SymbolKind::Function));
420 assert_eq!(sel.name(), Some("authenticate"));
421 }
422
423 #[test]
424 fn parse_fn_with_parens() {
425 let sel = parse_selector("src/foo.rs::fn authenticate()").unwrap();
426 assert_eq!(sel.name(), Some("authenticate"));
427 }
428
429 #[test]
430 fn parse_line_range_disambiguator() {
431 let sel = parse_selector("src/foo.rs::fn authenticate #L10-L20").unwrap();
432 assert_eq!(sel.name(), Some("authenticate"));
433 assert_eq!(sel.line_range(), Some((10, 20)));
434 }
435
436 #[test]
437 fn invalid_line_range_disambiguator_is_error() {
438 assert!(parse_selector("src/foo.rs::fn authenticate #L20-L10").is_err());
439 assert!(parse_selector("src/foo.rs::fn authenticate #Labc-L20").is_err());
440 }
441
442 #[test]
443 fn parse_struct_selector() {
444 let sel = parse_selector("src/lib.rs::struct Config").unwrap();
445 assert_eq!(sel.kind(), Some(&SymbolKind::Struct));
446 assert_eq!(sel.name(), Some("Config"));
447 }
448
449 #[test]
450 fn parse_enum_selector() {
451 let sel = parse_selector("src/types.rs::enum Color").unwrap();
452 assert_eq!(sel.kind(), Some(&SymbolKind::Enum));
453 assert_eq!(sel.name(), Some("Color"));
454 }
455
456 #[test]
457 fn parse_trait_selector() {
458 let sel = parse_selector("src/lib.rs::trait Serialize").unwrap();
459 assert_eq!(sel.kind(), Some(&SymbolKind::Trait));
460 assert_eq!(sel.name(), Some("Serialize"));
461 }
462
463 #[test]
464 fn parse_impl_selector() {
465 let sel = parse_selector("src/foo.rs::impl MyStruct").unwrap();
466 assert_eq!(sel.kind(), Some(&SymbolKind::Impl));
467 assert_eq!(sel.name(), Some("MyStruct"));
468 }
469
470 #[test]
471 fn parse_impl_for_selector() {
472 let sel = parse_selector("src/foo.rs::impl Display for MyStruct").unwrap();
473 assert_eq!(sel.kind(), Some(&SymbolKind::Impl));
474 assert_eq!(sel.name(), Some("Display"));
475 }
476
477 #[test]
478 fn parse_bare_name() {
479 let sel = parse_selector("src/foo.rs::authenticate").unwrap();
480 assert_eq!(sel.kind(), Some(&SymbolKind::Unknown));
481 assert_eq!(sel.name(), Some("authenticate"));
482 }
483
484 #[test]
485 fn parse_with_spaces_in_path() {
486 let sel = parse_selector("some dir/file.rs::fn hello").unwrap();
488 assert_eq!(sel.file_path, PathBuf::from("some dir/file.rs"));
489 assert_eq!(sel.name(), Some("hello"));
490 }
491
492 #[test]
493 fn missing_double_colon_is_error() {
494 assert!(parse_selector("src/foo.rs").is_err());
495 }
496
497 #[test]
498 fn empty_file_path_is_error() {
499 assert!(parse_selector("::fn foo").is_err());
500 }
501
502 #[test]
503 fn empty_symbol_is_error() {
504 assert!(parse_selector("src/foo.rs::").is_err());
505 }
506 #[test]
507 fn parse_scope_range_and_context_selectors() {
508 let sel = parse_selector("src/foo.rs#L120-L180").unwrap();
509 assert_eq!(sel.file_path, PathBuf::from("src/foo.rs"));
510 assert_eq!(
511 sel.target,
512 SelectorTarget::LineRange {
513 start_line: 120,
514 end_line: 180
515 }
516 );
517
518 let sel = parse_selector("src/foo.rs#around:L150±40").unwrap();
519 assert_eq!(
520 sel.target,
521 SelectorTarget::AroundLine {
522 line: 150,
523 context: 40
524 }
525 );
526
527 let sel = parse_selector("src/foo.rs#enclosing:L150").unwrap();
528 assert_eq!(sel.target, SelectorTarget::Enclosing { line: 150 });
529
530 let sel = parse_selector("src/foo.rs#before:L150").unwrap();
531 assert_eq!(sel.target, SelectorTarget::BeforeLine { line: 150 });
532
533 let sel = parse_selector("src/foo.rs#after:L150").unwrap();
534 assert_eq!(sel.target, SelectorTarget::AfterLine { line: 150 });
535
536 let sel = parse_selector("src/foo.rs#outline").unwrap();
537 assert_eq!(sel.target, SelectorTarget::Outline);
538 }
539
540 #[test]
541 fn parse_scope_match_selectors() {
542 let sel = parse_selector("src/foo.rs#match:/ProjectInstructions/").unwrap();
543 assert_eq!(
544 sel.target,
545 SelectorTarget::Match {
546 pattern: "ProjectInstructions".to_string(),
547 around: None
548 }
549 );
550
551 let sel = parse_selector("src/foo.rs#match:/ProjectInstructions/#around:40").unwrap();
552 assert_eq!(
553 sel.target,
554 SelectorTarget::Match {
555 pattern: "ProjectInstructions".to_string(),
556 around: Some(40)
557 }
558 );
559 }
560}