use anyhow::{Context, Result};
use gnaw_core::{
configuration::{GnawConfig, TomlConfig},
session::GnawSession,
sort::FileSortMethod,
template::{OutputFormat, extract_undefined_variables},
tokenizer::TokenizerType,
};
use inquire::Text;
use log::error;
use std::path::PathBuf;
use crate::{args::Cli, config_loader::ConfigSource};
use gnaw_core::configuration::CompressionOptions;
const STRIP_TOKENS: [&str; 4] = ["tests", "fn-bodies", "doc-comments", "private-bodies"];
pub fn build_session(
base: Option<&ConfigSource>,
args: &Cli,
tui_mode: bool,
) -> Result<GnawSession> {
let mut configuration = GnawConfig::builder();
let cfg = base.map(|b| &b.config);
if let Some(c) = cfg {
if let Some(path) = &c.path {
configuration.path(PathBuf::from(path));
} else {
configuration.path(args.path.clone());
}
} else {
configuration.path(args.path.clone());
}
let (cfg_include, cfg_exclude) = cfg
.map(|c| (c.include_patterns.clone(), c.exclude_patterns.clone()))
.unwrap_or_default();
let mut include_patterns = cfg_include;
include_patterns.extend(expand_comma_separated_patterns(&args.include));
let mut exclude_patterns = cfg_exclude;
exclude_patterns.extend(expand_comma_separated_patterns(&args.exclude));
configuration
.include_patterns(include_patterns)
.exclude_patterns(exclude_patterns);
let cfg_line_numbers = cfg.map(|c| c.line_numbers).unwrap_or(false);
let cfg_absolute = cfg.map(|c| c.absolute_path).unwrap_or(false);
let cfg_full_tree = cfg.map(|c| c.full_directory_tree).unwrap_or(false);
configuration
.line_numbers(args.line_numbers || cfg_line_numbers)
.absolute_path(args.absolute_paths || cfg_absolute)
.full_directory_tree(args.full_directory_tree || cfg_full_tree);
let output_format = if let Some(output_format_str) = args.output_format {
output_format_str
} else if let Some(c) = cfg {
c.output_format.unwrap_or(OutputFormat::Markdown)
} else {
OutputFormat::Markdown
};
configuration.output_format(output_format);
let sort_method = if let Some(sort_str) = args.sort {
sort_str
} else if let Some(c) = cfg {
c.sort_method.unwrap_or(FileSortMethod::NameAsc)
} else {
FileSortMethod::NameAsc
};
configuration.sort_method(sort_method);
let tokenizer_type = if let Some(encoding) = args.encoding {
encoding
} else if let Some(c) = cfg {
c.encoding.unwrap_or(TokenizerType::Cl100kBase)
} else {
TokenizerType::Cl100kBase
};
let token_format = if let Some(format) = args.token_format {
format
} else if let Some(c) = cfg {
c.token_format
.unwrap_or(gnaw_core::tokenizer::TokenFormat::Format)
} else {
gnaw_core::tokenizer::TokenFormat::Format
};
configuration
.encoding(tokenizer_type)
.token_format(token_format);
let (template_str, template_name) = if args.template.is_some() {
parse_template(&args.template).map_err(|e| {
error!("Failed to parse template: {}", e);
e
})?
} else if let Some(c) = cfg {
(
c.template_str.clone().unwrap_or_default(),
c.template_name
.clone()
.unwrap_or_else(|| "default".to_string()),
)
} else {
("".to_string(), "default".to_string())
};
configuration
.template_str(template_str)
.template_name(template_name);
let diff_branches = parse_branch_argument(&args.git_diff_branch).or_else(|| {
cfg.and_then(|c| {
c.diff_branches.as_ref().and_then(|branches| {
if branches.len() == 2 {
Some((branches[0].clone(), branches[1].clone()))
} else {
None
}
})
})
});
let log_branches = parse_branch_argument(&args.git_log_branch).or_else(|| {
cfg.and_then(|c| {
c.log_branches.as_ref().and_then(|branches| {
if branches.len() == 2 {
Some((branches[0].clone(), branches[1].clone()))
} else {
None
}
})
})
});
let cfg_diff_enabled = cfg.map(|c| c.diff_enabled).unwrap_or(false);
let cfg_token_map_enabled = cfg.map(|c| c.token_map_enabled).unwrap_or(false);
let cfg_deselected = cfg.map(|c| c.deselected).unwrap_or(false);
configuration.compression(resolve_compression(args, cfg)?);
configuration
.diff_enabled(args.diff || cfg_diff_enabled)
.diff_mode(args.diff_mode.unwrap_or_default())
.diff_branches(diff_branches)
.log_branches(log_branches)
.no_ignore(args.no_ignore)
.hidden(args.hidden)
.no_codeblock(args.no_codeblock)
.follow_symlinks(args.follow_symlinks)
.token_map_enabled(args.token_map || cfg_token_map_enabled || tui_mode)
.deselected(args.deselected || cfg_deselected);
if let Some(c) = cfg {
configuration.user_variables(c.user_variables.clone());
}
let session = GnawSession::new(configuration.build()?);
Ok(session)
}
pub fn parse_branch_argument(branch_arg: &Option<Vec<String>>) -> Option<(String, String)> {
match branch_arg {
Some(branches) if branches.len() == 2 => Some((branches[0].clone(), branches[1].clone())),
_ => None,
}
}
pub fn parse_template(template_arg: &Option<String>) -> Result<(String, String)> {
match template_arg {
Some(arg) => {
if let Some(t) = gnaw_core::builtin_templates::BuiltinTemplates::get_template(arg) {
return Ok((t.content.to_string(), arg.clone()));
}
let content = std::fs::read_to_string(arg).with_context(|| {
let keys = gnaw_core::builtin_templates::BuiltinTemplates::get_template_keys();
format!(
"'{arg}' is not a built-in template and no file exists at that path.\n\
Available built-ins: {}",
keys.join(", ")
)
})?;
Ok((content, "custom".to_string()))
}
None => Ok(("".to_string(), "default".to_string())),
}
}
pub fn handle_undefined_variables(session: &mut GnawSession, template_content: &str) -> Result<()> {
let undefined_variables = extract_undefined_variables(template_content);
for var in undefined_variables.iter() {
if !session.config.user_variables.contains_key(var) {
let prompt = format!("Enter value for '{}': ", var);
let answer = Text::new(&prompt)
.with_help_message("Fill user defined variable in template")
.prompt()
.unwrap_or_default();
session.config.user_variables.insert(var.clone(), answer);
}
}
Ok(())
}
fn expand_comma_separated_patterns(patterns: &[String]) -> Vec<String> {
let mut expanded = Vec::new();
for pattern in patterns {
if pattern.contains('{') && pattern.contains('}') {
expanded.push(pattern.clone());
} else {
for part in pattern.split(',') {
let trimmed = part.trim();
if !trimmed.is_empty() {
expanded.push(trimmed.to_string());
}
}
}
}
expanded
}
fn resolve_compression(args: &Cli, cfg: Option<&TomlConfig>) -> Result<CompressionOptions> {
let base = match args.compress {
Some(level) => level.options(),
None => cfg.and_then(|c| c.compression).unwrap_or_default(),
};
match &args.compress_strip {
Some(csv) => apply_strip_overrides(base, csv),
None => Ok(base),
}
}
fn apply_strip_overrides(mut o: CompressionOptions, csv: &str) -> Result<CompressionOptions> {
for raw in csv.split(',').map(str::trim).filter(|s| !s.is_empty()) {
let (on, name) = match raw.strip_prefix("no-") {
Some(rest) => (false, rest),
None => (true, raw),
};
match name {
"tests" => o.strip_test_modules = on,
"fn-bodies" => o.strip_fn_bodies = on,
"doc-comments" => o.strip_doc_comments = on,
"private-bodies" => o.strip_private_bodies = on,
other => {
let hint = closest(other)
.map(|s| format!(" — did you mean '{s}'?"))
.unwrap_or_default();
anyhow::bail!(
"unknown compression flag '{other}'{hint}\n valid tokens: {} \
(each optionally prefixed with `no-` to disable)",
STRIP_TOKENS.join(", ")
);
}
}
}
Ok(o)
}
fn closest(input: &str) -> Option<&'static str> {
STRIP_TOKENS
.iter()
.map(|&t| (t, levenshtein(input, t)))
.filter(|(_, d)| *d <= 3)
.min_by_key(|(_, d)| *d)
.map(|(t, _)| t)
}
fn levenshtein(a: &str, b: &str) -> usize {
let (a, b): (Vec<char>, Vec<char>) = (a.chars().collect(), b.chars().collect());
let mut prev: Vec<usize> = (0..=b.len()).collect();
let mut curr = vec![0usize; b.len() + 1];
for (i, ca) in a.iter().enumerate() {
curr[0] = i + 1;
for (j, cb) in b.iter().enumerate() {
let cost = usize::from(ca != cb);
curr[j + 1] = (prev[j + 1] + 1).min(curr[j] + 1).min(prev[j] + cost);
}
std::mem::swap(&mut prev, &mut curr);
}
prev[b.len()]
}