use std::path::Path;
use super::DeepContextConfig;
#[derive(Debug, Clone, Default)]
pub struct FileScope {
include_patterns: Vec<String>,
exclude_patterns: Vec<String>,
}
impl FileScope {
#[must_use]
pub fn from_config(config: &DeepContextConfig) -> Self {
Self::new(&config.include_patterns, &config.exclude_patterns)
}
#[must_use]
pub fn new(include_patterns: &[String], exclude_patterns: &[String]) -> Self {
Self {
include_patterns: include_patterns
.iter()
.filter(|p| !p.trim().is_empty())
.cloned()
.collect(),
exclude_patterns: exclude_patterns
.iter()
.filter(|p| !p.trim().is_empty())
.cloned()
.collect(),
}
}
#[must_use]
pub fn has_include_filter(&self) -> bool {
!self.include_patterns.is_empty()
}
#[must_use]
pub fn is_unrestricted(&self) -> bool {
self.include_patterns.is_empty() && self.exclude_patterns.is_empty()
}
#[must_use]
pub fn is_excluded(&self, path: &Path) -> bool {
let path_str = path.to_string_lossy();
self.exclude_patterns
.iter()
.any(|pattern| path_str.contains(pattern.trim_matches('*')))
}
#[must_use]
pub fn matches_include(&self, path: &Path) -> bool {
if self.include_patterns.is_empty() {
return true;
}
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
let path_str = path.to_string_lossy();
self.include_patterns.iter().any(|pattern| {
if let Some(ext_from_pattern) = pattern
.strip_prefix("**/")
.and_then(|p| p.strip_prefix("*."))
{
return ext == ext_from_pattern;
}
path.file_name()
.and_then(|n| n.to_str())
.is_some_and(|name| name.contains(pattern.as_str()))
|| path_str.contains(pattern.as_str())
})
}
#[must_use]
pub fn contains_file(&self, path: &Path) -> bool {
!self.is_excluded(path) && self.matches_include(path)
}
#[must_use]
pub fn contains_file_str(&self, path: &str) -> bool {
self.contains_file(Path::new(path))
}
#[must_use]
pub fn may_contain_files(&self, path: &Path) -> bool {
!self.is_excluded(path)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn scope(include: &[&str], exclude: &[&str]) -> FileScope {
FileScope::new(
&include.iter().map(|s| (*s).to_string()).collect::<Vec<_>>(),
&exclude.iter().map(|s| (*s).to_string()).collect::<Vec<_>>(),
)
}
#[test]
fn no_include_filter_admits_everything() {
let s = scope(&[], &[]);
assert!(!s.has_include_filter());
assert!(s.contains_file(&PathBuf::from("/p/hot.rs")));
assert!(s.contains_file(&PathBuf::from("/p/hot.py")));
}
#[test]
fn extension_shorthand_selects_only_that_extension() {
let s = scope(&["**/*.py"], &[]);
assert!(s.has_include_filter());
assert!(s.contains_file(&PathBuf::from("/p/hot.py")));
assert!(!s.contains_file(&PathBuf::from("/p/hot.rs")));
}
#[test]
fn exclude_still_wins_over_include() {
let s = scope(&["**/*.rs"], &["**/target/**"]);
assert!(s.contains_file(&PathBuf::from("/p/src/lib.rs")));
assert!(!s.contains_file(&PathBuf::from("/p/target/debug/build.rs")));
}
#[test]
fn whitespace_only_patterns_do_not_narrow_the_scope() {
let s = scope(&[" "], &[]);
assert!(!s.has_include_filter());
assert!(s.contains_file(&PathBuf::from("/p/hot.rs")));
}
#[test]
fn string_and_path_predicates_agree() {
let s = scope(&["**/*.py"], &[]);
assert_eq!(
s.contains_file(&PathBuf::from("/p/hot.rs")),
s.contains_file_str("/p/hot.rs")
);
assert_eq!(
s.contains_file(&PathBuf::from("/p/hot.py")),
s.contains_file_str("/p/hot.py")
);
}
}