Add language tools package
This commit is contained in:
@@ -0,0 +1,851 @@
|
||||
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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user