Skip to main content

vibe_workspace/git/
search.rs

1use anyhow::Result;
2use console::style;
3use dialoguer::{theme::ColorfulTheme, Select};
4use inquire::Text;
5
6use super::provider::{ProviderFactory, SearchProvider};
7use super::{GitConfig, Repository, SearchQuery};
8use crate::ui::workflows::{execute_workflow, CloneWorkflow};
9use crate::workspace::manager::WorkspaceManager;
10
11pub struct SearchEngine {
12    providers: Vec<Box<dyn SearchProvider>>,
13}
14
15impl SearchEngine {
16    pub fn new(config: &GitConfig) -> Result<Self> {
17        let mut providers = Vec::new();
18
19        for provider_name in &config.search_providers {
20            match ProviderFactory::create_provider(provider_name) {
21                Ok(provider) => providers.push(provider),
22                Err(e) => eprintln!("Warning: Failed to create provider '{provider_name}': {e}"),
23            }
24        }
25
26        if providers.is_empty() {
27            anyhow::bail!("No search providers available");
28        }
29
30        Ok(Self { providers })
31    }
32
33    pub fn add_provider(&mut self, provider: Box<dyn SearchProvider>) {
34        self.providers.push(provider);
35    }
36
37    pub async fn search(&self, query: &SearchQuery) -> Result<Vec<Repository>> {
38        let mut all_results = Vec::new();
39
40        for provider in &self.providers {
41            match provider.search(query).await {
42                Ok(results) => all_results.extend(results),
43                Err(e) => eprintln!(
44                    "Warning: Search failed for provider '{}': {}",
45                    provider.name(),
46                    e
47                ),
48            }
49        }
50
51        // Remove duplicates based on full_name
52        all_results.sort_by(|a, b| a.full_name.cmp(&b.full_name));
53        all_results.dedup_by(|a, b| a.full_name == b.full_name);
54
55        Ok(all_results)
56    }
57}
58
59pub struct SearchCommand;
60
61impl SearchCommand {
62    /// Execute search with workflow integration for seamless user experience
63    pub async fn execute_with_workflow(
64        workspace_manager: &mut WorkspaceManager,
65        config: &GitConfig,
66    ) -> Result<()> {
67        Self::execute_interactive(workspace_manager, config).await
68    }
69
70    /// Execute search with a predefined query (used for fallback searches)
71    pub async fn execute_with_query(
72        query: &str,
73        workspace_manager: &mut WorkspaceManager,
74        config: &GitConfig,
75    ) -> Result<()> {
76        println!(
77            "\n{} Searching for repositories matching '{}'...",
78            style("🔍").blue(),
79            style(query).cyan().bold()
80        );
81
82        let search_query = SearchQuery {
83            keywords: query.split_whitespace().map(|s| s.to_string()).collect(),
84            tags: vec![],
85            language: None,
86            organization: None,
87            limit: Some(20),
88            sort: Default::default(),
89        };
90
91        let engine = SearchEngine::new(config)?;
92        let results = engine.search(&search_query).await?;
93
94        if results.is_empty() {
95            println!(
96                "{} No repositories found for '{}'",
97                style("❌").red(),
98                query
99            );
100            return Ok(());
101        }
102
103        println!(
104            "\n{} Found {} matching repositories:",
105            style("đŸ“Ļ").green(),
106            style(results.len()).green().bold()
107        );
108
109        // Display and select repository
110        let selected_repo = Self::display_interactive_results(&results, workspace_manager)?;
111
112        if let Some(repo) = selected_repo {
113            // Use workflow system for seamless clone + configure + open experience
114            let workflow = CloneWorkflow {
115                url: repo.url.clone(),
116                app: None, // Let user choose during workflow
117            };
118
119            execute_workflow(Box::new(workflow), workspace_manager).await?;
120        } else {
121            println!("{} No repository selected", style("â„šī¸").blue());
122        }
123
124        Ok(())
125    }
126
127    pub async fn execute_interactive(
128        workspace_manager: &mut WorkspaceManager,
129        config: &GitConfig,
130    ) -> Result<()> {
131        println!(
132            "\n{} {} {}",
133            style("🔍").blue(),
134            style("GitHub Repository Search").cyan().bold(),
135            style("- Find and clone repositories").dim()
136        );
137
138        // Get search query from user
139        let query = Text::new("\nSearch GitHub repositories:")
140            .with_placeholder("e.g., rust web framework")
141            .prompt()?;
142
143        if query.trim().is_empty() {
144            println!("{} Search cancelled", style("❌").red());
145            return Ok(());
146        }
147
148        println!("\n{} Searching repositories...", style("🔍").blue());
149
150        let search_query = SearchQuery {
151            keywords: query.split_whitespace().map(|s| s.to_string()).collect(),
152            tags: vec![],
153            language: None,
154            organization: None,
155            limit: Some(20),          // Show more results
156            sort: Default::default(), // Uses BestMatch by default
157        };
158
159        let engine = SearchEngine::new(config)?;
160        let results = engine.search(&search_query).await?;
161
162        if results.is_empty() {
163            println!(
164                "{} No repositories found for '{}'",
165                style("❌").red(),
166                query
167            );
168            return Ok(());
169        }
170
171        println!(
172            "\n{} {} {} {}",
173            style("đŸ“Ļ").green(),
174            style("Found").green().bold(),
175            style(format!("{} repositories", results.len())).dim(),
176            style(format!("(sorted by: {})", search_query.sort.display_name())).dim()
177        );
178
179        // Display and select repository
180        let selected_repo = Self::display_interactive_results(&results, workspace_manager)?;
181
182        if let Some(repo) = selected_repo {
183            // Use workflow system for seamless clone + configure + open experience
184            let workflow = CloneWorkflow {
185                url: repo.url.clone(),
186                app: None, // Let user choose during workflow
187            };
188
189            execute_workflow(Box::new(workflow), workspace_manager).await?;
190        } else {
191            println!("{} No repository selected", style("â„šī¸").blue());
192        }
193
194        Ok(())
195    }
196
197    fn display_interactive_results(
198        results: &[Repository],
199        workspace_manager: &WorkspaceManager,
200    ) -> Result<Option<Repository>> {
201        let items: Vec<String> = results
202            .iter()
203            .map(|repo| {
204                let stars = if repo.stars > 0 {
205                    format!("⭐ {} ", Self::format_stars(repo.stars))
206                } else {
207                    "".to_string()
208                };
209
210                let lang = if let Some(language) = &repo.language {
211                    format!(" [{language}]")
212                } else {
213                    "".to_string()
214                };
215
216                let license = if let Some(license_key) = &repo.license {
217                    format!(" [{}]", license_key.to_uppercase())
218                } else {
219                    "".to_string()
220                };
221
222                let desc = repo.description.as_deref().unwrap_or("No description");
223                let truncated_desc = if desc.chars().count() > 40 {
224                    let truncated: String = desc.chars().take(40).collect();
225                    format!("{truncated}...")
226                } else {
227                    desc.to_string()
228                };
229
230                format!(
231                    "{}{}{}{} - {}",
232                    stars, repo.full_name, lang, license, truncated_desc
233                )
234            })
235            .collect();
236
237        let selection = Select::with_theme(&ColorfulTheme::default())
238            .with_prompt("Select repository to clone (ESC to cancel)")
239            .items(&items)
240            .default(0)
241            .max_length(workspace_manager.get_git_search_results_page_size())
242            .interact_opt()?;
243
244        Ok(selection.map(|i| results[i].clone()))
245    }
246
247    fn format_stars(count: u32) -> String {
248        if count >= 1000 {
249            format!("{:.1}k", count as f32 / 1000.0)
250        } else {
251            count.to_string()
252        }
253    }
254
255    /// Enhanced search with advanced options and workflow integration
256    pub async fn execute_advanced_search(
257        workspace_manager: &mut WorkspaceManager,
258        config: &GitConfig,
259        query: Option<String>,
260        language: Option<String>,
261        organization: Option<String>,
262    ) -> Result<()> {
263        println!(
264            "\n{} {} {}",
265            style("🔍").blue(),
266            style("Advanced GitHub Search").cyan().bold(),
267            style("- Find repositories with filters").dim()
268        );
269
270        // Get search query if not provided
271        let query = if let Some(q) = query {
272            q
273        } else {
274            Text::new("\nSearch GitHub repositories:")
275                .with_placeholder("e.g., rust web framework")
276                .prompt()?
277        };
278
279        if query.trim().is_empty() {
280            println!("{} Search cancelled", style("❌").red());
281            return Ok(());
282        }
283
284        println!("\n{} Searching repositories...", style("🔍").blue());
285
286        let search_query = SearchQuery {
287            keywords: query.split_whitespace().map(|s| s.to_string()).collect(),
288            tags: vec![],
289            language,
290            organization,
291            limit: Some(30), // More results for advanced search
292            sort: Default::default(),
293        };
294
295        let engine = SearchEngine::new(config)?;
296        let results = engine.search(&search_query).await?;
297
298        if results.is_empty() {
299            println!(
300                "{} No repositories found for '{}'",
301                style("❌").red(),
302                query
303            );
304            return Ok(());
305        }
306
307        println!(
308            "\n{} {} {} {}",
309            style("đŸ“Ļ").green(),
310            style("Found").green().bold(),
311            style(format!("{} repositories", results.len())).dim(),
312            style(format!("(sorted by: {})", search_query.sort.display_name())).dim()
313        );
314
315        // Display enhanced results with workflow integration
316        let selected_repo = Self::display_enhanced_results(&results, workspace_manager)?;
317
318        if let Some(repo) = selected_repo {
319            // Use workflow system for complete clone + configure + open experience
320            let workflow = CloneWorkflow {
321                url: repo.url.clone(),
322                app: None, // User will be prompted to configure during workflow
323            };
324
325            execute_workflow(Box::new(workflow), workspace_manager).await?;
326        } else {
327            println!("{} No repository selected", style("â„šī¸").blue());
328        }
329
330        Ok(())
331    }
332
333    /// Enhanced results display with more repository information
334    fn display_enhanced_results(
335        results: &[Repository],
336        workspace_manager: &WorkspaceManager,
337    ) -> Result<Option<Repository>> {
338        let items: Vec<String> = results
339            .iter()
340            .enumerate()
341            .map(|(i, repo)| {
342                let stars = if repo.stars > 0 {
343                    format!("⭐ {} ", Self::format_stars(repo.stars))
344                } else {
345                    "".to_string()
346                };
347
348                let lang = if let Some(language) = &repo.language {
349                    format!(" [{language}]")
350                } else {
351                    "".to_string()
352                };
353
354                let license = if let Some(license_key) = &repo.license {
355                    format!(" [{}]", license_key.to_uppercase())
356                } else {
357                    "".to_string()
358                };
359
360                let desc = repo.description.as_deref().unwrap_or("No description");
361                let truncated_desc = if desc.chars().count() > 50 {
362                    let truncated: String = desc.chars().take(50).collect();
363                    format!("{truncated}...")
364                } else {
365                    desc.to_string()
366                };
367
368                // Enhanced format with index numbers for quick selection
369                format!(
370                    "{:2}. {}{}{}{} - {}",
371                    i + 1,
372                    stars,
373                    repo.full_name,
374                    lang,
375                    license,
376                    truncated_desc
377                )
378            })
379            .collect();
380
381        let selection = Select::with_theme(&ColorfulTheme::default())
382            .with_prompt("Select repository to clone (ESC to cancel)")
383            .items(&items)
384            .default(0)
385            .max_length(workspace_manager.get_git_search_results_page_size())
386            .interact_opt()?;
387
388        Ok(selection.map(|i| results[i].clone()))
389    }
390}