use rowan::TextRange;
use crate::formatter::ir::Ir;
use crate::formatter::printer::{print, print_at};
use crate::formatter::rules::{base_indent_level, lower, lower_body_range};
use crate::formatter::style::FormatStyle;
use crate::parser::parse;
use crate::syntax::{SyntaxKind, SyntaxNode};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FormatError {
Unsupported(String),
}
impl std::fmt::Display for FormatError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FormatError::Unsupported(what) => write!(f, "unsupported construct: {what}"),
}
}
}
impl std::error::Error for FormatError {}
pub fn format(input: &str) -> Result<String, FormatError> {
format_with_style(input, FormatStyle::default())
}
pub fn format_with_style(input: &str, style: FormatStyle) -> Result<String, FormatError> {
format_node(&parse(input).cst, style)
}
pub fn format_node(
root: &crate::syntax::SyntaxNode,
style: FormatStyle,
) -> Result<String, FormatError> {
let doc = lower(root);
Ok(print(&doc, style))
}
pub fn print_document(doc: &Ir, style: FormatStyle) -> String {
print(doc, style)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RangeFormatted {
pub range: TextRange,
pub text: String,
}
pub fn format_range(
root: &crate::syntax::SyntaxNode,
range: TextRange,
style: FormatStyle,
) -> Result<Option<RangeFormatted>, FormatError> {
let container = statement_container(root, range);
let Some((ir, span)) = lower_body_range(&container, range) else {
return Ok(None);
};
let indent = base_indent_level(&container) * style.indent_width as usize;
let mut text = print_at(&ir, style, indent);
while text.ends_with('\n') {
text.pop();
}
Ok(Some(RangeFormatted { range: span, text }))
}
fn statement_container(root: &SyntaxNode, range: TextRange) -> SyntaxNode {
let is_container = |kind: SyntaxKind| matches!(kind, SyntaxKind::ROOT | SyntaxKind::BLOCK);
let found = match root.covering_element(range) {
rowan::NodeOrToken::Node(node) => node.ancestors().find(|n| is_container(n.kind())),
rowan::NodeOrToken::Token(token) => {
token.parent_ancestors().find(|n| is_container(n.kind()))
}
};
found.unwrap_or_else(|| root.clone())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalizes_operator_spacing() {
for (input, expected) in [
("x=1\n", "x = 1\n"),
("y= a+b\n", "y = a + b\n"),
(
"function g(x)\n x ^ 2\nend\n",
"function g(x)\n x^2\nend\n",
),
("# comment\ny = a + b\n", "# comment\ny = a + b\n"),
] {
assert_eq!(format(input).unwrap(), expected);
}
}
#[test]
fn format_is_idempotent() {
for input in ["x=1\n", "z = a*b + c\n", "if a\n b\nelse\n c\nend\n"] {
let once = format(input).unwrap();
assert_eq!(format(&once).unwrap(), once, "not idempotent for {input:?}");
}
}
}