Skip to main content

codesearch/
interactive.rs

1//! Interactive Mode Module
2//!
3//! Provides an interactive REPL for code searching and analysis.
4
5use 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
14/// Run interactive search mode
15pub 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 &current_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 &current_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 &current_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            // Default: perform search
289            _ => {
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}