oli_server/tools/fs/
search.rs

1use 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        // Sort by last modified time (most recent first)
23        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("}", "|*."); // Convert *.{ts,tsx} to *.ts|*.tsx
51                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            // Skip if doesn't match include pattern
75            if let Some(ref include_regex) = include_regex {
76                if !include_regex.is_match(&path.to_string_lossy()) {
77                    continue;
78                }
79            }
80
81            // Try to open file
82            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        // Sort by last modified time (most recent first)
95        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}