cersei_tools/tool_primitives/
search.rs1use std::path::{Path, PathBuf};
4
5#[derive(Debug, Clone)]
7pub struct SearchMatch {
8 pub file: PathBuf,
9 pub line_number: usize,
10 pub line_content: String,
11}
12
13#[derive(Debug, Clone, Default)]
15pub struct GrepOptions {
16 pub glob_filter: Option<String>,
17 pub max_results: Option<usize>,
18 pub case_insensitive: bool,
19}
20
21#[derive(Debug)]
23pub enum SearchError {
24 InvalidPattern(String),
25 IoError(std::io::Error),
26 CommandFailed(String),
27}
28
29impl std::fmt::Display for SearchError {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 match self {
32 Self::InvalidPattern(p) => write!(f, "invalid pattern: {p}"),
33 Self::IoError(e) => write!(f, "I/O error: {e}"),
34 Self::CommandFailed(msg) => write!(f, "command failed: {msg}"),
35 }
36 }
37}
38
39impl std::error::Error for SearchError {}
40
41impl From<std::io::Error> for SearchError {
42 fn from(e: std::io::Error) -> Self {
43 Self::IoError(e)
44 }
45}
46
47pub async fn grep(
52 pattern: &str,
53 path: &Path,
54 opts: GrepOptions,
55) -> Result<Vec<SearchMatch>, SearchError> {
56 let (cmd, use_rg) = if which::which("rg").is_ok() {
57 ("rg".to_string(), true)
58 } else {
59 ("grep".to_string(), false)
60 };
61
62 let mut args = Vec::new();
63 args.push("-n".to_string()); if opts.case_insensitive {
66 args.push("-i".to_string());
67 }
68
69 if let Some(max) = opts.max_results {
70 if use_rg {
71 args.push(format!("--max-count={max}"));
72 } else {
73 args.push(format!("-m{max}"));
74 }
75 }
76
77 if let Some(ref glob_filter) = opts.glob_filter {
78 if use_rg {
79 args.push(format!("--glob={glob_filter}"));
80 } else {
81 args.push(format!("--include={glob_filter}"));
82 }
83 }
84
85 args.push(pattern.to_string());
86 args.push(path.display().to_string());
87
88 if use_rg {
89 args.push("--no-heading".to_string());
90 } else {
91 args.push("-r".to_string()); }
93
94 let output = tokio::process::Command::new(&cmd)
95 .args(&args)
96 .stdout(std::process::Stdio::piped())
97 .stderr(std::process::Stdio::piped())
98 .output()
99 .await?;
100
101 if !output.status.success() && output.status.code() != Some(1) {
103 let stderr = String::from_utf8_lossy(&output.stderr);
104 return Err(SearchError::CommandFailed(stderr.to_string()));
105 }
106
107 let stdout = String::from_utf8_lossy(&output.stdout);
108 let mut matches = Vec::new();
109
110 for line in stdout.lines() {
111 if line.is_empty() {
112 continue;
113 }
114 if let Some(m) = parse_grep_line(line) {
116 matches.push(m);
117 }
118 }
119
120 Ok(matches)
121}
122
123fn parse_grep_line(line: &str) -> Option<SearchMatch> {
124 let mut parts = line.splitn(3, ':');
126 let file = parts.next()?;
127 let line_num_str = parts.next()?;
128 let content = parts.next().unwrap_or("");
129
130 let line_number: usize = line_num_str.parse().ok()?;
131
132 Some(SearchMatch {
133 file: PathBuf::from(file),
134 line_number,
135 line_content: content.to_string(),
136 })
137}
138
139pub async fn glob(pattern: &str, base_dir: &Path) -> Result<Vec<PathBuf>, SearchError> {
141 let full_pattern = base_dir.join(pattern).display().to_string();
142
143 let paths = tokio::task::spawn_blocking(move || -> Result<Vec<PathBuf>, SearchError> {
145 let mut results = Vec::new();
146 for entry in
147 ::glob::glob(&full_pattern).map_err(|e| SearchError::InvalidPattern(e.to_string()))?
148 {
149 if let Ok(path) = entry {
150 results.push(path);
151 }
152 }
153 Ok(results)
154 })
155 .await
156 .map_err(|e| SearchError::CommandFailed(e.to_string()))??;
157
158 Ok(paths)
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[tokio::test]
166 async fn test_glob_basic() {
167 let results = glob("*.toml", Path::new(".")).await.unwrap();
168 assert!(!results.is_empty() || true); }
171
172 #[tokio::test]
173 async fn test_parse_grep_line() {
174 let m = parse_grep_line("src/main.rs:42:fn main() {").unwrap();
175 assert_eq!(m.file, PathBuf::from("src/main.rs"));
176 assert_eq!(m.line_number, 42);
177 assert_eq!(m.line_content, "fn main() {");
178 }
179
180 #[tokio::test]
181 async fn test_parse_grep_line_with_colons() {
182 let m = parse_grep_line("file.rs:10:let x = \"a:b:c\";").unwrap();
183 assert_eq!(m.line_number, 10);
184 assert_eq!(m.line_content, "let x = \"a:b:c\";");
185 }
186}