oli_server/tools/fs/
search.rs1use anyhow::{Context, Result};
2use glob::glob;
3use regex::Regex;
4use std::fs::File;
5use std::io::{BufRead, BufReader};
6use std::path::{Path, PathBuf};
7use walkdir::WalkDir;
8
9pub struct SearchTools;
10
11impl SearchTools {
12 pub fn glob_search(pattern: &str) -> Result<Vec<PathBuf>> {
13 let entries =
14 glob(pattern).with_context(|| format!("Invalid glob pattern: {}", pattern))?;
15
16 let mut matches = Vec::new();
17 for entry in entries {
18 let path = entry.context("Failed to read glob entry")?;
19 matches.push(path);
20 }
21
22 matches.sort_by(|a, b| {
24 let a_modified = std::fs::metadata(a).and_then(|m| m.modified()).ok();
25 let b_modified = std::fs::metadata(b).and_then(|m| m.modified()).ok();
26 b_modified.cmp(&a_modified)
27 });
28
29 Ok(matches)
30 }
31
32 pub fn glob_search_in_dir(dir: &Path, pattern: &str) -> Result<Vec<PathBuf>> {
33 let dir_str = dir.to_string_lossy();
34 let full_pattern = format!("{}/{}", dir_str, pattern);
35 Self::glob_search(&full_pattern)
36 }
37
38 pub fn grep_search(
39 pattern: &str,
40 include_pattern: Option<&str>,
41 search_dir: Option<&Path>,
42 ) -> Result<Vec<(PathBuf, usize, String)>> {
43 let regex =
44 Regex::new(pattern).with_context(|| format!("Invalid regex pattern: {}", pattern))?;
45
46 let dir = search_dir.unwrap_or_else(|| Path::new("."));
47
48 let include_regex = match include_pattern {
49 Some(pattern) => {
50 let pattern = pattern.replace("*.{", "*.").replace("}", "|*."); let parts: Vec<&str> = pattern.split('|').collect();
52 let regex_parts: Vec<String> = parts
53 .iter()
54 .map(|p| format!("({})", glob_to_regex(p)))
55 .collect();
56 let joined = regex_parts.join("|");
57 Some(Regex::new(&joined).with_context(|| {
58 format!("Invalid include pattern: {}", include_pattern.unwrap())
59 })?)
60 }
61 None => None,
62 };
63
64 let mut matches = Vec::new();
65
66 for entry in WalkDir::new(dir)
67 .follow_links(true)
68 .into_iter()
69 .filter_map(|e| e.ok())
70 .filter(|e| e.file_type().is_file())
71 {
72 let path = entry.path();
73
74 if let Some(ref include_regex) = include_regex {
76 if !include_regex.is_match(&path.to_string_lossy()) {
77 continue;
78 }
79 }
80
81 if let Ok(file) = File::open(path) {
83 let reader = BufReader::new(file);
84 for (line_num, line_result) in reader.lines().enumerate() {
85 if let Ok(line) = line_result {
86 if regex.is_match(&line) {
87 matches.push((path.to_path_buf(), line_num + 1, line.clone()));
88 }
89 }
90 }
91 }
92 }
93
94 matches.sort_by(|a, b| {
96 let a_modified = std::fs::metadata(&a.0).and_then(|m| m.modified()).ok();
97 let b_modified = std::fs::metadata(&b.0).and_then(|m| m.modified()).ok();
98 b_modified.cmp(&a_modified)
99 });
100
101 Ok(matches)
102 }
103}
104
105fn glob_to_regex(glob_pattern: &str) -> String {
106 let mut regex_pattern = String::new();
107
108 for c in glob_pattern.chars() {
109 match c {
110 '*' => regex_pattern.push_str(".*"),
111 '?' => regex_pattern.push('.'),
112 '.' | '+' | '(' | ')' | '[' | ']' | '{' | '}' | '^' | '$' | '|' | '\\' => {
113 regex_pattern.push('\\');
114 regex_pattern.push(c);
115 }
116 _ => regex_pattern.push(c),
117 }
118 }
119
120 format!("^{}$", regex_pattern)
121}