| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
use super::cache::Loaded; |
| 6 |
use oxy_core::state::FileContent; |
| 7 |
use std::collections::HashMap; |
| 8 |
use std::sync::Mutex; |
| 9 |
use std::time::{Duration, Instant}; |
| 10 |
|
| 11 |
#[derive(Clone, Debug, PartialEq, Eq)] |
| 12 |
pub struct Hit { |
| 13 |
pub path: String, |
| 14 |
|
| 15 |
pub line: usize, |
| 16 |
pub text: String, |
| 17 |
} |
| 18 |
|
| 19 |
#[derive(Clone, Debug, Default)] |
| 20 |
pub struct Results { |
| 21 |
pub hits: Vec<Hit>, |
| 22 |
pub files: usize, |
| 23 |
|
| 24 |
pub truncated: bool, |
| 25 |
} |
| 26 |
|
| 27 |
pub struct Query<'a> { |
| 28 |
pub text: &'a str, |
| 29 |
pub regex: bool, |
| 30 |
pub case_sensitive: bool, |
| 31 |
|
| 32 |
pub path_prefix: &'a str, |
| 33 |
} |
| 34 |
|
| 35 |
pub const MAX_HITS: usize = 300; |
| 36 |
pub const BUDGET: Duration = Duration::from_secs(2); |
| 37 |
|
| 38 |
pub fn run(loaded: &Loaded, q: &Query<'_>) -> Result<Results, String> { |
| 39 |
let pattern = if q.regex { q.text.to_string() } else { regex::escape(q.text) }; |
| 40 |
let re = regex::RegexBuilder::new(&pattern) |
| 41 |
.case_insensitive(!q.case_sensitive) |
| 42 |
.size_limit(1 << 20) |
| 43 |
.build() |
| 44 |
.map_err(|e| format!("not a valid regular expression: {e}"))?; |
| 45 |
let started = Instant::now(); |
| 46 |
let mut out = Results::default(); |
| 47 |
for (path, f) in &loaded.snapshot.files { |
| 48 |
if !path.starts_with(q.path_prefix) { |
| 49 |
continue; |
| 50 |
} |
| 51 |
if started.elapsed() > BUDGET || out.hits.len() >= MAX_HITS { |
| 52 |
out.truncated = true; |
| 53 |
break; |
| 54 |
} |
| 55 |
let mut found = false; |
| 56 |
if re.is_match(path) { |
| 57 |
out.hits.push(Hit { path: path.clone(), line: 0, text: String::new() }); |
| 58 |
found = true; |
| 59 |
} |
| 60 |
let FileContent::Text { bytes, .. } = &f.content else { continue }; |
| 61 |
if bytes.len() > 1 << 20 || bytes.contains(&0) { |
| 62 |
continue; |
| 63 |
} |
| 64 |
let text = String::from_utf8_lossy(bytes); |
| 65 |
for (i, line) in text.lines().enumerate() { |
| 66 |
if re.is_match(line) { |
| 67 |
out.hits.push(Hit { path: path.clone(), line: i + 1, text: line.chars().take(300).collect() }); |
| 68 |
found = true; |
| 69 |
if out.hits.len() >= MAX_HITS { |
| 70 |
out.truncated = true; |
| 71 |
break; |
| 72 |
} |
| 73 |
} |
| 74 |
} |
| 75 |
if found { |
| 76 |
out.files += 1; |
| 77 |
} |
| 78 |
} |
| 79 |
Ok(out) |
| 80 |
} |
| 81 |
|
| 82 |
|
| 83 |
#[derive(Default)] |
| 84 |
pub struct SearchCache { |
| 85 |
inner: Mutex<HashMap<String, Results>>, |
| 86 |
} |
| 87 |
|
| 88 |
impl SearchCache { |
| 89 |
pub fn get(&self, key: &str) -> Option<Results> { |
| 90 |
self.inner.lock().unwrap().get(key).cloned() |
| 91 |
} |
| 92 |
|
| 93 |
pub fn put(&self, key: String, r: Results) { |
| 94 |
let mut m = self.inner.lock().unwrap(); |
| 95 |
if m.len() > 256 { |
| 96 |
m.clear(); |
| 97 |
} |
| 98 |
m.insert(key, r); |
| 99 |
} |
| 100 |
} |