use anyhow::Result;
+use glob::Pattern;
+use ignore::WalkBuilder;
+use serde_json::json;
+use std::fs;
+use std::path::Path;
+use termtree::Tree;
+
+pub fn parse_comma_delim_patterns(patterns: &Option<String>) -> Vec<String> {
+ if let Some(patterns) = patterns {
+ if !patterns.is_empty() {
+ return patterns.split(',').map(|s| s.trim().to_owned()).collect();
+ }
+ }
+ vec![]
+}
+
+pub fn traverse_directory(
+ root: &Path,
+ include: &[String],
+ exclude: &[String],
+ include_priority: bool,
+ line_numbers: bool,
+ relative_paths: bool,
+ exclude_from_tree: bool,
+ no_codeblock: bool,
+ gitignore: bool,
+ verbose: bool,
+) -> Result<(String, Vec<serde_json::Value>)> {
+ let mut files = Vec::new();
+ let canonical_root_path = root.canonicalize()?;
+ let parent_dir = basename(&canonical_root_path);
+
+ if verbose {
+ println!(
+ "Include patterns: {:?}\nExclude patterns: {:?}",
+ include, exclude
+ );
+ }
+
+ let tree = WalkBuilder::new(&canonical_root_path)
+ .git_ignore(gitignore)
+ .build()
+ .filter_map(|e| e.ok())
+ .fold(Tree::new(parent_dir.to_owned()), |mut root, entry| {
+ let path = entry.path();
+ if let Ok(relative_path) = path.strip_prefix(&canonical_root_path) {
+ let mut current_tree = &mut root;
+ for component in relative_path.components() {
+ let component_string = component.as_os_str().to_string_lossy().to_string();
+ if exclude_from_tree && !include_file(path, include, exclude, include_priority, relative_paths, verbose)
+ {
+ break;
+ }
+
+ current_tree = if let Some(index) = current_tree
+ .leaves
+ .iter_mut()
+ .position(|child| child.root == component_string)
+ {
+ &mut current_tree.leaves[index]
+ } else {
+ let new_tree = Tree::new(component_string.clone());
+ current_tree.leaves.push(new_tree);
+ current_tree.leaves.last_mut().unwrap()
+ };
+ }
+
+ if path.is_file() && include_file(path, include, exclude, include_priority, relative_paths, verbose) {
+ if let Ok(file_bytes) = fs::read(path) {
+ let code_string = String::from_utf8_lossy(&file_bytes);
+ let formatted_block = wrap_content(
+ &code_string,
+ path.extension().and_then(|ext| ext.to_str()).unwrap_or(""),
+ line_numbers,
+ no_codeblock,
+ );
+
+ if !formatted_block.trim().is_empty()
+ && !formatted_block.contains(char::REPLACEMENT_CHARACTER)
+ {
+ let file_path = if relative_paths {
+ format!("{}/{}", parent_dir, relative_path.display())
+ } else {
+ path.display().to_string()
+ };
+
+ files.push(json!({
+ "path": file_path,
+ "extension": path.extension().and_then(|ext| ext.to_str()).unwrap_or(""),
+ "code": formatted_block
+ }));
+ }
+ }
+ }
+ }
+ root
+ });
+ Ok((tree.to_string(), files))
+}
+
+pub fn basename<P>(p: P) -> String
+where
+ P: AsRef<Path>,
+{
+ let path = p.as_ref();
+ match path.file_name() {
+ Some(name) => name.to_string_lossy().into_owned(),
+ None => handle_special_case(path),
+ }
+}
+
+fn handle_special_case(p: &Path) -> String {
+ if p.as_os_str().is_empty() || p == Path::new(".") || p == Path::new("..") {
+ std::env::current_dir()
+ .ok()
+ .and_then(|d| d.file_name().map(|n| n.to_string_lossy().into_owned()))
+ .unwrap_or_else(|| ".".to_owned())
+ } else {
+ p.to_string_lossy().into_owned()
+ }
+}
+
+fn include_file(
+ path: &Path,
+ include: &[String],
+ exclude: &[String],
+ include_priority: bool,
+ relative_paths: bool,
+ verbose: bool,
+) -> bool {
+ let canonical_root_path = match fs::canonicalize(path) {
+ Ok(path) => path,
+ Err(e) => {
+ println!("Failed to canonicalize path: {}", e);
+ return false;
+ }
+ };
+ let path_string = canonical_root_path.to_str().unwrap();
+ let relative_path = path
+ .strip_prefix(std::env::current_dir().unwrap())
+ .unwrap_or(path);
+ let relative_path_string = relative_path.to_str().unwrap();
+
+ if verbose {
+ if relative_paths {
+ println!("=> Target path: {}", relative_path_string);
+ } else {
+ println!("=> Target path: {}", path_string);
+ }
+ }
+
+ let strip_relative_prefix: for<'a> fn(&'a str) -> &'a str = |s| s.strip_prefix("./").unwrap_or(s);
+
+ let include_bool = include.iter().any(|pattern| {
+ let matches = if relative_paths {
+ let stripped_pattern = strip_relative_prefix(pattern);
+ Pattern::new(stripped_pattern).unwrap().matches(relative_path_string)
+ } else {
+ Pattern::new(pattern).unwrap().matches(path_string)
+ };
+ if verbose {
+ println!("\tChecking include pattern '{}': {}", pattern, matches);
+ }
+ matches
+ });
+
+ let exclude_bool = exclude.iter().any(|pattern| {
+ let matches = if relative_paths {
+ let stripped_pattern = strip_relative_prefix(pattern);
+ Pattern::new(stripped_pattern).unwrap().matches(relative_path_string)
+ } else {
+ Pattern::new(pattern).unwrap().matches(path_string)
+ };
+ if verbose {
+ println!("\tChecking exclude pattern '{}': {}", pattern, matches);
+ }
+ matches
+ });
+
+ let result = match (include_bool, exclude_bool) {
+ (true, true) => {
+ if verbose {
+ println!("\tMatch conflict, include priority: {}", include_priority);
+ }
+ include_priority
+ }
+ (true, false) => {
+ if verbose {
+ println!("\tInclude: true");
+ }
+ true
+ }
+ (false, true) => {
+ if verbose {
+ println!("\tInclude: false");
+ }
+ false
+ }
+ (false, false) => {
+ if verbose {
+ println!(
+ "\tNot in either condition, fallback: {}",
+ include.is_empty()
+ );
+ }
+ include.is_empty()
+ }
+ };
+
+ result
+}
+
+fn wrap_content(content: &str, extension: &str, line_numbers: bool, no_codeblock: bool) -> String {
+ let codeblock_tick = "`".repeat(3);
+ let mut formatted_block = String::new();
+
+ if line_numbers {
+ for (idx, line) in content.lines().enumerate() {
+ formatted_block.push_str(&format!("{:4} | {}\n", idx + 1, line));
+ }
+ } else {
+ formatted_block = content.to_owned();
+ }
+
+ if no_codeblock {
+ formatted_block
+ } else {
+ format!(
+ "{}{}\n{}\n{}",
+ codeblock_tick, extension, formatted_block, codeblock_tick
+ )
+ }
+}
+