Skip to main content

tldr_cli/commands/
search.rs

1//! Smart Search command - Enriched BM25 search with structure + call graph context.
2//!
3//! Returns enriched "search result cards" containing function-level context
4//! (signature, callers, callees) for each BM25 match, minimizing round-trips
5//! for LLM agents exploring a codebase.
6
7use std::path::PathBuf;
8
9use anyhow::Result;
10use clap::Args;
11
12use tldr_core::{enriched_search, EnrichedSearchOptions, Language, SearchMode};
13
14use crate::output::{format_enriched_search_text, OutputFormat, OutputWriter};
15
16/// Enriched search: BM25 search with function-level context cards
17#[derive(Debug, Args)]
18pub struct SmartSearchArgs {
19    /// Search query (natural language or code terms)
20    pub query: String,
21
22    /// Directory to search in (default: current directory)
23    #[arg(default_value = ".")]
24    pub path: PathBuf,
25
26    /// Programming language (auto-detect if not specified)
27    #[arg(long, short = 'l')]
28    pub lang: Option<Language>,
29
30    /// Maximum number of result cards to return
31    #[arg(long, short = 'k', default_value = "10")]
32    pub top_k: usize,
33
34    /// Skip call graph enrichment (much faster, no callers/callees)
35    #[arg(long)]
36    pub no_callgraph: bool,
37
38    /// Use regex pattern matching instead of BM25 ranking.
39    /// The query is interpreted as a regex pattern.
40    #[arg(long, conflicts_with = "hybrid")]
41    pub regex: bool,
42
43    /// Hybrid mode: combine BM25 relevance with regex filtering.
44    /// The positional query is used for BM25 ranking, this pattern for regex filtering.
45    #[arg(long, conflicts_with = "regex")]
46    pub hybrid: Option<String>,
47}
48
49impl SmartSearchArgs {
50    /// Run the search command
51    pub fn run(&self, format: OutputFormat, quiet: bool) -> Result<()> {
52        let writer = OutputWriter::new(format, quiet);
53
54        // Determine language (auto-detect from directory, default to Python)
55        let language = self
56            .lang
57            .unwrap_or_else(|| Language::from_directory(&self.path).unwrap_or(Language::Python));
58
59        writer.progress(&format!(
60            "Smart searching for '{}' in {} ({})...",
61            self.query,
62            self.path.display(),
63            language.as_str()
64        ));
65
66        let search_mode = if self.regex {
67            SearchMode::Regex(self.query.clone())
68        } else if let Some(ref pattern) = self.hybrid {
69            SearchMode::Hybrid {
70                query: self.query.clone(),
71                pattern: pattern.clone(),
72            }
73        } else {
74            SearchMode::Bm25
75        };
76
77        let options = EnrichedSearchOptions {
78            top_k: self.top_k,
79            include_callgraph: !self.no_callgraph,
80            search_mode,
81        };
82
83        // Run enriched search
84        let report = enriched_search(&self.query, &self.path, language, options)?;
85
86        // Output based on format
87        if writer.is_text() {
88            let text = format_enriched_search_text(&report);
89            writer.write_text(&text)?;
90        } else {
91            writer.write(&report)?;
92        }
93
94        Ok(())
95    }
96}