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, apply_line_ending};
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);
let eol = style
.line_ending
.resolve_detected(node_source_is_crlf(root));
Ok(apply_line_ending(&print(&doc, style), eol))
}
fn node_source_is_crlf(root: &SyntaxNode) -> bool {
let text = root.text();
match text.find_char('\n') {
Some(offset) => {
let idx = u32::from(offset);
idx > 0 && text.char_at((idx - 1).into()) == Some('\r')
}
None => false,
}
}
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();
}
let eol = style
.line_ending
.resolve_detected(node_source_is_crlf(root));
let text = apply_line_ending(&text, eol);
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:?}");
}
}
#[test]
fn line_ending_auto_mirrors_source() {
use crate::formatter::LineEnding;
let style = FormatStyle {
line_ending: LineEnding::Auto,
..FormatStyle::default()
};
assert_eq!(
format_with_style("x=1\r\ny=2\r\n", style).unwrap(),
"x = 1\r\ny = 2\r\n",
);
assert_eq!(
format_with_style("x=1\ny=2\n", style).unwrap(),
"x = 1\ny = 2\n",
);
}
#[test]
fn line_ending_explicit_overrides_source() {
use crate::formatter::LineEnding;
let to_crlf = FormatStyle {
line_ending: LineEnding::Crlf,
..FormatStyle::default()
};
assert_eq!(
format_with_style("x=1\ny=2\n", to_crlf).unwrap(),
"x = 1\r\ny = 2\r\n",
);
let to_lf = FormatStyle {
line_ending: LineEnding::Lf,
..FormatStyle::default()
};
assert_eq!(
format_with_style("x=1\r\ny=2\r\n", to_lf).unwrap(),
"x = 1\ny = 2\n",
);
}
}