use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::Instant;
use crate::commands::watch;
use anyhow::{Context, Result};
use colored::*;
use indicatif::{ProgressBar, ProgressStyle};
use tracing::{debug, info, warn};
use xore_ai::{Document, EmbeddingModel, VectorSearcher};
use xore_config::XorePaths;
use xore_core::{
format_time_ago, get_default_history_path, RecommendationEngine, SearchHistoryEntry,
SearchType, TokenSavings,
};
use xore_search::{
index_exists, FileScanner, FileTypeFilter, IncrementalConfig, IncrementalIndexer, IndexBuilder,
IndexConfig, MtimeFilter, ScanConfig, Searcher, SizeFilter, WatcherConfig,
};
#[allow(dead_code)]
pub struct FindArgs {
pub query: Option<String>,
pub path: String,
pub file_type: Option<String>,
pub size: Option<String>,
pub mtime: Option<String>,
pub max_depth: Option<usize>,
pub hidden: bool,
pub no_ignore: bool,
pub follow_links: bool,
pub threads: Option<usize>,
pub semantic: bool,
pub index: bool,
pub rebuild: bool,
pub index_dir: Option<String>,
pub watch: bool,
pub watch_daemon: bool,
pub watch_include: Option<String>,
pub watch_exclude: Option<String>,
pub history: bool,
pub recommend: bool,
pub clear_history: bool,
pub output: Option<String>,
pub max_tokens: Option<usize>,
}
fn format_size(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if bytes >= GB {
format!("{:.2} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.2} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.2} KB", bytes as f64 / KB as f64)
} else {
format!("{} B", bytes)
}
}
fn get_index_path(args: &FindArgs) -> PathBuf {
if let Some(ref dir) = args.index_dir {
return PathBuf::from(dir);
}
let search_path = Path::new(&args.path);
let project_index_path = if search_path.is_absolute() {
search_path.join(".xore/index")
} else {
PathBuf::from(".xore/index")
};
if project_index_path.exists() || args.rebuild {
return project_index_path;
}
if let Ok(xore_paths) = XorePaths::new() {
return xore_paths.index_dir();
}
project_index_path
}
pub fn execute(args: FindArgs) -> Result<()> {
info!("Starting find command with path: {}", args.path);
if args.watch_daemon {
return execute_watch_daemon(&args);
}
if args.watch && !args.index {
anyhow::bail!("--watch mode requires --index to be enabled");
}
if args.history {
return show_search_history();
}
if args.recommend {
return show_recommendations();
}
if args.clear_history {
return clear_history();
}
if args.index {
return execute_index_search(&args);
}
let mut config = ScanConfig::new(&args.path);
if let Some(ref type_str) = args.file_type {
let filter = FileTypeFilter::parse(type_str)
.with_context(|| format!("Invalid file type filter: {}", type_str))?;
config = config.with_file_type(filter);
debug!("File type filter applied: {:?}", type_str);
}
if let Some(ref size_str) = args.size {
let filter = SizeFilter::parse(size_str)
.with_context(|| format!("Invalid size filter: {}", size_str))?;
config = config.with_size_filter(filter);
debug!("Size filter applied: {:?}", size_str);
}
if let Some(ref mtime_str) = args.mtime {
let filter = MtimeFilter::parse(mtime_str)
.with_context(|| format!("Invalid mtime filter: {}", mtime_str))?;
config = config.with_mtime_filter(filter);
debug!("Mtime filter applied: {:?}", mtime_str);
}
if let Some(depth) = args.max_depth {
config = config.with_max_depth(depth);
}
config = config
.with_include_hidden(args.hidden)
.with_respect_gitignore(!args.no_ignore)
.with_follow_links(args.follow_links);
if let Some(threads) = args.threads {
config = config.with_threads(threads);
}
println!("{}", "扫描文件中...".cyan());
let spinner = if atty::is(atty::Stream::Stdout) {
let pb = ProgressBar::new_spinner();
pb.set_style(ProgressStyle::default_spinner().template("{spinner:.cyan} {msg}").unwrap());
pb.set_message("正在扫描...");
pb.enable_steady_tick(std::time::Duration::from_millis(100));
Some(pb)
} else {
None
};
let scanner = FileScanner::new(config);
let (files, stats) = scanner.scan()?;
if let Some(pb) = spinner {
pb.finish_and_clear();
}
let matched_files: Vec<_> = if let Some(ref query) = args.query {
if args.semantic {
execute_semantic_search(&args, files)?
} else {
let query_lower = query.to_lowercase();
files
.into_iter()
.filter(|f| f.path.to_string_lossy().to_lowercase().contains(&query_lower))
.collect()
}
} else {
files
};
let mut sorted_files = matched_files;
sorted_files.sort_by(|a, b| a.path.cmp(&b.path));
println!();
if sorted_files.is_empty() {
println!("{}", "未找到匹配的文件".yellow());
} else {
for file in &sorted_files {
let size_str = format_size(file.size);
let path_str = file.path.display().to_string();
if let Some(ref query) = args.query {
let highlighted = highlight_match(&path_str, query);
println!("{:>10} {}", size_str.dimmed(), highlighted);
} else {
println!("{:>10} {}", size_str.dimmed(), path_str);
}
}
}
println!();
println!(
"{} 找到 {} 个文件 (共扫描 {} 个文件, {} 个目录, 耗时 {} ms)",
"✓".green(),
sorted_files.len().to_string().green().bold(),
stats.total_files.to_string().cyan(),
stats.directories.to_string().cyan(),
stats.elapsed_ms.to_string().yellow()
);
if stats.total_size > 0 {
println!(" 总大小: {}", format_size(stats.total_size).cyan());
}
if stats.skipped > 0 {
println!(" 已跳过: {} 个文件 (不匹配过滤条件)", stats.skipped.to_string().dimmed());
}
if stats.errors > 0 {
println!(" {} {} 个文件访问错误", "⚠".yellow(), stats.errors.to_string().yellow());
}
print_token_savings(stats.total_size, stats.total_files);
if let Some(ref query) = args.query {
let search_type = if args.semantic { SearchType::Semantic } else { SearchType::FullText };
match record_search_history(
query,
search_type,
&args.path,
sorted_files.len(),
stats.elapsed_ms,
args.file_type.clone(),
) {
Ok(_) => {
tracing::debug!("已记录搜索历史: {}", query);
}
Err(e) => {
tracing::warn!("记录搜索历史失败: {}", e);
}
}
}
Ok(())
}
fn execute_index_search(args: &FindArgs) -> Result<()> {
let index_path = get_index_path(args);
if args.watch {
return execute_watch_mode(args, &index_path);
}
let start = Instant::now();
let need_build = args.rebuild || !index_exists(&index_path);
if need_build {
build_index(args, &index_path)?;
}
let query = match &args.query {
Some(q) => q,
None => {
println!("{}", "索引已准备就绪".green());
return Ok(());
}
};
println!("{} 搜索 \"{}\"...", "🔍".cyan(), query);
let searcher =
Searcher::new(&index_path).with_context(|| format!("无法打开索引: {:?}", index_path))?;
let file_type_filter = args.file_type.as_deref();
let results = if file_type_filter.is_some() {
searcher.search_with_filter(query, file_type_filter, 100)?
} else {
searcher.search_smart(query, 100)?
};
let search_elapsed = start.elapsed();
println!();
if results.is_empty() {
println!("{}", "未找到匹配结果".yellow());
} else {
for result in &results {
let path_str = result.path.display().to_string();
let score_str = format!("{:.2}", result.score);
let location = if let Some(line) = result.line {
format!("{}:{}", path_str, line)
} else {
path_str
};
println!("{} {}", score_str.dimmed(), location.cyan());
if let Some(ref snippet) = result.snippet {
let indented = snippet
.lines()
.map(|line| format!(" {}", line))
.collect::<Vec<_>>()
.join("\n");
println!("{}", indented);
}
println!();
}
}
println!(
"{} 找到 {} 个匹配 (索引包含 {} 个文档, 耗时 {:.2?})",
"✓".green(),
results.len().to_string().green().bold(),
searcher.num_docs().to_string().cyan(),
search_elapsed
);
let search_type =
if args.file_type.is_some() { SearchType::FileType } else { SearchType::FullText };
if let Err(e) = record_search_history(
query,
search_type,
&args.path,
results.len(),
search_elapsed.as_millis() as u64,
args.file_type.clone(),
) {
debug!("Failed to record search history: {}", e);
} else {
println!(" {} 已记录搜索历史", "✓".dimmed());
}
Ok(())
}
fn execute_watch_mode(args: &FindArgs, index_path: &Path) -> Result<()> {
println!("{}", "启动增量索引监控模式...".cyan());
if args.rebuild || !index_exists(index_path) {
build_index(args, index_path)?;
println!();
}
let index_config = IndexConfig {
index_path: index_path.to_path_buf(),
writer_buffer_size: 50_000_000,
max_file_size: 100 * 1024 * 1024,
use_mmap: true,
mmap_threshold: 1024 * 1024,
};
let watcher_config = WatcherConfig {
debounce_duration: std::time::Duration::from_millis(500),
batch_size: 50,
exclude_patterns: vec![
".git".to_string(),
"node_modules".to_string(),
"target".to_string(),
".xore".to_string(),
"*.tmp".to_string(),
"*.swp".to_string(),
],
include_hidden: args.hidden,
};
let incremental_config = IncrementalConfig {
index_config,
watcher_config,
commit_threshold: 50,
auto_commit_interval: 30,
};
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let indexer = IncrementalIndexer::new(incremental_config)
.await
.context("Failed to create incremental indexer")?;
let watch_path = PathBuf::from(&args.path);
indexer.watch(&watch_path).await?;
println!("{} 监控目录: {}", "🔍".cyan(), watch_path.display());
println!("{} 按 Ctrl+C 停止监控", "💡".yellow());
println!();
let stats_indexer = std::sync::Arc::new(indexer);
let stats_indexer_clone = stats_indexer.clone();
let stats_task = tokio::spawn(async move {
loop {
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
let stats = stats_indexer_clone.stats().await;
if stats.created_count > 0 || stats.modified_count > 0 || stats.deleted_count > 0 {
println!(
"{} 统计: 创建 {}, 修改 {}, 删除 {}, 待提交 {}",
"📊".dimmed(),
stats.created_count.to_string().green(),
stats.modified_count.to_string().yellow(),
stats.deleted_count.to_string().red(),
stats.pending_changes.to_string().cyan()
);
}
}
});
tokio::select! {
result = stats_indexer.run() => {
result?;
}
_ = tokio::signal::ctrl_c() => {
println!();
println!("{}", "停止监控...".yellow());
}
}
stats_task.abort();
println!("{}", "提交最后的变更...".cyan());
stats_indexer.commit().await?;
Ok::<(), anyhow::Error>(())
})?;
println!("{}", "✓ 监控已停止".green());
Ok(())
}
fn build_index(args: &FindArgs, index_path: &Path) -> Result<()> {
let start = Instant::now();
println!("{} 构建索引中...", "📑".cyan());
let spinner = if atty::is(atty::Stream::Stdout) {
let pb = ProgressBar::new_spinner();
pb.set_style(ProgressStyle::default_spinner().template("{spinner:.cyan} {msg}").unwrap());
pb.set_message("扫描文件...");
pb.enable_steady_tick(std::time::Duration::from_millis(100));
Some(pb)
} else {
None
};
let mut scan_config = ScanConfig::new(&args.path);
if let Some(ref type_str) = args.file_type {
if let Ok(filter) = FileTypeFilter::parse(type_str) {
scan_config = scan_config.with_file_type(filter);
}
}
if let Some(depth) = args.max_depth {
scan_config = scan_config.with_max_depth(depth);
}
scan_config = scan_config
.with_include_hidden(args.hidden)
.with_respect_gitignore(!args.no_ignore)
.with_follow_links(args.follow_links);
if let Some(threads) = args.threads {
scan_config = scan_config.with_threads(threads);
}
let scanner = FileScanner::new(scan_config);
let (files, scan_stats) = scanner.scan()?;
if let Some(ref pb) = spinner {
pb.set_message(format!("索引 {} 个文件...", files.len()));
}
let index_config = IndexConfig {
index_path: index_path.to_path_buf(),
writer_buffer_size: 50_000_000, max_file_size: 100 * 1024 * 1024, use_mmap: true,
mmap_threshold: 1024 * 1024, };
let mut builder = IndexBuilder::with_config(index_config)?;
builder.add_documents_batch(&files)?;
let index_stats = builder.build()?;
if let Some(pb) = spinner {
pb.finish_and_clear();
}
let elapsed = start.elapsed();
println!(
"{} 索引构建完成: {} 个文档 (扫描 {} 个文件, {} 个错误, 耗时 {:.2?})",
"✓".green(),
index_stats.documents_added.to_string().green().bold(),
scan_stats.total_files.to_string().cyan(),
index_stats.errors.len().to_string().yellow(),
elapsed
);
if !index_stats.errors.is_empty() && index_stats.errors.len() <= 5 {
println!(" 错误:");
for err in &index_stats.errors {
println!(" {} {}", "•".red(), err.dimmed());
}
} else if index_stats.errors.len() > 5 {
println!(" {} 超过 5 个错误,已省略详情", "⚠".yellow());
}
Ok(())
}
fn get_model_path() -> PathBuf {
if let Ok(path) = env::var("XORE_MODEL_PATH") {
return PathBuf::from(path);
}
if let Ok(paths) = XorePaths::new() {
return paths.models_dir().join("onnx/model.onnx");
}
PathBuf::from("assets/models/onnx/model.onnx")
}
fn get_tokenizer_path() -> PathBuf {
if let Ok(path) = env::var("XORE_TOKENIZER_PATH") {
return PathBuf::from(path);
}
if let Ok(paths) = XorePaths::new() {
return paths.models_dir().join("tokenizer.json");
}
PathBuf::from("assets/models/tokenizer.json")
}
fn read_file_content(path: &Path, max_size: u64) -> Result<String> {
let metadata = fs::metadata(path)?;
if metadata.len() > max_size {
anyhow::bail!("File too large: {} bytes", metadata.len());
}
let content = fs::read_to_string(path)
.with_context(|| format!("Failed to read file: {}", path.display()))?;
Ok(content)
}
fn execute_semantic_search(
args: &FindArgs,
files: Vec<xore_search::ScannedFile>,
) -> Result<Vec<xore_search::ScannedFile>> {
let query = match &args.query {
Some(q) => q,
None => {
println!("{}", "语义搜索需要提供查询字符串".yellow());
return Ok(files);
}
};
println!("{}", "正在加载语义搜索模型...".cyan());
let model_path = get_model_path();
let tokenizer_path = get_tokenizer_path();
if !model_path.exists() {
anyhow::bail!(
"模型文件不存在: {}\n提示: 请先下载模型,参考 docs/semantic-search-guide.md",
model_path.display()
);
}
if !tokenizer_path.exists() {
anyhow::bail!(
"Tokenizer 文件不存在: {}\n提示: 请先下载模型,参考 docs/semantic-search-guide.md",
tokenizer_path.display()
);
}
let model = EmbeddingModel::load(&model_path, &tokenizer_path)
.context("Failed to load embedding model")?;
println!("{}", "✓ 模型加载成功".green());
let mut searcher = VectorSearcher::new(model);
println!("{}", "正在索引文件内容...".cyan());
let max_file_size = 1024 * 1024; let max_files = 1000;
let pb = ProgressBar::new(files.len().min(max_files) as u64);
pb.set_style(
ProgressStyle::default_bar()
.template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}")
.unwrap()
.progress_chars("#>-"),
);
let mut indexed_count = 0;
let mut skipped_count = 0;
for file in files.iter().take(max_files) {
pb.inc(1);
match read_file_content(&file.path, max_file_size) {
Ok(content) => {
if content.trim().is_empty() {
skipped_count += 1;
continue;
}
let doc = Document {
id: file.path.to_string_lossy().to_string(),
path: file.path.clone(),
content,
};
match searcher.add_document(doc) {
Ok(_) => {
indexed_count += 1;
}
Err(e) => {
warn!("Failed to index {}: {:#}", file.path.display(), e);
skipped_count += 1;
}
}
}
Err(e) => {
debug!("Skipped {}: {}", file.path.display(), e);
skipped_count += 1;
}
}
}
pb.finish_and_clear();
println!(
"{} 已索引 {} 个文件 (跳过 {} 个)",
"✓".green(),
indexed_count.to_string().green().bold(),
skipped_count.to_string().dimmed()
);
if indexed_count == 0 {
println!("{}", "没有可索引的文件内容".yellow());
return Ok(vec![]);
}
println!("{}", format!("正在搜索: \"{}\"", query).cyan());
let top_k = 20; let results = searcher.search(query, top_k).context("Failed to perform semantic search")?;
if results.is_empty() {
println!("{}", "未找到相关结果".yellow());
return Ok(vec![]);
}
let matched_files: Vec<_> = results
.into_iter()
.filter_map(|result| {
files.iter().find(|f| f.path == result.document.path).map(|f| {
println!(
" {} (相似度: {:.4})",
f.path.display().to_string().cyan(),
result.score.to_string().yellow()
);
f.clone()
})
})
.collect();
Ok(matched_files)
}
fn highlight_match(text: &str, query: &str) -> String {
let text_lower = text.to_lowercase();
let query_lower = query.to_lowercase();
if let Some(start) = text_lower.find(&query_lower) {
let end = start + query.len();
let before = &text[..start];
let matched = &text[start..end];
let after = &text[end..];
format!("{}{}{}", before, matched.magenta().bold(), after)
} else {
text.to_string()
}
}
mod atty {
pub enum Stream {
Stdout,
}
pub fn is(_stream: Stream) -> bool {
#[cfg(unix)]
{
use std::os::unix::io::AsRawFd;
unsafe { libc::isatty(std::io::stdout().as_raw_fd()) != 0 }
}
#[cfg(windows)]
{
true
}
#[cfg(not(any(unix, windows)))]
{
true
}
}
}
fn show_search_history() -> Result<()> {
println!("{}", "📜 搜索历史".cyan());
println!();
let history_path = get_default_history_path();
let engine = RecommendationEngine::new(Some(history_path))?;
let recent = engine.get_recent_searches(10);
if recent.is_empty() {
println!("{}", "暂无搜索历史".yellow());
return Ok(());
}
for (i, entry) in recent.iter().enumerate() {
let time_ago = format_time_ago(&entry.timestamp);
let type_str = entry.search_type.to_string();
println!(
" {}. \"{}\" ({}) - {} - {} 结果 - {}",
i + 1,
entry.query.cyan(),
type_str.dimmed(),
entry.path.dimmed(),
entry.result_count.to_string().green(),
time_ago.dimmed()
);
}
println!();
println!(" 总计: {} 条记录", engine.history_len().to_string().cyan());
Ok(())
}
fn show_recommendations() -> Result<()> {
println!("{}", "💡 智能推荐".cyan());
println!();
let history_path = get_default_history_path();
let engine = RecommendationEngine::new(Some(history_path))?;
if engine.history_len() == 0 {
println!("{}", "暂无足够的历史数据生成推荐".yellow());
println!("{}", "请先进行一些搜索操作".dimmed());
return Ok(());
}
let recommendations = engine.generate_recommendations("");
if recommendations.is_empty() {
println!("{}", "暂无推荐".yellow());
return Ok(());
}
for (i, rec) in recommendations.iter().enumerate() {
println!(" {}. {}", i + 1, rec.message.cyan());
println!(" 💡 {}", rec.suggestion.yellow());
println!();
}
Ok(())
}
fn clear_history() -> Result<()> {
println!("{}", "🗑️ 清除搜索历史".cyan());
println!();
let history_path = get_default_history_path();
let engine = RecommendationEngine::new(Some(history_path))?;
let count = engine.clear_history()?;
println!("{} 已清除 {} 条搜索记录", "✓".green(), count.to_string().green().bold());
Ok(())
}
fn record_search_history(
query: &str,
search_type: SearchType,
path: &str,
result_count: usize,
execution_time_ms: u64,
file_type: Option<String>,
) -> Result<()> {
let history_path = get_default_history_path();
info!("Creating recommendation engine at: {:?}", history_path);
let engine = RecommendationEngine::new(Some(history_path))?;
let entry = SearchHistoryEntry::new(
query.to_string(),
search_type,
path.to_string(),
result_count,
execution_time_ms,
file_type,
);
info!("Calling record_search with entry: {:?}", entry);
engine.record_search(entry)?;
info!("Search history recorded successfully");
Ok(())
}
fn print_token_savings(total_size: u64, file_count: usize) {
let output_length = file_count.saturating_mul(50);
let savings = TokenSavings::calculate(total_size, output_length);
if savings.saved_tokens > 0 {
let output = savings.format_minimal();
if !output.is_empty() {
println!("\n{}", output);
}
}
}
fn execute_watch_daemon(args: &FindArgs) -> Result<()> {
let watch_path = PathBuf::from(&args.path);
let watch_path = if watch_path.is_absolute() {
watch_path
} else {
std::env::current_dir()?.join(watch_path)
};
let include_patterns: Vec<String> = args
.watch_include
.as_deref()
.unwrap_or("")
.split(',')
.filter(|s| !s.is_empty())
.map(String::from)
.collect();
let exclude_patterns: Vec<String> = args
.watch_exclude
.as_deref()
.unwrap_or("")
.split(',')
.filter(|s| !s.is_empty())
.map(String::from)
.collect();
println!("{} 正在启动后台文件监控守护进程...", "🔄".cyan());
println!(" 监控路径: {}", watch_path.display().to_string().cyan());
let pid = watch::start_daemon(&watch_path, include_patterns, exclude_patterns)?;
println!("{} 后台监控已启动!", "✅".green());
println!(" PID: {}", pid.to_string().yellow());
println!(" 路径: {}", watch_path.display().to_string().cyan());
if let Ok(log_file) = watch::get_log_file(&watch_path) {
println!(" 日志: {}", log_file.display().to_string().dimmed());
}
println!();
println!(
"提示:使用 {} 查看监控状态,{} 停止监控",
"xore watch status".cyan(),
"xore watch stop".yellow()
);
Ok(())
}