cargo-cheers 0.1.0-alpha.1

Cargo subcommand for Cheers development tooling.
use std::ops::Range;

use anyhow::{Context, Result};
use ast::{DatastarSourceNodes, Document, ScriptSourceNodes};
use crop::Rope;
use syn::{
    parse::{Parse, ParseStream, Parser},
    spanned::Spanned,
};

use crate::{
    collect::MaudMacro,
    print::{print, print_datastar_source, print_js_script},
};

pub struct FormatOptions {
    pub line_length: usize,
    pub macro_names: Vec<String>,
}

impl Default for FormatOptions {
    fn default() -> Self {
        FormatOptions {
            line_length: 100,
            macro_names: vec![
                String::from("html"),
                String::from("svg"),
                String::from("datastar_source"),
                String::from("js_script"),
            ],
        }
    }
}

#[derive(Debug)]
struct TextEdit {
    range: Range<usize>,
    new_text: String,
}

pub fn format_source(
    source: &mut Rope,
    macros: Vec<MaudMacro<'_>>,
    options: &FormatOptions,
) -> String {
    let mut edits = Vec::new();

    for maud_mac in macros {
        let mac = maud_mac.macro_;
        let start = mac.path.span().start();
        let end = mac.delimiter.span().close().end();
        let start_byte = line_column_to_byte(source, start);
        let end_byte = line_column_to_byte(source, end);

        match format_macro(&maud_mac, source, options) {
            Ok(new_text) => edits.push(TextEdit {
                range: start_byte..end_byte,
                new_text,
            }),
            Err(e) => eprintln!("{e}"),
        }
    }

    let mut last_offset: isize = 0;
    for edit in edits {
        let start = edit.range.start;
        let end = edit.range.end;
        let new_text = edit.new_text;

        source.replace(
            (start as isize + last_offset) as usize..(end as isize + last_offset) as usize,
            &new_text,
        );
        last_offset += new_text.len() as isize - (end as isize - start as isize);
    }

    source.to_string()
}

fn format_macro(mac: &MaudMacro, source: &Rope, options: &FormatOptions) -> Result<String> {
    if mac.macro_name == "datastar_source" {
        let document: DatastarSourceNodes = Parser::parse2(
            |input: ParseStream| DatastarSourceNodes::parse(input),
            mac.macro_.tokens.clone(),
        )
        .context("Failed to parse datastar_source macro")?;

        return Ok(print_datastar_source(document, mac, source, options));
    }

    if mac.macro_name == "js_script" {
        let document: ScriptSourceNodes = Parser::parse2(
            |input: ParseStream| ScriptSourceNodes::parse(input),
            mac.macro_.tokens.clone(),
        )
        .context("Failed to parse js_script macro")?;

        return Ok(print_js_script(document, mac, source, options));
    }

    let document: Document = Parser::parse2(
        |input: ParseStream| Document::parse(input),
        mac.macro_.tokens.clone(),
    )
    .context("Failed to parse maud macro")?;

    Ok(print(document, mac, source, options))
}

pub fn line_column_to_byte(source: &Rope, point: proc_macro2::LineColumn) -> usize {
    let line_byte = source.byte_of_line(point.line - 1);
    let line = source.line(point.line - 1);
    let char_byte: usize = line.chars().take(point.column).map(|c| c.len_utf8()).sum();
    line_byte + char_byte
}