Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added --text parameter for matching with keywords within files #55

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 39 additions & 5 deletions src/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -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) {
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
}

fn main() -> Result<()> {
Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 3 additions & 2 deletions src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<serde_json::Value>)> {
// ~~~ Initialization ~~~
let mut files = Vec::new();
Expand All @@ -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;
}

Expand All @@ -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);

Expand Down
133 changes: 118 additions & 15 deletions tests/test_filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ mod tests {
&path,
&include_patterns,
&exclude_patterns,
include_priority
include_priority,
None
));
}
}
Expand All @@ -112,7 +113,8 @@ mod tests {
&path,
&include_patterns,
&exclude_patterns,
include_priority
include_priority,
None
));
}

Expand All @@ -129,7 +131,8 @@ mod tests {
&path,
&include_patterns,
&exclude_patterns,
include_priority
include_priority,
None
));
}
}
Expand All @@ -155,7 +158,8 @@ mod tests {
&path,
&include_patterns,
&exclude_patterns,
include_priority
include_priority,
None
));
}

Expand All @@ -172,7 +176,8 @@ mod tests {
&path,
&include_patterns,
&exclude_patterns,
include_priority
include_priority,
None
));
}
}
Expand All @@ -191,7 +196,8 @@ mod tests {
&path,
&include_patterns,
&exclude_patterns,
include_priority
include_priority,
None
));
}

Expand All @@ -212,7 +218,8 @@ mod tests {
&path,
&include_patterns,
&exclude_patterns,
include_priority
include_priority,
None
));
}
}
Expand All @@ -231,7 +238,8 @@ mod tests {
&path,
&include_patterns,
&exclude_patterns,
include_priority
include_priority,
None
));
}

Expand All @@ -252,7 +260,8 @@ mod tests {
&path,
&include_patterns,
&exclude_patterns,
include_priority
include_priority,
None
));
}
}
Expand All @@ -271,7 +280,8 @@ mod tests {
&path,
&include_patterns,
&exclude_patterns,
include_priority
include_priority,
None
));
}

Expand All @@ -293,7 +303,8 @@ mod tests {
&path,
&include_patterns,
&exclude_patterns,
include_priority
include_priority,
None
));
}
}
Expand All @@ -319,7 +330,8 @@ mod tests {
&path,
&include_patterns,
&exclude_patterns,
include_priority
include_priority,
None
));
}

Expand All @@ -336,7 +348,8 @@ mod tests {
&path,
&include_patterns,
&exclude_patterns,
include_priority
include_priority,
None
));
}
}
Expand All @@ -351,7 +364,8 @@ mod tests {
&path,
&include_patterns,
&exclude_patterns,
include_priority
include_priority,
None
));
}

Expand All @@ -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")
));
}
}