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