scheme-edit 0.1.0

Lossless Scheme S-expression parser and editor (toml_edit-style CST)
Documentation
use crate::cst::{Item, ListKind, Node};

const MAX_WIDTH: usize = 78;

/// Emacs-style lead counts: the first N arguments of a form are plain
/// arguments (rendered flat), everything after is body and breaks to
/// its own line.
fn leading_args(head: &str) -> usize {
    match head {
        "make-channel-introduction" => 1,
        _ => 0,
    }
}

fn delims(kind: &ListKind) -> (&'static str, &'static str) {
    match kind {
        ListKind::Paren => ("(", ")"),
        ListKind::Bracket => ("[", "]"),
        ListKind::Vector => ("#(", ")"),
    }
}

/// The prefix token itself, with any trivia folded in by the parser removed.
fn prefix_core(prefix: &str) -> &str {
    for cand in [
        "#,@", "#$@", ",@", "#'", "#`", "#,", "#~", "#$", "#+", "'", "`", ",",
    ] {
        if prefix.starts_with(cand) {
            return cand;
        }
    }
    prefix
}

fn has_comment(node: &Node) -> bool {
    match node {
        Node::List { items, .. } => items.iter().any(|i| match i {
            Item::LineComment(_) | Item::BlockComment(_) | Item::DatumComment(_) => true,
            Item::Node(n) => has_comment(n),
            Item::Ws(_) => false,
        }),
        Node::Prefixed { inner, .. } => has_comment(inner),
        _ => false,
    }
}

/// Normalized single-line rendering: single spaces, trivia dropped.
fn flat_source(node: &Node) -> String {
    match node {
        Node::Atom(s) | Node::Str(s) => s.clone(),
        Node::Prefixed { prefix, inner } => {
            format!("{}{}", prefix_core(prefix), flat_source(inner))
        }
        Node::List { kind, items } => {
            let parts: Vec<String> = items
                .iter()
                .filter_map(|i| match i {
                    Item::Node(n) => Some(flat_source(n)),
                    _ => None,
                })
                .collect();
            let (open, close) = delims(kind);
            format!("{open}{}{close}", parts.join(" "))
        }
    }
}

enum El<'a> {
    Data(&'a Node),
    Comment(&'a str),
}

fn break_list(kind: &ListKind, items: &[Item], indent: usize) -> String {
    let (open, close) = delims(kind);
    let els: Vec<El> = items
        .iter()
        .filter_map(|i| match i {
            Item::Node(n) => Some(El::Data(n)),
            Item::LineComment(s) | Item::BlockComment(s) | Item::DatumComment(s) => {
                Some(El::Comment(s))
            }
            Item::Ws(_) => None,
        })
        .collect();

    let mut out = String::from(open);
    let mut rest = els.as_slice();
    let mut lead = 0;
    if let Some(El::Data(head)) = els.first() {
        out.push_str(&pretty(head, indent + open.len()));
        if let Node::Atom(a) = head {
            lead = leading_args(a);
        }
        rest = &els[1..];
    }

    let child_indent = indent + 2;
    let pad = format!("\n{}", " ".repeat(child_indent));
    let mut args_seen = 0usize;
    for el in rest {
        out.push_str(&pad);
        match el {
            El::Comment(s) => out.push_str(s),
            El::Data(n) => {
                args_seen += 1;
                if args_seen <= lead {
                    out.push_str(&flat_source(n));
                } else {
                    out.push_str(&pretty(n, child_indent));
                }
            }
        }
    }
    out.push_str(close);
    out
}

/// Render NODE in guix house style starting at column INDENT.
pub fn pretty(node: &Node, indent: usize) -> String {
    let flat = flat_source(node);
    if !has_comment(node) && indent + flat.len() <= MAX_WIDTH {
        return flat;
    }
    match node {
        Node::List { kind, items } => break_list(kind, items, indent),
        Node::Prefixed { prefix, inner } => {
            let p = prefix_core(prefix);
            format!("{p}{}", pretty(inner, indent + p.len()))
        }
        // Atoms and strings never split.
        _ => flat,
    }
}

impl Node {
    pub fn to_pretty(&self, indent: usize) -> String {
        pretty(self, indent)
    }
}