1use std::path::{Path, PathBuf};
7
8use anyhow::{Result, bail};
9use streaming_iterator::StreamingIterator;
10use tree_sitter::{Parser, Query, QueryCursor};
11
12use crate::cli::CodeArgs;
13use crate::examples;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum ItemKind {
20 Fn,
21 Struct,
22 Enum,
23 Trait,
24 Field,
25 Type,
26 Impl,
27 Macro,
28 ProcMacro,
29 ProcAttrMacro,
30 ProcDeriveMacro,
31 Const,
32 Use,
33}
34
35impl ItemKind {
36 pub fn parse(s: &str) -> Option<Self> {
38 match s {
39 "fn" => Some(Self::Fn),
40 "struct" => Some(Self::Struct),
41 "enum" => Some(Self::Enum),
42 "trait" => Some(Self::Trait),
43 "field" => Some(Self::Field),
44 "type" => Some(Self::Type),
45 "impl" => Some(Self::Impl),
46 "macro" => Some(Self::Macro),
47 "proc-macro" => Some(Self::ProcMacro),
48 "attr-macro" => Some(Self::ProcAttrMacro),
49 "derive-macro" => Some(Self::ProcDeriveMacro),
50 "const" => Some(Self::Const),
51 "use" => Some(Self::Use),
52 _ => None,
53 }
54 }
55
56 pub fn keyword(self) -> &'static str {
58 match self {
59 Self::Fn => "fn",
60 Self::Struct => "struct",
61 Self::Enum => "enum",
62 Self::Trait => "trait",
63 Self::Field => "field",
64 Self::Type => "type",
65 Self::Impl => "impl",
66 Self::Macro => "macro",
67 Self::ProcMacro => "proc-macro",
68 Self::ProcAttrMacro => "attr-macro",
69 Self::ProcDeriveMacro => "derive-macro",
70 Self::Const => "const",
71 Self::Use => "use",
72 }
73 }
74}
75
76pub struct ResolvedCodeArgs {
80 pub target: String,
82 pub kind: Option<ItemKind>,
83 pub name: String,
84}
85
86pub fn resolve_code_args(args: &CodeArgs) -> Result<ResolvedCodeArgs> {
95 match args.args.len() {
96 1 => {
97 let a = &args.args[0];
98 if ItemKind::parse(a).is_some() {
99 bail!(
100 "'{}' is an item kind. Usage: cargo brief code [TARGET] {} <name>",
101 a,
102 a
103 );
104 }
105 Ok(ResolvedCodeArgs {
106 target: "self".to_string(),
107 kind: None,
108 name: a.clone(),
109 })
110 }
111 2 => {
112 let a0 = &args.args[0];
113 let a1 = &args.args[1];
114 if let Some(kind) = ItemKind::parse(a0) {
115 Ok(ResolvedCodeArgs {
116 target: "self".to_string(),
117 kind: Some(kind),
118 name: a1.clone(),
119 })
120 } else {
121 Ok(ResolvedCodeArgs {
122 target: a0.clone(),
123 kind: None,
124 name: a1.clone(),
125 })
126 }
127 }
128 3 => {
129 let target = &args.args[0];
130 let kind_str = &args.args[1];
131 let name = &args.args[2];
132 match ItemKind::parse(kind_str) {
133 Some(kind) => Ok(ResolvedCodeArgs {
134 target: target.clone(),
135 kind: Some(kind),
136 name: name.clone(),
137 }),
138 None => bail!(
139 "Unknown item kind '{}'. Valid kinds: fn, struct, enum, trait, field, type, impl, macro, proc-macro, attr-macro, derive-macro, const, use",
140 kind_str
141 ),
142 }
143 }
144 _ => bail!(
145 "Expected 1–3 positional arguments: [TARGET] [KIND] NAME\n\
146 Usage: cargo brief code [TARGET] [KIND] NAME"
147 ),
148 }
149}
150
151fn build_query(kind: Option<ItemKind>) -> String {
156 let mut parts = Vec::new();
157
158 let add = |parts: &mut Vec<&str>, k: ItemKind| match k {
159 ItemKind::Fn => {
160 parts.push("(function_item name: (identifier) @name) @item");
161 parts.push("(function_signature_item name: (identifier) @name) @item");
162 }
163 ItemKind::Struct => {
164 parts.push("(struct_item name: (type_identifier) @name) @item");
165 }
166 ItemKind::Enum => {
167 parts.push("(enum_item name: (type_identifier) @name) @item");
168 }
169 ItemKind::Trait => {
170 parts.push("(trait_item name: (type_identifier) @name) @item");
171 }
172 ItemKind::Field => {
173 parts.push("(field_declaration name: (field_identifier) @name) @item");
174 }
175 ItemKind::Type => {
176 parts.push("(type_item name: (type_identifier) @name) @item");
177 }
178 ItemKind::Impl => {
179 parts.push("(impl_item type: (type_identifier) @name) @item");
180 parts.push("(impl_item type: (generic_type type: (type_identifier) @name)) @item");
181 parts.push(
182 "(impl_item type: (scoped_type_identifier name: (type_identifier) @name)) @item",
183 );
184 }
185 ItemKind::Macro => {
186 parts.push("(macro_definition name: (identifier) @name) @item");
187 }
188 ItemKind::ProcMacro | ItemKind::ProcAttrMacro | ItemKind::ProcDeriveMacro => {
191 parts.push("(function_item name: (identifier) @name) @item");
192 parts.push("(function_signature_item name: (identifier) @name) @item");
193 }
194 ItemKind::Const => {
195 parts.push("(const_item name: (identifier) @name) @item");
196 parts.push("(static_item name: (identifier) @name) @item");
197 }
198 ItemKind::Use => {
199 parts.push(
200 "(use_declaration argument: (use_as_clause alias: (identifier) @name)) @item",
201 );
202 parts.push(
203 "(use_declaration argument: (scoped_identifier name: (identifier) @name)) @item",
204 );
205 parts.push("(use_declaration argument: (identifier) @name) @item");
206 }
207 };
208
209 if let Some(k) = kind {
210 add(&mut parts, k);
211 } else {
212 for k in [
216 ItemKind::Fn,
217 ItemKind::Struct,
218 ItemKind::Enum,
219 ItemKind::Trait,
220 ItemKind::Field,
221 ItemKind::Type,
222 ItemKind::Impl,
223 ItemKind::Macro,
224 ItemKind::Const,
225 ] {
226 add(&mut parts, k);
227 }
228 }
229
230 parts.join("\n")
231}
232
233fn is_case_sensitive(pattern: &str) -> bool {
237 pattern.chars().any(|c| c.is_uppercase())
238}
239
240fn name_matches(captured: &str, pattern: &str, case_sensitive: bool) -> bool {
241 if case_sensitive {
242 captured == pattern
243 } else {
244 captured.eq_ignore_ascii_case(pattern)
245 }
246}
247
248fn collect_source_files(source_root: &Path, src_only: bool) -> Vec<PathBuf> {
251 let mut files = Vec::new();
252 let dirs: &[&str] = if src_only {
253 &["src"]
254 } else {
255 &["src", "examples", "tests", "benches"]
256 };
257 for dir_name in dirs {
258 let dir = source_root.join(dir_name);
259 if dir.is_dir() {
260 files.extend(examples::collect_rs_files(&dir, 999));
261 }
262 }
263 files.sort();
264 files
265}
266
267fn derive_module_path(file_path: &Path, source_root: &Path) -> String {
273 let rel = file_path.strip_prefix(source_root).unwrap_or(file_path);
274
275 let rel = rel.strip_prefix("src").unwrap_or(rel);
277
278 let s = rel.to_string_lossy();
279 let s = s.strip_suffix(".rs").unwrap_or(&s);
280
281 let s = s
283 .strip_suffix("/mod")
284 .or_else(|| s.strip_suffix("\\mod"))
285 .unwrap_or(s);
286
287 if s == "lib" || s == "main" || s == "/lib" || s == "/main" || s == "\\lib" || s == "\\main" {
289 return String::new();
290 }
291
292 let s = s
294 .strip_prefix('/')
295 .or_else(|| s.strip_prefix('\\'))
296 .unwrap_or(s);
297
298 s.replace(['/', '\\'], "::")
299}
300
301fn collect_inline_module_names(node: tree_sitter::Node, source: &str) -> Vec<String> {
303 let mut names = Vec::new();
304 let mut current = node.parent();
305 while let Some(parent) = current {
306 if parent.kind() == "mod_item"
307 && let Some(name_node) = parent.child_by_field_name("name")
308 {
309 names.push(source[name_node.start_byte()..name_node.end_byte()].to_string());
310 }
311 current = parent.parent();
312 }
313 names.reverse();
314 names
315}
316
317fn build_module_context(
319 crate_name: &str,
320 file_path: &Path,
321 source_root: &Path,
322 node: tree_sitter::Node,
323 source: &str,
324) -> String {
325 let file_mod = derive_module_path(file_path, source_root);
326 let inline_mods = collect_inline_module_names(node, source);
327
328 let mut path = String::from(crate_name);
329 if !file_mod.is_empty() {
330 path.push_str("::");
331 path.push_str(&file_mod);
332 }
333 if !inline_mods.is_empty() {
334 path.push_str("::");
335 path.push_str(&inline_mods.join("::"));
336 }
337 path
338}
339
340fn find_parent_context(node: tree_sitter::Node, source: &str) -> Option<String> {
345 let mut current = node.parent();
346 while let Some(parent) = current {
347 match parent.kind() {
348 "impl_item" => {
349 let trait_part = parent
351 .child_by_field_name("trait")
352 .map(|t| &source[t.start_byte()..t.end_byte()]);
353 let type_part = parent
354 .child_by_field_name("type")
355 .map(|t| &source[t.start_byte()..t.end_byte()]);
356 return match (trait_part, type_part) {
357 (Some(tr), Some(ty)) => Some(format!("impl {tr} for {ty}")),
358 (None, Some(ty)) => Some(format!("impl {ty}")),
359 _ => None,
360 };
361 }
362 "trait_item" => {
363 if let Some(name_node) = parent.child_by_field_name("name") {
364 let name = &source[name_node.start_byte()..name_node.end_byte()];
365 return Some(format!("trait {name}"));
366 }
367 }
368 "struct_item" => {
369 if let Some(name_node) = parent.child_by_field_name("name") {
370 let name = &source[name_node.start_byte()..name_node.end_byte()];
371 return Some(format!("struct {name}"));
372 }
373 }
374 "enum_item" => {
375 if let Some(name_node) = parent.child_by_field_name("name") {
376 let name = &source[name_node.start_byte()..name_node.end_byte()];
377 return Some(format!("enum {name}"));
378 }
379 }
380 "mod_item" => return None, _ => {}
383 }
384 current = parent.parent();
385 }
386 None
387}
388
389fn parent_type_name(node: tree_sitter::Node, source: &str) -> Option<String> {
394 let mut current = node.parent();
395 while let Some(parent) = current {
396 match parent.kind() {
397 "impl_item" => {
398 let type_node = parent.child_by_field_name("type")?;
399 return extract_type_identifier(type_node, source);
400 }
401 "trait_item" | "struct_item" | "enum_item" => {
402 let name_node = parent.child_by_field_name("name")?;
403 return Some(source[name_node.start_byte()..name_node.end_byte()].to_string());
404 }
405 "mod_item" => return None,
406 _ => {}
407 }
408 current = parent.parent();
409 }
410 None
411}
412
413fn extract_type_identifier(node: tree_sitter::Node, source: &str) -> Option<String> {
416 match node.kind() {
417 "type_identifier" => Some(source[node.start_byte()..node.end_byte()].to_string()),
418 "generic_type" => {
419 let type_node = node.child_by_field_name("type")?;
420 extract_type_identifier(type_node, source)
421 }
422 "scoped_type_identifier" => {
423 let name_node = node.child_by_field_name("name")?;
424 Some(source[name_node.start_byte()..name_node.end_byte()].to_string())
425 }
426 _ => None,
427 }
428}
429
430fn parse_limit(raw: Option<&str>) -> (usize, Option<usize>) {
433 let Some(raw) = raw else {
434 return (0, None);
435 };
436 if let Some((offset_str, limit_str)) = raw.split_once(':') {
437 (
438 offset_str.parse().unwrap_or(0),
439 Some(limit_str.parse().unwrap_or(0)),
440 )
441 } else {
442 (0, Some(raw.parse().unwrap_or(0)))
443 }
444}
445
446pub fn search_code(
452 sources: &[(String, PathBuf)],
453 name: &str,
454 kind: Option<ItemKind>,
455 args: &CodeArgs,
456 in_type: Option<&str>,
457) -> Result<String> {
458 let language: tree_sitter::Language = tree_sitter_rust::LANGUAGE.into();
459 let query_src = build_query(kind);
460 let query = Query::new(&language, &query_src)
461 .map_err(|e| anyhow::anyhow!("Failed to compile tree-sitter query: {e}"))?;
462
463 let mut parser = Parser::new();
464 parser
465 .set_language(&language)
466 .map_err(|e| anyhow::anyhow!("Failed to set tree-sitter language: {e}"))?;
467
468 let capture_names = query.capture_names().to_vec();
469 let name_idx = capture_names
470 .iter()
471 .position(|n| *n == "name")
472 .expect("query must have @name capture") as u32;
473 let item_idx = capture_names
474 .iter()
475 .position(|n| *n == "item")
476 .expect("query must have @item capture") as u32;
477
478 let case_sensitive = is_case_sensitive(name);
479 let (offset, limit) = parse_limit(args.limit.as_deref());
480
481 let mut output = String::new();
482 let mut match_count = 0usize;
483 let mut emitted = 0usize;
484
485 let name_lower = name.to_ascii_lowercase();
487
488 'outer: for (crate_name, source_root) in sources {
489 let files = collect_source_files(source_root, args.src_only);
490
491 for file_path in &files {
492 let source = match std::fs::read_to_string(file_path) {
493 Ok(s) => s,
494 Err(_) => continue,
495 };
496
497 let contains = if case_sensitive {
499 source.contains(name)
500 } else {
501 source.to_ascii_lowercase().contains(&name_lower)
502 };
503 if !contains {
504 continue;
505 }
506
507 let Some(tree) = parser.parse(&source, None) else {
508 continue;
509 };
510
511 let root = tree.root_node();
512 let mut cursor = QueryCursor::new();
513 let mut matches = cursor.matches(&query, root, source.as_bytes());
514
515 while let Some(query_match) = matches.next() {
516 let name_node = query_match.captures.iter().find(|c| c.index == name_idx);
518 let item_node = query_match.captures.iter().find(|c| c.index == item_idx);
519
520 let (Some(name_cap), Some(item_cap)) = (name_node, item_node) else {
521 continue;
522 };
523
524 let captured_name = &source[name_cap.node.start_byte()..name_cap.node.end_byte()];
525 if !name_matches(captured_name, name, case_sensitive) {
526 continue;
527 }
528
529 if let Some(filter_type) = in_type {
531 let filter_case_sensitive = is_case_sensitive(filter_type);
532 match parent_type_name(item_cap.node, &source) {
533 Some(ref parent_name) => {
534 if !name_matches(parent_name, filter_type, filter_case_sensitive) {
535 continue;
536 }
537 }
538 None => continue,
539 }
540 }
541
542 match_count += 1;
543 if match_count <= offset {
544 continue;
545 }
546 if let Some(n) = limit
547 && emitted >= n
548 {
549 break 'outer;
550 }
551 emitted += 1;
552
553 let item_node = item_cap.node;
554 let start_line = item_node.start_position().row + 1;
555 let rel = file_path.strip_prefix(source_root).unwrap_or(file_path);
556
557 let mod_ctx =
559 build_module_context(crate_name, file_path, source_root, item_node, &source);
560
561 let parent_ctx = find_parent_context(item_node, &source);
563
564 if !output.is_empty() {
566 output.push('\n');
567 }
568
569 output.push_str(&format!("@{}:{}\n", rel.display(), start_line));
570
571 output.push_str(" in ");
573 output.push_str(&mod_ctx);
574 if let Some(ref ctx) = parent_ctx {
575 output.push_str(", ");
576 output.push_str(ctx);
577 }
578 output.push('\n');
579
580 if !args.quiet {
581 output.push('\n');
582 let text = &source[item_node.start_byte()..item_node.end_byte()];
583 output.push_str(text);
584 if !text.ends_with('\n') {
585 output.push('\n');
586 }
587 }
588 }
589 }
590 }
591
592 if match_count == 0 {
593 let kind_str = kind.map_or("", |k| k.keyword());
594 if kind_str.is_empty() {
595 output.push_str(&format!("// no definitions found for '{name}'\n"));
596 } else {
597 output.push_str(&format!(
598 "// no {kind_str} definitions found for '{name}'\n"
599 ));
600 }
601 }
602
603 Ok(output)
604}
605
606fn digit_count(mut n: usize) -> usize {
609 if n == 0 {
610 return 1;
611 }
612 let mut count = 0;
613 while n > 0 {
614 count += 1;
615 n /= 10;
616 }
617 count
618}
619
620pub fn search_references(
622 sources: &[(String, PathBuf)],
623 name: &str,
624 src_only: bool,
625 quiet: bool,
626 limit: Option<&str>,
627) -> String {
628 let case_sensitive = is_case_sensitive(name);
629 let name_lower = name.to_ascii_lowercase();
630 let (offset, limit_n) = parse_limit(limit);
631
632 let ctx_lines: usize = 2;
633 let mut output = String::new();
634 let mut total_matches = 0usize;
635 let mut emitted = 0usize;
636
637 'outer: for (_crate_name, source_root) in sources {
638 let files = collect_source_files(source_root, src_only);
639
640 for file_path in &files {
641 let content = match std::fs::read_to_string(file_path) {
642 Ok(c) => c,
643 Err(_) => continue,
644 };
645
646 let lines: Vec<&str> = content.lines().collect();
647 let total = lines.len();
648
649 let matches: Vec<usize> = lines
651 .iter()
652 .enumerate()
653 .filter(|(_, line)| {
654 if case_sensitive {
655 line.contains(name)
656 } else {
657 line.to_ascii_lowercase().contains(&name_lower)
658 }
659 })
660 .map(|(i, _)| i)
661 .collect();
662
663 if matches.is_empty() {
664 continue;
665 }
666
667 let rel = file_path
668 .strip_prefix(source_root)
669 .unwrap_or(file_path)
670 .to_string_lossy()
671 .replace('\\', "/");
672
673 if quiet {
674 for &m in &matches {
675 total_matches += 1;
676 if total_matches <= offset {
677 continue;
678 }
679 if let Some(n) = limit_n
680 && emitted >= n
681 {
682 break 'outer;
683 }
684 emitted += 1;
685 output.push_str(&format!("@{}:{}\n", rel, m + 1));
686 }
687 } else {
688 let mut file_match_indices: Vec<usize> = Vec::new();
690 for &m in &matches {
691 total_matches += 1;
692 if total_matches <= offset {
693 continue;
694 }
695 if let Some(n) = limit_n
696 && emitted >= n
697 {
698 break;
699 }
700 emitted += 1;
701 file_match_indices.push(m);
702 }
703
704 if file_match_indices.is_empty() {
705 if let Some(n) = limit_n
706 && emitted >= n
707 {
708 break 'outer;
709 }
710 continue;
711 }
712
713 let mut ranges: Vec<(usize, usize)> = Vec::new();
715 for &m in &file_match_indices {
716 let start = m.saturating_sub(ctx_lines);
717 let end = (m + ctx_lines).min(total.saturating_sub(1));
718 if let Some(last) = ranges.last_mut()
719 && start <= last.1 + 1
720 {
721 last.1 = last.1.max(end);
722 continue;
723 }
724 ranges.push((start, end));
725 }
726
727 let max_line_no = ranges.last().map_or(1, |r| r.1 + 1);
729 let width = digit_count(max_line_no).max(4);
730
731 output.push_str(&format!("@{rel}\n"));
732
733 let match_set: std::collections::HashSet<usize> =
734 file_match_indices.iter().copied().collect();
735
736 for (range_idx, &(start, end)) in ranges.iter().enumerate() {
737 if range_idx > 0 {
738 output.push_str(" ...\n");
739 }
740 for (i, line) in lines.iter().enumerate().take(end + 1).skip(start) {
741 let line_no = i + 1;
742 let marker = if match_set.contains(&i) { '*' } else { ' ' };
743 output.push_str(&format!(
744 "{marker}{line_no:>width$}: {line}\n",
745 width = width,
746 ));
747 }
748 }
749
750 output.push('\n');
751
752 if let Some(n) = limit_n
753 && emitted >= n
754 {
755 break 'outer;
756 }
757 }
758 }
759 }
760
761 if total_matches == 0 {
762 output.push_str(&format!("// no references found for '{name}'\n"));
763 }
764
765 output
766}