diff --git a/src/filter.rs b/src/filter.rs index 739ebe4..10faaf6 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -5,6 +5,7 @@ use glob::Pattern; use log::{debug, error}; use std::fs; use std::path::Path; +use std::io::Read; /// Determines whether a file should be included based on include and exclude patterns. /// @@ -18,11 +19,36 @@ use std::path::Path; /// # Returns /// /// * `bool` - `true` if the file should be included, `false` otherwise. +/// Checks if a file's content contains the specified text +/// +/// # Arguments +/// +/// * `path` - The path to the file to check +/// * `text` - The text to search for in the file +/// +/// # Returns +/// +/// * `bool` - `true` if the text is found, `false` otherwise +fn file_contains_text(path: &Path, text: &str) -> bool { + let mut file = match fs::File::open(path) { + Ok(file) => file, + Err(_) => return false, + }; + + let mut contents = String::new(); + if file.read_to_string(&mut contents).is_err() { + return false; + } + + contents.contains(text) +} + pub fn should_include_file( path: &Path, include_patterns: &[String], exclude_patterns: &[String], include_priority: bool, + text_filter: Option<&str>, ) -> bool { // ~~~ Clean path ~~~ let canonical_path = match fs::canonicalize(path) { @@ -42,21 +68,29 @@ pub fn should_include_file( .iter() .any(|pattern| Pattern::new(pattern).unwrap().matches(path_str)); + // ~~~ Check text filter ~~~ + let text_match = match text_filter { + Some(text) => file_contains_text(path, text), + None => true, + }; + // ~~~ Decision ~~~ let result = match (included, excluded) { - (true, true) => include_priority, // If both include and exclude patterns match, use the include_priority flag - (true, false) => true, // If the path is included and not excluded, include it - (false, true) => false, // If the path is excluded, exclude it - (false, false) => include_patterns.is_empty(), // If no include patterns are provided, include everything + (true, true) => include_priority && text_match, // If both include and exclude patterns match, use the include_priority flag + (true, false) => text_match, // If the path is included and not excluded, include it if text matches + (false, true) => false, // If the path is excluded, exclude it + (false, false) => include_patterns.is_empty() && text_match, // If no include patterns are provided, include everything if text matches }; debug!( - "Checking path: {:?}, {}: {}, {}: {}, decision: {}", + "Checking path: {:?}, {}: {}, {}: {}, {}: {}, decision: {}", path_str, "included".bold().green(), included, "excluded".bold().red(), excluded, + "text_match".bold().blue(), + text_match, result ); result diff --git a/src/main.rs b/src/main.rs index 964010f..2e46eb4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -94,6 +94,10 @@ struct Cli { /// Print output as JSON #[clap(long)] json: bool, + + /// Filter files by content containing this text + #[clap(long)] + text: Option, } fn main() -> Result<()> { @@ -121,6 +125,7 @@ fn main() -> Result<()> { args.relative_paths, args.exclude_from_tree, args.no_codeblock, + args.text.as_deref(), ); let (tree, files) = match create_tree { diff --git a/src/path.rs b/src/path.rs index 9ef3a37..36cc012 100644 --- a/src/path.rs +++ b/src/path.rs @@ -33,6 +33,7 @@ pub fn traverse_directory( relative_paths: bool, exclude_from_tree: bool, no_codeblock: bool, + text_filter: Option<&str>, ) -> Result<(String, Vec)> { // ~~~ Initialization ~~~ let mut files = Vec::new(); @@ -52,7 +53,7 @@ pub fn traverse_directory( let component_str = component.as_os_str().to_string_lossy().to_string(); // Check if the current component should be excluded from the tree - if exclude_from_tree && !should_include_file(path, include, exclude, include_priority) { + if exclude_from_tree && !should_include_file(path, include, exclude, include_priority, text_filter) { break; } @@ -70,7 +71,7 @@ pub fn traverse_directory( } // ~~~ Process the file ~~~ - if path.is_file() && should_include_file(path, include, exclude, include_priority) { + if path.is_file() && should_include_file(path, include, exclude, include_priority, text_filter) { if let Ok(code_bytes) = fs::read(path) { let code = String::from_utf8_lossy(&code_bytes); diff --git a/tests/test_filter.rs b/tests/test_filter.rs index 175a624..6067e17 100644 --- a/tests/test_filter.rs +++ b/tests/test_filter.rs @@ -86,7 +86,8 @@ mod tests { &path, &include_patterns, &exclude_patterns, - include_priority + include_priority, + None )); } } @@ -112,7 +113,8 @@ mod tests { &path, &include_patterns, &exclude_patterns, - include_priority + include_priority, + None )); } @@ -129,7 +131,8 @@ mod tests { &path, &include_patterns, &exclude_patterns, - include_priority + include_priority, + None )); } } @@ -155,7 +158,8 @@ mod tests { &path, &include_patterns, &exclude_patterns, - include_priority + include_priority, + None )); } @@ -172,7 +176,8 @@ mod tests { &path, &include_patterns, &exclude_patterns, - include_priority + include_priority, + None )); } } @@ -191,7 +196,8 @@ mod tests { &path, &include_patterns, &exclude_patterns, - include_priority + include_priority, + None )); } @@ -212,7 +218,8 @@ mod tests { &path, &include_patterns, &exclude_patterns, - include_priority + include_priority, + None )); } } @@ -231,7 +238,8 @@ mod tests { &path, &include_patterns, &exclude_patterns, - include_priority + include_priority, + None )); } @@ -252,7 +260,8 @@ mod tests { &path, &include_patterns, &exclude_patterns, - include_priority + include_priority, + None )); } } @@ -271,7 +280,8 @@ mod tests { &path, &include_patterns, &exclude_patterns, - include_priority + include_priority, + None )); } @@ -293,7 +303,8 @@ mod tests { &path, &include_patterns, &exclude_patterns, - include_priority + include_priority, + None )); } } @@ -319,7 +330,8 @@ mod tests { &path, &include_patterns, &exclude_patterns, - include_priority + include_priority, + None )); } @@ -336,7 +348,8 @@ mod tests { &path, &include_patterns, &exclude_patterns, - include_priority + include_priority, + None )); } } @@ -351,7 +364,8 @@ mod tests { &path, &include_patterns, &exclude_patterns, - include_priority + include_priority, + None )); } @@ -365,7 +379,96 @@ mod tests { &path, &include_patterns, &exclude_patterns, - include_priority + include_priority, + None + )); + } + + #[test] + fn test_text_filter_inclusion() { + let base_path = TEST_DIR.path(); + let path = base_path.join("lowercase/foo.py"); + let include_patterns = vec![]; + let exclude_patterns = vec![]; + let include_priority = false; + + // File contains "content foo.py" + assert!(should_include_file( + &path, + &include_patterns, + &exclude_patterns, + include_priority, + Some("content foo.py") + )); + } + + #[test] + fn test_text_filter_exclusion() { + let base_path = TEST_DIR.path(); + let path = base_path.join("lowercase/foo.py"); + let include_patterns = vec![]; + let exclude_patterns = vec![]; + let include_priority = false; + + // File does not contain "missing text" + assert!(!should_include_file( + &path, + &include_patterns, + &exclude_patterns, + include_priority, + Some("missing text") + )); + } + + #[test] + fn test_text_filter_case_sensitivity() { + let base_path = TEST_DIR.path(); + let path = base_path.join("uppercase/FOO.py"); + let include_patterns = vec![]; + let exclude_patterns = vec![]; + let include_priority = false; + + // File contains "CONTENT FOO.PY" but not "content foo.py" + assert!(should_include_file( + &path, + &include_patterns, + &exclude_patterns, + include_priority, + Some("CONTENT FOO.PY") + )); + assert!(!should_include_file( + &path, + &include_patterns, + &exclude_patterns, + include_priority, + Some("content foo.py") + )); + } + + #[test] + fn test_text_filter_with_patterns() { + let base_path = TEST_DIR.path(); + let path = base_path.join("lowercase/foo.py"); + let include_patterns = vec!["*.py".to_string()]; + let exclude_patterns = vec![]; + let include_priority = false; + + // File matches pattern and contains text + assert!(should_include_file( + &path, + &include_patterns, + &exclude_patterns, + include_priority, + Some("content foo.py") + )); + + // File matches pattern but doesn't contain text + assert!(!should_include_file( + &path, + &include_patterns, + &exclude_patterns, + include_priority, + Some("missing text") )); } }