use clap::Args;
use octocode::config::Config;
use octocode::constants::MAX_QUERIES;
use octocode::indexer;
use octocode::storage;
use octocode::store::Store;
use crate::commands::OutputFormat;
fn validate_detail_level(s: &str) -> Result<String, String> {
match s {
"signatures" | "partial" | "full" => Ok(s.to_string()),
_ => Err(format!(
"Invalid detail level '{}'. Use 'signatures', 'partial', or 'full'.",
s
)),
}
}
fn validate_queries(queries: &[String]) -> Result<(), anyhow::Error> {
if queries.is_empty() {
return Err(anyhow::anyhow!("At least one query is required"));
}
if queries.len() > MAX_QUERIES {
return Err(anyhow::anyhow!(
"Maximum {} queries allowed, got {}. Use fewer, more specific terms.",
MAX_QUERIES,
queries.len()
));
}
for (i, query) in queries.iter().enumerate() {
let query = query.trim();
if query.len() < 3 {
return Err(anyhow::anyhow!(
"Query {} must be at least 3 characters long",
i + 1
));
}
if query.len() > 500 {
return Err(anyhow::anyhow!(
"Query {} must be no more than 500 characters long",
i + 1
));
}
}
Ok(())
}
#[derive(Debug, Args)]
pub struct SearchArgs {
#[arg(required = true)]
pub queries: Vec<String>,
#[arg(short, long, default_value = "all")]
pub mode: String,
#[arg(short, long, default_value = "cli")]
pub format: OutputFormat,
#[arg(short, long)]
pub threshold: Option<f32>,
#[arg(short, long)]
pub expand: bool,
#[arg(short = 'd', long, value_parser = validate_detail_level)]
pub detail_level: Option<String>,
#[arg(short = 'l', long)]
pub language: Option<String>,
}
pub async fn execute(
store: &Store,
args: &SearchArgs,
config: &Config,
) -> Result<(), anyhow::Error> {
let current_dir = std::env::current_dir()?;
let index_path = storage::get_project_database_path(¤t_dir)?;
if !index_path.exists() {
return Err(anyhow::anyhow!(
"No index found. Please run 'octocode index' first to create an index."
));
}
validate_queries(&args.queries)?;
let threshold = args.threshold.unwrap_or(config.search.similarity_threshold);
if !(0.0..=1.0).contains(&threshold) {
return Err(anyhow::anyhow!(
"Similarity threshold must be between 0.0 and 1.0, got: {}",
threshold
));
}
let search_mode = match args.mode.as_str() {
"all" | "code" | "docs" | "text" => args.mode.as_str(),
_ => {
return Err(anyhow::anyhow!(
"Invalid search mode '{}'. Use 'all', 'code', 'docs', or 'text'.",
args.mode
));
}
};
if let Some(ref language) = args.language {
use octocode::indexer::languages;
if languages::get_language(language).is_none() {
return Err(anyhow::anyhow!(
"Invalid language '{}'. Supported languages: rust, javascript, typescript, python, go, cpp, php, bash, ruby, json, svelte, css",
language
));
}
}
if args.detail_level.is_some() {
if args.format.is_json() {
return Err(anyhow::anyhow!(
"--detail-level is not supported with JSON format. Use --format=cli or --format=text instead."
));
}
if args.format.is_md() {
return Err(anyhow::anyhow!(
"--detail-level is not supported with Markdown format. Use --format=cli or --format=text instead."
));
}
}
let effective_detail_level = args.detail_level.as_deref().unwrap_or("partial");
let embeddings =
indexer::search::generate_batch_embeddings_for_queries(&args.queries, search_mode, config)
.await?;
let query_embeddings: Vec<_> = args
.queries
.iter()
.cloned()
.zip(embeddings.into_iter())
.collect();
let search_results = indexer::search::execute_parallel_searches(
store,
query_embeddings,
search_mode,
config.search.max_results,
threshold, args.language.as_deref(),
)
.await?;
let distance_threshold = 1.0 - threshold;
let (mut code_blocks, mut doc_blocks, mut text_blocks) =
indexer::search::deduplicate_and_merge_results(
search_results,
&args.queries,
distance_threshold,
);
code_blocks.truncate(config.search.max_results);
doc_blocks.truncate(config.search.max_results);
text_blocks.truncate(config.search.max_results);
if args.expand && !code_blocks.is_empty() {
println!("Expanding symbols...");
code_blocks = indexer::expand_symbols(store, code_blocks).await?;
}
match search_mode {
"code" => {
if args.format.is_json() {
indexer::render_results_json(&code_blocks)?
} else if args.format.is_md() {
let markdown = indexer::code_blocks_to_markdown_with_config(&code_blocks, config);
println!("{}", markdown);
} else if args.format.is_text() {
let text_output = indexer::search::format_code_search_results_as_text(
&code_blocks,
effective_detail_level,
);
println!("{}", text_output);
} else {
indexer::render_code_blocks_with_config(
&code_blocks,
config,
effective_detail_level,
);
}
}
"docs" => {
if args.format.is_json() {
let json = serde_json::to_string_pretty(&doc_blocks)?;
println!("{}", json);
} else if args.format.is_md() {
let markdown =
indexer::document_blocks_to_markdown_with_config(&doc_blocks, config);
println!("{}", markdown);
} else if args.format.is_text() {
let text_output = indexer::search::format_doc_search_results_as_text(
&doc_blocks,
effective_detail_level,
);
println!("{}", text_output);
} else {
render_document_blocks_with_config(&doc_blocks, config, effective_detail_level);
}
}
"text" => {
if args.format.is_json() {
let json = serde_json::to_string_pretty(&text_blocks)?;
println!("{}", json);
} else if args.format.is_md() {
let markdown = indexer::text_blocks_to_markdown_with_config(&text_blocks, config);
println!("{}", markdown);
} else if args.format.is_text() {
let text_output = indexer::search::format_text_search_results_as_text(
&text_blocks,
effective_detail_level,
);
println!("{}", text_output);
} else {
render_text_blocks_with_config(&text_blocks, config, effective_detail_level);
}
}
"all" => {
code_blocks.retain(|block| {
if let Some(distance) = block.distance {
distance <= distance_threshold
} else {
true
}
});
doc_blocks.retain(|block| {
if let Some(distance) = block.distance {
distance <= distance_threshold
} else {
true
}
});
text_blocks.retain(|block| {
if let Some(distance) = block.distance {
distance <= distance_threshold
} else {
true
}
});
let mut final_code_results = code_blocks;
if args.expand {
println!("Expanding symbols...");
final_code_results = indexer::expand_symbols(store, final_code_results).await?;
}
if args.format.is_json() {
let combined = serde_json::json!({
"code_blocks": final_code_results,
"document_blocks": doc_blocks,
"text_blocks": text_blocks
});
println!("{}", serde_json::to_string_pretty(&combined)?);
} else if args.format.is_md() {
let mut combined_markdown = String::new();
if !doc_blocks.is_empty() {
combined_markdown.push_str("# Documentation Results\n\n");
combined_markdown.push_str(&indexer::document_blocks_to_markdown_with_config(
&doc_blocks,
config,
));
combined_markdown.push('\n');
}
if !final_code_results.is_empty() {
combined_markdown.push_str("# Code Results\n\n");
combined_markdown.push_str(&indexer::code_blocks_to_markdown_with_config(
&final_code_results,
config,
));
combined_markdown.push('\n');
}
if !text_blocks.is_empty() {
combined_markdown.push_str("# Text Results\n\n");
combined_markdown.push_str(&indexer::text_blocks_to_markdown_with_config(
&text_blocks,
config,
));
}
if combined_markdown.is_empty() {
combined_markdown.push_str("No results found for the query.");
}
println!("{}", combined_markdown);
} else if args.format.is_text() {
let text_output = indexer::search::format_combined_search_results_as_text(
&final_code_results,
&text_blocks,
&doc_blocks,
effective_detail_level,
);
println!("{}", text_output);
} else {
if !doc_blocks.is_empty() {
println!("=== DOCUMENTATION RESULTS ===\n");
render_document_blocks_with_config(&doc_blocks, config, effective_detail_level);
println!("\n");
}
if !final_code_results.is_empty() {
println!("=== CODE RESULTS ===\n");
indexer::render_code_blocks_with_config(
&final_code_results,
config,
effective_detail_level,
);
println!("\n");
}
if !text_blocks.is_empty() {
println!("=== TEXT RESULTS ===\n");
render_text_blocks_with_config(&text_blocks, config, effective_detail_level);
}
if doc_blocks.is_empty() && final_code_results.is_empty() && text_blocks.is_empty()
{
println!("No results found for the query.");
}
}
}
_ => unreachable!(),
}
Ok(())
}
fn render_text_blocks_with_config(
blocks: &[octocode::store::TextBlock],
_config: &Config,
detail_level: &str,
) {
if blocks.is_empty() {
println!("No text blocks found.");
return;
}
println!("Found {} text blocks:\n", blocks.len());
let mut blocks_by_file: std::collections::HashMap<String, Vec<&octocode::store::TextBlock>> =
std::collections::HashMap::new();
for block in blocks {
blocks_by_file
.entry(block.path.clone())
.or_default()
.push(block);
}
for (file_path, file_blocks) in blocks_by_file.iter() {
println!("╔══════════════════ File: {} ══════════════════", file_path);
for (idx, block) in file_blocks.iter().enumerate() {
println!("║");
println!("║ Block {} of {}: Text Block", idx + 1, file_blocks.len());
println!("║ Lines: {}-{}", block.start_line, block.end_line);
if let Some(distance) = block.distance {
println!("║ Similarity: {:.4}", 1.0 - distance);
}
println!("║");
match detail_level {
"signatures" => {
if let Some(first_line) = block.content.lines().next() {
println!("║ {:4} │ {}", block.start_line, first_line.trim());
}
}
"partial" => {
let lines: Vec<&str> = block.content.lines().collect();
if lines.len() <= 10 {
for (i, line) in lines.iter().enumerate() {
println!("║ {:4} │ {}", block.start_line + i + 1, line);
}
} else {
for (i, line) in lines.iter().take(4).enumerate() {
println!("║ {:4} │ {}", block.start_line + i + 1, line);
}
let omitted_lines = lines.len() - 7; if omitted_lines > 0 {
println!("║ │ ... ({} more lines)", omitted_lines);
}
let last_3_start = lines.len() - 3;
for (i, line) in lines.iter().skip(last_3_start).enumerate() {
println!("║ {:4} │ {}", block.start_line + last_3_start + i + 1, line);
}
}
}
"full" => {
let lines: Vec<&str> = block.content.lines().collect();
for (i, line) in lines.iter().enumerate() {
println!("║ {:4} │ {}", block.start_line + i + 1, line);
}
}
_ => {
let lines: Vec<&str> = block.content.lines().collect();
if lines.len() <= 10 {
for (i, line) in lines.iter().enumerate() {
println!("║ {:4} │ {}", block.start_line + i + 1, line);
}
} else {
for (i, line) in lines.iter().take(4).enumerate() {
println!("║ {:4} │ {}", block.start_line + i + 1, line);
}
let omitted_lines = lines.len() - 7;
if omitted_lines > 0 {
println!("║ │ ... ({} more lines)", omitted_lines);
}
let last_3_start = lines.len() - 3;
for (i, line) in lines.iter().skip(last_3_start).enumerate() {
println!("║ {:4} │ {}", block.start_line + last_3_start + i + 1, line);
}
}
}
}
if idx < file_blocks.len() - 1 {
println!("║");
println!("╠══════════════════════════════════════════════");
}
}
println!("╚══════════════════════════════════════════════\n");
}
}
fn render_document_blocks_with_config(
blocks: &[octocode::store::DocumentBlock],
_config: &Config,
detail_level: &str,
) {
if blocks.is_empty() {
println!("No document blocks found.");
return;
}
println!("Found {} document blocks:\n", blocks.len());
let mut blocks_by_file: std::collections::HashMap<
String,
Vec<&octocode::store::DocumentBlock>,
> = std::collections::HashMap::new();
for block in blocks {
blocks_by_file
.entry(block.path.clone())
.or_default()
.push(block);
}
for (file_path, file_blocks) in blocks_by_file.iter() {
println!("╔══════════════════ File: {} ══════════════════", file_path);
for (idx, block) in file_blocks.iter().enumerate() {
println!("║");
println!(
"║ Block {} of {}: Document Block",
idx + 1,
file_blocks.len()
);
println!("║ Lines: {}-{}", block.start_line, block.end_line);
if let Some(distance) = block.distance {
println!("║ Similarity: {:.4}", 1.0 - distance);
}
println!("║");
match detail_level {
"signatures" => {
if !block.title.is_empty() {
println!("║ Title: {}", block.title);
} else if let Some(first_line) = block.content.lines().next() {
println!("║ {}: {}", block.start_line, first_line.trim());
}
}
"partial" => {
if !block.title.is_empty() {
println!("║ Title: {}", block.title);
}
let lines: Vec<&str> = block.content.lines().collect();
if lines.len() <= 10 {
for (i, line) in lines.iter().enumerate() {
println!("║ {:4} │ {}", block.start_line + i + 1, line);
}
} else {
for (i, line) in lines.iter().take(4).enumerate() {
println!("║ {:4} │ {}", block.start_line + i + 1, line);
}
let omitted_lines = lines.len() - 7; if omitted_lines > 0 {
println!("║ │ ... ({} more lines)", omitted_lines);
}
let last_3_start = lines.len() - 3;
for (i, line) in lines.iter().skip(last_3_start).enumerate() {
println!("║ {:4} │ {}", block.start_line + last_3_start + i + 1, line);
}
}
}
"full" => {
if !block.title.is_empty() {
println!("║ Title: {}", block.title);
}
let lines: Vec<&str> = block.content.lines().collect();
for (i, line) in lines.iter().enumerate() {
println!("║ {:4} │ {}", block.start_line + i + 1, line);
}
}
_ => {
if !block.title.is_empty() {
println!("║ Title: {}", block.title);
}
let lines: Vec<&str> = block.content.lines().collect();
if lines.len() <= 10 {
for (i, line) in lines.iter().enumerate() {
println!("║ {:4} │ {}", block.start_line + i + 1, line);
}
} else {
for (i, line) in lines.iter().take(4).enumerate() {
println!("║ {:4} │ {}", block.start_line + i + 1, line);
}
let omitted_lines = lines.len() - 7;
if omitted_lines > 0 {
println!("║ │ ... ({} more lines)", omitted_lines);
}
let last_3_start = lines.len() - 3;
for (i, line) in lines.iter().skip(last_3_start).enumerate() {
println!("║ {:4} │ {}", block.start_line + last_3_start + i + 1, line);
}
}
}
}
if idx < file_blocks.len() - 1 {
println!("║");
println!("╠══════════════════════════════════════════════");
}
}
println!("╚══════════════════════════════════════════════\n");
}
}