use crate::model::DisplayFileNode;
use anyhow::Result;
use code2prompt_core::session::Code2PromptSession;
use regex::Regex;
use std::path::Path;
pub fn build_file_tree_from_session(
session: &mut Code2PromptSession,
) -> Result<Vec<DisplayFileNode>> {
let mut root_nodes = Vec::new();
use ignore::WalkBuilder;
let walker = WalkBuilder::new(&session.config.path)
.max_depth(Some(1))
.git_ignore(!session.config.no_ignore) .hidden(!session.config.hidden) .build();
for entry in walker {
let entry = entry?;
let path = entry.path();
if path == session.config.path {
continue; }
let mut node = DisplayFileNode::new(path.to_path_buf(), 0);
if node.is_directory {
auto_expand_recursively(&mut node, session);
}
root_nodes.push(node);
}
root_nodes.sort_by(|a, b| match (a.is_directory, b.is_directory) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => a.name.cmp(&b.name),
});
Ok(root_nodes)
}
fn auto_expand_recursively(node: &mut DisplayFileNode, session: &mut Code2PromptSession) {
if !node.is_directory {
return;
}
if directory_contains_selected_files(&node.path, session) {
node.is_expanded = true;
if let Err(e) = node.load_children(session) {
eprintln!("Warning: Failed to load children for {}: {}", node.name, e);
return;
}
for child in &mut node.children {
if child.is_directory {
auto_expand_recursively(child, session);
}
}
}
}
pub(crate) fn directory_contains_selected_files(
dir_path: &Path,
session: &mut Code2PromptSession,
) -> bool {
if let Ok(entries) = std::fs::read_dir(dir_path) {
for entry in entries.flatten() {
let path = entry.path();
let relative_path = if let Ok(rel) = path.strip_prefix(&session.config.path) {
rel
} else {
continue;
};
if session.is_file_selected(relative_path) {
return true;
}
if path.is_dir() && directory_contains_selected_files(&path, session) {
return true;
}
}
}
false
}
pub fn get_visible_nodes(
nodes: &[DisplayFileNode],
search_query: &str,
session: &mut Code2PromptSession,
) -> Vec<DisplayNodeWithSelection> {
let mut visible = Vec::new();
let search_active = !search_query.is_empty();
let matcher = build_query_matcher(search_query);
collect_visible_nodes_recursive(nodes, &matcher, session, &mut visible, search_active);
visible
}
enum QueryMatcher {
Substr(String),
Regex(Regex),
}
fn build_query_matcher(raw: &str) -> QueryMatcher {
let raw = raw.trim();
let has_wildcards = raw.contains('*') || raw.contains('?');
if has_wildcards {
let mut pat = regex::escape(raw);
pat = pat.replace(r"\*", ".*").replace(r"\?", ".");
let anchored = format!("(?i)^{}$", pat); QueryMatcher::Regex(Regex::new(&anchored).unwrap_or_else(|_| Regex::new(".*").unwrap()))
} else {
QueryMatcher::Substr(raw.to_lowercase())
}
}
fn matches(m: &QueryMatcher, text: &str) -> bool {
match m {
QueryMatcher::Substr(needle) => text.to_lowercase().contains(needle),
QueryMatcher::Regex(re) => re.is_match(text),
}
}
#[derive(Debug, Clone)]
pub struct DisplayNodeWithSelection {
pub node: DisplayFileNode,
pub is_selected: bool,
}
fn collect_visible_nodes_recursive(
nodes: &[DisplayFileNode],
matcher: &QueryMatcher,
session: &mut Code2PromptSession,
visible: &mut Vec<DisplayNodeWithSelection>,
search_active: bool,
) {
for node in nodes {
let matches_current = if matches!(matcher, QueryMatcher::Substr(s) if s.is_empty()) {
true
} else {
matches(matcher, &node.name) || matches(matcher, &node.path.to_string_lossy())
};
if search_active {
let mut child_results: Vec<DisplayNodeWithSelection> = Vec::new();
if node.is_directory {
let children = get_children_for_search(node, session);
collect_visible_nodes_recursive(
&children,
matcher,
session,
&mut child_results,
true,
);
}
let include_self = matches_current || !child_results.is_empty();
if include_self {
let relative_path = if let Ok(rel) = node.path.strip_prefix(&session.config.path) {
rel
} else {
&node.path
};
let is_selected = session.is_file_selected(relative_path);
let mut node_clone = node.clone();
if node_clone.is_directory {
node_clone.is_expanded = true;
}
visible.push(DisplayNodeWithSelection {
node: node_clone,
is_selected,
});
visible.extend(child_results);
}
} else {
if matches_current {
let relative_path = if let Ok(rel) = node.path.strip_prefix(&session.config.path) {
rel
} else {
&node.path
};
let is_selected = session.is_file_selected(relative_path);
visible.push(DisplayNodeWithSelection {
node: node.clone(),
is_selected,
});
if node.is_directory && node.is_expanded {
collect_visible_nodes_recursive(
&node.children,
matcher,
session,
visible,
false,
);
}
}
}
}
}
pub fn save_to_file(path: &Path, content: &str) -> Result<()> {
std::fs::write(path, content)?;
Ok(())
}
pub fn format_number(num: usize, format: &code2prompt_core::tokenizer::TokenFormat) -> String {
use code2prompt_core::tokenizer::TokenFormat;
match format {
TokenFormat::Raw => num.to_string(),
TokenFormat::Format => {
let s = num.to_string();
let chars: Vec<char> = s.chars().collect();
let mut result = String::new();
for (i, c) in chars.iter().enumerate() {
if i > 0 && (chars.len() - i).is_multiple_of(3) {
result.push(',');
}
result.push(*c);
}
result
}
}
}
fn get_children_for_search(
node: &DisplayFileNode,
session: &mut Code2PromptSession,
) -> Vec<DisplayFileNode> {
if !node.is_directory {
return Vec::new();
}
if node.children_loaded {
return node.children.clone();
}
let mut children: Vec<DisplayFileNode> = Vec::new();
use ignore::WalkBuilder;
let walker = WalkBuilder::new(&node.path)
.max_depth(Some(1))
.git_ignore(!session.config.no_ignore) .hidden(!session.config.hidden) .build();
for entry in walker.flatten() {
let path = entry.path();
if path == node.path {
continue;
}
let mut child = DisplayFileNode::new(path.to_path_buf(), node.level + 1);
if child.is_directory && directory_contains_selected_files(&child.path, session) {
child.is_expanded = true;
}
children.push(child);
}
children.sort_by(|a, b| match (a.is_directory, b.is_directory) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => a.name.cmp(&b.name),
});
children
}
pub fn save_template_to_custom_dir(filename: &Path, content: &str) -> Result<()> {
let templates_dir = if let Some(cfg) = dirs::config_dir() {
cfg.join("code2prompt").join("templates")
} else {
std::env::current_dir()?.join("templates")
};
std::fs::create_dir_all(&templates_dir)?;
let full_path = templates_dir.join(filename);
std::fs::write(full_path, content)?;
Ok(())
}
pub fn load_all_templates() -> Result<Vec<(String, String)>> {
let mut out = Vec::new();
let mut roots = Vec::new();
roots.push(std::env::current_dir()?.join("templates"));
if let Some(cfg) = dirs::config_dir() {
roots.push(cfg.join("code2prompt").join("templates"));
}
let is_template = |p: &Path| {
matches!(
p.extension().and_then(|e| e.to_str()),
Some("hbs") | Some("handlebars") | Some("md") | Some("tmpl")
)
};
for root in roots {
if !root.exists() {
continue;
}
for entry in walkdir::WalkDir::new(&root).min_depth(1).max_depth(2) {
let entry = entry?;
let p = entry.path();
if p.is_file() && is_template(p) {
let name = p
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("template")
.to_string();
out.push((
name,
p.canonicalize()
.unwrap_or_else(|_| p.to_path_buf())
.to_string_lossy()
.into(),
));
}
}
}
out.sort_by(|a: &(String, String), b: &(String, String)| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
out.dedup_by(|a, b| a.1 == b.1);
Ok(out)
}
pub fn ensure_path_exists_in_tree(
root_nodes: &mut Vec<DisplayFileNode>,
target_path: &Path,
session: &mut Code2PromptSession,
) -> Result<()> {
let root_path = &session.config.path;
let relative_path = if let Ok(rel) = target_path.strip_prefix(root_path) {
rel
} else {
return Ok(()); };
let components: Vec<_> = relative_path.components().collect();
if components.is_empty() {
return Ok(());
}
let mut current_path = root_path.to_path_buf();
let mut current_nodes = root_nodes;
for (level, component) in components.into_iter().enumerate() {
current_path.push(component);
let node_name = component.as_os_str().to_string_lossy().to_string();
let existing_index = current_nodes.iter().position(|n| n.name == node_name);
if let Some(index) = existing_index {
let node = &mut current_nodes[index];
if node.is_directory && !node.children_loaded {
let _ = node.load_children(session);
}
current_nodes = &mut current_nodes[index].children;
} else {
let mut new_node = DisplayFileNode::new(current_path.clone(), level);
if new_node.is_directory {
let _ = new_node.load_children(session);
}
current_nodes.push(new_node);
current_nodes.sort_by(|a, b| match (a.is_directory, b.is_directory) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => a.name.cmp(&b.name),
});
let new_index = current_nodes
.iter()
.position(|n| n.name == node_name)
.unwrap();
current_nodes = &mut current_nodes[new_index].children;
}
}
Ok(())
}