1use std::path::PathBuf;
5
6use anyhow::{Context, Result};
7use serde::{Deserialize, Serialize};
8
9use crate::editor::RustEditor;
10use crate::files::{collect_rust_files_with_exclusions, expand_kind_to_node_types};
11use crate::operations::{FieldLocation, InspectResult};
12
13#[derive(Debug, Clone, Default, Serialize, Deserialize)]
14pub struct FindArgs {
15 pub paths: Vec<PathBuf>,
16 pub exclude: Vec<String>,
17 pub kind: Option<String>,
18 pub node_type: Option<String>,
19 pub name: Option<String>,
20 pub variant: Option<String>,
21 pub content_filter: Option<String>,
22 pub field_name: Option<String>,
23 pub include_comments: bool,
24 #[serde(default)]
26 pub context: Option<usize>,
27}
28
29#[derive(Debug, Serialize, Deserialize)]
30#[serde(tag = "kind", rename_all = "snake_case")]
31pub enum FindResult {
32 Field { matches: Vec<FieldLocation> },
33 Nodes { matches: Vec<InspectResult> },
34}
35
36impl FindResult {
37 pub const fn is_empty(&self) -> bool {
38 match self {
39 Self::Field { matches } => matches.is_empty(),
40 Self::Nodes { matches } => matches.is_empty(),
41 }
42 }
43}
44
45pub fn run(args: &FindArgs) -> Result<FindResult> {
46 let files = collect_rust_files_with_exclusions(&args.paths, &args.exclude)?;
47
48 if let Some(field) = &args.field_name {
49 return Ok(FindResult::Field {
50 matches: find_field(&files, field)?,
51 });
52 }
53
54 let node_types_to_search: Vec<Option<&str>> = if let Some(k) = &args.kind {
55 let expanded = expand_kind_to_node_types(k);
56 if expanded.is_empty() {
57 anyhow::bail!(
58 "Unknown kind '{}'. Valid kinds: struct, function, enum, match, identifier, type, macro, const, trait, mod, use",
59 k
60 );
61 }
62 expanded.into_iter().map(Some).collect()
63 } else if let Some(nt) = &args.node_type {
64 vec![Some(nt.as_str())]
65 } else {
66 vec![None]
67 };
68
69 let mut all_results: Vec<InspectResult> = Vec::new();
70
71 for file in &files {
72 let content = std::fs::read_to_string(file)
73 .with_context(|| format!("Failed to read file: {:?}", file))?;
74
75 let editor = match RustEditor::new(&content) {
76 Ok(e) => e,
77 Err(e) => {
78 eprintln!("⚠️ Skipping {}: {}", file.display(), e);
79 continue;
80 }
81 };
82
83 for node_type_to_search in &node_types_to_search {
84 let mut results = editor.inspect(
85 *node_type_to_search,
86 args.name.as_deref(),
87 args.variant.as_deref(),
88 args.include_comments,
89 )?;
90
91 for result in &mut results {
92 result.file_path = file.to_string_lossy().to_string();
93 }
94
95 if let Some(filter) = &args.content_filter {
96 results.retain(|r| r.snippet.contains(filter));
97 }
98
99 all_results.extend(results);
100 }
101 }
102
103 Ok(FindResult::Nodes {
104 matches: all_results,
105 })
106}
107
108pub fn run_unfiltered_by_node_type(args: &FindArgs) -> Result<Vec<InspectResult>> {
112 let files = collect_rust_files_with_exclusions(&args.paths, &args.exclude)?;
113 let mut hint_results: Vec<InspectResult> = Vec::new();
114
115 for file in &files {
116 let content = std::fs::read_to_string(file)
117 .with_context(|| format!("Failed to read file: {:?}", file))?;
118
119 let editor = match RustEditor::new(&content) {
120 Ok(e) => e,
121 Err(_) => continue,
122 };
123 let mut results =
124 editor.inspect(None, args.name.as_deref(), args.variant.as_deref(), false)?;
125
126 for result in &mut results {
127 result.file_path = file.to_string_lossy().to_string();
128 }
129
130 if let Some(filter) = &args.content_filter {
131 results.retain(|r| r.snippet.contains(filter));
132 }
133
134 hint_results.extend(results);
135 }
136
137 Ok(hint_results)
138}
139
140fn find_field(files: &[PathBuf], field: &str) -> Result<Vec<FieldLocation>> {
141 let mut all_locations: Vec<FieldLocation> = Vec::new();
142
143 for file in files {
144 let content = std::fs::read_to_string(file)
145 .with_context(|| format!("Failed to read file: {:?}", file))?;
146
147 let editor = match RustEditor::new(&content) {
148 Ok(e) => e,
149 Err(e) => {
150 eprintln!("⚠️ Skipping {}: {}", file.display(), e);
151 continue;
152 }
153 };
154 let mut locations = editor.find_field_locations(field)?;
155 for location in &mut locations {
156 location.file_path = file.to_string_lossy().to_string();
157 }
158 all_locations.extend(locations);
159 }
160
161 Ok(all_locations)
162}