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 fn is_empty(&self) -> bool {
38 match self {
39 FindResult::Field { matches } => matches.is_empty(),
40 FindResult::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 { matches: all_results })
104}
105
106pub fn run_unfiltered_by_node_type(args: &FindArgs) -> Result<Vec<InspectResult>> {
110 let files = collect_rust_files_with_exclusions(&args.paths, &args.exclude)?;
111 let mut hint_results: Vec<InspectResult> = Vec::new();
112
113 for file in &files {
114 let content = std::fs::read_to_string(file)
115 .with_context(|| format!("Failed to read file: {:?}", file))?;
116
117 let editor = match RustEditor::new(&content) {
118 Ok(e) => e,
119 Err(_) => continue,
120 };
121 let mut results = editor.inspect(
122 None,
123 args.name.as_deref(),
124 args.variant.as_deref(),
125 false,
126 )?;
127
128 for result in &mut results {
129 result.file_path = file.to_string_lossy().to_string();
130 }
131
132 if let Some(filter) = &args.content_filter {
133 results.retain(|r| r.snippet.contains(filter));
134 }
135
136 hint_results.extend(results);
137 }
138
139 Ok(hint_results)
140}
141
142fn find_field(files: &[PathBuf], field: &str) -> Result<Vec<FieldLocation>> {
143 let mut all_locations: Vec<FieldLocation> = Vec::new();
144
145 for file in files {
146 let content = std::fs::read_to_string(file)
147 .with_context(|| format!("Failed to read file: {:?}", file))?;
148
149 let editor = match RustEditor::new(&content) {
150 Ok(e) => e,
151 Err(e) => {
152 eprintln!("⚠️ Skipping {}: {}", file.display(), e);
153 continue;
154 }
155 };
156 let mut locations = editor.find_field_locations(field)?;
157 for location in &mut locations {
158 location.file_path = file.to_string_lossy().to_string();
159 }
160 all_locations.extend(locations);
161 }
162
163 Ok(all_locations)
164}