use crate::constants::DEFAULT_LINE_NUMBER_WIDTH;
use crate::core_types::FileInfo;
use crate::output::formatter::format_path_for_display;
use crate::output::OutputConfig;
use anyhow::Result;
use log::debug; use std::io::Write;
use std::path::PathBuf;
pub fn write_file_block(
writer: &mut dyn Write,
file_info: &FileInfo,
opts: &OutputConfig,
) -> Result<()> {
let path_to_display = if opts.filename_only_header {
file_info
.relative_path
.file_name()
.map(PathBuf::from)
.unwrap_or_else(|| file_info.relative_path.clone()) } else {
file_info.relative_path.clone()
};
debug!(
"Path used for header display: '{}'",
path_to_display.display()
);
let header_path_str = format_path_for_display(&path_to_display, opts);
writeln!(writer, "## File: {}", header_path_str)?;
let extension_hint = file_info
.relative_path
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("");
let fence = "`".repeat(opts.num_ticks as usize);
writeln!(writer, "{}{}", fence, extension_hint)?;
if let Some(content) = &file_info.processed_content {
let lines = content.lines().collect::<Vec<_>>();
let num_width = calculate_line_number_width(lines.len(), opts);
if lines.is_empty() && content.is_empty() {
} else if lines.is_empty() && !content.is_empty() {
if opts.line_numbers {
write!(writer, "{:>width$} | ", 1, width = num_width)?;
}
writeln!(writer, "{}", content)?;
} else {
for (i, line) in lines.iter().enumerate() {
if opts.line_numbers {
write!(writer, "{:>width$} | ", i + 1, width = num_width)?;
}
writeln!(writer, "{}", line)?;
}
}
} else {
log::warn!(
"Content not available for file: {}",
file_info.absolute_path.display()
);
writeln!(writer, "// Content not available")?;
}
writeln!(writer, "{}", fence)?;
Ok(())
}
fn calculate_line_number_width(line_count: usize, opts: &OutputConfig) -> usize {
if !opts.line_numbers {
return 0; }
if line_count == 0 {
1 } else {
((line_count as f64).log10().floor() as usize) + 1
}
.max(DEFAULT_LINE_NUMBER_WIDTH) }
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
use std::path::PathBuf;
fn create_test_opts(line_numbers: bool, filename_only: bool, backticks: bool) -> OutputConfig {
crate::output::tests::create_mock_output_config(
backticks,
filename_only,
line_numbers,
false,
)
}
fn create_file_info(relative_path: &str, content: Option<&str>) -> FileInfo {
FileInfo {
absolute_path: PathBuf::from("/absolute/path/to").join(relative_path),
relative_path: PathBuf::from(relative_path),
size: content.map_or(0, |c| c.len() as u64),
processed_content: content.map(String::from),
..Default::default()
}
}
#[test]
fn test_calculate_line_number_width() {
let opts_ln_on = create_test_opts(true, false, false);
let opts_ln_off = create_test_opts(false, false, false);
assert_eq!(
calculate_line_number_width(0, &opts_ln_on),
DEFAULT_LINE_NUMBER_WIDTH
); assert_eq!(
calculate_line_number_width(9, &opts_ln_on),
DEFAULT_LINE_NUMBER_WIDTH
); assert_eq!(
calculate_line_number_width(10, &opts_ln_on),
DEFAULT_LINE_NUMBER_WIDTH
); assert_eq!(calculate_line_number_width(99999, &opts_ln_on), 5); assert_eq!(calculate_line_number_width(100000, &opts_ln_on), 6);
assert_eq!(calculate_line_number_width(0, &opts_ln_off), 0);
assert_eq!(calculate_line_number_width(1000, &opts_ln_off), 0);
}
#[test]
fn test_write_file_block_basic() -> Result<()> {
let opts = create_test_opts(false, false, false);
let file_info =
create_file_info("src/main.rs", Some("fn main() {\n println!(\"Hi\");\n}"));
let mut writer = Cursor::new(Vec::new());
write_file_block(&mut writer, &file_info, &opts)?;
let output = String::from_utf8(writer.into_inner())?;
let expected = "## File: src/main.rs\n```rs\nfn main() {\n println!(\"Hi\");\n}\n```\n";
assert_eq!(output, expected);
Ok(())
}
#[test]
fn test_write_file_block_no_extension() -> Result<()> {
let opts = create_test_opts(false, false, false);
let file_info = create_file_info("Makefile", Some("all: build"));
let mut writer = Cursor::new(Vec::new());
write_file_block(&mut writer, &file_info, &opts)?;
let output = String::from_utf8(writer.into_inner())?;
let expected = "## File: Makefile\n```\nall: build\n```\n"; assert_eq!(output, expected);
Ok(())
}
#[test]
fn test_write_file_block_line_numbers() -> Result<()> {
let opts = create_test_opts(true, false, false); let file_info = create_file_info("script.sh", Some("#!/bin/bash\necho $1")); let mut writer = Cursor::new(Vec::new());
write_file_block(&mut writer, &file_info, &opts)?;
let output = String::from_utf8(writer.into_inner())?;
let expected = "## File: script.sh\n```sh\n 1 | #!/bin/bash\n 2 | echo $1\n```\n";
assert_eq!(output, expected);
Ok(())
}
#[test]
fn test_write_file_block_line_numbers_single_line_no_newline() -> Result<()> {
let opts = create_test_opts(true, false, false); let file_info = create_file_info("single.txt", Some("Just one line")); let mut writer = Cursor::new(Vec::new());
write_file_block(&mut writer, &file_info, &opts)?;
let output = String::from_utf8(writer.into_inner())?;
let expected = "## File: single.txt\n```txt\n 1 | Just one line\n```\n";
assert_eq!(output, expected);
Ok(())
}
#[test]
fn test_write_file_block_line_numbers_empty_file() -> Result<()> {
let opts = create_test_opts(true, false, false); let file_info = create_file_info("empty.txt", Some("")); let mut writer = Cursor::new(Vec::new());
write_file_block(&mut writer, &file_info, &opts)?;
let output = String::from_utf8(writer.into_inner())?;
let expected = "## File: empty.txt\n```txt\n```\n"; assert_eq!(output, expected);
Ok(())
}
#[test]
fn test_write_file_block_filename_only() -> Result<()> {
let opts = create_test_opts(false, true, false); let file_info = create_file_info("path/to/file.py", Some("print('Hello')"));
let mut writer = Cursor::new(Vec::new());
write_file_block(&mut writer, &file_info, &opts)?;
let output = String::from_utf8(writer.into_inner())?;
let expected = "## File: file.py\n```py\nprint('Hello')\n```\n";
assert_eq!(output, expected);
Ok(())
}
#[test]
fn test_write_file_block_backticks() -> Result<()> {
let opts = create_test_opts(false, false, true); let file_info = create_file_info("data/config.toml", Some("[section]"));
let mut writer = Cursor::new(Vec::new());
write_file_block(&mut writer, &file_info, &opts)?;
let output = String::from_utf8(writer.into_inner())?;
let expected = "## File: `data/config.toml`\n```toml\n[section]\n```\n";
assert_eq!(output, expected);
Ok(())
}
#[test]
fn test_write_file_block_all_options() -> Result<()> {
let opts = create_test_opts(true, true, true); let file_info =
create_file_info("utils/helper.js", Some("function help(){\nreturn true;\n}")); let mut writer = Cursor::new(Vec::new());
write_file_block(&mut writer, &file_info, &opts)?;
let output = String::from_utf8(writer.into_inner())?;
let expected = "## File: `helper.js`\n```js\n 1 | function help(){\n 2 | return true;\n 3 | }\n```\n";
assert_eq!(output, expected);
Ok(())
}
#[test]
fn test_write_file_block_no_content() -> Result<()> {
let opts = create_test_opts(false, false, false);
let file_info = create_file_info("no_content.txt", None); let mut writer = Cursor::new(Vec::new());
write_file_block(&mut writer, &file_info, &opts)?;
let output = String::from_utf8(writer.into_inner())?;
let expected = "## File: no_content.txt\n```txt\n// Content not available\n```\n";
assert_eq!(output, expected);
Ok(())
}
}