//! Code search (hub spec ยง11): substring or regular expression over the //! text files of one state, with a time budget and a result cap. Results //! are cached by state hash and query, since a state never changes. use super::cache::Loaded; use oxy_core::state::FileContent; use std::collections::HashMap; use std::sync::Mutex; use std::time::{Duration, Instant}; #[derive(Clone, Debug, PartialEq, Eq)] pub struct Hit { pub path: String, /// 1-based; 0 for a match on the path itself. pub line: usize, pub text: String, } #[derive(Clone, Debug, Default)] pub struct Results { pub hits: Vec, pub files: usize, /// The budget or the cap stopped the search early. pub truncated: bool, } pub struct Query<'a> { pub text: &'a str, pub regex: bool, pub case_sensitive: bool, /// Only paths starting with this. pub path_prefix: &'a str, } pub const MAX_HITS: usize = 300; pub const BUDGET: Duration = Duration::from_secs(2); pub fn run(loaded: &Loaded, q: &Query<'_>) -> Result { let pattern = if q.regex { q.text.to_string() } else { regex::escape(q.text) }; let re = regex::RegexBuilder::new(&pattern) .case_insensitive(!q.case_sensitive) .size_limit(1 << 20) .build() .map_err(|e| format!("not a valid regular expression: {e}"))?; let started = Instant::now(); let mut out = Results::default(); for (path, f) in &loaded.snapshot.files { if !path.starts_with(q.path_prefix) { continue; } if started.elapsed() > BUDGET || out.hits.len() >= MAX_HITS { out.truncated = true; break; } let mut found = false; if re.is_match(path) { out.hits.push(Hit { path: path.clone(), line: 0, text: String::new() }); found = true; } let FileContent::Text { bytes, .. } = &f.content else { continue }; if bytes.len() > 1 << 20 || bytes.contains(&0) { continue; } let text = String::from_utf8_lossy(bytes); for (i, line) in text.lines().enumerate() { if re.is_match(line) { out.hits.push(Hit { path: path.clone(), line: i + 1, text: line.chars().take(300).collect() }); found = true; if out.hits.len() >= MAX_HITS { out.truncated = true; break; } } } if found { out.files += 1; } } Ok(out) } /// A small cache of results by (state, query). #[derive(Default)] pub struct SearchCache { inner: Mutex>, } impl SearchCache { pub fn get(&self, key: &str) -> Option { self.inner.lock().unwrap().get(key).cloned() } pub fn put(&self, key: String, r: Results) { let mut m = self.inner.lock().unwrap(); if m.len() > 256 { m.clear(); } m.insert(key, r); } }