use crate::cli::FmtArgs;
use crate::document::{Document, must_still_parse};
use crate::ledger::{Ledger, load, resolve_path};
use crate::schema::{ArchiveOrder, Section};
use anyhow::{Context, Result, bail};
use std::fs;
pub fn normalized(source: &str, ledger: &Ledger) -> Result<String> {
let style = &ledger.style;
let mut document = Document::new(source.to_owned());
if style.archive_order == ArchiveOrder::NewestFirst {
document.reorder_rows("archive", &newest_first(ledger))?;
}
for section in Section::ALL {
document.set_indent(section.key(), style.indent.into())?;
}
let order: Vec<&str> = style.section_order.iter().map(Section::key).collect();
document.reorder_sections(&order)?;
let source = tidied(&document.into_source());
must_still_parse(&source)?;
Ok(source)
}
fn newest_first(ledger: &Ledger) -> Vec<String> {
let mut rows: Vec<&crate::schema::ArchivedTask> = ledger.archive.iter().collect();
rows.sort_by(|left, right| right.completed.cmp(&left.completed));
rows.into_iter().map(|row| row.id.clone()).collect()
}
fn tidied(source: &str) -> String {
let mut out = String::with_capacity(source.len());
let mut blank_run = 0;
let mut after_key = false;
for line in source.lines() {
let line = line.trim_end();
if line.is_empty() {
blank_run += 1;
if blank_run > 1 || after_key {
continue;
}
out.push('\n');
continue;
}
blank_run = 0;
after_key = line.ends_with(':') && !line.starts_with(char::is_whitespace);
out.push_str(line);
out.push('\n');
}
while out.ends_with("\n\n") {
out.pop();
}
out
}
pub fn run(args: &FmtArgs) -> Result<()> {
let path = resolve_path(&args.ledger);
let ledger = load(&path)?;
let source = fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?;
let wanted = normalized(&source, &ledger)?;
if source == wanted {
println!("ok {}", path.display());
return Ok(());
}
if args.check {
for line in changes(&source, &wanted) {
eprintln!("qctl: {line}");
}
bail!("{} is not in its declared style", path.display());
}
fs::write(&path, wanted).with_context(|| format!("write {}", path.display()))?;
println!("wrote {}", path.display());
Ok(())
}
fn changes(source: &str, wanted: &str) -> Vec<String> {
let (from, to): (Vec<&str>, Vec<&str>) = (source.lines().collect(), wanted.lines().collect());
if from.len() != to.len() {
return vec![format!("{} lines, would be {}", from.len(), to.len())];
}
from.iter()
.zip(&to)
.enumerate()
.filter(|(_, (before, after))| before != after)
.map(|(at, (before, after))| format!("line {}: {:?} would be {:?}", at + 1, before, after))
.collect()
}