use anyhow::{bail, Result};
use clap::Args;
use serde::Serialize;
use std::path::Path;
use crate::config::workspace::WorkspaceConfig;
use crate::config::ProjectConfig;
use crate::core::searcher::{SearchResult, Searcher};
use crate::output::formatter;
use crate::output::json::JsonOutput;
use crate::Context;
#[derive(Args, Debug)]
pub struct FindArgs {
pub query: String,
#[arg(long)]
pub kind: Option<String>,
#[arg(long)]
pub lang: Option<String>,
#[arg(long, default_value = "10")]
pub limit: usize,
#[arg(long, short = 'j')]
pub json: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct WorkspaceSearchResult {
pub project: String,
#[serde(flatten)]
pub result: SearchResult,
}
pub fn run(args: &FindArgs, ctx: &Context) -> Result<()> {
match ctx {
Context::SingleProject { root } => run_single(args, root),
Context::Workspace {
workspace_root,
config,
..
} => run_workspace(args, workspace_root, config),
}
}
fn run_single(args: &FindArgs, project_root: &Path) -> Result<()> {
let scope_dir = project_root.join(".scope");
if !scope_dir.exists() {
bail!("No .scope/ directory found. Run 'scope init' first.");
}
let db_path = scope_dir.join("graph.db");
if !db_path.exists() {
bail!("No index found. Run 'scope index' to build one first.");
}
let searcher = Searcher::open(&db_path)?;
let vendor_patterns = ProjectConfig::load(&scope_dir)
.map(|c| c.index.vendor_patterns)
.unwrap_or_default();
let results = searcher.search_with_vendor_derank(
&args.query,
args.limit,
args.kind.as_deref(),
&vendor_patterns,
)?;
if args.json {
let total = results.len();
let output = JsonOutput {
command: "find",
symbol: None,
data: &results,
truncated: false,
total,
};
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
formatter::print_find_results(&args.query, &results);
}
Ok(())
}
fn run_workspace(args: &FindArgs, workspace_root: &Path, config: &WorkspaceConfig) -> Result<()> {
let mut all_results: Vec<WorkspaceSearchResult> = Vec::new();
for entry in &config.workspace.members {
let name = WorkspaceConfig::resolve_member_name(entry);
let member_path = workspace_root.join(&entry.path);
let db_path = member_path.join(".scope").join("graph.db");
if !db_path.exists() {
continue;
}
let searcher = match Searcher::open(&db_path) {
Ok(s) => s,
Err(e) => {
tracing::warn!("Failed to open searcher for '{}': {}", name, e);
continue;
}
};
let member_vendor = ProjectConfig::load(&member_path.join(".scope"))
.map(|c| c.index.vendor_patterns)
.unwrap_or_default();
match searcher.search_with_vendor_derank(
&args.query,
args.limit,
args.kind.as_deref(),
&member_vendor,
) {
Ok(results) => {
for r in results {
all_results.push(WorkspaceSearchResult {
project: name.clone(),
result: r,
});
}
}
Err(e) => {
tracing::warn!("Search error in '{}': {}", name, e);
}
}
}
all_results.sort_by(|a, b| {
b.result
.score
.partial_cmp(&a.result.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
all_results.truncate(args.limit);
let total = all_results.len();
if args.json {
let output = JsonOutput {
command: "find",
symbol: None,
data: &all_results,
truncated: false,
total,
};
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
formatter::print_workspace_find_results(&args.query, &all_results);
}
Ok(())
}