diff --git a/Cargo.lock b/Cargo.lock index 80a6e1a..085fe77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,6 +63,7 @@ name = "rustgrep" version = "0.1.0" dependencies = [ "crossbeam", + "termcolor", "walkdir", ] @@ -75,6 +76,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 6c6448d..47e785c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,3 +13,4 @@ strip = true # 自动剥离所有的调试信息和符号表(相当于 [dependencies] walkdir = "2" # 目录递归遍历库 crossbeam = "0.8" # crossbeam 线程池 +termcolor = "1.4" # 颜色管理 diff --git a/src/lib.rs b/src/lib.rs index 0449f83..705ef41 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,12 @@ use std::fs; use std::path::Path; use crossbeam::channel; +use termcolor::{Color, ColorSpec, StandardStream, WriteColor}; use walkdir::WalkDir; +use std::io::Write; pub mod searcher; -pub use searcher::{ should_skip_dir, is_valid_extension, FileResult }; +pub use searcher::{ should_skip_dir, is_valid_extension, FileResult, MatchLine}; pub fn search_lines(root_dir: &str,extensions_str: Option, real_query: &str,ignore_case: bool) { @@ -33,20 +35,55 @@ pub fn search_lines(root_dir: &str,extensions_str: Option, real_query: & drop(tx); + // 初始化工业级标准输出流(ColorChoice::Auto 代表自动检测:是终端就上色,是文件就纯文本) + let mut stdout = StandardStream::stdout(termcolor::ColorChoice::Auto); + + // 路径字体样式 + let mut path_color = ColorSpec::new(); + path_color.set_fg(Some(Color::Magenta)).set_bold(true); + + // 行号字体样式 + let mut line_num_color = ColorSpec::new(); + line_num_color.set_fg(Some(Color::Green)); + + // 关键字(搜索的词)的字体样式 + let mut keyword_color = ColorSpec::new(); + keyword_color.set_fg(Some(Color::Red)).set_bold(true); + while let Ok(file_result) = rx.recv() { - // let normalized_path = Path::new(&file_result.path_name) - // .components() - // .map(|comp| comp.as_os_str().to_string_lossy()) - // .collect::>() - // .join(&MAIN_SEPARATOR.to_string()); let normalized_path = file_result.path_name; + + // 输出路径 + stdout.set_color(&line_num_color).unwrap(); + write!(&mut stdout, "\n\"{}\":\n", normalized_path).unwrap(); + stdout.reset().unwrap(); + + // 最大行号 let max_width = file_result.line_count.to_string().len(); - println!("\x1b[35m{:?} :\x1b[0m", normalized_path); - for (line_num, line_str) in file_result.matches { - println!("\x1b[32m{:>width$} |\x1b[0m {}", line_num, line_str, width = max_width); + for mat in file_result.matches { + // 🎨 打印高亮行号并精准对齐 + stdout.set_color(&line_num_color).unwrap(); + write!(&mut stdout,"{:>width$} | ", mat.line_num, width = max_width).unwrap(); + stdout.reset().unwrap(); + + let mut last_idx = 0; + + for (start,end) in mat.sub_matches { + // 先打印普通文本 + write!(&mut stdout, "{}", &mat.content[last_idx..start]).unwrap(); + + // 打印关键字 + stdout.set_color(&keyword_color).unwrap(); + write!(&mut stdout, "{}", &mat.content[start..end]).unwrap(); + stdout.reset().unwrap(); + + // 移动起始索引 + last_idx = end; + } + // 关键字列表外可能还有内容,以普通样式打印 + writeln!(&mut stdout, "{}", &mat.content[last_idx..]).unwrap(); } - println!(); } }).unwrap() } @@ -59,20 +96,21 @@ pub fn search_in_file(query: &str, path: &Path, ignore_case: bool) -> Option text, Err(_) => return None }; + // 🌟 2. 跨模块调用:通过包名或相对路径调用 searcher 里的函数 let results = searcher::search(query, &contents, ignore_case); if !results.is_empty() { - let mut matches_vec = Vec::new(); - - for (line_num, line_str) in results { - matches_vec.push((line_num, line_str.to_string())); - } - let file_line_count = contents.lines().count(); Some(FileResult { path_name: path.to_string_lossy().replace("\\","/"), - matches: matches_vec, + matches: results.into_iter() + .map(|(line_num, line_str, sub_matches)|MatchLine { + line_num, + content: line_str.to_string(), + sub_matches + }) + .collect(), line_count: file_line_count }) } else { diff --git a/src/searcher/core.rs b/src/searcher/core.rs index 66fde89..d8780c5 100644 --- a/src/searcher/core.rs +++ b/src/searcher/core.rs @@ -1,11 +1,21 @@ + /// 核心搜索函数 <'a> 是显式生命周期注解 -pub fn search<'a>(query: &str, contents: &'a str, ignore_case: bool) -> Vec<(usize, &'a str)> { +pub fn search<'a>(query: &str, contents: &'a str, ignore_case: bool) -> Vec<(usize,&'a str, Vec<(usize, usize)>)> { contents .lines() .enumerate() .map(|(idx, line)| (idx + 1, line)) - .filter(|(_, line)| { - if ignore_case { line.to_lowercase().contains(&query) } else { line.contains(query) } + .filter_map(|(line_num, line)|{ + let haystack = if ignore_case { line.to_lowercase() } else { line.to_string() }; + if haystack.contains(query) { + let sub_matches: Vec<(usize,usize)> = haystack + .match_indices(query) + .map(|(start_idx,_)| (start_idx, start_idx+query.len())) + .collect(); + Some((line_num,line,sub_matches)) + } else { + None + } }) .collect() } diff --git a/src/searcher/mod.rs b/src/searcher/mod.rs index 5c99f5a..243f1e8 100644 --- a/src/searcher/mod.rs +++ b/src/searcher/mod.rs @@ -6,6 +6,12 @@ pub use filter::{ should_skip_dir, is_valid_extension }; pub struct FileResult { pub path_name: String, - pub matches: Vec<(usize, String)>, pub line_count: usize, + pub matches: Vec, +} + +pub struct MatchLine { + pub line_num: usize, + pub content: String, + pub sub_matches: Vec<(usize, usize)> }