852 lines
27 KiB
Rust
852 lines
27 KiB
Rust
use std::{error::Error, fmt};
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
use tree_sitter::{Node, Parser, TreeCursor};
|
|
use wasm_bindgen::prelude::*;
|
|
|
|
const INDENT: usize = 4;
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct FormatError {
|
|
message: String,
|
|
}
|
|
|
|
impl FormatError {
|
|
fn new(message: impl Into<String>) -> Self {
|
|
Self {
|
|
message: message.into(),
|
|
}
|
|
}
|
|
|
|
pub fn message(&self) -> &str {
|
|
&self.message
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for FormatError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.write_str(&self.message)
|
|
}
|
|
}
|
|
|
|
impl Error for FormatError {}
|
|
|
|
pub fn format_source(source: &str) -> Result<String, FormatError> {
|
|
decodal::parse_source(source)
|
|
.map_err(|diagnostic| FormatError::new(format!("parse error: {}", diagnostic.message)))?;
|
|
format_source_impl(source)
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
fn format_source_impl(source: &str) -> Result<String, FormatError> {
|
|
let mut parser = Parser::new();
|
|
parser
|
|
.set_language(&tree_sitter_decodal::language())
|
|
.map_err(|error| {
|
|
FormatError::new(format!("failed to load tree-sitter grammar: {error}"))
|
|
})?;
|
|
let tree = parser
|
|
.parse(source, None)
|
|
.ok_or_else(|| FormatError::new("tree-sitter failed to parse source"))?;
|
|
let root = tree.root_node();
|
|
if root.has_error() {
|
|
return Err(FormatError::new(
|
|
"parse error: source contains tree-sitter errors",
|
|
));
|
|
}
|
|
|
|
let mut formatter = Formatter::new(source);
|
|
let mut out = formatter.format_source_file(root);
|
|
if !out.ends_with('\n') {
|
|
out.push('\n');
|
|
}
|
|
Ok(out)
|
|
}
|
|
|
|
#[wasm_bindgen(js_name = formatSource)]
|
|
pub fn format_source_json(source: &str) -> String {
|
|
match format_source(source) {
|
|
Ok(formatted) => serde_json::json!({ "ok": true, "source": formatted }).to_string(),
|
|
Err(error) => serde_json::json!({ "ok": false, "error": error.message() }).to_string(),
|
|
}
|
|
}
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
fn format_source_impl(source: &str) -> Result<String, FormatError> {
|
|
Ok(LexicalFormatter::new(source).format())
|
|
}
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
struct LexicalFormatter<'a> {
|
|
chars: core::iter::Peekable<core::str::Chars<'a>>,
|
|
out: String,
|
|
indent: usize,
|
|
line_start: bool,
|
|
}
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
impl<'a> LexicalFormatter<'a> {
|
|
fn new(source: &'a str) -> Self {
|
|
Self {
|
|
chars: source.chars().peekable(),
|
|
out: String::new(),
|
|
indent: 0,
|
|
line_start: true,
|
|
}
|
|
}
|
|
|
|
fn format(mut self) -> String {
|
|
while let Some(ch) = self.chars.next() {
|
|
match ch {
|
|
' ' | '\t' | '\r' | '\n' => self.consume_whitespace(ch),
|
|
'#' => self.write_comment(),
|
|
'"' => self.write_string(),
|
|
'{' => self.open_block('{'),
|
|
'[' => self.open_block('['),
|
|
'}' => self.close_block('}'),
|
|
']' => self.close_block(']'),
|
|
';' => self.end_statement(),
|
|
',' => self.write_comma(),
|
|
'.' => self.write_compact('.'),
|
|
':' => self.write_spaced_operator(":"),
|
|
'(' => self.write_compact('('),
|
|
')' => self.write_compact(')'),
|
|
'+' | '-' | '*' | '/' | '=' | '!' | '&' | '|' | '>' | '<' => {
|
|
self.write_operator(ch)
|
|
}
|
|
_ => self.write_word(ch),
|
|
}
|
|
}
|
|
self.trim_trailing_spaces();
|
|
if !self.out.ends_with('\n') {
|
|
self.out.push('\n');
|
|
}
|
|
self.out
|
|
}
|
|
|
|
fn consume_whitespace(&mut self, ch: char) {
|
|
if ch == '\n' && self.out.ends_with('\n') && !self.out.ends_with("\n\n") {
|
|
self.out.push('\n');
|
|
self.line_start = true;
|
|
}
|
|
}
|
|
|
|
fn write_comment(&mut self) {
|
|
if self.line_start {
|
|
self.write_indent();
|
|
} else {
|
|
self.ensure_space();
|
|
}
|
|
self.out.push('#');
|
|
for ch in self.chars.by_ref() {
|
|
if ch == '\n' {
|
|
break;
|
|
}
|
|
self.out.push(ch);
|
|
}
|
|
self.trim_trailing_spaces();
|
|
self.out.push('\n');
|
|
self.line_start = true;
|
|
}
|
|
|
|
fn write_string(&mut self) {
|
|
self.write_indent_if_needed();
|
|
self.ensure_word_boundary();
|
|
self.out.push('"');
|
|
let mut escaped = false;
|
|
for ch in self.chars.by_ref() {
|
|
self.out.push(ch);
|
|
if escaped {
|
|
escaped = false;
|
|
} else if ch == '\\' {
|
|
escaped = true;
|
|
} else if ch == '"' {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn open_block(&mut self, ch: char) {
|
|
self.trim_trailing_spaces();
|
|
self.out.push(' ');
|
|
self.out.push(ch);
|
|
self.out.push('\n');
|
|
self.indent += INDENT;
|
|
self.line_start = true;
|
|
}
|
|
|
|
fn close_block(&mut self, ch: char) {
|
|
self.trim_trailing_spaces();
|
|
if !self.out.ends_with('\n') {
|
|
self.out.push('\n');
|
|
}
|
|
self.indent = self.indent.saturating_sub(INDENT);
|
|
self.write_indent();
|
|
self.out.push(ch);
|
|
self.line_start = false;
|
|
}
|
|
|
|
fn end_statement(&mut self) {
|
|
self.trim_trailing_spaces();
|
|
self.out.push(';');
|
|
self.out.push('\n');
|
|
self.line_start = true;
|
|
}
|
|
|
|
fn write_comma(&mut self) {
|
|
self.trim_trailing_spaces();
|
|
self.out.push(',');
|
|
self.out.push(' ');
|
|
self.line_start = false;
|
|
}
|
|
|
|
fn write_operator(&mut self, ch: char) {
|
|
let mut op = String::new();
|
|
op.push(ch);
|
|
if let Some(next) = self.chars.peek().copied() {
|
|
let two_char = matches!(
|
|
(ch, next),
|
|
('=', '=')
|
|
| ('!', '=')
|
|
| ('>', '=')
|
|
| ('<', '=')
|
|
| ('+', '+')
|
|
| ('/', '/')
|
|
| ('&', '&')
|
|
| ('|', '|')
|
|
| ('=', '>')
|
|
);
|
|
if two_char {
|
|
op.push(next);
|
|
self.chars.next();
|
|
}
|
|
}
|
|
self.write_spaced_operator(&op);
|
|
}
|
|
|
|
fn write_spaced_operator(&mut self, op: &str) {
|
|
self.trim_trailing_spaces();
|
|
self.ensure_space();
|
|
self.out.push_str(op);
|
|
self.out.push(' ');
|
|
self.line_start = false;
|
|
}
|
|
|
|
fn write_compact(&mut self, ch: char) {
|
|
self.trim_trailing_spaces();
|
|
self.write_indent_if_needed();
|
|
self.out.push(ch);
|
|
self.line_start = false;
|
|
}
|
|
|
|
fn write_word(&mut self, first: char) {
|
|
self.write_indent_if_needed();
|
|
self.ensure_word_boundary();
|
|
self.out.push(first);
|
|
while let Some(next) = self.chars.peek().copied() {
|
|
if next.is_alphanumeric() || next == '_' {
|
|
self.out.push(next);
|
|
self.chars.next();
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
self.line_start = false;
|
|
}
|
|
|
|
fn write_indent_if_needed(&mut self) {
|
|
if self.line_start {
|
|
self.write_indent();
|
|
self.line_start = false;
|
|
}
|
|
}
|
|
|
|
fn write_indent(&mut self) {
|
|
for _ in 0..self.indent {
|
|
self.out.push(' ');
|
|
}
|
|
}
|
|
|
|
fn ensure_word_boundary(&mut self) {
|
|
let needs_space = self
|
|
.out
|
|
.chars()
|
|
.last()
|
|
.is_some_and(|ch| ch.is_alphanumeric() || matches!(ch, '_' | '"'));
|
|
if needs_space {
|
|
self.out.push(' ');
|
|
}
|
|
}
|
|
|
|
fn ensure_space(&mut self) {
|
|
let needs_space = self
|
|
.out
|
|
.chars()
|
|
.last()
|
|
.is_some_and(|ch| !matches!(ch, ' ' | '\n' | '.' | '(' | '['));
|
|
if needs_space {
|
|
self.out.push(' ');
|
|
}
|
|
}
|
|
|
|
fn trim_trailing_spaces(&mut self) {
|
|
while self.out.ends_with(' ') || self.out.ends_with('\t') {
|
|
self.out.pop();
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
struct Formatter<'a> {
|
|
source: &'a str,
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
impl<'a> Formatter<'a> {
|
|
fn new(source: &'a str) -> Self {
|
|
Self { source }
|
|
}
|
|
|
|
fn format_source_file(&mut self, node: Node<'a>) -> String {
|
|
let mut out = String::new();
|
|
let children = named_children(node);
|
|
self.write_statement_list(&mut out, &children, 0);
|
|
out
|
|
}
|
|
|
|
fn write_statement_list(&mut self, out: &mut String, children: &[Node<'a>], indent: usize) {
|
|
let mut pending_comments = Vec::new();
|
|
let mut pending_blank = false;
|
|
let mut previous_end = 0;
|
|
let mut previous_statement_end_row = None;
|
|
|
|
for &child in children {
|
|
let is_trailing_comment = child.kind() == "comment"
|
|
&& previous_statement_end_row == Some(child.start_position().row);
|
|
if previous_statement_end_row.is_some() && !is_trailing_comment {
|
|
if !out.ends_with('\n') {
|
|
out.push('\n');
|
|
}
|
|
previous_statement_end_row = None;
|
|
}
|
|
|
|
if previous_end > 0 && has_blank_line(self.slice(previous_end, child.start_byte())) {
|
|
pending_blank = true;
|
|
}
|
|
|
|
if child.kind() == "comment" {
|
|
if is_trailing_comment {
|
|
if !out.ends_with(' ') {
|
|
out.push(' ');
|
|
}
|
|
out.push_str(self.raw_trimmed(child));
|
|
out.push('\n');
|
|
previous_statement_end_row = None;
|
|
} else {
|
|
pending_comments.push(child);
|
|
}
|
|
previous_end = child.end_byte();
|
|
continue;
|
|
}
|
|
|
|
if pending_blank && !out.is_empty() && !out.ends_with("\n\n") {
|
|
out.push('\n');
|
|
}
|
|
pending_blank = false;
|
|
|
|
for comment in pending_comments.drain(..) {
|
|
write_indent(out, indent);
|
|
out.push_str(self.raw_trimmed(comment));
|
|
out.push('\n');
|
|
}
|
|
|
|
write_indent(out, indent);
|
|
self.write_statement(out, child, indent);
|
|
previous_statement_end_row = Some(child.end_position().row);
|
|
previous_end = child.end_byte();
|
|
}
|
|
|
|
if previous_statement_end_row.is_some() && !out.ends_with('\n') {
|
|
out.push('\n');
|
|
}
|
|
|
|
if pending_blank && !out.is_empty() && !out.ends_with("\n\n") {
|
|
out.push('\n');
|
|
}
|
|
for comment in pending_comments {
|
|
write_indent(out, indent);
|
|
out.push_str(self.raw_trimmed(comment));
|
|
out.push('\n');
|
|
}
|
|
}
|
|
|
|
fn write_statement(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
|
|
match node.kind() {
|
|
"field_definition" => {
|
|
self.write_field_definition(out, node, indent);
|
|
out.push(';');
|
|
}
|
|
_ => {
|
|
self.write_expr(out, node, indent, 0);
|
|
out.push(';');
|
|
}
|
|
}
|
|
}
|
|
|
|
fn write_field_definition(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
|
|
if let Some(path) = node.child_by_field_name("path") {
|
|
self.write_field_path(out, path);
|
|
} else {
|
|
out.push_str(self.raw_trimmed(node));
|
|
return;
|
|
}
|
|
out.push_str(" = ");
|
|
if let Some(value) = node.child_by_field_name("value") {
|
|
self.write_expr(out, value, indent, 0);
|
|
}
|
|
}
|
|
|
|
fn write_field_path(&mut self, out: &mut String, node: Node<'a>) {
|
|
let identifiers: Vec<_> = named_children(node)
|
|
.into_iter()
|
|
.filter(|child| child.kind() == "identifier")
|
|
.collect();
|
|
if identifiers.is_empty() {
|
|
out.push_str(self.raw_trimmed(node));
|
|
return;
|
|
}
|
|
for (index, identifier) in identifiers.into_iter().enumerate() {
|
|
if index > 0 {
|
|
out.push('.');
|
|
}
|
|
out.push_str(self.raw_trimmed(identifier));
|
|
}
|
|
}
|
|
|
|
fn write_expr(&mut self, out: &mut String, node: Node<'a>, indent: usize, parent_prec: u8) {
|
|
if expression_contains_comment(node)
|
|
&& !matches!(
|
|
node.kind(),
|
|
"object" | "array" | "let_expression" | "match_expression"
|
|
)
|
|
{
|
|
out.push_str(self.raw_trimmed(node));
|
|
return;
|
|
}
|
|
|
|
let prec = self.precedence(node);
|
|
let parenthesize = prec < parent_prec;
|
|
if parenthesize {
|
|
out.push('(');
|
|
}
|
|
|
|
match node.kind() {
|
|
"literal" => {
|
|
if let Some(child) = named_children(node).first().copied() {
|
|
out.push_str(self.raw_trimmed(child));
|
|
} else {
|
|
out.push_str(self.raw_trimmed(node));
|
|
}
|
|
}
|
|
"identifier" | "string" | "integer" | "float" | "boolean" | "regex_literal" => {
|
|
out.push_str(self.raw_trimmed(node));
|
|
}
|
|
"comparison_constraint" => self.write_comparison_constraint(out, node, indent),
|
|
"object" => self.write_object(out, node, indent),
|
|
"array" => self.write_array(out, node, indent),
|
|
"let_expression" => self.write_let(out, node, indent),
|
|
"function_expression" => self.write_function(out, node, indent),
|
|
"match_expression" => self.write_match(out, node, indent),
|
|
"import_expression" => self.write_import(out, node),
|
|
"parenthesized_expression" => {
|
|
out.push('(');
|
|
if let Some(expr) = named_children(node)
|
|
.into_iter()
|
|
.find(|child| child.kind() != "comment")
|
|
{
|
|
self.write_expr(out, expr, indent, 0);
|
|
}
|
|
out.push(')');
|
|
}
|
|
"call_expression" => self.write_call(out, node, indent),
|
|
"path_expression" => self.write_path(out, node, indent),
|
|
"unary_expression" => self.write_unary(out, node, indent),
|
|
"binary_expression" | "default_expression" => {
|
|
self.write_binary_like(out, node, indent, prec)
|
|
}
|
|
_ => out.push_str(self.raw_trimmed(node)),
|
|
}
|
|
|
|
if parenthesize {
|
|
out.push(')');
|
|
}
|
|
}
|
|
|
|
fn write_object(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
|
|
let children = named_children(node);
|
|
if children.is_empty() {
|
|
out.push_str("{}");
|
|
return;
|
|
}
|
|
out.push('{');
|
|
out.push('\n');
|
|
self.write_statement_list(out, &children, indent + INDENT);
|
|
write_indent(out, indent);
|
|
out.push('}');
|
|
}
|
|
|
|
fn write_array(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
|
|
let children = named_children(node);
|
|
if children.is_empty() {
|
|
out.push_str("[]");
|
|
return;
|
|
}
|
|
let has_comment = children.iter().any(|child| child.kind() == "comment");
|
|
let inline = !has_comment && children.iter().all(|child| is_inline_expr(*child));
|
|
if inline {
|
|
out.push('[');
|
|
for (index, child) in children.into_iter().enumerate() {
|
|
if index > 0 {
|
|
out.push_str(", ");
|
|
}
|
|
self.write_expr(out, child, indent, 0);
|
|
}
|
|
out.push(']');
|
|
return;
|
|
}
|
|
|
|
out.push('[');
|
|
out.push('\n');
|
|
let mut pending_comments = Vec::new();
|
|
for child in children {
|
|
if child.kind() == "comment" {
|
|
pending_comments.push(child);
|
|
continue;
|
|
}
|
|
for comment in pending_comments.drain(..) {
|
|
write_indent(out, indent + INDENT);
|
|
out.push_str(self.raw_trimmed(comment));
|
|
out.push('\n');
|
|
}
|
|
write_indent(out, indent + INDENT);
|
|
self.write_expr(out, child, indent + INDENT, 0);
|
|
out.push_str(",\n");
|
|
}
|
|
for comment in pending_comments {
|
|
write_indent(out, indent + INDENT);
|
|
out.push_str(self.raw_trimmed(comment));
|
|
out.push('\n');
|
|
}
|
|
write_indent(out, indent);
|
|
out.push(']');
|
|
}
|
|
|
|
fn write_let(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
|
|
let body = node.child_by_field_name("body");
|
|
out.push_str("let");
|
|
out.push('\n');
|
|
let children: Vec<_> = named_children(node)
|
|
.into_iter()
|
|
.filter(|child| Some(*child) != body)
|
|
.collect();
|
|
self.write_statement_list(out, &children, indent + INDENT);
|
|
write_indent(out, indent);
|
|
out.push_str("in");
|
|
if let Some(body) = body {
|
|
out.push('\n');
|
|
write_indent(out, indent + INDENT);
|
|
self.write_expr(out, body, indent + INDENT, 0);
|
|
}
|
|
}
|
|
|
|
fn write_function(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
|
|
let body = node.child_by_field_name("body");
|
|
let params: Vec<_> = named_children(node)
|
|
.into_iter()
|
|
.filter(|child| child.kind() == "parameter")
|
|
.collect();
|
|
out.push('(');
|
|
for (index, param) in params.into_iter().enumerate() {
|
|
if index > 0 {
|
|
out.push_str(", ");
|
|
}
|
|
self.write_parameter(out, param, indent);
|
|
}
|
|
out.push_str(") =>");
|
|
if let Some(body) = body {
|
|
if is_inline_expr(body) {
|
|
out.push(' ');
|
|
self.write_expr(out, body, indent, 0);
|
|
} else {
|
|
out.push('\n');
|
|
write_indent(out, indent + INDENT);
|
|
self.write_expr(out, body, indent + INDENT, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn write_parameter(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
|
|
if let Some(name) = node.child_by_field_name("name") {
|
|
out.push_str(self.raw_trimmed(name));
|
|
}
|
|
if let Some(constraint) = node.child_by_field_name("constraint") {
|
|
out.push_str(": ");
|
|
self.write_expr(out, constraint, indent, 0);
|
|
}
|
|
}
|
|
|
|
fn write_match(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
|
|
out.push_str("match ");
|
|
if let Some(scrutinee) = node.child_by_field_name("scrutinee") {
|
|
self.write_expr(out, scrutinee, indent, 0);
|
|
}
|
|
out.push_str(" {");
|
|
let children: Vec<_> = named_children(node)
|
|
.into_iter()
|
|
.filter(|child| child.kind() == "match_arm" || child.kind() == "comment")
|
|
.collect();
|
|
if !children.is_empty() {
|
|
out.push('\n');
|
|
self.write_match_arms(out, &children, indent + INDENT);
|
|
write_indent(out, indent);
|
|
}
|
|
out.push('}');
|
|
}
|
|
|
|
fn write_match_arms(&mut self, out: &mut String, children: &[Node<'a>], indent: usize) {
|
|
let mut pending_comments = Vec::new();
|
|
for &child in children {
|
|
if child.kind() == "comment" {
|
|
pending_comments.push(child);
|
|
continue;
|
|
}
|
|
for comment in pending_comments.drain(..) {
|
|
write_indent(out, indent);
|
|
out.push_str(self.raw_trimmed(comment));
|
|
out.push('\n');
|
|
}
|
|
write_indent(out, indent);
|
|
if let Some(pattern) = child.child_by_field_name("pattern") {
|
|
self.write_expr(out, pattern, indent, 0);
|
|
}
|
|
out.push_str(": ");
|
|
if let Some(body) = child.child_by_field_name("body") {
|
|
self.write_expr(out, body, indent, 0);
|
|
}
|
|
out.push_str(";\n");
|
|
}
|
|
for comment in pending_comments {
|
|
write_indent(out, indent);
|
|
out.push_str(self.raw_trimmed(comment));
|
|
out.push('\n');
|
|
}
|
|
}
|
|
|
|
fn write_import(&mut self, out: &mut String, node: Node<'a>) {
|
|
out.push_str("import ");
|
|
if let Some(specifier) = node.child_by_field_name("specifier") {
|
|
out.push_str(self.raw_trimmed(specifier));
|
|
}
|
|
}
|
|
|
|
fn write_call(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
|
|
let callee = node.child_by_field_name("function");
|
|
if let Some(callee) = callee {
|
|
self.write_expr(out, callee, indent, self.precedence(node));
|
|
}
|
|
out.push('(');
|
|
let args: Vec<_> = named_children(node)
|
|
.into_iter()
|
|
.filter(|child| Some(*child) != callee && child.kind() != "comment")
|
|
.collect();
|
|
for (index, arg) in args.into_iter().enumerate() {
|
|
if index > 0 {
|
|
out.push_str(", ");
|
|
}
|
|
self.write_expr(out, arg, indent, 0);
|
|
}
|
|
out.push(')');
|
|
}
|
|
|
|
fn write_path(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
|
|
if let Some(object) = node.child_by_field_name("object") {
|
|
self.write_expr(out, object, indent, self.precedence(node));
|
|
}
|
|
out.push('.');
|
|
if let Some(field) = node.child_by_field_name("field") {
|
|
out.push_str(self.raw_trimmed(field));
|
|
}
|
|
}
|
|
|
|
fn write_unary(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
|
|
if let Some(operand) = node.child_by_field_name("operand") {
|
|
let operator = self.slice(node.start_byte(), operand.start_byte()).trim();
|
|
out.push_str(operator);
|
|
self.write_expr(out, operand, indent, self.precedence(node));
|
|
} else {
|
|
out.push_str(self.raw_trimmed(node));
|
|
}
|
|
}
|
|
|
|
fn write_comparison_constraint(&mut self, out: &mut String, node: Node<'a>, indent: usize) {
|
|
if let Some(value) = node.child_by_field_name("value") {
|
|
let operator = self.slice(node.start_byte(), value.start_byte()).trim();
|
|
out.push_str(operator);
|
|
out.push(' ');
|
|
self.write_expr(out, value, indent, self.precedence(node));
|
|
} else {
|
|
out.push_str(self.raw_trimmed(node));
|
|
}
|
|
}
|
|
|
|
fn write_binary_like(&mut self, out: &mut String, node: Node<'a>, indent: usize, prec: u8) {
|
|
let left = node
|
|
.child_by_field_name("left")
|
|
.or_else(|| node.child_by_field_name("base"));
|
|
let right = node
|
|
.child_by_field_name("right")
|
|
.or_else(|| node.child_by_field_name("fallback"));
|
|
let (Some(left), Some(right)) = (left, right) else {
|
|
out.push_str(self.raw_trimmed(node));
|
|
return;
|
|
};
|
|
self.write_expr(out, left, indent, prec);
|
|
out.push(' ');
|
|
if node.kind() == "default_expression" {
|
|
out.push_str("default");
|
|
} else {
|
|
out.push_str(self.slice(left.end_byte(), right.start_byte()).trim());
|
|
}
|
|
out.push(' ');
|
|
self.write_expr(out, right, indent, prec + 1);
|
|
}
|
|
|
|
fn precedence(&self, node: Node<'a>) -> u8 {
|
|
match node.kind() {
|
|
"default_expression" => 1,
|
|
"binary_expression" => self.binary_precedence(node),
|
|
"unary_expression" | "comparison_constraint" => 17,
|
|
"call_expression" | "path_expression" => 19,
|
|
_ => 20,
|
|
}
|
|
}
|
|
|
|
fn binary_precedence(&self, node: Node<'a>) -> u8 {
|
|
let Some(left) = node.child_by_field_name("left") else {
|
|
return 10;
|
|
};
|
|
let Some(right) = node.child_by_field_name("right") else {
|
|
return 10;
|
|
};
|
|
match self.slice(left.end_byte(), right.start_byte()).trim() {
|
|
"//" => 3,
|
|
"&" => 5,
|
|
"||" => 7,
|
|
"&&" => 9,
|
|
"==" | "!=" | ">" | ">=" | "<" | "<=" => 11,
|
|
"++" => 12,
|
|
"+" | "-" => 13,
|
|
"*" | "/" => 15,
|
|
_ => 10,
|
|
}
|
|
}
|
|
|
|
fn raw_trimmed(&self, node: Node<'a>) -> &'a str {
|
|
self.slice(node.start_byte(), node.end_byte()).trim()
|
|
}
|
|
|
|
fn slice(&self, start: usize, end: usize) -> &'a str {
|
|
&self.source[start..end]
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
fn named_children<'a>(node: Node<'a>) -> Vec<Node<'a>> {
|
|
let mut cursor: TreeCursor<'a> = node.walk();
|
|
node.named_children(&mut cursor).collect()
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
fn expression_contains_comment(node: Node<'_>) -> bool {
|
|
if node.kind() == "comment" {
|
|
return true;
|
|
}
|
|
named_children(node)
|
|
.into_iter()
|
|
.any(expression_contains_comment)
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
fn is_inline_expr(node: Node<'_>) -> bool {
|
|
match node.kind() {
|
|
"literal"
|
|
| "identifier"
|
|
| "string"
|
|
| "integer"
|
|
| "float"
|
|
| "boolean"
|
|
| "regex_literal"
|
|
| "comparison_constraint"
|
|
| "import_expression" => true,
|
|
"path_expression"
|
|
| "call_expression"
|
|
| "unary_expression"
|
|
| "binary_expression"
|
|
| "default_expression"
|
|
| "parenthesized_expression" => {
|
|
!expression_contains_comment(node)
|
|
&& named_children(node).into_iter().all(is_inline_expr)
|
|
}
|
|
"array" => {
|
|
!expression_contains_comment(node)
|
|
&& named_children(node).into_iter().all(is_inline_expr)
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
fn has_blank_line(text: &str) -> bool {
|
|
text.bytes().filter(|byte| *byte == b'\n').count() >= 2
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
fn write_indent(out: &mut String, indent: usize) {
|
|
for _ in 0..indent {
|
|
out.push(' ');
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn formats_fields_and_preserves_comments() {
|
|
let source =
|
|
"Server={\n# host\nhost=String default \"localhost\"; # trailing\n\nport=Int&>0;\n};";
|
|
let formatted = format_source(source).unwrap();
|
|
assert_eq!(
|
|
formatted,
|
|
"Server = {\n # host\n host = String default \"localhost\"; # trailing\n\n port = Int & > 0;\n};\n"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn formats_arrays_and_match_comments() {
|
|
let source = "result=match x{# c\n_: [1,{a=2;}];};";
|
|
let formatted = format_source(source).unwrap();
|
|
assert_eq!(
|
|
formatted,
|
|
"result = match x {\n # c\n _: [\n 1,\n {\n a = 2;\n },\n ];\n};\n"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn wasm_export_returns_json() {
|
|
let output = format_source_json("value={a=1;};");
|
|
assert!(output.contains("\"ok\":true"));
|
|
assert!(output.contains("value = {\\n a = 1;\\n};\\n"));
|
|
}
|
|
}
|