1use crate::search::print_results;
6use crate::search::print_search_stats;
7use crate::search::search_code;
8use crate::types::{SearchOptions, SearchResult};
9use crate::{analysis, circular, complexity, deadcode, duplicates, export};
10use colored::*;
11use std::io::{self, Write};
12use std::path::Path;
13
14pub fn run(
16 path: &Path,
17 extensions: Option<&[String]>,
18 exclude: Option<&[String]>,
19) -> Result<(), Box<dyn std::error::Error>> {
20 println!("{}", "🎮 Interactive Search Mode".cyan().bold());
21 println!(
22 "{}",
23 "Type 'help' for commands and shortcuts, 'quit' to exit"
24 .dimmed()
25 .italic()
26 );
27 println!();
28
29 let mut last_query: Option<String> = None;
30 let mut current_extensions = extensions.map(|exts| exts.to_vec());
31 let mut current_exclude = exclude.map(|excl| excl.to_vec());
32 let mut fuzzy_mode = false;
33 let mut case_insensitive = true;
34 let mut ranking_mode = false;
35 let mut semantic_mode = false;
36 let mut last_results: Vec<SearchResult> = Vec::new();
37
38 loop {
39 let mode_indicator = format!(
40 "[{}{}{}{}]",
41 if fuzzy_mode { "F" } else { "-" },
42 if case_insensitive { "I" } else { "-" },
43 if ranking_mode { "R" } else { "-" },
44 if semantic_mode { "S" } else { "-" }
45 );
46
47 print!(
48 "{} {} ",
49 mode_indicator.blue(),
50 "codesearch>".green().bold()
51 );
52 io::stdout().flush()?;
53
54 let mut input = String::new();
55 io::stdin().read_line(&mut input)?;
56 let input = input.trim();
57
58 if input.is_empty() {
59 continue;
60 }
61
62 let parts: Vec<&str> = input.split_whitespace().collect();
63 if parts.is_empty() {
64 continue;
65 }
66
67 match parts[0] {
68 "quit" | "exit" | "q" | ":q" => {
69 println!("{}", "👋 Goodbye!".green().italic());
70 break;
71 }
72 "help" | "h" | "?" => {
73 print_help();
74 }
75 "/f" | ":f" | "fuzzy" => {
76 fuzzy_mode = !fuzzy_mode;
77 println!(
78 "Fuzzy mode: {}",
79 if fuzzy_mode {
80 "ON".green()
81 } else {
82 "OFF".dimmed()
83 }
84 );
85 }
86 "/i" | ":i" | "case" => {
87 case_insensitive = !case_insensitive;
88 println!(
89 "Case insensitive: {}",
90 if case_insensitive {
91 "ON".green()
92 } else {
93 "OFF".dimmed()
94 }
95 );
96 }
97 "/r" | ":r" | "rank" => {
98 ranking_mode = !ranking_mode;
99 println!(
100 "Ranking mode: {}",
101 if ranking_mode {
102 "ON".green()
103 } else {
104 "OFF".dimmed()
105 }
106 );
107 }
108 "/s" | ":s" | "semantic" => {
109 semantic_mode = !semantic_mode;
110 println!(
111 "Semantic search: {}",
112 if semantic_mode {
113 "ON".green()
114 } else {
115 "OFF".dimmed()
116 }
117 );
118 }
119 "!!" | "repeat" => {
120 if let Some(ref query) = last_query {
121 println!("{}", format!("Repeating: {}", query).blue());
122 let options = SearchOptions {
123 extensions: current_extensions.clone(),
124 ignore_case: case_insensitive,
125 fuzzy: fuzzy_mode,
126 fuzzy_threshold: 0.6,
127 max_results: 20,
128 exclude: current_exclude.clone(),
129 rank: ranking_mode,
130 cache: false,
131 semantic: semantic_mode,
132 benchmark: false,
133 vs_grep: false,
134 use_bm25: false,
135 context_filter: None,
136 fixed_string: false,
137 declaration_filter: None,
138 context: 0,
139 };
140 let results = search_code(query, path, &options)?;
141 last_results = results.clone();
142 if results.is_empty() {
143 println!("{}", "No matches found.".dimmed());
144 } else {
145 print_results(&results, true, ranking_mode);
146 print_search_stats(&results, query);
147 }
148 } else {
149 println!("{}", "No previous search.".dimmed());
150 }
151 }
152 "ext" | "extensions" => {
153 if parts.len() > 1 {
154 let new_exts: Vec<String> = parts[1..].iter().map(|s| s.to_string()).collect();
155 current_extensions = Some(new_exts.clone());
156 println!(
157 "{}",
158 format!("Extensions set: {}", new_exts.join(", ")).green()
159 );
160 } else {
161 match ¤t_extensions {
162 Some(exts) => {
163 println!("Current extensions: {}", exts.join(", ").blue())
164 }
165 None => println!("No extensions filter (searching all files)"),
166 }
167 }
168 }
169 "exclude" => {
170 if parts.len() > 1 {
171 let new_excl: Vec<String> = parts[1..].iter().map(|s| s.to_string()).collect();
172 current_exclude = Some(new_excl.clone());
173 println!(
174 "{}",
175 format!("Exclude directories: {}", new_excl.join(", ")).green()
176 );
177 } else {
178 match ¤t_exclude {
179 Some(excl) => {
180 println!("Excluded directories: {}", excl.join(", ").yellow())
181 }
182 None => println!("No excluded directories"),
183 }
184 }
185 }
186 "clear" | "cls" => {
187 print!("\x1B[2J\x1B[1;1H");
188 io::stdout().flush()?;
189 }
190 "status" | "settings" => {
191 println!("{}", "Current Settings:".cyan().bold());
192 println!(
193 " Fuzzy mode: {}",
194 if fuzzy_mode {
195 "ON".green()
196 } else {
197 "OFF".dimmed()
198 }
199 );
200 println!(
201 " Case insensitive: {}",
202 if case_insensitive {
203 "ON".green()
204 } else {
205 "OFF".dimmed()
206 }
207 );
208 println!(
209 " Ranking mode: {}",
210 if ranking_mode {
211 "ON".green()
212 } else {
213 "OFF".dimmed()
214 }
215 );
216 println!(
217 " Semantic search: {}",
218 if semantic_mode {
219 "ON".green()
220 } else {
221 "OFF".dimmed()
222 }
223 );
224 match ¤t_extensions {
225 Some(exts) => println!(" Extensions: {}", exts.join(", ").blue()),
226 None => println!(" Extensions: {}", "all".blue()),
227 }
228 }
229 "export" => {
230 if parts.len() > 1 {
231 let export_path = parts[1];
232 if last_results.is_empty() {
233 println!("{}", "No results to export. Run a search first.".dimmed());
234 } else {
235 let query = last_query.as_deref().unwrap_or("");
236 export::export_results(&last_results, export_path, query)?;
237 println!(
238 "{}",
239 format!("Results exported to: {}", export_path).green()
240 );
241 }
242 } else {
243 println!("{}", "Usage: export <filename.csv|.md>".dimmed());
244 }
245 }
246 "analyze" => {
247 analysis::analyze_codebase(
248 path,
249 current_extensions.as_deref(),
250 current_exclude.as_deref(),
251 )?;
252 }
253 "complexity" => {
254 complexity::analyze_complexity(
255 path,
256 current_extensions.as_deref(),
257 current_exclude.as_deref(),
258 None,
259 true,
260 )?;
261 }
262 "duplicates" | "dups" => {
263 duplicates::detect_duplicates(
264 path,
265 current_extensions.as_deref(),
266 current_exclude.as_deref(),
267 3,
268 0.9,
269 )?;
270 }
271 "deadcode" | "dead" => {
272 deadcode::detect_dead_code(
273 path,
274 current_extensions.as_deref(),
275 current_exclude.as_deref(),
276 )?;
277 }
278 "circular" | "cycle" | "cycles" => {
279 circular::detect_circular_calls(
280 path,
281 current_extensions.as_deref(),
282 current_exclude.as_deref(),
283 )?;
284 }
285 "languages" | "langs" => {
286 analysis::list_supported_languages()?;
287 }
288 _ => {
290 let query = input;
291 last_query = Some(query.to_string());
292
293 let options = SearchOptions {
294 extensions: current_extensions.clone(),
295 ignore_case: case_insensitive,
296 fuzzy: fuzzy_mode,
297 fuzzy_threshold: 0.6,
298 max_results: 20,
299 exclude: current_exclude.clone(),
300 rank: ranking_mode,
301 cache: false,
302 semantic: semantic_mode,
303 benchmark: false,
304 vs_grep: false,
305 use_bm25: false,
306 context_filter: None,
307 fixed_string: false,
308 declaration_filter: None,
309 context: 0,
310 };
311 let results = search_code(query, path, &options)?;
312
313 last_results = results.clone();
314
315 if results.is_empty() {
316 println!("{}", "No matches found.".dimmed());
317 } else {
318 print_results(&results, true, ranking_mode);
319 print_search_stats(&results, query);
320 }
321 println!();
322 }
323 }
324 }
325
326 Ok(())
327}
328
329fn print_help() {
330 println!("{}", "📖 Interactive Search Commands".cyan().bold());
331 println!("{}", "─".repeat(35).cyan());
332 println!();
333 println!("{}", "Search:".yellow().bold());
334 println!(" <pattern> - Search for text pattern");
335 println!(" !! - Repeat last search");
336 println!();
337 println!("{}", "âš¡ Toggles:".yellow().bold());
338 println!(" /f - Toggle fuzzy search");
339 println!(" /i - Toggle case insensitivity");
340 println!(" /r - Toggle relevance ranking");
341 println!(" /s - Toggle semantic search");
342 println!();
343 println!("{}", "Config:".yellow().bold());
344 println!(" ext <e1 e2> - Set file extensions");
345 println!(" exclude <d1> - Set exclude directories");
346 println!(" status - Show current settings");
347 println!();
348 println!("{}", "Analysis:".yellow().bold());
349 println!(" analyze - Codebase metrics");
350 println!(" complexity - Code complexity");
351 println!(" duplicates - Duplicate detection");
352 println!(" deadcode - Dead code detection");
353 println!(" circular - Circular call detection");
354 println!(" languages - Supported languages");
355 println!();
356 println!("{}", "Other:".yellow().bold());
357 println!(" export - Export results");
358 println!(" clear - Clear screen");
359 println!(" help - This help");
360 println!(" quit - Exit");
361 println!();
362}