Skip to main content

kcl_lib/
unparser.rs

1use std::fmt::Write;
2
3use crate::KclError;
4use crate::ModuleId;
5use crate::parsing::DeprecationKind;
6use crate::parsing::PIPE_OPERATOR;
7use crate::parsing::ast::types::Annotation;
8use crate::parsing::ast::types::ArrayExpression;
9use crate::parsing::ast::types::ArrayRangeExpression;
10use crate::parsing::ast::types::AscribedExpression;
11use crate::parsing::ast::types::Associativity;
12use crate::parsing::ast::types::BinaryExpression;
13use crate::parsing::ast::types::BinaryOperator;
14use crate::parsing::ast::types::BinaryPart;
15use crate::parsing::ast::types::Block;
16use crate::parsing::ast::types::BodyItem;
17use crate::parsing::ast::types::CallExpressionKw;
18use crate::parsing::ast::types::CommentStyle;
19use crate::parsing::ast::types::DefaultParamVal;
20use crate::parsing::ast::types::EnumDeclaration;
21use crate::parsing::ast::types::Expr;
22use crate::parsing::ast::types::FormatOptions;
23use crate::parsing::ast::types::FunctionExpression;
24use crate::parsing::ast::types::Identifier;
25use crate::parsing::ast::types::IfExpression;
26use crate::parsing::ast::types::ImportSelector;
27use crate::parsing::ast::types::ImportStatement;
28use crate::parsing::ast::types::ItemVisibility;
29use crate::parsing::ast::types::LabeledArg;
30use crate::parsing::ast::types::Literal;
31use crate::parsing::ast::types::LiteralValue;
32use crate::parsing::ast::types::MemberExpression;
33use crate::parsing::ast::types::Name;
34use crate::parsing::ast::types::Node;
35use crate::parsing::ast::types::NodeList;
36use crate::parsing::ast::types::NonCodeMeta;
37use crate::parsing::ast::types::NonCodeNode;
38use crate::parsing::ast::types::NonCodeValue;
39use crate::parsing::ast::types::NumericLiteral;
40use crate::parsing::ast::types::ObjectExpression;
41use crate::parsing::ast::types::Parameter;
42use crate::parsing::ast::types::PipeExpression;
43use crate::parsing::ast::types::Program;
44use crate::parsing::ast::types::SketchBlock;
45use crate::parsing::ast::types::SketchVar;
46use crate::parsing::ast::types::TagDeclarator;
47use crate::parsing::ast::types::TypeDeclaration;
48use crate::parsing::ast::types::TypeDeclarationDefinition;
49use crate::parsing::ast::types::UnaryExpression;
50use crate::parsing::ast::types::VariableDeclaration;
51use crate::parsing::ast::types::VariableKind;
52use crate::parsing::deprecation;
53
54#[allow(dead_code)]
55pub fn fmt(input: &str) -> Result<String, KclError> {
56    let program = crate::parsing::parse_str(input, ModuleId::default()).parse_errs_as_err()?;
57    Ok(program.ast.recast_top(&Default::default(), 0))
58}
59
60impl Program {
61    pub fn recast_top(&self, options: &FormatOptions, indentation_level: usize) -> String {
62        let mut buf = String::with_capacity(1024);
63        self.recast(&mut buf, options, indentation_level);
64        buf
65    }
66
67    pub fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize) {
68        if let Some(sh) = self.shebang.as_ref() {
69            write!(buf, "{}\n\n", sh.inner.content).no_fail();
70        }
71
72        recast_body(
73            &self.body,
74            &self.non_code_meta,
75            &self.inner_attrs,
76            buf,
77            options,
78            indentation_level,
79        );
80    }
81}
82
83fn recast_body(
84    items: &[BodyItem],
85    non_code_meta: &NonCodeMeta,
86    inner_attrs: &NodeList<Annotation>,
87    buf: &mut String,
88    options: &FormatOptions,
89    indentation_level: usize,
90) {
91    let indentation = options.get_indentation(indentation_level);
92
93    let has_non_newline_start_node = non_code_meta
94        .start_nodes
95        .iter()
96        .any(|noncode| !matches!(noncode.value, NonCodeValue::NewLine));
97    if has_non_newline_start_node {
98        let mut pending_newline = false;
99        for start_node in &non_code_meta.start_nodes {
100            match start_node.value {
101                NonCodeValue::NewLine => pending_newline = true,
102                _ => {
103                    if pending_newline {
104                        // If the previous emission already ended with '\n', only add one more.
105                        if buf.ends_with('\n') {
106                            buf.push('\n');
107                        } else {
108                            buf.push_str("\n\n");
109                        }
110                        pending_newline = false;
111                    }
112                    let noncode_recast = start_node.recast(options, indentation_level);
113                    buf.push_str(&noncode_recast);
114                }
115            }
116        }
117        // Handle any trailing newlines that weren't flushed yet.
118        if pending_newline {
119            if buf.ends_with('\n') {
120                buf.push('\n');
121            } else {
122                buf.push_str("\n\n");
123            }
124        }
125    }
126
127    for attr in inner_attrs {
128        options.write_indentation(buf, indentation_level);
129        attr.recast(buf, options, indentation_level);
130    }
131    if !inner_attrs.is_empty() {
132        buf.push('\n');
133    }
134
135    let body_item_lines = items.iter().map(|body_item| {
136        let mut result = String::with_capacity(256);
137        for comment in body_item.get_comments() {
138            if !comment.is_empty() {
139                result.push_str(&indentation);
140                result.push_str(comment);
141            }
142            if comment.is_empty() && !result.ends_with("\n") {
143                result.push('\n');
144            }
145            if !result.ends_with("\n\n") && result != "\n" {
146                result.push('\n');
147            }
148        }
149        for attr in body_item.get_attrs() {
150            attr.recast(&mut result, options, indentation_level);
151        }
152        match body_item {
153            BodyItem::ImportStatement(stmt) => {
154                result.push_str(&stmt.recast(options, indentation_level));
155            }
156            BodyItem::ExpressionStatement(expression_statement) => {
157                let mut tmp_buf = String::new();
158                expression_statement
159                    .expression
160                    .recast(&mut tmp_buf, options, indentation_level, ExprContext::Other);
161                options.write_indentation(&mut result, indentation_level);
162                result.push_str(tmp_buf.trim_start());
163            }
164            BodyItem::VariableDeclaration(variable_declaration) => {
165                variable_declaration.recast(&mut result, options, indentation_level);
166            }
167            BodyItem::TypeDeclaration(ty_declaration) => ty_declaration.recast(&mut result, options, indentation_level),
168            BodyItem::ReturnStatement(return_statement) => {
169                write!(&mut result, "{indentation}return ").no_fail();
170                let mut tmp_buf = String::with_capacity(256);
171                return_statement
172                    .argument
173                    .recast(&mut tmp_buf, options, indentation_level, ExprContext::Other);
174                write!(&mut result, "{}", tmp_buf.trim_start()).no_fail();
175            }
176        };
177        result
178    });
179    for (index, recast_str) in body_item_lines.enumerate() {
180        write!(buf, "{recast_str}").no_fail();
181
182        // determine the value of the end string
183        // basically if we are inside a nested function we want to end with a new line
184        let needs_line_break = !(index == items.len() - 1 && indentation_level == 0);
185
186        let custom_white_space_or_comment = non_code_meta.non_code_nodes.get(&index).map(|noncodes| {
187            noncodes.iter().enumerate().map(|(i, custom_white_space_or_comment)| {
188                let formatted = custom_white_space_or_comment.recast(options, indentation_level);
189                if i == 0 && !formatted.trim().is_empty() {
190                    if let NonCodeValue::BlockComment { .. } = custom_white_space_or_comment.value {
191                        format!("\n{formatted}")
192                    } else {
193                        formatted
194                    }
195                } else {
196                    formatted
197                }
198            })
199        });
200
201        if let Some(custom) = custom_white_space_or_comment {
202            for to_write in custom {
203                write!(buf, "{to_write}").no_fail();
204            }
205        } else if needs_line_break {
206            buf.push('\n')
207        }
208    }
209    trim_end(buf);
210
211    // Insert a final new line if the user wants it.
212    if options.insert_final_newline && !buf.is_empty() {
213        buf.push('\n');
214    }
215}
216
217impl NonCodeValue {
218    fn should_cause_array_newline(&self) -> bool {
219        match self {
220            Self::InlineComment { .. } => false,
221            Self::BlockComment { .. } | Self::NewLine => true,
222        }
223    }
224}
225
226impl Node<NonCodeNode> {
227    fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
228        let indentation = options.get_indentation(indentation_level);
229        match &self.value {
230            NonCodeValue::InlineComment {
231                value,
232                style: CommentStyle::Line,
233            } => format!(" // {value}\n"),
234            NonCodeValue::InlineComment {
235                value,
236                style: CommentStyle::Block,
237            } => format!(" /* {value} */"),
238            NonCodeValue::BlockComment { value, style } => match style {
239                CommentStyle::Block => format!("{indentation}/* {value} */"),
240                CommentStyle::Line => {
241                    if value.trim().is_empty() {
242                        format!("{indentation}//\n")
243                    } else {
244                        format!("{}// {}\n", indentation, value.trim())
245                    }
246                }
247            },
248            NonCodeValue::NewLine => "\n\n".to_string(),
249        }
250    }
251}
252
253impl Node<Annotation> {
254    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize) {
255        let indentation = options.get_indentation(indentation_level);
256        let mut result = String::new();
257        for comment in &self.pre_comments {
258            if !comment.is_empty() {
259                result.push_str(&indentation);
260                result.push_str(comment);
261            }
262            if !result.ends_with("\n\n") && result != "\n" {
263                result.push('\n');
264            }
265        }
266        result.push('@');
267        if let Some(name) = &self.name {
268            result.push_str(&name.name);
269        }
270        if let Some(properties) = &self.properties {
271            result.push('(');
272            result.push_str(
273                &properties
274                    .iter()
275                    .map(|prop| {
276                        let mut temp = format!("{} = ", prop.key.name);
277                        prop.value
278                            .recast(&mut temp, options, indentation_level + 1, ExprContext::Other);
279                        temp.trim().to_owned()
280                    })
281                    .collect::<Vec<String>>()
282                    .join(", "),
283            );
284            result.push(')');
285            result.push('\n');
286        }
287
288        buf.push_str(&result)
289    }
290}
291
292impl ImportStatement {
293    pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
294        let indentation = options.get_indentation(indentation_level);
295        let vis = if self.visibility == ItemVisibility::Export {
296            "export "
297        } else {
298            ""
299        };
300        let mut string = format!("{vis}{indentation}import ");
301        match &self.selector {
302            ImportSelector::List { items } => {
303                for (i, item) in items.iter().enumerate() {
304                    if i > 0 {
305                        string.push_str(", ");
306                    }
307                    string.push_str(&item.name.name);
308                    if let Some(alias) = &item.alias {
309                        // If the alias is the same, don't output it.
310                        if item.name.name != alias.name {
311                            string.push_str(&format!(" as {}", alias.name));
312                        }
313                    }
314                }
315                string.push_str(" from ");
316            }
317            ImportSelector::Glob(_) => string.push_str("* from "),
318            ImportSelector::None { .. } => {}
319        }
320        string.push_str(&format!("\"{}\"", self.path));
321
322        if let ImportSelector::None { alias: Some(alias) } = &self.selector {
323            string.push_str(" as ");
324            string.push_str(&alias.name);
325        }
326        string
327    }
328}
329
330#[derive(Copy, Clone, Debug, Eq, PartialEq)]
331pub(crate) enum ExprContext {
332    Pipe,
333    /// The first expression in a pipe. It should not get pipe-specific child indentation,
334    /// but it also should not emit a leading indent before the expression itself.
335    PipeHead,
336    FnDecl,
337    /// Being used as an argument to a call expression, which is in a pipe expression.
338    PipeCallArg,
339    /// Being used as an argument to a call expression.
340    CallArg,
341    /// An inline operator operand whose continuation lines retain the enclosing pipe offset.
342    OperatorOperand {
343        in_pipe: bool,
344    },
345    Other,
346}
347
348impl ExprContext {
349    fn in_pipe(self) -> bool {
350        matches!(
351            self,
352            ExprContext::Pipe | ExprContext::PipeCallArg | ExprContext::OperatorOperand { in_pipe: true }
353        )
354    }
355
356    fn needs_leading_indent(self) -> bool {
357        !matches!(
358            self,
359            ExprContext::PipeHead
360                | ExprContext::CallArg
361                | ExprContext::PipeCallArg
362                | ExprContext::OperatorOperand { .. }
363        )
364    }
365
366    fn call_arg_context(self) -> ExprContext {
367        if self.in_pipe() {
368            ExprContext::PipeCallArg
369        } else {
370            ExprContext::CallArg
371        }
372    }
373}
374
375impl Expr {
376    pub(crate) fn recast(
377        &self,
378        buf: &mut String,
379        options: &FormatOptions,
380        indentation_level: usize,
381        mut ctxt: ExprContext,
382    ) {
383        let is_decl = matches!(ctxt, ExprContext::FnDecl);
384        if is_decl {
385            // Just because this expression is being bound to a variable, doesn't mean that every child
386            // expression is being bound. So, reset the expression context if necessary.
387            // This will still preserve the "::Pipe" context though.
388            ctxt = ExprContext::Other;
389        }
390        match &self {
391            Expr::BinaryExpression(bin_exp) => bin_exp.recast(buf, options, indentation_level, ctxt),
392            Expr::ArrayExpression(array_exp) => array_exp.recast(buf, options, indentation_level, ctxt),
393            Expr::ArrayRangeExpression(range_exp) => range_exp.recast(buf, options, indentation_level, ctxt),
394            Expr::ObjectExpression(obj_exp) => obj_exp.recast(buf, options, indentation_level, ctxt),
395            Expr::MemberExpression(mem_exp) => mem_exp.recast(buf, options, indentation_level, ctxt),
396            Expr::Literal(literal) => {
397                literal.recast(buf);
398            }
399            Expr::FunctionExpression(func_exp) => {
400                if !is_decl {
401                    buf.push_str("fn");
402                    if let Some(name) = &func_exp.name {
403                        buf.push(' ');
404                        buf.push_str(&name.name);
405                    }
406                }
407                func_exp.recast(buf, options, indentation_level);
408            }
409            Expr::CallExpressionKw(call_exp) => call_exp.recast(buf, options, indentation_level, ctxt),
410            Expr::Name(name) => {
411                let result = &name.inner.name.inner.name;
412                match deprecation(result, DeprecationKind::Const) {
413                    Some(suggestion) => buf.push_str(suggestion),
414                    None => {
415                        for prefix in &name.path {
416                            buf.push_str(&prefix.name);
417                            buf.push(':');
418                            buf.push(':');
419                        }
420                        buf.push_str(result);
421                    }
422                }
423            }
424            Expr::TagDeclarator(tag) => tag.recast(buf),
425            Expr::PipeExpression(pipe_exp) => {
426                pipe_exp.recast(buf, options, indentation_level, !is_decl && ctxt.needs_leading_indent())
427            }
428            Expr::UnaryExpression(unary_exp) => unary_exp.recast(buf, options, indentation_level, ctxt),
429            Expr::IfExpression(e) => e.recast(buf, options, indentation_level, ctxt),
430            Expr::PipeSubstitution(_) => buf.push_str(crate::parsing::PIPE_SUBSTITUTION_OPERATOR),
431            Expr::LabelledExpression(e) => {
432                e.expr.recast(buf, options, indentation_level, ctxt);
433                buf.push_str(" as ");
434                buf.push_str(&e.label.name);
435            }
436            Expr::AscribedExpression(e) => e.recast(buf, options, indentation_level, ctxt),
437            Expr::SketchBlock(e) => e.recast(buf, options, indentation_level, ctxt),
438            Expr::SketchVar(e) => e.recast(buf),
439            Expr::None(_) => {
440                unimplemented!("there is no literal None, see https://github.com/KittyCAD/modeling-app/issues/1115")
441            }
442        }
443    }
444}
445
446impl AscribedExpression {
447    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
448        if matches!(
449            self.expr,
450            Expr::BinaryExpression(..) | Expr::PipeExpression(..) | Expr::UnaryExpression(..)
451        ) {
452            buf.push('(');
453            self.expr.recast(buf, options, indentation_level, ctxt);
454            buf.push(')');
455        } else {
456            self.expr.recast(buf, options, indentation_level, ctxt);
457        }
458        buf.push_str(": ");
459        write!(buf, "{}", self.ty).no_fail();
460    }
461}
462
463impl BinaryPart {
464    pub(crate) fn recast(
465        &self,
466        buf: &mut String,
467        options: &FormatOptions,
468        indentation_level: usize,
469        ctxt: ExprContext,
470    ) {
471        let operand_ctxt = match ctxt {
472            ExprContext::OperatorOperand { .. } => ctxt,
473            _ => ExprContext::Other,
474        };
475        match &self {
476            BinaryPart::Literal(literal) => {
477                literal.recast(buf);
478            }
479            BinaryPart::Name(name) => match deprecation(&name.inner.name.inner.name, DeprecationKind::Const) {
480                Some(suggestion) => write!(buf, "{suggestion}").no_fail(),
481                None => name.write_to(buf).no_fail(),
482            },
483            BinaryPart::BinaryExpression(binary_expression) => {
484                binary_expression.recast(buf, options, indentation_level, ctxt)
485            }
486            BinaryPart::CallExpressionKw(call_expression) => {
487                call_expression.recast(buf, options, indentation_level, operand_ctxt)
488            }
489            BinaryPart::UnaryExpression(unary_expression) => {
490                unary_expression.recast(buf, options, indentation_level, ctxt)
491            }
492            BinaryPart::MemberExpression(member_expression) => {
493                member_expression.recast(buf, options, indentation_level, ctxt)
494            }
495            BinaryPart::ArrayExpression(e) => e.recast(buf, options, indentation_level, ctxt),
496            BinaryPart::ArrayRangeExpression(e) => e.recast(buf, options, indentation_level, ctxt),
497            BinaryPart::ObjectExpression(e) => e.recast(buf, options, indentation_level, ctxt),
498            BinaryPart::IfExpression(e) => e.recast(buf, options, indentation_level, operand_ctxt),
499            BinaryPart::AscribedExpression(e) => e.recast(buf, options, indentation_level, operand_ctxt),
500            BinaryPart::SketchVar(e) => e.recast(buf),
501        }
502    }
503}
504
505impl CallExpressionKw {
506    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
507        recast_call(
508            &self.callee,
509            self.unlabeled.as_ref(),
510            &self.arguments,
511            &self.non_code_meta,
512            buf,
513            options,
514            indentation_level,
515            ctxt,
516        );
517    }
518}
519
520fn recast_args(
521    unlabeled: Option<&Expr>,
522    arguments: &[LabeledArg],
523    options: &FormatOptions,
524    indentation_level: usize,
525    ctxt: ExprContext,
526) -> Vec<String> {
527    let arg_ctxt = ctxt.call_arg_context();
528    let mut arg_list = if let Some(first_arg) = unlabeled {
529        let mut first = String::with_capacity(256);
530        first_arg.recast(&mut first, options, indentation_level, arg_ctxt);
531        vec![first.trim().to_owned()]
532    } else {
533        Vec::with_capacity(arguments.len())
534    };
535    arg_list.extend(arguments.iter().map(|arg| {
536        let mut buf = String::with_capacity(256);
537        arg.recast(&mut buf, options, indentation_level, arg_ctxt);
538        buf
539    }));
540    arg_list
541}
542
543#[allow(clippy::too_many_arguments)]
544fn recast_call(
545    callee: &Name,
546    unlabeled: Option<&Expr>,
547    arguments: &[LabeledArg],
548    non_code_meta: &NonCodeMeta,
549    buf: &mut String,
550    options: &FormatOptions,
551    indentation_level: usize,
552    ctxt: ExprContext,
553) {
554    let smart_indent_level = if ctxt.in_pipe() { 0 } else { indentation_level };
555    let name = callee;
556
557    if let Some(suggestion) = deprecation(&name.name.inner.name, DeprecationKind::Function) {
558        if ctxt.needs_leading_indent() {
559            options.write_indentation(buf, smart_indent_level);
560        }
561        return write!(buf, "{suggestion}").no_fail();
562    }
563
564    // An item is an argument (possibly with block comments glued before it),
565    // or a comment that needs its own line.
566    struct FormatItem {
567        text: String,
568        is_arg: bool,
569    }
570
571    // Reconstruct the order of arguments and non-code items (e.g. comments).
572    // Non-code nodes are keyed by their position in the source argument
573    // sequence, the same scheme as ArrayExpression. The unlabeled argument is
574    // not part of that sequence; it always comes first. Iterate enough slots
575    // that no argument or comment is dropped, even if AST edits left the
576    // indices inconsistent.
577    let build_items = |arg_indent: usize| -> Vec<FormatItem> {
578        let arg_list = recast_args(unlabeled, arguments, options, arg_indent, ctxt);
579        let mut arg_iter = arg_list.into_iter();
580        let mut items = Vec::with_capacity(arguments.len() + non_code_meta.non_code_nodes_len() + 1);
581        if unlabeled.is_some()
582            && let Some(first_arg) = arg_iter.next()
583        {
584            items.push(FormatItem {
585                text: first_arg,
586                is_arg: true,
587            });
588        }
589        let num_items = arguments.len() + non_code_meta.non_code_nodes_len();
590        let num_slots = non_code_meta
591            .non_code_nodes
592            .keys()
593            .max()
594            .map_or(num_items, |max| num_items.max(max + 1));
595        // Block comments don't need a line break, so they glue to whatever
596        // comes next instead of getting their own line.
597        let mut pending_block_comments = String::new();
598        for i in 0..num_slots {
599            if let Some(noncode) = non_code_meta.non_code_nodes.get(&i) {
600                for nc in noncode {
601                    match &nc.value {
602                        NonCodeValue::BlockComment {
603                            style: CommentStyle::Block,
604                            ..
605                        }
606                        | NonCodeValue::InlineComment {
607                            style: CommentStyle::Block,
608                            ..
609                        } => {
610                            pending_block_comments.push_str(nc.recast(options, 0).trim());
611                            pending_block_comments.push(' ');
612                        }
613                        _ => {
614                            // Line comments (and blank lines) need their own
615                            // line.
616                            let mut text = std::mem::take(&mut pending_block_comments);
617                            text.push_str(nc.recast(options, 0).trim_end_matches('\n'));
618                            items.push(FormatItem {
619                                text: text.trim().to_owned(),
620                                is_arg: false,
621                            });
622                        }
623                    }
624                }
625            } else if let Some(arg) = arg_iter.next() {
626                let mut text = std::mem::take(&mut pending_block_comments);
627                text.push_str(&arg);
628                items.push(FormatItem { text, is_arg: true });
629            }
630        }
631        items.extend(arg_iter.map(|arg| FormatItem {
632            text: arg,
633            is_arg: true,
634        }));
635        // Trailing block comments have no argument to glue to.
636        if !pending_block_comments.is_empty() {
637            items.push(FormatItem {
638                text: pending_block_comments.trim_end().to_owned(),
639                is_arg: false,
640            });
641        }
642        items
643    };
644
645    let items = build_items(indentation_level);
646    let has_lots_of_args = items.iter().filter(|item| item.is_arg).count() >= 4;
647    // Comments which need their own line force the multi-line layout.
648    let has_own_line_comment = items.iter().any(|item| !item.is_arg);
649    let some_arg_is_already_multiline = items.len() > 1 && items.iter().any(|item| item.text.contains('\n'));
650    let multiline = has_lots_of_args || some_arg_is_already_multiline || has_own_line_comment;
651    if multiline {
652        let next_indent = indentation_level + 1;
653        let inner_indentation = if ctxt.in_pipe() {
654            options.get_indentation_offset_pipe(next_indent)
655        } else {
656            options.get_indentation(next_indent)
657        };
658        let items = build_items(next_indent);
659        let end_indent = if ctxt.in_pipe() {
660            options.get_indentation_offset_pipe(indentation_level)
661        } else {
662            options.get_indentation(indentation_level)
663        };
664        if ctxt.needs_leading_indent() {
665            options.write_indentation(buf, smart_indent_level);
666        }
667        name.write_to(buf).no_fail();
668        buf.push('(');
669        buf.push('\n');
670        for item in items {
671            if item.is_arg {
672                writeln!(buf, "{inner_indentation}{},", item.text).no_fail();
673            } else if item.text.is_empty() {
674                // A blank line between arguments.
675                buf.push('\n');
676            } else {
677                writeln!(buf, "{inner_indentation}{}", item.text).no_fail();
678            }
679        }
680        write!(buf, "{end_indent}").no_fail();
681        buf.push(')');
682    } else {
683        if ctxt.needs_leading_indent() {
684            options.write_indentation(buf, smart_indent_level);
685        }
686        name.write_to(buf).no_fail();
687        buf.push('(');
688        let args = items
689            .iter()
690            .map(|item| item.text.as_str())
691            .collect::<Vec<_>>()
692            .join(", ");
693        write!(buf, "{args}").no_fail();
694        buf.push(')');
695    }
696}
697
698impl LabeledArg {
699    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
700        if let Some(l) = &self.label {
701            buf.push_str(&l.name);
702            buf.push_str(" = ");
703        }
704        self.arg.recast(buf, options, indentation_level, ctxt);
705    }
706}
707
708impl VariableDeclaration {
709    pub fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize) {
710        options.write_indentation(buf, indentation_level);
711        match self.visibility {
712            ItemVisibility::Default => {}
713            ItemVisibility::Export => buf.push_str("export "),
714        };
715
716        let (keyword, eq, ctxt) = match self.kind {
717            VariableKind::Fn => ("fn ", "", ExprContext::FnDecl),
718            VariableKind::Const => ("", " = ", ExprContext::Other),
719        };
720        buf.push_str(keyword);
721        buf.push_str(&self.declaration.id.name);
722        buf.push_str(eq);
723
724        // Unfortunately, allocate a temporary buffer here so that we can trim the start.
725        // Otherwise, some expression kinds will write indentation at the start, because
726        // they don't know they're inside a declaration.
727        // TODO: Pass the ExprContext throughout every Expr kind, so that they can conditionally
728        // emit whitespace in an ExprStmt and not when they're in a DeclarationStmt.
729        let mut tmp_buf = String::new();
730        self.declaration
731            .init
732            .recast(&mut tmp_buf, options, indentation_level, ctxt);
733        buf.push_str(tmp_buf.trim_start());
734    }
735}
736
737impl TypeDeclaration {
738    pub fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize) {
739        options.write_indentation(buf, indentation_level);
740        match self.visibility {
741            ItemVisibility::Default => {}
742            ItemVisibility::Export => buf.push_str("export "),
743        };
744        buf.push_str("type ");
745        buf.push_str(&self.name.name);
746
747        if let Some(args) = &self.args {
748            buf.push('(');
749            for (i, a) in args.iter().enumerate() {
750                buf.push_str(&a.name);
751                if i < args.len() - 1 {
752                    buf.push_str(", ");
753                }
754            }
755            buf.push(')');
756        }
757        match &self.definition {
758            TypeDeclarationDefinition::Bare => {}
759            TypeDeclarationDefinition::Alias { ty } => {
760                buf.push_str(" = ");
761                write!(buf, "{ty}").no_fail();
762            }
763            TypeDeclarationDefinition::Enum(e) => e.recast(buf, options, indentation_level),
764        }
765    }
766}
767
768impl EnumDeclaration {
769    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize) {
770        // Only comments decide the layout: blank lines in a body with no arms
771        // carry no information and are not re-emitted, so counting them here
772        // would make formatting non-idempotent (the multi-line output would
773        // reparse with no non-code at all and then collapse).
774        let has_start_comment = self
775            .non_code_meta
776            .start_nodes
777            .iter()
778            .any(|noncode| !matches!(noncode.value, NonCodeValue::NewLine));
779
780        // An enum with no variants and no comments stays on one line, keeping
781        // the standalone arm marker which classifies the body as an enum.
782        if self.variants.is_empty() && !has_start_comment {
783            buf.push_str(" { | }");
784            return;
785        }
786
787        let body_level = indentation_level + 1;
788        let body_indentation = options.get_indentation(body_level);
789        buf.push_str(" {\n");
790
791        // Comments before the first arm (or around a standalone `|`),
792        // mirroring how `recast_body` renders `start_nodes`.
793        if has_start_comment {
794            let mut pending_newline = false;
795            for start_node in &self.non_code_meta.start_nodes {
796                if matches!(start_node.value, NonCodeValue::NewLine) {
797                    pending_newline = true;
798                    continue;
799                }
800                if pending_newline {
801                    buf.push('\n');
802                    pending_newline = false;
803                }
804                buf.push_str(&start_node.recast(options, body_level));
805                if !buf.ends_with('\n') {
806                    buf.push('\n');
807                }
808            }
809            if pending_newline {
810                buf.push('\n');
811            }
812        }
813
814        if self.variants.is_empty() {
815            buf.push_str(&body_indentation);
816            buf.push_str("|\n");
817        }
818
819        for (index, variant) in self.variants.iter().enumerate() {
820            // Comments strongly associated with this arm, mirroring how
821            // `recast_body` renders body-item pre-comments.
822            let mut arm = String::new();
823            for comment in &variant.pre_comments {
824                if !comment.is_empty() {
825                    arm.push_str(&body_indentation);
826                    arm.push_str(comment);
827                }
828                if comment.is_empty() && !arm.ends_with('\n') {
829                    arm.push('\n');
830                }
831                if !arm.ends_with("\n\n") && arm != "\n" {
832                    arm.push('\n');
833                }
834            }
835            arm.push_str(&body_indentation);
836            arm.push_str("| ");
837            arm.push_str(&variant.name.name);
838            buf.push_str(&arm);
839
840            // Trailing comments and blank lines after this arm, mirroring how
841            // `recast_body` renders per-item non-code nodes.
842            if let Some(noncodes) = self.non_code_meta.non_code_nodes.get(&index) {
843                for (i, noncode) in noncodes.iter().enumerate() {
844                    let formatted = noncode.recast(options, body_level);
845                    if i == 0
846                        && !formatted.trim().is_empty()
847                        && matches!(noncode.value, NonCodeValue::BlockComment { .. })
848                    {
849                        buf.push('\n');
850                    }
851                    buf.push_str(&formatted);
852                }
853                if !buf.ends_with('\n') {
854                    buf.push('\n');
855                }
856            } else {
857                buf.push('\n');
858            }
859        }
860
861        options.write_indentation(buf, indentation_level);
862        buf.push('}');
863    }
864}
865
866fn write<W: std::fmt::Write>(f: &mut W, s: impl std::fmt::Display) {
867    f.write_fmt(format_args!("{s}"))
868        .expect("writing to a string should always succeed")
869}
870
871fn write_dbg<W: std::fmt::Write>(f: &mut W, s: impl std::fmt::Debug) {
872    f.write_fmt(format_args!("{s:?}"))
873        .expect("writing to a string should always succeed")
874}
875
876impl NumericLiteral {
877    fn recast(&self, buf: &mut String) {
878        if self.raw.contains('.') && self.value.fract() == 0.0 {
879            write_dbg(buf, self.value);
880            write(buf, self.suffix);
881        } else {
882            write(buf, &self.raw);
883        }
884    }
885}
886
887impl Literal {
888    fn recast(&self, buf: &mut String) {
889        match self.value {
890            LiteralValue::Number { value, suffix } => {
891                if self.raw.contains('.') && value.fract() == 0.0 {
892                    write_dbg(buf, value);
893                    write(buf, suffix);
894                } else {
895                    write(buf, &self.raw);
896                }
897            }
898            LiteralValue::String(ref s) => {
899                if let Some(suggestion) = deprecation(s, DeprecationKind::String) {
900                    return write!(buf, "{suggestion}").unwrap();
901                }
902                let quote = if self.raw.trim().starts_with('"') { '"' } else { '\'' };
903                write(buf, quote);
904                write(buf, s);
905                write(buf, quote);
906            }
907            LiteralValue::Bool(_) => {
908                write(buf, &self.raw);
909            }
910        }
911    }
912}
913
914impl TagDeclarator {
915    pub fn recast(&self, buf: &mut String) {
916        // TagDeclarators are always prefixed with a dollar sign.
917        buf.push('$');
918        buf.push_str(&self.name);
919    }
920}
921
922impl ArrayExpression {
923    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
924        fn indent_multiline_item(item: &str, indent: &str) -> String {
925            if !item.contains('\n') {
926                return item.to_owned();
927            }
928            let mut out = String::with_capacity(item.len() + indent.len() * 2);
929            let mut first = true;
930            for segment in item.split_inclusive('\n') {
931                if first {
932                    out.push_str(segment);
933                    first = false;
934                    continue;
935                }
936                out.push_str(indent);
937                out.push_str(segment);
938            }
939            out
940        }
941
942        // Reconstruct the order of items in the array.
943        // An item can be an element (i.e. an expression for a KCL value),
944        // or a non-code item (e.g. a comment)
945        let num_items = self.elements.len() + self.non_code_meta.non_code_nodes_len();
946        let mut elems = self.elements.iter();
947        let mut found_line_comment = false;
948        let mut format_items: Vec<_> = Vec::with_capacity(num_items);
949        for i in 0..num_items {
950            if let Some(noncode) = self.non_code_meta.non_code_nodes.get(&i) {
951                format_items.extend(noncode.iter().map(|nc| {
952                    found_line_comment |= nc.value.should_cause_array_newline();
953                    nc.recast(options, 0)
954                }));
955            } else {
956                let el = elems.next().unwrap();
957                let mut s = String::with_capacity(256);
958                el.recast(&mut s, options, 0, ExprContext::Other);
959                s.push_str(", ");
960                format_items.push(s);
961            }
962        }
963
964        // Format these items into a one-line array.
965        if let Some(item) = format_items.last_mut()
966            && let Some(norm) = item.strip_suffix(", ")
967        {
968            *item = norm.to_owned();
969        }
970        let mut flat_recast = String::with_capacity(256);
971        flat_recast.push('[');
972        for fi in &format_items {
973            flat_recast.push_str(fi)
974        }
975        flat_recast.push(']');
976
977        // We might keep the one-line representation, if it's short enough.
978        let max_array_length = 40;
979        let multi_line = flat_recast.len() > max_array_length || found_line_comment;
980        if !multi_line {
981            buf.push_str(&flat_recast);
982            return;
983        }
984
985        // Otherwise, we format a multi-line representation.
986        buf.push_str("[\n");
987        let inner_indentation = if ctxt.in_pipe() {
988            options.get_indentation_offset_pipe(indentation_level + 1)
989        } else {
990            options.get_indentation(indentation_level + 1)
991        };
992        for format_item in format_items {
993            let item = if let Some(x) = format_item.strip_suffix(" ") {
994                x
995            } else {
996                &format_item
997            };
998            let item = indent_multiline_item(item, &inner_indentation);
999            buf.push_str(&inner_indentation);
1000            buf.push_str(&item);
1001            if !format_item.ends_with('\n') {
1002                buf.push('\n')
1003            }
1004        }
1005        let end_indent = if ctxt.in_pipe() {
1006            options.get_indentation_offset_pipe(indentation_level)
1007        } else {
1008            options.get_indentation(indentation_level)
1009        };
1010        buf.push_str(&end_indent);
1011        buf.push(']');
1012    }
1013}
1014
1015/// An expression is syntactically trivial: i.e., a literal, identifier, or similar.
1016fn expr_is_trivial(expr: &Expr) -> bool {
1017    matches!(
1018        expr,
1019        Expr::Literal(_) | Expr::Name(_) | Expr::TagDeclarator(_) | Expr::PipeSubstitution(_) | Expr::None(_)
1020    )
1021}
1022
1023trait CannotActuallyFail {
1024    fn no_fail(self);
1025}
1026
1027impl CannotActuallyFail for std::fmt::Result {
1028    fn no_fail(self) {
1029        self.expect("writing to a string cannot fail, there's no IO happening")
1030    }
1031}
1032
1033impl ArrayRangeExpression {
1034    fn recast(&self, buf: &mut String, options: &FormatOptions, _: usize, _: ExprContext) {
1035        buf.push('[');
1036        self.start_element.recast(buf, options, 0, ExprContext::Other);
1037
1038        let range_op = if self.end_inclusive { ".." } else { "..<" };
1039        // Format these items into a one-line array. Put spaces around the `..` if either expression
1040        // is non-trivial. This is a bit arbitrary but people seem to like simple ranges to be formatted
1041        // tightly, but this is a misleading visual representation of the precedence if the range
1042        // components are compound expressions.
1043        let no_spaces = expr_is_trivial(&self.start_element) && expr_is_trivial(&self.end_element);
1044        if no_spaces {
1045            write!(buf, "{range_op}").no_fail()
1046        } else {
1047            write!(buf, " {range_op} ").no_fail()
1048        }
1049        self.end_element.recast(buf, options, 0, ExprContext::Other);
1050        buf.push(']');
1051        // Assume a range expression fits on one line.
1052    }
1053}
1054
1055fn trim_end(buf: &mut String) {
1056    buf.truncate(buf.trim_end().len())
1057}
1058
1059impl ObjectExpression {
1060    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
1061        if self
1062            .non_code_meta
1063            .non_code_nodes
1064            .values()
1065            .any(|nc| nc.iter().any(|nc| nc.value.should_cause_array_newline()))
1066        {
1067            return self.recast_multi_line(buf, options, indentation_level, ctxt);
1068        }
1069        let mut flat_recast_buf = String::new();
1070        flat_recast_buf.push_str("{ ");
1071        for (i, prop) in self.properties.iter().enumerate() {
1072            let obj_key = &prop.key.name;
1073            write!(flat_recast_buf, "{obj_key} = ").no_fail();
1074            prop.value
1075                .recast(&mut flat_recast_buf, options, indentation_level, ctxt);
1076            if i < self.properties.len() - 1 {
1077                flat_recast_buf.push_str(", ");
1078            }
1079        }
1080        flat_recast_buf.push_str(" }");
1081        let max_array_length = 40;
1082        let needs_multiple_lines = flat_recast_buf.len() > max_array_length;
1083        if !needs_multiple_lines {
1084            buf.push_str(&flat_recast_buf);
1085        } else {
1086            self.recast_multi_line(buf, options, indentation_level, ctxt);
1087        }
1088    }
1089
1090    /// Recast, but always outputs the object with newlines between each property.
1091    fn recast_multi_line(
1092        &self,
1093        buf: &mut String,
1094        options: &FormatOptions,
1095        indentation_level: usize,
1096        ctxt: ExprContext,
1097    ) {
1098        let inner_indentation = if ctxt.in_pipe() {
1099            options.get_indentation_offset_pipe(indentation_level + 1)
1100        } else {
1101            options.get_indentation(indentation_level + 1)
1102        };
1103        let num_items = self.properties.len() + self.non_code_meta.non_code_nodes_len();
1104        let mut props = self.properties.iter();
1105        let format_items: Vec<_> = (0..num_items)
1106            .flat_map(|i| {
1107                if let Some(noncode) = self.non_code_meta.non_code_nodes.get(&i) {
1108                    noncode.iter().map(|nc| nc.recast(options, 0)).collect::<Vec<_>>()
1109                } else {
1110                    let prop = props.next().unwrap();
1111                    // Use a comma unless it's the last item
1112                    let comma = if i == num_items - 1 { "" } else { ",\n" };
1113                    let mut s = String::new();
1114                    prop.value.recast(&mut s, options, indentation_level + 1, ctxt);
1115                    // TODO: Get rid of this vector allocation
1116                    vec![format!("{} = {}{comma}", prop.key.name, s.trim())]
1117                }
1118            })
1119            .collect();
1120        let end_indent = if ctxt.in_pipe() {
1121            options.get_indentation_offset_pipe(indentation_level)
1122        } else {
1123            options.get_indentation(indentation_level)
1124        };
1125        write!(
1126            buf,
1127            "{{\n{inner_indentation}{}\n{end_indent}}}",
1128            format_items.join(&inner_indentation),
1129        )
1130        .no_fail();
1131    }
1132}
1133
1134impl MemberExpression {
1135    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
1136        // The object
1137        self.object.recast(buf, options, indentation_level, ctxt);
1138        // The key
1139        if self.computed {
1140            buf.push('[');
1141            self.property.recast(buf, options, indentation_level, ctxt);
1142            buf.push(']');
1143        } else {
1144            buf.push('.');
1145            self.property.recast(buf, options, indentation_level, ctxt);
1146        };
1147    }
1148}
1149
1150impl BinaryExpression {
1151    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
1152        let maybe_wrap_it = |a: String, doit: bool| -> String { if doit { format!("({a})") } else { a } };
1153
1154        // It would be better to always preserve the user's parentheses but since we've dropped that
1155        // info from the AST, we bracket expressions as necessary.
1156        let should_wrap_left = match &self.left {
1157            BinaryPart::BinaryExpression(bin_exp) => {
1158                self.precedence() > bin_exp.precedence()
1159                    || ((self.precedence() == bin_exp.precedence())
1160                        && (!(self.operator.associative() && self.operator == bin_exp.operator)
1161                            && self.operator.associativity() == Associativity::Right))
1162            }
1163            _ => false,
1164        };
1165
1166        let should_wrap_right = match &self.right {
1167            BinaryPart::BinaryExpression(bin_exp) => {
1168                self.precedence() > bin_exp.precedence()
1169                    // These two lines preserve previous reformatting behaviour.
1170                    || self.operator == BinaryOperator::Sub
1171                    || self.operator == BinaryOperator::Div
1172                    || ((self.precedence() == bin_exp.precedence())
1173                        && (!(self.operator.associative() && self.operator == bin_exp.operator)
1174                            && self.operator.associativity() == Associativity::Left))
1175            }
1176            _ => false,
1177        };
1178
1179        let operand_ctxt = ExprContext::OperatorOperand {
1180            in_pipe: ctxt.in_pipe(),
1181        };
1182        let mut left = String::new();
1183        self.left.recast(&mut left, options, indentation_level, operand_ctxt);
1184        let mut right = String::new();
1185        self.right.recast(&mut right, options, indentation_level, operand_ctxt);
1186        write!(
1187            buf,
1188            "{} {} {}",
1189            maybe_wrap_it(left, should_wrap_left),
1190            self.operator,
1191            maybe_wrap_it(right, should_wrap_right)
1192        )
1193        .no_fail();
1194    }
1195}
1196
1197impl UnaryExpression {
1198    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
1199        let operand_ctxt = ExprContext::OperatorOperand {
1200            in_pipe: ctxt.in_pipe(),
1201        };
1202        match self.argument {
1203            BinaryPart::Literal(_)
1204            | BinaryPart::Name(_)
1205            | BinaryPart::MemberExpression(_)
1206            | BinaryPart::ArrayExpression(_)
1207            | BinaryPart::ArrayRangeExpression(_)
1208            | BinaryPart::ObjectExpression(_)
1209            | BinaryPart::IfExpression(_)
1210            | BinaryPart::AscribedExpression(_)
1211            | BinaryPart::CallExpressionKw(_) => {
1212                write!(buf, "{}", self.operator).no_fail();
1213                self.argument.recast(buf, options, indentation_level, operand_ctxt)
1214            }
1215            BinaryPart::BinaryExpression(_) | BinaryPart::UnaryExpression(_) | BinaryPart::SketchVar(_) => {
1216                write!(buf, "{}", self.operator).no_fail();
1217                buf.push('(');
1218                self.argument.recast(buf, options, indentation_level, operand_ctxt);
1219                buf.push(')');
1220            }
1221        }
1222    }
1223}
1224
1225impl IfExpression {
1226    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) {
1227        // We can calculate how many lines this will take, so let's do it and avoid growing the vec.
1228        // Total lines = starting lines, else-if lines, ending lines.
1229        let n = 2 + (self.else_ifs.len() * 2) + 3;
1230        let mut lines = Vec::with_capacity(n);
1231
1232        let cond = {
1233            let mut tmp_buf = String::new();
1234            self.cond.recast(&mut tmp_buf, options, indentation_level, ctxt);
1235            tmp_buf
1236        };
1237        lines.push((0, format!("if {cond} {{")));
1238        lines.push((1, {
1239            let mut tmp_buf = String::new();
1240            self.then_val.recast(&mut tmp_buf, options, indentation_level + 1);
1241            tmp_buf
1242        }));
1243        for else_if in &self.else_ifs {
1244            let cond = {
1245                let mut tmp_buf = String::new();
1246                else_if.cond.recast(&mut tmp_buf, options, indentation_level, ctxt);
1247                tmp_buf
1248            };
1249            lines.push((0, format!("}} else if {cond} {{")));
1250            lines.push((1, {
1251                let mut tmp_buf = String::new();
1252                else_if.then_val.recast(&mut tmp_buf, options, indentation_level + 1);
1253                tmp_buf
1254            }));
1255        }
1256        lines.push((0, "} else {".to_owned()));
1257        lines.push((1, {
1258            let mut tmp_buf = String::new();
1259            self.final_else.recast(&mut tmp_buf, options, indentation_level + 1);
1260            tmp_buf
1261        }));
1262        lines.push((0, "}".to_owned()));
1263        let out = lines
1264            .into_iter()
1265            .enumerate()
1266            .map(|(idx, (ind, line))| {
1267                let indentation = if idx == 0 && (ctxt.in_pipe() || matches!(ctxt, ExprContext::OperatorOperand { .. }))
1268                {
1269                    String::new()
1270                } else {
1271                    options.get_indentation(indentation_level + ind)
1272                };
1273                format!("{indentation}{}", line.trim())
1274            })
1275            .collect::<Vec<_>>()
1276            .join("\n");
1277        buf.push_str(&out);
1278    }
1279}
1280
1281impl Node<PipeExpression> {
1282    fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize, preceding_indent: bool) {
1283        if preceding_indent {
1284            options.write_indentation(buf, indentation_level);
1285        }
1286        for (index, statement) in self.body.iter().enumerate() {
1287            let (statement_indentation, statement_ctxt) = if index == 0 {
1288                (indentation_level, ExprContext::PipeHead)
1289            } else {
1290                (indentation_level + 1, ExprContext::Pipe)
1291            };
1292            statement.recast(buf, options, statement_indentation, statement_ctxt);
1293            let non_code_meta = &self.non_code_meta;
1294            if let Some(non_code_meta_value) = non_code_meta.non_code_nodes.get(&index) {
1295                for val in non_code_meta_value {
1296                    if let NonCodeValue::NewLine = val.value {
1297                        buf.push('\n');
1298                        continue;
1299                    }
1300                    // TODO: Remove allocation here by switching val.recast to accept buf.
1301                    let formatted = if val.end == self.end {
1302                        val.recast(options, indentation_level)
1303                            .trim_end_matches('\n')
1304                            .to_string()
1305                    } else {
1306                        val.recast(options, indentation_level + 1)
1307                            .trim_end_matches('\n')
1308                            .to_string()
1309                    };
1310                    if let NonCodeValue::BlockComment { .. } = val.value
1311                        && !buf.ends_with('\n')
1312                    {
1313                        buf.push('\n');
1314                    }
1315                    buf.push_str(&formatted);
1316                }
1317            }
1318
1319            if index != self.body.len() - 1 {
1320                buf.push('\n');
1321                options.write_indentation(buf, indentation_level + 1);
1322                buf.push_str(PIPE_OPERATOR);
1323                buf.push(' ');
1324            }
1325        }
1326    }
1327}
1328
1329impl FunctionExpression {
1330    pub fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize) {
1331        // We don't want to end with a new line inside nested functions.
1332        let mut new_options = options.clone();
1333        new_options.insert_final_newline = false;
1334
1335        buf.push('(');
1336        for (i, param) in self.params.iter().enumerate() {
1337            param.recast(buf, options, indentation_level);
1338            if i < self.params.len() - 1 {
1339                buf.push_str(", ");
1340            }
1341        }
1342        buf.push(')');
1343        if let Some(return_type) = &self.return_type {
1344            write!(buf, ": {return_type}").no_fail();
1345        }
1346        writeln!(buf, " {{").no_fail();
1347        self.body.recast(buf, &new_options, indentation_level + 1);
1348        buf.push('\n');
1349        options.write_indentation(buf, indentation_level);
1350        buf.push('}');
1351    }
1352}
1353
1354impl Parameter {
1355    pub fn recast(&self, buf: &mut String, _options: &FormatOptions, _indentation_level: usize) {
1356        if !self.labeled {
1357            buf.push('@');
1358        }
1359        buf.push_str(&self.identifier.name);
1360        if self.default_value.is_some() {
1361            buf.push('?');
1362        };
1363        if let Some(ty) = &self.param_type {
1364            buf.push_str(": ");
1365            write!(buf, "{ty}").no_fail();
1366        }
1367        if let Some(DefaultParamVal::Literal(ref literal)) = self.default_value {
1368            buf.push_str(" = ");
1369            literal.recast(buf);
1370        };
1371    }
1372}
1373
1374impl SketchBlock {
1375    pub(crate) fn recast(
1376        &self,
1377        buf: &mut String,
1378        options: &FormatOptions,
1379        indentation_level: usize,
1380        ctxt: ExprContext,
1381    ) {
1382        let name = Name {
1383            name: Node {
1384                inner: Identifier {
1385                    name: SketchBlock::CALLEE_NAME.to_owned(),
1386                    digest: None,
1387                },
1388                start: Default::default(),
1389                end: Default::default(),
1390                module_id: Default::default(),
1391                node_path: None,
1392                outer_attrs: Default::default(),
1393                pre_comments: Default::default(),
1394                comment_start: Default::default(),
1395            },
1396            path: Vec::new(),
1397            abs_path: false,
1398            digest: None,
1399        };
1400        recast_call(
1401            &name,
1402            None,
1403            &self.arguments,
1404            &self.non_code_meta,
1405            buf,
1406            options,
1407            indentation_level,
1408            ctxt,
1409        );
1410
1411        // We don't want to end with a new line inside nested blocks.
1412        let mut new_options = options.clone();
1413        new_options.insert_final_newline = false;
1414
1415        writeln!(buf, " {{").no_fail();
1416        self.body.recast(buf, &new_options, indentation_level + 1);
1417        buf.push('\n');
1418        options.write_indentation(buf, indentation_level);
1419        buf.push('}');
1420    }
1421}
1422
1423impl Block {
1424    pub fn recast(&self, buf: &mut String, options: &FormatOptions, indentation_level: usize) {
1425        recast_body(
1426            &self.items,
1427            &self.non_code_meta,
1428            &self.inner_attrs,
1429            buf,
1430            options,
1431            indentation_level,
1432        );
1433    }
1434}
1435
1436impl SketchVar {
1437    fn recast(&self, buf: &mut String) {
1438        if let Some(initial) = &self.initial {
1439            write!(buf, "var ").no_fail();
1440            initial.recast(buf);
1441        } else {
1442            write!(buf, "var").no_fail();
1443        }
1444    }
1445}
1446
1447/// Collect all the kcl (and other relevant) files in a directory, recursively.
1448#[cfg(not(target_arch = "wasm32"))]
1449#[async_recursion::async_recursion]
1450pub async fn walk_dir(dir: &std::path::PathBuf) -> Result<Vec<std::path::PathBuf>, anyhow::Error> {
1451    // Make sure we actually have a directory.
1452    if !dir.is_dir() {
1453        anyhow::bail!("`{}` is not a directory", dir.display());
1454    }
1455
1456    let mut entries = tokio::fs::read_dir(dir).await?;
1457
1458    let mut files = Vec::new();
1459    while let Some(entry) = entries.next_entry().await? {
1460        let path = entry.path();
1461
1462        if path.is_dir() {
1463            files.extend(walk_dir(&path).await?);
1464        } else if path
1465            .extension()
1466            .is_some_and(|ext| crate::RELEVANT_FILE_EXTENSIONS.contains(&ext.to_string_lossy().to_lowercase()))
1467        {
1468            files.push(path);
1469        }
1470    }
1471
1472    Ok(files)
1473}
1474
1475/// Recast all the kcl files in a directory, recursively.
1476#[cfg(not(target_arch = "wasm32"))]
1477pub async fn recast_dir(dir: &std::path::Path, options: &crate::FormatOptions) -> Result<(), anyhow::Error> {
1478    let files = walk_dir(&dir.to_path_buf()).await.map_err(|err| {
1479        crate::KclError::new_internal(crate::errors::KclErrorDetails::new(
1480            format!("Failed to walk directory `{}`: {:?}", dir.display(), err),
1481            vec![crate::SourceRange::default()],
1482        ))
1483    })?;
1484
1485    let futures = files
1486        .into_iter()
1487        .filter(|file| file.extension().is_some_and(|ext| ext == "kcl")) // We only care about kcl
1488        // files here.
1489        .map(|file| {
1490            let options = options.clone();
1491            tokio::spawn(async move {
1492                let contents = tokio::fs::read_to_string(&file)
1493                    .await
1494                    .map_err(|err| anyhow::anyhow!("Failed to read file `{}`: {:?}", file.display(), err))?;
1495                let (program, ces) = crate::Program::parse(&contents).map_err(|err| {
1496                    let report = crate::Report {
1497                        kcl_source: contents.to_string(),
1498                        error: err,
1499                        filename: file.to_string_lossy().to_string(),
1500                        label: file.to_string_lossy().to_string(),
1501                    };
1502                    let report = miette::Report::new(report);
1503                    anyhow::anyhow!("{:?}", report)
1504                })?;
1505                for ce in &ces {
1506                    if ce.severity != crate::errors::Severity::Warning {
1507                        let report = crate::Report {
1508                            kcl_source: contents.to_string(),
1509                            error: crate::KclError::new_semantic(ce.clone().into()),
1510                            filename: file.to_string_lossy().to_string(),
1511                            label: file.to_string_lossy().to_string(),
1512                        };
1513                        let report = miette::Report::new(report);
1514                        anyhow::bail!("{:?}", report);
1515                    }
1516                }
1517                let Some(program) = program else {
1518                    anyhow::bail!("Failed to parse file `{}`", file.display());
1519                };
1520                let recast = program.recast_with_options(&options);
1521                tokio::fs::write(&file, recast)
1522                    .await
1523                    .map_err(|err| anyhow::anyhow!("Failed to write file `{}`: {:?}", file.display(), err))?;
1524
1525                Ok::<(), anyhow::Error>(())
1526            })
1527        })
1528        .collect::<Vec<_>>();
1529
1530    // Join all futures and await their completion
1531    let results = futures::future::join_all(futures).await;
1532
1533    // Check if any of the futures failed.
1534    let mut errors = Vec::new();
1535    for result in results {
1536        if let Err(err) = result? {
1537            errors.push(err);
1538        }
1539    }
1540
1541    if !errors.is_empty() {
1542        anyhow::bail!("Failed to recast some files: {:?}", errors);
1543    }
1544
1545    Ok(())
1546}
1547
1548#[cfg(test)]
1549mod tests {
1550    use pretty_assertions::assert_eq;
1551
1552    use super::*;
1553    use crate::ModuleId;
1554    use crate::parsing::ast::types::FormatOptions;
1555
1556    #[test]
1557    fn test_recast_annotations_without_body_items() {
1558        let input = r#"@settings(defaultLengthUnit = in)
1559"#;
1560        let program = crate::parsing::top_level_parse(input).unwrap();
1561        let output = program.recast_top(&Default::default(), 0);
1562        assert_eq!(output, input);
1563    }
1564
1565    #[test]
1566    fn test_recast_annotations_in_function_body() {
1567        let input = r#"fn myFunc() {
1568  @meta(yes = true)
1569
1570  x = 2
1571}
1572"#;
1573        let program = crate::parsing::top_level_parse(input).unwrap();
1574        let output = program.recast_top(&Default::default(), 0);
1575        assert_eq!(output, input);
1576    }
1577
1578    #[test]
1579    fn test_recast_annotations_in_function_body_without_items() {
1580        let input = "\
1581fn myFunc() {
1582  @meta(yes = true)
1583}
1584";
1585        let program = crate::parsing::top_level_parse(input).unwrap();
1586        let output = program.recast_top(&Default::default(), 0);
1587        assert_eq!(output, input);
1588    }
1589
1590    #[test]
1591    fn recast_annotations_with_comments() {
1592        let input = r#"// Start comment
1593
1594// Comment on attr
1595@settings(defaultLengthUnit = in)
1596
1597// Comment on item
1598foo = 42
1599
1600// Comment on another item
1601@(impl = kcl)
1602bar = 0
1603"#;
1604        let program = crate::parsing::top_level_parse(input).unwrap();
1605        let output = program.recast_top(&Default::default(), 0);
1606        assert_eq!(output, input);
1607    }
1608
1609    #[test]
1610    fn recast_annotations_with_block_comment() {
1611        let input = r#"/* Start comment
1612
1613sdfsdfsdfs */
1614@settings(defaultLengthUnit = in)
1615
1616foo = 42
1617"#;
1618        let program = crate::parsing::top_level_parse(input).unwrap();
1619        let output = program.recast_top(&Default::default(), 0);
1620        assert_eq!(output, input);
1621    }
1622
1623    /// Assert that `input` formats to `expected`, and that formatting is
1624    /// idempotent: recasting the output parses and formats to itself.
1625    #[track_caller]
1626    fn assert_recast(input: &str, expected: &str) {
1627        let program = crate::parsing::top_level_parse(input).unwrap();
1628        let output = program.recast_top(&Default::default(), 0);
1629        assert_eq!(output, expected);
1630        let reparsed = crate::parsing::top_level_parse(&output).unwrap();
1631        assert_eq!(reparsed.recast_top(&Default::default(), 0), output);
1632    }
1633
1634    #[test]
1635    fn recast_enum_multi_line_is_stable() {
1636        let input = r#"@settings(experimentalFeatures = allow)
1637
1638type Color {
1639  | Red
1640  | Green
1641  | Blue
1642}
1643"#;
1644        assert_recast(input, input);
1645    }
1646
1647    #[test]
1648    fn recast_enum_expands_single_line() {
1649        let input = r#"@settings(experimentalFeatures = allow)
1650
1651type Color { | Red | Green | Blue }
1652"#;
1653        let expected = r#"@settings(experimentalFeatures = allow)
1654
1655type Color {
1656  | Red
1657  | Green
1658  | Blue
1659}
1660"#;
1661        assert_recast(input, expected);
1662    }
1663
1664    #[test]
1665    fn recast_enum_export() {
1666        let input = r#"@settings(experimentalFeatures = allow)
1667
1668export type Color {
1669  | Red
1670}
1671"#;
1672        assert_recast(input, input);
1673    }
1674
1675    #[test]
1676    fn recast_enum_no_variants() {
1677        let input = r#"@settings(experimentalFeatures = allow)
1678
1679type Empty { | }
1680"#;
1681        assert_recast(input, input);
1682    }
1683
1684    #[test]
1685    fn recast_enum_no_variants_collapses_blank_lines() {
1686        // Blank lines around a lone arm marker are not content: the body
1687        // collapses in one pass, not two.
1688        let input = r#"@settings(experimentalFeatures = allow)
1689
1690type Empty {
1691
1692  |
1693
1694}
1695"#;
1696        let expected = r#"@settings(experimentalFeatures = allow)
1697
1698type Empty { | }
1699"#;
1700        assert_recast(input, expected);
1701    }
1702
1703    #[test]
1704    fn recast_enum_no_variants_with_comments() {
1705        let input = r#"@settings(experimentalFeatures = allow)
1706
1707type Empty { /* a */ | /* b */ }
1708"#;
1709        let expected = r#"@settings(experimentalFeatures = allow)
1710
1711type Empty {
1712  /* a */
1713  /* b */
1714  |
1715}
1716"#;
1717        assert_recast(input, expected);
1718    }
1719
1720    #[test]
1721    fn recast_enum_with_comments() {
1722        let input = r#"@settings(experimentalFeatures = allow)
1723
1724type Color {
1725  // before red
1726  | Red // after red
1727  | /* inside green arm */ Green
1728
1729  | Blue
1730  // trailing
1731}
1732"#;
1733        // Comments between an arm's `|` and its name normalize to a line
1734        // above the arm; everything else keeps its position.
1735        let expected = r#"@settings(experimentalFeatures = allow)
1736
1737type Color {
1738  // before red
1739  | Red // after red
1740  /* inside green arm */
1741  | Green
1742
1743  | Blue
1744  // trailing
1745}
1746"#;
1747        assert_recast(input, expected);
1748    }
1749
1750    #[test]
1751    fn recast_enum_with_comment_above_declaration() {
1752        let input = r#"@settings(experimentalFeatures = allow)
1753
1754// palette
1755type Color {
1756  // before red
1757  | Red
1758}
1759"#;
1760        assert_recast(input, input);
1761    }
1762
1763    #[test]
1764    fn recast_enum_with_outer_annotation() {
1765        let input = r#"@settings(experimentalFeatures = allow)
1766
1767@(impl = kcl)
1768type Color {
1769  | Red
1770}
1771"#;
1772        assert_recast(input, input);
1773    }
1774
1775    #[test]
1776    fn recast_enum_export_annotation_and_comments() {
1777        let input = r#"@settings(experimentalFeatures = allow)
1778
1779// palette
1780@(impl = kcl)
1781export type Color {
1782  | Red // warm
1783}
1784"#;
1785        assert_recast(input, input);
1786    }
1787
1788    #[test]
1789    fn recast_enum_in_function_body() {
1790        let input = r#"@settings(experimentalFeatures = allow)
1791
1792fn palette() {
1793  type Color {
1794    | Red
1795    | Green
1796  }
1797  return 0
1798}
1799"#;
1800        assert_recast(input, input);
1801    }
1802
1803    #[test]
1804    fn test_recast_if_else_if_same() {
1805        let input = r#"b = if false {
1806  3
1807} else if true {
1808  4
1809} else {
1810  5
1811}
1812"#;
1813        let program = crate::parsing::top_level_parse(input).unwrap();
1814        let output = program.recast_top(&Default::default(), 0);
1815        assert_eq!(output, input);
1816    }
1817
1818    #[test]
1819    fn test_recast_if_same() {
1820        let input = r#"b = if false {
1821  3
1822} else {
1823  5
1824}
1825"#;
1826        let program = crate::parsing::top_level_parse(input).unwrap();
1827        let output = program.recast_top(&Default::default(), 0);
1828        assert_eq!(output, input);
1829    }
1830
1831    #[test]
1832    fn test_recast_import() {
1833        let input = r#"import a from "a.kcl"
1834import a as aaa from "a.kcl"
1835import a, b from "a.kcl"
1836import a as aaa, b from "a.kcl"
1837import a, b as bbb from "a.kcl"
1838import a as aaa, b as bbb from "a.kcl"
1839import "a_b.kcl"
1840import "a-b.kcl" as b
1841import * from "a.kcl"
1842export import a as aaa from "a.kcl"
1843export import a, b from "a.kcl"
1844export import a as aaa, b from "a.kcl"
1845export import a, b as bbb from "a.kcl"
1846"#;
1847        let program = crate::parsing::top_level_parse(input).unwrap();
1848        let output = program.recast_top(&Default::default(), 0);
1849        assert_eq!(output, input);
1850    }
1851
1852    #[test]
1853    fn test_recast_import_as_same_name() {
1854        let input = r#"import a as a from "a.kcl"
1855"#;
1856        let program = crate::parsing::top_level_parse(input).unwrap();
1857        let output = program.recast_top(&Default::default(), 0);
1858        let expected = r#"import a from "a.kcl"
1859"#;
1860        assert_eq!(output, expected);
1861    }
1862
1863    #[test]
1864    fn test_recast_export_fn() {
1865        let input = r#"export fn a() {
1866  return 0
1867}
1868"#;
1869        let program = crate::parsing::top_level_parse(input).unwrap();
1870        let output = program.recast_top(&Default::default(), 0);
1871        assert_eq!(output, input);
1872    }
1873
1874    #[test]
1875    fn test_recast_sketch_block_with_no_args() {
1876        let input = r#"sketch() {
1877  return 0
1878}
1879"#;
1880        let program = crate::parsing::top_level_parse(input).unwrap();
1881        let output = program.recast_top(&Default::default(), 0);
1882        assert_eq!(output, input);
1883    }
1884
1885    #[test]
1886    fn test_recast_sketch_block_with_labeled_args() {
1887        let input = r#"sketch(on = XY) {
1888  return 0
1889}
1890"#;
1891        let program = crate::parsing::top_level_parse(input).unwrap();
1892        let output = program.recast_top(&Default::default(), 0);
1893        assert_eq!(output, input);
1894    }
1895
1896    #[test]
1897    fn test_recast_sketch_block_with_arg_shorthand() {
1898        let input = r#"on = XY
1899sketch(on) {
1900  return 0
1901}
1902"#;
1903        let program = crate::parsing::top_level_parse(input).unwrap();
1904        let output = program.recast_top(&Default::default(), 0);
1905        assert_eq!(output, input);
1906    }
1907
1908    #[test]
1909    fn test_recast_sketch_block_with_arg_shorthand_and_comment() {
1910        // The comment stays after the shorthand argument: the parser moves the
1911        // shorthand `on` from the unlabeled slot into the argument list, which
1912        // must also shift the comment positions.
1913        let input = r#"on = XY
1914sketch(
1915  on,
1916  // plane
1917) {
1918  return 0
1919}
1920"#;
1921        let program = crate::parsing::top_level_parse(input).unwrap();
1922        let output = program.recast_top(&Default::default(), 0);
1923        assert_eq!(output, input);
1924
1925        // Check idempotency.
1926        let program = crate::parsing::top_level_parse(&output).unwrap();
1927        let output2 = program.recast_top(&Default::default(), 0);
1928        assert_eq!(output2, output);
1929    }
1930
1931    #[test]
1932    fn test_recast_sketch_block_with_statements_in_block() {
1933        let input = r#"sketch() {
1934  // Comments inside block.
1935  x = 5
1936  y = 2
1937}
1938"#;
1939        let program = crate::parsing::top_level_parse(input).unwrap();
1940        let output = program.recast_top(&Default::default(), 0);
1941        assert_eq!(output, input);
1942    }
1943
1944    #[test]
1945    fn test_recast_angle_dimension_constraint_indentation() {
1946        let input = r#"@settings(kclVersion = 2.0)
1947
1948sketch001 = sketch(on = XZ) {
1949  line1 = line(start = [var 0mm, var 127mm], end = [var 146.65mm, var 127mm])
1950  line2 = line(start = [var 146.65mm, var 127mm], end = [var 0mm, var -127mm])
1951  angleDimension(
1952  lines = [line1, line2],
1953  sector = 2,
1954  inverse = true,
1955  labelPosition = [137.6mm, 121.05mm],
1956) == 60deg
1957}
1958"#;
1959        let expected = r#"@settings(kclVersion = 2.0)
1960
1961sketch001 = sketch(on = XZ) {
1962  line1 = line(start = [var 0mm, var 127mm], end = [var 146.65mm, var 127mm])
1963  line2 = line(start = [var 146.65mm, var 127mm], end = [var 0mm, var -127mm])
1964  angleDimension(
1965    lines = [line1, line2],
1966    sector = 2,
1967    inverse = true,
1968    labelPosition = [137.6mm, 121.05mm],
1969  ) == 60deg
1970}
1971"#;
1972        assert_recast(input, expected);
1973    }
1974
1975    #[test]
1976    fn test_recast_bug_fn_in_fn() {
1977        let some_program_string = r#"// Start point (top left)
1978zoo_x = -20
1979zoo_y = 7
1980// Scale
1981s = 1 // s = 1 -> height of Z is 13.4mm
1982// Depth
1983d = 1
1984
1985fn rect(x, y, w, h) {
1986  startSketchOn(XY)
1987    |> startProfile(at = [x, y])
1988    |> xLine(length = w)
1989    |> yLine(length = h)
1990    |> xLine(length = -w)
1991    |> close()
1992    |> extrude(d)
1993}
1994
1995fn quad(x1, y1, x2, y2, x3, y3, x4, y4) {
1996  startSketchOn(XY)
1997    |> startProfile(at = [x1, y1])
1998    |> line(endAbsolute = [x2, y2])
1999    |> line(endAbsolute = [x3, y3])
2000    |> line(endAbsolute = [x4, y4])
2001    |> close()
2002    |> extrude(d)
2003}
2004
2005fn crosshair(x, y) {
2006  startSketchOn(XY)
2007    |> startProfile(at = [x, y])
2008    |> yLine(length = 1)
2009    |> yLine(length = -2)
2010    |> yLine(length = 1)
2011    |> xLine(length = 1)
2012    |> xLine(length = -2)
2013}
2014
2015fn z(z_x, z_y) {
2016  z_end_w = s * 8.4
2017  z_end_h = s * 3
2018  z_corner = s * 2
2019  z_w = z_end_w + 2 * z_corner
2020  z_h = z_w * 1.08130081300813
2021  rect(
2022    z_x,
2023    a = z_y,
2024    b = z_end_w,
2025    c = -z_end_h,
2026  )
2027  rect(
2028    z_x + z_w,
2029    a = z_y,
2030    b = -z_corner,
2031    c = -z_corner,
2032  )
2033  rect(
2034    z_x + z_w,
2035    a = z_y - z_h,
2036    b = -z_end_w,
2037    c = z_end_h,
2038  )
2039  rect(
2040    z_x,
2041    a = z_y - z_h,
2042    b = z_corner,
2043    c = z_corner,
2044  )
2045}
2046
2047fn o(c_x, c_y) {
2048  // Outer and inner radii
2049  o_r = s * 6.95
2050  i_r = 0.5652173913043478 * o_r
2051
2052  // Angle offset for diagonal break
2053  a = 7
2054
2055  // Start point for the top sketch
2056  o_x1 = c_x + o_r * cos((45 + a) / 360 * TAU)
2057  o_y1 = c_y + o_r * sin((45 + a) / 360 * TAU)
2058
2059  // Start point for the bottom sketch
2060  o_x2 = c_x + o_r * cos((225 + a) / 360 * TAU)
2061  o_y2 = c_y + o_r * sin((225 + a) / 360 * TAU)
2062
2063  // End point for the bottom startSketch
2064  o_x3 = c_x + o_r * cos((45 - a) / 360 * TAU)
2065  o_y3 = c_y + o_r * sin((45 - a) / 360 * TAU)
2066
2067  // Where is the center?
2068  // crosshair(c_x, c_y)
2069
2070
2071  startSketchOn(XY)
2072    |> startProfile(at = [o_x1, o_y1])
2073    |> arc(radius = o_r, angle_start = 45 + a, angle_end = 225 - a)
2074    |> angledLine(angle = 45, length = o_r - i_r)
2075    |> arc(radius = i_r, angle_start = 225 - a, angle_end = 45 + a)
2076    |> close()
2077    |> extrude(d)
2078
2079  startSketchOn(XY)
2080    |> startProfile(at = [o_x2, o_y2])
2081    |> arc(radius = o_r, angle_start = 225 + a, angle_end = 360 + 45 - a)
2082    |> angledLine(angle = 225, length = o_r - i_r)
2083    |> arc(radius = i_r, angle_start = 45 - a, angle_end = 225 + a - 360)
2084    |> close()
2085    |> extrude(d)
2086}
2087
2088fn zoo(x0, y0) {
2089  z(x = x0, y = y0)
2090  o(x = x0 + s * 20, y = y0 - (s * 6.7))
2091  o(x = x0 + s * 35, y = y0 - (s * 6.7))
2092}
2093
2094zoo(x = zoo_x, y = zoo_y)
2095"#;
2096        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2097
2098        let recasted = program.recast_top(&Default::default(), 0);
2099        assert_eq!(recasted, some_program_string);
2100    }
2101
2102    #[test]
2103    fn test_nested_fns_indent() {
2104        let some_program_string = "\
2105x = 1
2106fn rect(x, y, w, h) {
2107  y = 2
2108  z = 3
2109  startSketchOn(XY)
2110    |> startProfile(at = [x, y])
2111    |> xLine(length = w)
2112    |> yLine(length = h)
2113    |> xLine(length = -w)
2114    |> close()
2115    |> extrude(d)
2116}
2117";
2118        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2119
2120        let recasted = program.recast_top(&Default::default(), 0);
2121        assert_eq!(recasted, some_program_string);
2122    }
2123
2124    #[test]
2125    fn test_recast_bug_extra_parens() {
2126        let some_program_string = r#"// Ball Bearing
2127// A ball bearing is a type of rolling-element bearing that uses balls to maintain the separation between the bearing races. The primary purpose of a ball bearing is to reduce rotational friction and support radial and axial loads. 
2128
2129// Define constants like ball diameter, inside diameter, overhange length, and thickness
2130sphereDia = 0.5
2131insideDia = 1
2132thickness = 0.25
2133overHangLength = .4
2134
2135// Sketch and revolve the inside bearing piece
2136insideRevolve = startSketchOn(XZ)
2137  |> startProfile(at = [insideDia / 2, 0])
2138  |> line(end = [0, thickness + sphereDia / 2])
2139  |> line(end = [overHangLength, 0])
2140  |> line(end = [0, -thickness])
2141  |> line(end = [-overHangLength + thickness, 0])
2142  |> line(end = [0, -sphereDia])
2143  |> line(end = [overHangLength - thickness, 0])
2144  |> line(end = [0, -thickness])
2145  |> line(end = [-overHangLength, 0])
2146  |> close()
2147  |> revolve(axis = Y)
2148
2149// Sketch and revolve one of the balls and duplicate it using a circular pattern. (This is currently a workaround, we have a bug with rotating on a sketch that touches the rotation axis)
2150sphere = startSketchOn(XZ)
2151  |> startProfile(at = [
2152       0.05 + insideDia / 2 + thickness,
2153       0 - 0.05
2154     ])
2155  |> line(end = [sphereDia - 0.1, 0])
2156  |> arc(
2157       angle_start = 0,
2158       angle_end = -180,
2159       radius = sphereDia / 2 - 0.05
2160     )
2161  |> close()
2162  |> revolve(axis = X)
2163  |> patternCircular3d(
2164       axis = [0, 0, 1],
2165       center = [0, 0, 0],
2166       repetitions = 10,
2167       arcDegrees = 360,
2168       rotateDuplicates = true
2169     )
2170
2171// Sketch and revolve the outside bearing
2172outsideRevolve = startSketchOn(XZ)
2173  |> startProfile(at = [
2174       insideDia / 2 + thickness + sphereDia,
2175       0
2176       ]
2177     )
2178  |> line(end = [0, sphereDia / 2])
2179  |> line(end = [-overHangLength + thickness, 0])
2180  |> line(end = [0, thickness])
2181  |> line(end = [overHangLength, 0])
2182  |> line(end = [0, -2 * thickness - sphereDia])
2183  |> line(end = [-overHangLength, 0])
2184  |> line(end = [0, thickness])
2185  |> line(end = [overHangLength - thickness, 0])
2186  |> close()
2187  |> revolve(axis = Y)"#;
2188        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2189
2190        let recasted = program.recast_top(&Default::default(), 0);
2191        assert_eq!(
2192            recasted,
2193            r#"// Ball Bearing
2194// A ball bearing is a type of rolling-element bearing that uses balls to maintain the separation between the bearing races. The primary purpose of a ball bearing is to reduce rotational friction and support radial and axial loads.
2195
2196// Define constants like ball diameter, inside diameter, overhange length, and thickness
2197sphereDia = 0.5
2198insideDia = 1
2199thickness = 0.25
2200overHangLength = .4
2201
2202// Sketch and revolve the inside bearing piece
2203insideRevolve = startSketchOn(XZ)
2204  |> startProfile(at = [insideDia / 2, 0])
2205  |> line(end = [0, thickness + sphereDia / 2])
2206  |> line(end = [overHangLength, 0])
2207  |> line(end = [0, -thickness])
2208  |> line(end = [-overHangLength + thickness, 0])
2209  |> line(end = [0, -sphereDia])
2210  |> line(end = [overHangLength - thickness, 0])
2211  |> line(end = [0, -thickness])
2212  |> line(end = [-overHangLength, 0])
2213  |> close()
2214  |> revolve(axis = Y)
2215
2216// Sketch and revolve one of the balls and duplicate it using a circular pattern. (This is currently a workaround, we have a bug with rotating on a sketch that touches the rotation axis)
2217sphere = startSketchOn(XZ)
2218  |> startProfile(at = [
2219       0.05 + insideDia / 2 + thickness,
2220       0 - 0.05
2221     ])
2222  |> line(end = [sphereDia - 0.1, 0])
2223  |> arc(angle_start = 0, angle_end = -180, radius = sphereDia / 2 - 0.05)
2224  |> close()
2225  |> revolve(axis = X)
2226  |> patternCircular3d(
2227       axis = [0, 0, 1],
2228       center = [0, 0, 0],
2229       repetitions = 10,
2230       arcDegrees = 360,
2231       rotateDuplicates = true,
2232     )
2233
2234// Sketch and revolve the outside bearing
2235outsideRevolve = startSketchOn(XZ)
2236  |> startProfile(at = [
2237       insideDia / 2 + thickness + sphereDia,
2238       0
2239     ])
2240  |> line(end = [0, sphereDia / 2])
2241  |> line(end = [-overHangLength + thickness, 0])
2242  |> line(end = [0, thickness])
2243  |> line(end = [overHangLength, 0])
2244  |> line(end = [0, -2 * thickness - sphereDia])
2245  |> line(end = [-overHangLength, 0])
2246  |> line(end = [0, thickness])
2247  |> line(end = [overHangLength - thickness, 0])
2248  |> close()
2249  |> revolve(axis = Y)
2250"#
2251        );
2252    }
2253
2254    #[test]
2255    fn test_recast_fn_in_object() {
2256        let some_program_string = r#"bing = { yo = 55 }
2257myNestedVar = [{ prop = callExp(bing.yo) }]
2258"#;
2259        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2260
2261        let recasted = program.recast_top(&Default::default(), 0);
2262        assert_eq!(recasted, some_program_string);
2263    }
2264
2265    #[test]
2266    fn test_recast_fn_in_array() {
2267        let some_program_string = r#"bing = { yo = 55 }
2268myNestedVar = [callExp(bing.yo)]
2269"#;
2270        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2271
2272        let recasted = program.recast_top(&Default::default(), 0);
2273        assert_eq!(recasted, some_program_string);
2274    }
2275
2276    #[test]
2277    fn test_recast_ranges() {
2278        let some_program_string = r#"foo = [0..10]
2279ten = 10
2280bar = [0 + 1 .. ten]
2281"#;
2282        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2283
2284        let recasted = program.recast_top(&Default::default(), 0);
2285        assert_eq!(recasted, some_program_string);
2286    }
2287
2288    #[test]
2289    fn test_recast_space_in_fn_call() {
2290        let some_program_string = r#"fn thing (x) {
2291    return x + 1
2292}
2293
2294thing ( 1 )
2295"#;
2296        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2297
2298        let recasted = program.recast_top(&Default::default(), 0);
2299        assert_eq!(
2300            recasted,
2301            r#"fn thing(x) {
2302  return x + 1
2303}
2304
2305thing(1)
2306"#
2307        );
2308    }
2309
2310    #[test]
2311    fn test_recast_typed_fn() {
2312        let some_program_string = r#"fn thing(x: string, y: [bool]): number {
2313  return x + 1
2314}
2315"#;
2316        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2317
2318        let recasted = program.recast_top(&Default::default(), 0);
2319        assert_eq!(recasted, some_program_string);
2320    }
2321
2322    #[test]
2323    fn test_recast_typed_consts() {
2324        let some_program_string = r#"a = 42: number
2325export b = 3.2: number(ft)
2326c = "dsfds": A | B | C
2327d = [1]: [number]
2328e = foo: [number; 3]
2329f = [1, 2, 3]: [number; 1+]
2330f = [1, 2, 3]: [number; 3+]
2331"#;
2332        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2333
2334        let recasted = program.recast_top(&Default::default(), 0);
2335        assert_eq!(recasted, some_program_string);
2336    }
2337
2338    #[test]
2339    fn test_recast_object_fn_in_array_weird_bracket() {
2340        let some_program_string = r#"bing = { yo = 55 }
2341myNestedVar = [
2342  {
2343  prop:   line(a = [bing.yo, 21], b = sketch001)
2344}
2345]
2346"#;
2347        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2348
2349        let recasted = program.recast_top(&Default::default(), 0);
2350        let expected = r#"bing = { yo = 55 }
2351myNestedVar = [
2352  {
2353    prop = line(a = [bing.yo, 21], b = sketch001)
2354  }
2355]
2356"#;
2357        assert_eq!(recasted, expected,);
2358    }
2359
2360    #[test]
2361    fn test_recast_empty_file() {
2362        let some_program_string = r#""#;
2363        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2364
2365        let recasted = program.recast_top(&Default::default(), 0);
2366        // Its VERY important this comes back with zero new lines.
2367        assert_eq!(recasted, r#""#);
2368    }
2369
2370    #[test]
2371    fn test_recast_empty_file_new_line() {
2372        let some_program_string = r#"
2373"#;
2374        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2375
2376        let recasted = program.recast_top(&Default::default(), 0);
2377        // Its VERY important this comes back with zero new lines.
2378        assert_eq!(recasted, r#""#);
2379    }
2380
2381    #[test]
2382    fn test_recast_shebang() {
2383        let some_program_string = r#"#!/usr/local/env zoo kcl
2384part001 = startSketchOn(XY)
2385  |> startProfile(at = [-10, -10])
2386  |> line(end = [20, 0])
2387  |> line(end = [0, 20])
2388  |> line(end = [-20, 0])
2389  |> close()
2390"#;
2391
2392        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2393
2394        let recasted = program.recast_top(&Default::default(), 0);
2395        assert_eq!(
2396            recasted,
2397            r#"#!/usr/local/env zoo kcl
2398
2399part001 = startSketchOn(XY)
2400  |> startProfile(at = [-10, -10])
2401  |> line(end = [20, 0])
2402  |> line(end = [0, 20])
2403  |> line(end = [-20, 0])
2404  |> close()
2405"#
2406        );
2407    }
2408
2409    #[test]
2410    fn test_recast_shebang_new_lines() {
2411        let some_program_string = r#"#!/usr/local/env zoo kcl
2412        
2413
2414
2415part001 = startSketchOn(XY)
2416  |> startProfile(at = [-10, -10])
2417  |> line(end = [20, 0])
2418  |> line(end = [0, 20])
2419  |> line(end = [-20, 0])
2420  |> close()
2421"#;
2422
2423        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2424
2425        let recasted = program.recast_top(&Default::default(), 0);
2426        assert_eq!(
2427            recasted,
2428            r#"#!/usr/local/env zoo kcl
2429
2430part001 = startSketchOn(XY)
2431  |> startProfile(at = [-10, -10])
2432  |> line(end = [20, 0])
2433  |> line(end = [0, 20])
2434  |> line(end = [-20, 0])
2435  |> close()
2436"#
2437        );
2438    }
2439
2440    #[test]
2441    fn test_recast_shebang_with_comments() {
2442        let some_program_string = r#"#!/usr/local/env zoo kcl
2443        
2444// Yo yo my comments.
2445part001 = startSketchOn(XY)
2446  |> startProfile(at = [-10, -10])
2447  |> line(end = [20, 0])
2448  |> line(end = [0, 20])
2449  |> line(end = [-20, 0])
2450  |> close()
2451"#;
2452
2453        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2454
2455        let recasted = program.recast_top(&Default::default(), 0);
2456        assert_eq!(
2457            recasted,
2458            r#"#!/usr/local/env zoo kcl
2459
2460// Yo yo my comments.
2461part001 = startSketchOn(XY)
2462  |> startProfile(at = [-10, -10])
2463  |> line(end = [20, 0])
2464  |> line(end = [0, 20])
2465  |> line(end = [-20, 0])
2466  |> close()
2467"#
2468        );
2469    }
2470
2471    #[test]
2472    fn test_recast_empty_function_body_with_comments() {
2473        let input = r#"fn myFunc() {
2474  // Yo yo my comments.
2475}
2476"#;
2477
2478        let program = crate::parsing::top_level_parse(input).unwrap();
2479        let output = program.recast_top(&Default::default(), 0);
2480        assert_eq!(output, input);
2481    }
2482
2483    #[test]
2484    fn test_recast_large_file() {
2485        let some_program_string = r#"@settings(units=mm)
2486// define nts
2487radius = 6.0
2488width = 144.0
2489length = 83.0
2490depth = 45.0
2491thk = 5
2492hole_diam = 5
2493// define a rectangular shape func
2494fn rectShape(pos, w, l) {
2495  rr = startSketchOn(XY)
2496    |> startProfile(at = [pos[0] - (w / 2), pos[1] - (l / 2)])
2497    |> line(endAbsolute = [pos[0] + w / 2, pos[1] - (l / 2)], tag = $edge1)
2498    |> line(endAbsolute = [pos[0] + w / 2, pos[1] + l / 2], tag = $edge2)
2499    |> line(endAbsolute = [pos[0] - (w / 2), pos[1] + l / 2], tag = $edge3)
2500    |> close($edge4)
2501  return rr
2502}
2503// build the body of the focusrite scarlett solo gen 4
2504// only used for visualization
2505scarlett_body = rectShape(pos = [0, 0], w = width, l = length)
2506  |> extrude(depth)
2507  |> fillet(
2508       radius = radius,
2509       tags = [
2510  edge2,
2511  edge4,
2512  getOppositeEdge(edge2),
2513  getOppositeEdge(edge4)
2514]
2515   )
2516  // build the bracket sketch around the body
2517fn bracketSketch(w, d, t) {
2518  s = startSketchOn({
2519         plane = {
2520  origin = { x = 0, y = length / 2 + thk, z = 0 },
2521  x_axis = { x = 1, y = 0, z = 0 },
2522  y_axis = { x = 0, y = 0, z = 1 },
2523  z_axis = { x = 0, y = 1, z = 0 }
2524}
2525       })
2526    |> startProfile(at = [-w / 2 - t, d + t])
2527    |> line(endAbsolute = [-w / 2 - t, -t], tag = $edge1)
2528    |> line(endAbsolute = [w / 2 + t, -t], tag = $edge2)
2529    |> line(endAbsolute = [w / 2 + t, d + t], tag = $edge3)
2530    |> line(endAbsolute = [w / 2, d + t], tag = $edge4)
2531    |> line(endAbsolute = [w / 2, 0], tag = $edge5)
2532    |> line(endAbsolute = [-w / 2, 0], tag = $edge6)
2533    |> line(endAbsolute = [-w / 2, d + t], tag = $edge7)
2534    |> close($edge8)
2535  return s
2536}
2537// build the body of the bracket
2538bracket_body = bracketSketch(w = width, d = depth, t = thk)
2539  |> extrude(length + 10)
2540  |> fillet(
2541       radius = radius,
2542       tags = [
2543  getNextAdjacentEdge(edge7),
2544  getNextAdjacentEdge(edge2),
2545  getNextAdjacentEdge(edge3),
2546  getNextAdjacentEdge(edge6)
2547]
2548     )
2549  // build the tabs of the mounting bracket (right side)
2550tabs_r = startSketchOn({
2551       plane = {
2552  origin = { x = 0, y = 0, z = depth + thk },
2553  x_axis = { x = 1, y = 0, z = 0 },
2554  y_axis = { x = 0, y = 1, z = 0 },
2555  z_axis = { x = 0, y = 0, z = 1 }
2556}
2557     })
2558  |> startProfile(at = [width / 2 + thk, length / 2 + thk])
2559  |> line(end = [10, -5])
2560  |> line(end = [0, -10])
2561  |> line(end = [-10, -5])
2562  |> close()
2563  |> subtract2d(tool = circle(
2564       center = [
2565         width / 2 + thk + hole_diam,
2566         length / 2 - hole_diam
2567       ],
2568       radius = hole_diam / 2
2569     ))
2570  |> extrude(-thk)
2571  |> patternLinear3d(
2572       axis = [0, -1, 0],
2573       repetitions = 1,
2574       distance = length - 10
2575     )
2576  // build the tabs of the mounting bracket (left side)
2577tabs_l = startSketchOn({
2578       plane = {
2579  origin = { x = 0, y = 0, z = depth + thk },
2580  x_axis = { x = 1, y = 0, z = 0 },
2581  y_axis = { x = 0, y = 1, z = 0 },
2582  z_axis = { x = 0, y = 0, z = 1 }
2583}
2584     })
2585  |> startProfile(at = [-width / 2 - thk, length / 2 + thk])
2586  |> line(end = [-10, -5])
2587  |> line(end = [0, -10])
2588  |> line(end = [10, -5])
2589  |> close()
2590  |> subtract2d(tool = circle(
2591       center = [
2592         -width / 2 - thk - hole_diam,
2593         length / 2 - hole_diam
2594       ],
2595       radius = hole_diam / 2
2596     ))
2597  |> extrude(-thk)
2598  |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10ft)
2599"#;
2600        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2601
2602        let recasted = program.recast_top(&Default::default(), 0);
2603        // Its VERY important this comes back with zero new lines.
2604        assert_eq!(
2605            recasted,
2606            r#"@settings(units = mm)
2607
2608// define nts
2609radius = 6.0
2610width = 144.0
2611length = 83.0
2612depth = 45.0
2613thk = 5
2614hole_diam = 5
2615// define a rectangular shape func
2616fn rectShape(pos, w, l) {
2617  rr = startSketchOn(XY)
2618    |> startProfile(at = [pos[0] - (w / 2), pos[1] - (l / 2)])
2619    |> line(endAbsolute = [pos[0] + w / 2, pos[1] - (l / 2)], tag = $edge1)
2620    |> line(endAbsolute = [pos[0] + w / 2, pos[1] + l / 2], tag = $edge2)
2621    |> line(endAbsolute = [pos[0] - (w / 2), pos[1] + l / 2], tag = $edge3)
2622    |> close($edge4)
2623  return rr
2624}
2625// build the body of the focusrite scarlett solo gen 4
2626// only used for visualization
2627scarlett_body = rectShape(pos = [0, 0], w = width, l = length)
2628  |> extrude(depth)
2629  |> fillet(
2630       radius = radius,
2631       tags = [
2632         edge2,
2633         edge4,
2634         getOppositeEdge(edge2),
2635         getOppositeEdge(edge4)
2636       ],
2637     )
2638// build the bracket sketch around the body
2639fn bracketSketch(w, d, t) {
2640  s = startSketchOn({
2641    plane = {
2642      origin = { x = 0, y = length / 2 + thk, z = 0 },
2643      x_axis = { x = 1, y = 0, z = 0 },
2644      y_axis = { x = 0, y = 0, z = 1 },
2645      z_axis = { x = 0, y = 1, z = 0 }
2646    }
2647  })
2648    |> startProfile(at = [-w / 2 - t, d + t])
2649    |> line(endAbsolute = [-w / 2 - t, -t], tag = $edge1)
2650    |> line(endAbsolute = [w / 2 + t, -t], tag = $edge2)
2651    |> line(endAbsolute = [w / 2 + t, d + t], tag = $edge3)
2652    |> line(endAbsolute = [w / 2, d + t], tag = $edge4)
2653    |> line(endAbsolute = [w / 2, 0], tag = $edge5)
2654    |> line(endAbsolute = [-w / 2, 0], tag = $edge6)
2655    |> line(endAbsolute = [-w / 2, d + t], tag = $edge7)
2656    |> close($edge8)
2657  return s
2658}
2659// build the body of the bracket
2660bracket_body = bracketSketch(w = width, d = depth, t = thk)
2661  |> extrude(length + 10)
2662  |> fillet(
2663       radius = radius,
2664       tags = [
2665         getNextAdjacentEdge(edge7),
2666         getNextAdjacentEdge(edge2),
2667         getNextAdjacentEdge(edge3),
2668         getNextAdjacentEdge(edge6)
2669       ],
2670     )
2671// build the tabs of the mounting bracket (right side)
2672tabs_r = startSketchOn({
2673  plane = {
2674    origin = { x = 0, y = 0, z = depth + thk },
2675    x_axis = { x = 1, y = 0, z = 0 },
2676    y_axis = { x = 0, y = 1, z = 0 },
2677    z_axis = { x = 0, y = 0, z = 1 }
2678  }
2679})
2680  |> startProfile(at = [width / 2 + thk, length / 2 + thk])
2681  |> line(end = [10, -5])
2682  |> line(end = [0, -10])
2683  |> line(end = [-10, -5])
2684  |> close()
2685  |> subtract2d(tool = circle(
2686       center = [
2687         width / 2 + thk + hole_diam,
2688         length / 2 - hole_diam
2689       ],
2690       radius = hole_diam / 2,
2691     ))
2692  |> extrude(-thk)
2693  |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10)
2694// build the tabs of the mounting bracket (left side)
2695tabs_l = startSketchOn({
2696  plane = {
2697    origin = { x = 0, y = 0, z = depth + thk },
2698    x_axis = { x = 1, y = 0, z = 0 },
2699    y_axis = { x = 0, y = 1, z = 0 },
2700    z_axis = { x = 0, y = 0, z = 1 }
2701  }
2702})
2703  |> startProfile(at = [-width / 2 - thk, length / 2 + thk])
2704  |> line(end = [-10, -5])
2705  |> line(end = [0, -10])
2706  |> line(end = [10, -5])
2707  |> close()
2708  |> subtract2d(tool = circle(
2709       center = [
2710         -width / 2 - thk - hole_diam,
2711         length / 2 - hole_diam
2712       ],
2713       radius = hole_diam / 2,
2714     ))
2715  |> extrude(-thk)
2716  |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10ft)
2717"#
2718        );
2719    }
2720
2721    #[test]
2722    fn test_recast_nested_var_declaration_in_fn_body() {
2723        let some_program_string = r#"fn cube(pos, scale) {
2724   sg = startSketchOn(XY)
2725  |> startProfile(at = pos)
2726  |> line(end = [0, scale])
2727  |> line(end = [scale, 0])
2728  |> line(end = [0, -scale])
2729  |> close()
2730  |> extrude(scale)
2731}"#;
2732        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2733
2734        let recasted = program.recast_top(&Default::default(), 0);
2735        assert_eq!(
2736            recasted,
2737            r#"fn cube(pos, scale) {
2738  sg = startSketchOn(XY)
2739    |> startProfile(at = pos)
2740    |> line(end = [0, scale])
2741    |> line(end = [scale, 0])
2742    |> line(end = [0, -scale])
2743    |> close()
2744    |> extrude(scale)
2745}
2746"#
2747        );
2748    }
2749
2750    #[test]
2751    fn test_as() {
2752        let some_program_string = r#"fn cube(pos, scale) {
2753  x = dfsfs + dfsfsd as y
2754
2755  sg = startSketchOn(XY)
2756    |> startProfile(at = pos) as foo
2757    |> line([0, scale])
2758    |> line([scale, 0]) as bar
2759    |> line([0 as baz, -scale] as qux)
2760    |> close()
2761    |> extrude(length = scale)
2762}
2763
2764cube(pos = 0, scale = 0) as cub
2765"#;
2766        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2767
2768        let recasted = program.recast_top(&Default::default(), 0);
2769        assert_eq!(recasted, some_program_string,);
2770    }
2771
2772    #[test]
2773    fn test_recast_with_bad_indentation() {
2774        let some_program_string = r#"part001 = startSketchOn(XY)
2775  |> startProfile(at = [0.0, 5.0])
2776              |> line(end = [0.4900857016, -0.0240763666])
2777    |> line(end = [0.6804562304, 0.9087880491])"#;
2778        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2779
2780        let recasted = program.recast_top(&Default::default(), 0);
2781        assert_eq!(
2782            recasted,
2783            r#"part001 = startSketchOn(XY)
2784  |> startProfile(at = [0.0, 5.0])
2785  |> line(end = [0.4900857016, -0.0240763666])
2786  |> line(end = [0.6804562304, 0.9087880491])
2787"#
2788        );
2789    }
2790
2791    #[test]
2792    fn test_recast_with_bad_indentation_and_inline_comment() {
2793        let some_program_string = r#"part001 = startSketchOn(XY)
2794  |> startProfile(at = [0.0, 5.0])
2795              |> line(end = [0.4900857016, -0.0240763666]) // hello world
2796    |> line(end = [0.6804562304, 0.9087880491])"#;
2797        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2798
2799        let recasted = program.recast_top(&Default::default(), 0);
2800        assert_eq!(
2801            recasted,
2802            r#"part001 = startSketchOn(XY)
2803  |> startProfile(at = [0.0, 5.0])
2804  |> line(end = [0.4900857016, -0.0240763666]) // hello world
2805  |> line(end = [0.6804562304, 0.9087880491])
2806"#
2807        );
2808    }
2809    #[test]
2810    fn test_recast_with_bad_indentation_and_line_comment() {
2811        let some_program_string = r#"part001 = startSketchOn(XY)
2812  |> startProfile(at = [0.0, 5.0])
2813              |> line(end = [0.4900857016, -0.0240763666])
2814        // hello world
2815    |> line(end = [0.6804562304, 0.9087880491])"#;
2816        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2817
2818        let recasted = program.recast_top(&Default::default(), 0);
2819        assert_eq!(
2820            recasted,
2821            r#"part001 = startSketchOn(XY)
2822  |> startProfile(at = [0.0, 5.0])
2823  |> line(end = [0.4900857016, -0.0240763666])
2824  // hello world
2825  |> line(end = [0.6804562304, 0.9087880491])
2826"#
2827        );
2828    }
2829
2830    #[test]
2831    fn test_recast_comment_in_a_fn_block() {
2832        let some_program_string = r#"fn myFn() {
2833  // this is a comment
2834  yo = { a = { b = { c = '123' } } } /* block
2835  comment */
2836
2837  key = 'c'
2838  // this is also a comment
2839    return things
2840}"#;
2841        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2842
2843        let recasted = program.recast_top(&Default::default(), 0);
2844        assert_eq!(
2845            recasted,
2846            r#"fn myFn() {
2847  // this is a comment
2848  yo = { a = { b = { c = '123' } } } /* block
2849  comment */
2850
2851  key = 'c'
2852  // this is also a comment
2853  return things
2854}
2855"#
2856        );
2857    }
2858
2859    #[test]
2860    fn test_recast_comment_under_variable() {
2861        let some_program_string = r#"key = 'c'
2862// this is also a comment
2863thing = 'foo'
2864"#;
2865        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2866
2867        let recasted = program.recast_top(&Default::default(), 0);
2868        assert_eq!(
2869            recasted,
2870            r#"key = 'c'
2871// this is also a comment
2872thing = 'foo'
2873"#
2874        );
2875    }
2876
2877    #[test]
2878    fn test_recast_multiline_comment_start_file() {
2879        let some_program_string = r#"// hello world
2880// I am a comment
2881key = 'c'
2882// this is also a comment
2883// hello
2884thing = 'foo'
2885"#;
2886        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2887
2888        let recasted = program.recast_top(&Default::default(), 0);
2889        assert_eq!(
2890            recasted,
2891            r#"// hello world
2892// I am a comment
2893key = 'c'
2894// this is also a comment
2895// hello
2896thing = 'foo'
2897"#
2898        );
2899    }
2900
2901    #[test]
2902    fn test_recast_empty_comment() {
2903        let some_program_string = r#"// hello world
2904//
2905// I am a comment
2906key = 'c'
2907
2908//
2909// I am a comment
2910thing = 'c'
2911
2912foo = 'bar' //
2913"#;
2914        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2915
2916        let recasted = program.recast_top(&Default::default(), 0);
2917        assert_eq!(
2918            recasted,
2919            r#"// hello world
2920//
2921// I am a comment
2922key = 'c'
2923
2924//
2925// I am a comment
2926thing = 'c'
2927
2928foo = 'bar' //
2929"#
2930        );
2931    }
2932
2933    #[test]
2934    fn test_recast_multiline_comment_under_variable() {
2935        let some_program_string = r#"key = 'c'
2936// this is also a comment
2937// hello
2938thing = 'foo'
2939"#;
2940        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2941
2942        let recasted = program.recast_top(&Default::default(), 0);
2943        assert_eq!(
2944            recasted,
2945            r#"key = 'c'
2946// this is also a comment
2947// hello
2948thing = 'foo'
2949"#
2950        );
2951    }
2952
2953    #[test]
2954    fn test_recast_only_line_comments() {
2955        let code = r#"// comment at start
2956"#;
2957        let program = crate::parsing::top_level_parse(code).unwrap();
2958
2959        assert_eq!(program.recast_top(&Default::default(), 0), code);
2960    }
2961
2962    #[test]
2963    fn test_recast_comment_at_start() {
2964        let test_program = r#"
2965/* comment at start */
2966
2967mySk1 = startSketchOn(XY)
2968  |> startProfile(at = [0, 0])"#;
2969        let program = crate::parsing::top_level_parse(test_program).unwrap();
2970
2971        let recasted = program.recast_top(&Default::default(), 0);
2972        assert_eq!(
2973            recasted,
2974            r#"/* comment at start */
2975
2976mySk1 = startSketchOn(XY)
2977  |> startProfile(at = [0, 0])
2978"#
2979        );
2980    }
2981
2982    #[test]
2983    fn test_recast_lots_of_comments() {
2984        let some_program_string = r#"// comment at start
2985mySk1 = startSketchOn(XY)
2986  |> startProfile(at = [0, 0])
2987  |> line(endAbsolute = [1, 1])
2988  // comment here
2989  |> line(endAbsolute = [0, 1], tag = $myTag)
2990  |> line(endAbsolute = [1, 1])
2991  /* and
2992  here
2993  */
2994  // a comment between pipe expression statements
2995  |> rx(90)
2996  // and another with just white space between others below
2997  |> ry(45)
2998  |> rx(45)
2999// one more for good measure"#;
3000        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3001
3002        let recasted = program.recast_top(&Default::default(), 0);
3003        assert_eq!(
3004            recasted,
3005            r#"// comment at start
3006mySk1 = startSketchOn(XY)
3007  |> startProfile(at = [0, 0])
3008  |> line(endAbsolute = [1, 1])
3009  // comment here
3010  |> line(endAbsolute = [0, 1], tag = $myTag)
3011  |> line(endAbsolute = [1, 1])
3012  /* and
3013  here */
3014  // a comment between pipe expression statements
3015  |> rx(90)
3016  // and another with just white space between others below
3017  |> ry(45)
3018  |> rx(45)
3019// one more for good measure
3020"#
3021        );
3022    }
3023
3024    #[test]
3025    fn test_recast_multiline_object() {
3026        let some_program_string = r#"x = {
3027  a = 1000000000,
3028  b = 2000000000,
3029  c = 3000000000,
3030  d = 4000000000,
3031  e = 5000000000
3032}"#;
3033        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3034
3035        let recasted = program.recast_top(&Default::default(), 0);
3036        assert_eq!(recasted.trim(), some_program_string);
3037    }
3038
3039    #[test]
3040    fn test_recast_first_level_object() {
3041        let some_program_string = r#"three = 3
3042
3043yo = {
3044  aStr = 'str',
3045  anum = 2,
3046  identifier = three,
3047  binExp = 4 + 5
3048}
3049yo = [
3050  1,
3051  "  2,",
3052  "three",
3053  4 + 5,
3054  "  hey oooooo really long long long"
3055]
3056"#;
3057        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3058
3059        let recasted = program.recast_top(&Default::default(), 0);
3060        assert_eq!(recasted, some_program_string);
3061    }
3062
3063    #[test]
3064    fn test_recast_new_line_before_comment() {
3065        let some_program_string = r#"
3066// this is a comment
3067yo = { a = { b = { c = '123' } } }
3068
3069key = 'c'
3070things = "things"
3071
3072// this is also a comment"#;
3073        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3074
3075        let recasted = program.recast_top(&Default::default(), 0);
3076        let expected = some_program_string.trim();
3077        // Currently new parser removes an empty line
3078        let actual = recasted.trim();
3079        assert_eq!(actual, expected);
3080    }
3081
3082    #[test]
3083    fn test_recast_comment_tokens_inside_strings() {
3084        let some_program_string = r#"b = {
3085  end = 141,
3086  start = 125,
3087  type_ = "NonCodeNode",
3088  value = "
3089 // a comment
3090   "
3091}"#;
3092        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3093
3094        let recasted = program.recast_top(&Default::default(), 0);
3095        assert_eq!(recasted.trim(), some_program_string.trim());
3096    }
3097
3098    #[test]
3099    fn test_recast_array_new_line_in_pipe() {
3100        let some_program_string = r#"myVar = 3
3101myVar2 = 5
3102myVar3 = 6
3103myAng = 40
3104myAng2 = 134
3105part001 = startSketchOn(XY)
3106  |> startProfile(at = [0, 0])
3107  |> line(end = [1, 3.82], tag = $seg01) // ln-should-get-tag
3108  |> angledLine(angle = -foo(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
3109  |> angledLine(angle = -bar(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper"#;
3110        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3111
3112        let recasted = program.recast_top(&Default::default(), 0);
3113        assert_eq!(recasted.trim(), some_program_string);
3114    }
3115
3116    #[test]
3117    fn test_recast_array_new_line_in_pipe_custom() {
3118        let some_program_string = r#"myVar = 3
3119myVar2 = 5
3120myVar3 = 6
3121myAng = 40
3122myAng2 = 134
3123part001 = startSketchOn(XY)
3124   |> startProfile(at = [0, 0])
3125   |> line(end = [1, 3.82], tag = $seg01) // ln-should-get-tag
3126   |> angledLine(angle = -foo(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
3127   |> angledLine(angle = -bar(x = seg01, y = myVar, z = %), length = myVar) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper
3128"#;
3129        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3130
3131        let recasted = program.recast_top(
3132            &FormatOptions {
3133                tab_size: 3,
3134                use_tabs: false,
3135                insert_final_newline: true,
3136            },
3137            0,
3138        );
3139        assert_eq!(recasted, some_program_string);
3140    }
3141
3142    #[test]
3143    fn test_recast_after_rename_std() {
3144        let some_program_string = r#"part001 = startSketchOn(XY)
3145  |> startProfile(at = [0.0000000000, 5.0000000000])
3146    |> line(end = [0.4900857016, -0.0240763666])
3147
3148part002 = "part002"
3149things = [part001, 0.0]
3150blah = 1
3151foo = false
3152baz = {a: 1, part001: "thing"}
3153
3154fn ghi(part001) {
3155  return part001
3156}
3157"#;
3158        let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
3159        assert!(program.rename_symbol("mySuperCoolPart", 6));
3160
3161        let recasted = program.recast_top(&Default::default(), 0);
3162        assert_eq!(
3163            recasted,
3164            r#"mySuperCoolPart = startSketchOn(XY)
3165  |> startProfile(at = [0.0, 5.0])
3166  |> line(end = [0.4900857016, -0.0240763666])
3167
3168part002 = "part002"
3169things = [mySuperCoolPart, 0.0]
3170blah = 1
3171foo = false
3172baz = { a = 1, part001 = "thing" }
3173
3174fn ghi(part001) {
3175  return part001
3176}
3177"#
3178        );
3179    }
3180
3181    #[test]
3182    fn test_recast_after_rename_fn_args() {
3183        let some_program_string = r#"fn ghi(x, y, z) {
3184  return x
3185}"#;
3186        let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
3187        assert!(program.rename_symbol("newName", 7));
3188
3189        let recasted = program.recast_top(&Default::default(), 0);
3190        assert_eq!(
3191            recasted,
3192            r#"fn ghi(newName, y, z) {
3193  return newName
3194}
3195"#
3196        );
3197    }
3198
3199    #[test]
3200    fn test_recast_trailing_comma() {
3201        let some_program_string = r#"startSketchOn(XY)
3202  |> startProfile(at = [0, 0])
3203  |> arc({
3204    radius = 1,
3205    angle_start = 0,
3206    angle_end = 180,
3207  })"#;
3208        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3209
3210        let recasted = program.recast_top(&Default::default(), 0);
3211        assert_eq!(
3212            recasted,
3213            r#"startSketchOn(XY)
3214  |> startProfile(at = [0, 0])
3215  |> arc({
3216       radius = 1,
3217       angle_start = 0,
3218       angle_end = 180
3219     })
3220"#
3221        );
3222    }
3223
3224    #[test]
3225    fn test_recast_array_no_trailing_comma_with_comments() {
3226        let some_program_string = r#"[
3227  1, // one
3228  2, // two
3229  3  // three
3230]"#;
3231        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3232
3233        let recasted = program.recast_top(&Default::default(), 0);
3234        assert_eq!(
3235            recasted,
3236            r#"[
3237  1,
3238  // one
3239  2,
3240  // two
3241  3,
3242  // three
3243]
3244"#
3245        );
3246    }
3247
3248    #[test]
3249    fn test_recast_object_no_trailing_comma_with_comments() {
3250        let some_program_string = r#"{
3251  x=1, // one
3252  y=2, // two
3253  z=3  // three
3254}"#;
3255        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3256
3257        let recasted = program.recast_top(&Default::default(), 0);
3258        // TODO: We should probably not add an extra new line after the last
3259        // comment.
3260        assert_eq!(
3261            recasted,
3262            r#"{
3263  x = 1,
3264  // one
3265  y = 2,
3266  // two
3267  z = 3,
3268  // three
3269
3270}
3271"#
3272        );
3273    }
3274
3275    #[test]
3276    fn test_recast_comment_between_call_args() {
3277        let some_program_string = r#"rounded = fillet(
3278  body,
3279  radius = 1mm,
3280  // Keep this comment
3281  tags = [tag1, tag2],
3282)
3283"#;
3284        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3285
3286        let recasted = program.recast_top(&Default::default(), 0);
3287        assert_eq!(recasted, some_program_string);
3288
3289        // Check idempotency.
3290        let program = crate::parsing::top_level_parse(&recasted).unwrap();
3291        let recasted2 = program.recast_top(&Default::default(), 0);
3292        assert_eq!(recasted2, recasted);
3293    }
3294
3295    #[test]
3296    fn test_recast_line_comments_in_call_args_force_multiline() {
3297        let some_program_string = r#"edged = chamfer(
3298  body,
3299  // leading
3300  length = 1mm,
3301  tags = [tag1],
3302  // trailing
3303)
3304"#;
3305        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3306
3307        let recasted = program.recast_top(&Default::default(), 0);
3308        assert_eq!(recasted, some_program_string);
3309
3310        // Check idempotency.
3311        let program = crate::parsing::top_level_parse(&recasted).unwrap();
3312        let recasted2 = program.recast_top(&Default::default(), 0);
3313        assert_eq!(recasted2, recasted);
3314    }
3315
3316    #[test]
3317    fn test_recast_block_comment_in_call_args_stays_inline() {
3318        // A block comment shouldn't force the call onto multiple lines.
3319        let some_program_string = r#"rounded = fillet(body, radius = 1mm, /* mid */ tags = [tag1])
3320"#;
3321        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3322
3323        let recasted = program.recast_top(&Default::default(), 0);
3324        assert_eq!(recasted, some_program_string);
3325
3326        // Check idempotency.
3327        let program = crate::parsing::top_level_parse(&recasted).unwrap();
3328        let recasted2 = program.recast_top(&Default::default(), 0);
3329        assert_eq!(recasted2, recasted);
3330    }
3331
3332    #[test]
3333    fn test_recast_block_comment_in_multiline_call_args_stays_inline() {
3334        // In a call that's multi-line anyway, a block comment stays glued to
3335        // the argument it precedes.
3336        let some_program_string = r#"rounded = fillet(
3337  body,
3338  radius = 1mm,
3339  /* mid */ tags = [tag1],
3340  tag = $x,
3341)
3342"#;
3343        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3344
3345        let recasted = program.recast_top(&Default::default(), 0);
3346        assert_eq!(recasted, some_program_string);
3347
3348        // Check idempotency.
3349        let program = crate::parsing::top_level_parse(&recasted).unwrap();
3350        let recasted2 = program.recast_top(&Default::default(), 0);
3351        assert_eq!(recasted2, recasted);
3352    }
3353
3354    #[test]
3355    fn test_recast_comment_in_call_args_in_pipe() {
3356        let some_program_string = r#"part = startSketchOn(XY)
3357  |> startProfile(at = [0, 0])
3358  |> fillet(
3359       radius = 1,
3360       // why
3361       tags = [a],
3362     )
3363"#;
3364        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3365
3366        let recasted = program.recast_top(&Default::default(), 0);
3367        assert_eq!(recasted, some_program_string);
3368    }
3369
3370    #[test]
3371    fn test_recast_negative_var() {
3372        let some_program_string = r#"w = 20
3373l = 8
3374h = 10
3375
3376firstExtrude = startSketchOn(XY)
3377  |> startProfile(at = [0,0])
3378  |> line(end = [0, l])
3379  |> line(end = [w, 0])
3380  |> line(end = [0, -l])
3381  |> close()
3382  |> extrude(h)
3383"#;
3384        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3385
3386        let recasted = program.recast_top(&Default::default(), 0);
3387        assert_eq!(
3388            recasted,
3389            r#"w = 20
3390l = 8
3391h = 10
3392
3393firstExtrude = startSketchOn(XY)
3394  |> startProfile(at = [0, 0])
3395  |> line(end = [0, l])
3396  |> line(end = [w, 0])
3397  |> line(end = [0, -l])
3398  |> close()
3399  |> extrude(h)
3400"#
3401        );
3402    }
3403
3404    #[test]
3405    fn test_recast_multiline_comment() {
3406        let some_program_string = r#"w = 20
3407l = 8
3408h = 10
3409
3410// This is my comment
3411// It has multiple lines
3412// And it's really long
3413firstExtrude = startSketchOn(XY)
3414  |> startProfile(at = [0,0])
3415  |> line(end = [0, l])
3416  |> line(end = [w, 0])
3417  |> line(end = [0, -l])
3418  |> close()
3419  |> extrude(h)
3420"#;
3421        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3422
3423        let recasted = program.recast_top(&Default::default(), 0);
3424        assert_eq!(
3425            recasted,
3426            r#"w = 20
3427l = 8
3428h = 10
3429
3430// This is my comment
3431// It has multiple lines
3432// And it's really long
3433firstExtrude = startSketchOn(XY)
3434  |> startProfile(at = [0, 0])
3435  |> line(end = [0, l])
3436  |> line(end = [w, 0])
3437  |> line(end = [0, -l])
3438  |> close()
3439  |> extrude(h)
3440"#
3441        );
3442    }
3443
3444    #[test]
3445    fn test_recast_math_start_negative() {
3446        let some_program_string = r#"myVar = -5 + 6"#;
3447        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3448
3449        let recasted = program.recast_top(&Default::default(), 0);
3450        assert_eq!(recasted.trim(), some_program_string);
3451    }
3452
3453    #[test]
3454    fn test_recast_math_negate_parens() {
3455        let some_program_string = r#"wallMountL = 3.82
3456thickness = 0.5
3457
3458startSketchOn(XY)
3459  |> startProfile(at = [0, 0])
3460  |> line(end = [0, -(wallMountL - thickness)])
3461  |> line(end = [0, -(5 - thickness)])
3462  |> line(end = [0, -(5 - 1)])
3463  |> line(end = [0, -(-5 - 1)])"#;
3464        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3465
3466        let recasted = program.recast_top(&Default::default(), 0);
3467        assert_eq!(recasted.trim(), some_program_string);
3468    }
3469
3470    #[test]
3471    fn test_recast_math_nested_parens() {
3472        let some_program_string = r#"distance = 5
3473p = 3: Plane
3474FOS = { a = 3, b = 42 }: Sketch
3475sigmaAllow = 8: number(mm)
3476width = 20
3477thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
3478        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3479
3480        let recasted = program.recast_top(&Default::default(), 0);
3481        assert_eq!(recasted.trim(), some_program_string);
3482    }
3483
3484    #[test]
3485    fn no_vardec_keyword() {
3486        let some_program_string = r#"distance = 5"#;
3487        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3488
3489        let recasted = program.recast_top(&Default::default(), 0);
3490        assert_eq!(recasted.trim(), some_program_string);
3491    }
3492
3493    #[test]
3494    fn recast_types() {
3495        let some_program_string = r#"type foo
3496
3497// A comment
3498@(impl = primitive)
3499export type bar(unit, baz)
3500type baz = Foo | Bar
3501type UnionOfArrays = [Foo] | [Bar] | Foo | { a: T, b: Foo | Bar | [Baz] }
3502"#;
3503        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3504        let recasted = program.recast_top(&Default::default(), 0);
3505        assert_eq!(recasted, some_program_string);
3506    }
3507
3508    #[test]
3509    fn recast_nested_fn() {
3510        let some_program_string = r#"fn f() {
3511  return fn() {
3512  return 1
3513}
3514}"#;
3515        let program = crate::parsing::top_level_parse(some_program_string).unwrap();
3516        let recasted = program.recast_top(&Default::default(), 0);
3517        let expected = "\
3518fn f() {
3519  return fn() {
3520    return 1
3521  }
3522}";
3523        assert_eq!(recasted.trim(), expected);
3524    }
3525
3526    #[test]
3527    fn recast_literal() {
3528        use winnow::Parser;
3529        for (i, (raw, expected, reason)) in [
3530            (
3531                "5.0",
3532                "5.0",
3533                "fractional numbers should stay fractional, i.e. don't reformat this to '5'",
3534            ),
3535            (
3536                "5",
3537                "5",
3538                "integers should stay integral, i.e. don't reformat this to '5.0'",
3539            ),
3540            (
3541                "5.0000000",
3542                "5.0",
3543                "if the number is f64 but not fractional, use its canonical format",
3544            ),
3545            ("5.1", "5.1", "straightforward case works"),
3546        ]
3547        .into_iter()
3548        .enumerate()
3549        {
3550            let tokens = crate::parsing::token::lex(raw, ModuleId::default()).unwrap();
3551            let literal = crate::parsing::parser::unsigned_number_literal
3552                .parse(tokens.as_slice())
3553                .unwrap();
3554            let mut actual = String::new();
3555            literal.recast(&mut actual);
3556            assert_eq!(actual, expected, "failed test {i}, which is testing that {reason}");
3557        }
3558    }
3559
3560    #[test]
3561    fn recast_objects_no_comments() {
3562        let input = r#"
3563sketch002 = startSketchOn({
3564       plane: {
3565    origin: { x = 1, y = 2, z = 3 },
3566    x_axis = { x = 4, y = 5, z = 6 },
3567    y_axis = { x = 7, y = 8, z = 9 },
3568    z_axis = { x = 10, y = 11, z = 12 }
3569       }
3570  })
3571"#;
3572        let expected = r#"sketch002 = startSketchOn({
3573  plane = {
3574    origin = { x = 1, y = 2, z = 3 },
3575    x_axis = { x = 4, y = 5, z = 6 },
3576    y_axis = { x = 7, y = 8, z = 9 },
3577    z_axis = { x = 10, y = 11, z = 12 }
3578  }
3579})
3580"#;
3581        let ast = crate::parsing::top_level_parse(input).unwrap();
3582        let actual = ast.recast_top(&FormatOptions::new(), 0);
3583        assert_eq!(actual, expected);
3584    }
3585
3586    #[test]
3587    fn unparse_fn_unnamed() {
3588        let input = "\
3589squares_out = reduce(
3590  arr,
3591  n = 0: number,
3592  f = fn(@i, accum) {
3593    return 1
3594  },
3595)
3596";
3597        let ast = crate::parsing::top_level_parse(input).unwrap();
3598        let actual = ast.recast_top(&FormatOptions::new(), 0);
3599        assert_eq!(actual, input);
3600    }
3601
3602    #[test]
3603    fn unparse_fn_named() {
3604        let input = r#"fn f(x) {
3605  return 1
3606}
3607"#;
3608        let ast = crate::parsing::top_level_parse(input).unwrap();
3609        let actual = ast.recast_top(&FormatOptions::new(), 0);
3610        assert_eq!(actual, input);
3611    }
3612
3613    #[test]
3614    fn unparse_call_inside_function_single_line() {
3615        let input = r#"fn foo() {
3616  toDegrees(atan(0.5), foo = 1)
3617  return 0
3618}
3619"#;
3620        let ast = crate::parsing::top_level_parse(input).unwrap();
3621        let actual = ast.recast_top(&FormatOptions::new(), 0);
3622        assert_eq!(actual, input);
3623    }
3624
3625    #[test]
3626    fn recast_function_types() {
3627        let input = r#"foo = x: fn
3628foo = x: fn(number)
3629fn foo(x: fn(): number): fn {
3630  return 0
3631}
3632fn foo(x: fn(a, b: number(mm), c: d): number(Angle)): fn {
3633  return 0
3634}
3635type fn
3636type foo = fn
3637type foo = fn(a: string, b: { f: fn(): any })
3638type foo = fn([fn])
3639type foo = fn(fn, f: fn(number(_))): [fn([any]): string]
3640"#;
3641        let ast = crate::parsing::top_level_parse(input).unwrap();
3642        let actual = ast.recast_top(&FormatOptions::new(), 0);
3643        assert_eq!(actual, input);
3644    }
3645
3646    #[test]
3647    fn unparse_call_inside_function_args_multiple_lines() {
3648        let input = r#"fn foo() {
3649  toDegrees(
3650    atan(0.5),
3651    foo = 1,
3652    bar = 2,
3653    baz = 3,
3654    qux = 4,
3655  )
3656  return 0
3657}
3658"#;
3659        let ast = crate::parsing::top_level_parse(input).unwrap();
3660        let actual = ast.recast_top(&FormatOptions::new(), 0);
3661        assert_eq!(actual, input);
3662    }
3663
3664    #[test]
3665    fn unparse_call_inside_function_single_arg_multiple_lines() {
3666        let input = r#"fn foo() {
3667  toDegrees(
3668    [
3669      profile0,
3670      profile1,
3671      profile2,
3672      profile3,
3673      profile4,
3674      profile5
3675    ],
3676    key = 1,
3677  )
3678  return 0
3679}
3680"#;
3681        let ast = crate::parsing::top_level_parse(input).unwrap();
3682        let actual = ast.recast_top(&FormatOptions::new(), 0);
3683        assert_eq!(actual, input);
3684    }
3685
3686    #[test]
3687    fn recast_objects_with_comments() {
3688        use winnow::Parser;
3689        for (i, (input, expected, reason)) in [(
3690            "\
3691{
3692  a = 1,
3693  // b = 2,
3694  c = 3
3695}",
3696            "\
3697{
3698  a = 1,
3699  // b = 2,
3700  c = 3
3701}",
3702            "preserves comments",
3703        )]
3704        .into_iter()
3705        .enumerate()
3706        {
3707            let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
3708            crate::parsing::parser::print_tokens(tokens.as_slice());
3709            let expr = crate::parsing::parser::object.parse(tokens.as_slice()).unwrap();
3710            let mut actual = String::new();
3711            expr.recast(&mut actual, &FormatOptions::new(), 0, ExprContext::Other);
3712            assert_eq!(
3713                actual, expected,
3714                "failed test {i}, which is testing that recasting {reason}"
3715            );
3716        }
3717    }
3718
3719    #[test]
3720    fn recast_array_with_comments() {
3721        use winnow::Parser;
3722        for (i, (input, expected, reason)) in [
3723            (
3724                "\
3725[
3726  1,
3727  2,
3728  3,
3729  4,
3730  5,
3731  6,
3732  7,
3733  8,
3734  9,
3735  10,
3736  11,
3737  12,
3738  13,
3739  14,
3740  15,
3741  16,
3742  17,
3743  18,
3744  19,
3745  20,
3746]",
3747                "\
3748[
3749  1,
3750  2,
3751  3,
3752  4,
3753  5,
3754  6,
3755  7,
3756  8,
3757  9,
3758  10,
3759  11,
3760  12,
3761  13,
3762  14,
3763  15,
3764  16,
3765  17,
3766  18,
3767  19,
3768  20
3769]",
3770                "preserves multi-line arrays",
3771            ),
3772            (
3773                "\
3774[
3775  1,
3776  // 2,
3777  3
3778]",
3779                "\
3780[
3781  1,
3782  // 2,
3783  3
3784]",
3785                "preserves comments",
3786            ),
3787            (
3788                "\
3789[
3790  1,
3791  2,
3792  // 3
3793]",
3794                "\
3795[
3796  1,
3797  2,
3798  // 3
3799]",
3800                "preserves comments at the end of the array",
3801            ),
3802        ]
3803        .into_iter()
3804        .enumerate()
3805        {
3806            let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
3807            let expr = crate::parsing::parser::array_elem_by_elem
3808                .parse(tokens.as_slice())
3809                .unwrap();
3810            let mut actual = String::new();
3811            expr.recast(&mut actual, &FormatOptions::new(), 0, ExprContext::Other);
3812            assert_eq!(
3813                actual, expected,
3814                "failed test {i}, which is testing that recasting {reason}"
3815            );
3816        }
3817    }
3818
3819    #[test]
3820    fn code_with_comment_and_extra_lines() {
3821        let code = r#"yo = 'c'
3822
3823/* this is
3824a
3825comment */
3826yo = 'bing'
3827"#;
3828        let ast = crate::parsing::top_level_parse(code).unwrap();
3829        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3830        assert_eq!(recasted, code);
3831    }
3832
3833    #[test]
3834    fn comments_in_a_fn_block() {
3835        let code = r#"fn myFn() {
3836  // this is a comment
3837  yo = { a = { b = { c = '123' } } }
3838
3839  /* block
3840  comment */
3841  key = 'c'
3842  // this is also a comment
3843}
3844"#;
3845        let ast = crate::parsing::top_level_parse(code).unwrap();
3846        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3847        assert_eq!(recasted, code);
3848    }
3849
3850    #[test]
3851    fn array_range_end_exclusive() {
3852        let code = "myArray = [0..<4]\n";
3853        let ast = crate::parsing::top_level_parse(code).unwrap();
3854        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3855        assert_eq!(recasted, code);
3856    }
3857
3858    #[test]
3859    fn paren_precedence() {
3860        let code = r#"x = 1 - 2 - 3
3861x = (1 - 2) - 3
3862x = 1 - (2 - 3)
3863x = 1 + 2 + 3
3864x = (1 + 2) + 3
3865x = 1 + (2 + 3)
3866x = 2 * (y % 2)
3867x = (2 * y) % 2
3868x = 2 % (y * 2)
3869x = (2 % y) * 2
3870x = 2 * y % 2
3871"#;
3872
3873        let expected = r#"x = 1 - 2 - 3
3874x = 1 - 2 - 3
3875x = 1 - (2 - 3)
3876x = 1 + 2 + 3
3877x = 1 + 2 + 3
3878x = 1 + 2 + 3
3879x = 2 * (y % 2)
3880x = 2 * y % 2
3881x = 2 % (y * 2)
3882x = 2 % y * 2
3883x = 2 * y % 2
3884"#;
3885        let ast = crate::parsing::top_level_parse(code).unwrap();
3886        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3887        assert_eq!(recasted, expected);
3888    }
3889
3890    #[test]
3891    fn gap_between_body_item_and_documented_fn() {
3892        let code = "\
3893x = 360
3894
3895// Watermelon
3896fn myFn() {
3897}
3898";
3899        let ast = crate::parsing::top_level_parse(code).unwrap();
3900        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3901        let expected = code;
3902        assert_eq!(recasted, expected);
3903    }
3904
3905    #[test]
3906    fn simple_assignment_in_fn() {
3907        let code = "\
3908fn function001() {
3909  extrude002 = extrude()
3910}\n";
3911
3912        let ast = crate::parsing::top_level_parse(code).unwrap();
3913        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3914        let expected = code;
3915        assert_eq!(recasted, expected);
3916    }
3917
3918    #[test]
3919    fn no_weird_extra_lines() {
3920        // Regression test, this used to insert a lot of new lines
3921        // between the initial comment and the @settings.
3922        let code = "\
3923// Initial comment
3924
3925@settings(defaultLengthUnit = mm)
3926
3927x = 1
3928";
3929        let ast = crate::parsing::top_level_parse(code).unwrap();
3930        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3931        let expected = code;
3932        assert_eq!(recasted, expected);
3933    }
3934
3935    #[test]
3936    fn settings_then_code_is_stable() {
3937        let code = "\
3938@settings(defaultLengthUnit = in)
3939
3940import \"cube-inches.kcl\" as cubeIn
3941import \"cube-mm.kcl\" as cubeMm
3942
3943cubeIn
3944cubeMm
3945";
3946        let formatted_once = crate::parsing::top_level_parse(code)
3947            .unwrap()
3948            .recast_top(&FormatOptions::new(), 0);
3949        assert_eq!(formatted_once, code);
3950
3951        let formatted_twice = crate::parsing::top_level_parse(&formatted_once)
3952            .unwrap()
3953            .recast_top(&FormatOptions::new(), 0);
3954        assert_eq!(formatted_twice, formatted_once);
3955    }
3956
3957    #[test]
3958    fn settings_then_standalone_comment_is_stable() {
3959        let code = "\
3960@settings(defaultLengthUnit = mm)
3961@settings(defaultAngleUnit = deg)
3962
3963// Cap for gimbal stick
3964
3965x = 1
3966";
3967        let formatted_once = crate::parsing::top_level_parse(code)
3968            .unwrap()
3969            .recast_top(&FormatOptions::new(), 0);
3970        assert_eq!(formatted_once, code);
3971
3972        let formatted_twice = crate::parsing::top_level_parse(&formatted_once)
3973            .unwrap()
3974            .recast_top(&FormatOptions::new(), 0);
3975        assert_eq!(formatted_twice, formatted_once);
3976    }
3977
3978    #[test]
3979    fn module_prefix() {
3980        let code = "x = std::sweep::SKETCH_PLANE\n";
3981        let ast = crate::parsing::top_level_parse(code).unwrap();
3982        let recasted = ast.recast_top(&FormatOptions::new(), 0);
3983        let expected = code;
3984        assert_eq!(recasted, expected);
3985    }
3986
3987    #[test]
3988    fn inline_ifs() {
3989        let code = "y = true
3990startSketchOn(XY)
3991  |> startProfile(at = [0, 0])
3992  |> if y {
3993    yLine(length = 1)
3994  } else {
3995    xLine(length = 1)
3996  }
3997";
3998        let ast = crate::parsing::top_level_parse(code).unwrap();
3999        let recasted = ast.recast_top(&FormatOptions::new(), 0);
4000        let expected = code;
4001        assert_eq!(recasted, expected);
4002    }
4003
4004    #[test]
4005    fn indented_binary_expressions() {
4006        let code = "\
4007fn foo() {
4008  1 == 2
4009}
4010";
4011        let ast = crate::parsing::top_level_parse(code).unwrap();
4012        let recasted = ast.recast_top(&FormatOptions::new(), 0);
4013        let expected = code;
4014        assert_eq!(recasted, expected);
4015    }
4016
4017    #[test]
4018    fn recast_binary_call_operands_across_operators_and_versions() {
4019        for version in ["1.0", "2.0", "\"3.0-preview\""] {
4020            for operator in ["+", "-", "*", "/", "%", "^", "==", "!=", ">", ">=", "<", "<=", "&", "|"] {
4021                let input = format!(
4022                    "@settings(kclVersion = {version})\n\nfn example() {{\n  x = f(a = 1, b = 2, c = 3, d = 4) {operator} g(a = 1, b = 2, c = 3, d = 4)\n}}\n"
4023                );
4024                let expected = format!(
4025                    r#"@settings(kclVersion = {version})
4026
4027fn example() {{
4028  x = f(
4029    a = 1,
4030    b = 2,
4031    c = 3,
4032    d = 4,
4033  ) {operator} g(
4034    a = 1,
4035    b = 2,
4036    c = 3,
4037    d = 4,
4038  )
4039}}
4040"#
4041                );
4042                assert_recast(&input, &expected);
4043            }
4044        }
4045    }
4046
4047    #[test]
4048    fn recast_binary_operands_in_nested_contexts() {
4049        let code = r#"@settings(kclVersion = 2.0)
4050
4051fn example() {
4052  fn inner() {
4053    f(
4054      // argument
4055      a = 1,
4056    ) == 10
4057    x = outer(
4058      value = (f(
4059        a = 1,
4060        b = 2,
4061        c = 3,
4062        d = 4,
4063      ) + 10) * 20,
4064      other = [
4065        10 == f(
4066          a = 1,
4067          b = 2,
4068          c = 3,
4069          d = 4,
4070        )
4071      ],
4072      third = {
4073        value = f(
4074          a = 1,
4075          b = 2,
4076          c = 3,
4077          d = 4,
4078        ) == 10
4079      },
4080    )
4081    bounds = [base + 1 ..< end]
4082    return 10 == f(
4083      a = 1,
4084      b = 2,
4085      c = 3,
4086      d = 4,
4087    )
4088  }
4089}
4090"#;
4091        assert_recast(code, code);
4092    }
4093
4094    #[test]
4095    fn recast_binary_operands_with_member_and_type_wrappers() {
4096        let code = r#"@settings(kclVersion = 2.0)
4097
4098fn example() {
4099  member = f(
4100    // argument
4101    a = 1,
4102  ).result == 10
4103  ascribed = f(
4104    // argument
4105    a = 1,
4106  ): number == 10
4107  wrapped = (f(
4108    // argument
4109    a = 1,
4110  ) + 1): number == 10
4111}
4112"#;
4113        assert_recast(code, code);
4114    }
4115
4116    #[test]
4117    fn recast_deprecated_calls_as_binary_operands() {
4118        let input = "@settings(kclVersion = 2.0)\n\nfn example() {\n  x = pi() + tau()\n  y = f(a = pi() + 1)\n}\n";
4119        let expected = "@settings(kclVersion = 2.0)\n\nfn example() {\n  x = PI + TAU\n  y = f(a = PI + 1)\n}\n";
4120        assert_recast(input, expected);
4121    }
4122
4123    #[test]
4124    fn recast_binary_operands_in_conditionals_and_annotations() {
4125        let code = r#"@settings(kclVersion = 2.0)
4126
4127fn example() {
4128  @meta(value = f(
4129      // argument
4130      a = 1,
4131    ) == 10)
4132
4133  x = if f(
4134    // argument
4135    a = 1,
4136  ) == 10 {
4137    1
4138  } else {
4139    2
4140  } + if other {
4141    3
4142  } else {
4143    4
4144  }
4145}
4146"#;
4147        assert_recast(code, code);
4148    }
4149
4150    #[test]
4151    fn recast_binary_operands_in_pipelines() {
4152        let code = r#"@settings(kclVersion = 2.0)
4153
4154fn example() {
4155  x = f(
4156    a = 1,
4157    b = 2,
4158    c = 3,
4159    d = 4,
4160  ) == 10
4161    |> g(%)
4162  y = seed
4163    |> outer(
4164         value = 10 == f(
4165           a = 1,
4166           b = 2,
4167           c = 3,
4168           d = 4,
4169         ),
4170         other = 1,
4171       )
4172}
4173"#;
4174        assert_recast(code, code);
4175    }
4176
4177    #[test]
4178    fn recast_binary_operands_with_custom_indentation() {
4179        let input =
4180            "@settings(kclVersion = 2.0)\n\nfn example() {\n  x = f(a = 1, b = 2, c = 3, d = 4) == g(a = 1)\n}\n";
4181        let program = crate::parsing::top_level_parse(input).unwrap();
4182        for (options, expected) in [
4183            (
4184                FormatOptions {
4185                    tab_size: 4,
4186                    ..Default::default()
4187                },
4188                "@settings(kclVersion = 2.0)\n\nfn example() {\n    x = f(\n        a = 1,\n        b = 2,\n        c = 3,\n        d = 4,\n    ) == g(a = 1)\n}\n",
4189            ),
4190            (
4191                FormatOptions {
4192                    use_tabs: true,
4193                    ..Default::default()
4194                },
4195                "@settings(kclVersion = 2.0)\n\nfn example() {\n\tx = f(\n\t\ta = 1,\n\t\tb = 2,\n\t\tc = 3,\n\t\td = 4,\n\t) == g(a = 1)\n}\n",
4196            ),
4197        ] {
4198            let output = program.recast_top(&options, 0);
4199            assert_eq!(output, expected);
4200            let reparsed = crate::parsing::top_level_parse(&output).unwrap();
4201            assert_eq!(reparsed.recast_top(&options, 0), output);
4202        }
4203    }
4204
4205    #[test]
4206    fn indented_assignment() {
4207        let code = "\
4208fn foo() {
4209  x = 1
4210}
4211";
4212        let ast = crate::parsing::top_level_parse(code).unwrap();
4213        let recasted = ast.recast_top(&FormatOptions::new(), 0);
4214        let expected = code;
4215        assert_eq!(recasted, expected);
4216    }
4217
4218    #[test]
4219    fn indented_unary_expression() {
4220        let code = "\
4221fn foo() {
4222  -x
4223}
4224";
4225        let ast = crate::parsing::top_level_parse(code).unwrap();
4226        let recasted = ast.recast_top(&FormatOptions::new(), 0);
4227        let expected = code;
4228        assert_eq!(recasted, expected);
4229    }
4230
4231    #[test]
4232    fn recast_unary_call_operands_across_operators_and_versions() {
4233        for version in ["1.0", "2.0", "\"3.0-preview\""] {
4234            for operator in ["-", "+", "!"] {
4235                let input = format!(
4236                    "@settings(kclVersion = {version})\n\nfn example() {{\n  x = {operator}f(a = 1, b = 2, c = 3, d = 4)\n  y = {operator}g(a = 1)\n}}\n"
4237                );
4238                let expected = format!(
4239                    r#"@settings(kclVersion = {version})
4240
4241fn example() {{
4242  x = {operator}f(
4243    a = 1,
4244    b = 2,
4245    c = 3,
4246    d = 4,
4247  )
4248  y = {operator}g(a = 1)
4249}}
4250"#
4251                );
4252                assert_recast(&input, &expected);
4253            }
4254        }
4255    }
4256
4257    #[test]
4258    fn recast_nested_unary_operands() {
4259        let code = r#"@settings(kclVersion = 2.0)
4260
4261fn example() {
4262  nested = -(-f(
4263    // This comment precedes the argument.
4264    a = 1,
4265  ))
4266  binary = -(f(
4267    // This comment precedes the argument.
4268    a = 1,
4269  ) + 2)
4270  member = -f(
4271    // This comment precedes the argument.
4272    a = 1,
4273  ).result
4274  ascribed = (-f(
4275    // This comment precedes the argument.
4276    a = 1,
4277  )): number
4278  constant = -PI
4279}
4280"#;
4281        assert_recast(code, code);
4282        assert_recast(
4283            "@settings(kclVersion = 2.0)\n\nfn example() {\n  x = -pi()\n}\n",
4284            "@settings(kclVersion = 2.0)\n\nfn example() {\n  x = -PI\n}\n",
4285        );
4286    }
4287
4288    #[test]
4289    fn recast_unary_operands_in_nested_contexts() {
4290        let code = r#"@settings(kclVersion = 2.0)
4291
4292fn example() {
4293  fn inner() {
4294    -f(
4295      // This comment precedes the argument.
4296      a = 1,
4297    )
4298    x = outer(
4299      value = 10 + -f(
4300        // This comment precedes the argument.
4301        a = 1,
4302      ),
4303      other = [
4304        -f(
4305          // This comment precedes the argument.
4306          a = 1,
4307        )
4308      ],
4309      third = {
4310        value = -f(
4311          // This comment precedes the argument.
4312          a = 1,
4313        )
4314      },
4315    )
4316    return -f(
4317      // This comment precedes the argument.
4318      a = 1,
4319    )
4320  }
4321}
4322"#;
4323        assert_recast(code, code);
4324    }
4325
4326    #[test]
4327    fn recast_unary_operands_in_conditionals() {
4328        let code = r#"@settings(kclVersion = 2.0)
4329
4330fn example() {
4331  x = -if !f(
4332    // This comment precedes the argument.
4333    a = 1,
4334  ) {
4335    1
4336  } else {
4337    2
4338  }
4339}
4340"#;
4341        assert_recast(code, code);
4342    }
4343
4344    #[test]
4345    fn recast_unary_operands_in_pipelines() {
4346        let code = r#"@settings(kclVersion = 2.0)
4347
4348fn example() {
4349  x = -f(
4350    // This comment precedes the argument.
4351    a = 1,
4352  )
4353    |> g(%)
4354  y = seed
4355    |> outer(
4356         value = -f(
4357           // This comment precedes the argument.
4358           a = 1,
4359         ),
4360         other = 1,
4361       )
4362}
4363"#;
4364        assert_recast(code, code);
4365    }
4366
4367    #[test]
4368    fn recast_unary_operands_with_custom_indentation() {
4369        let input = "@settings(kclVersion = 2.0)\n\nfn example() {\n  x = -f(a = 1, b = 2, c = 3, d = 4)\n}\n";
4370        let program = crate::parsing::top_level_parse(input).unwrap();
4371        for (options, expected) in [
4372            (
4373                FormatOptions {
4374                    tab_size: 4,
4375                    ..Default::default()
4376                },
4377                "@settings(kclVersion = 2.0)\n\nfn example() {\n    x = -f(\n        a = 1,\n        b = 2,\n        c = 3,\n        d = 4,\n    )\n}\n",
4378            ),
4379            (
4380                FormatOptions {
4381                    use_tabs: true,
4382                    ..Default::default()
4383                },
4384                "@settings(kclVersion = 2.0)\n\nfn example() {\n\tx = -f(\n\t\ta = 1,\n\t\tb = 2,\n\t\tc = 3,\n\t\td = 4,\n\t)\n}\n",
4385            ),
4386        ] {
4387            let output = program.recast_top(&options, 0);
4388            assert_eq!(output, expected);
4389            let reparsed = crate::parsing::top_level_parse(&output).unwrap();
4390            assert_eq!(reparsed.recast_top(&options, 0), output);
4391        }
4392    }
4393
4394    #[test]
4395    fn indented_array_expression() {
4396        let code = "\
4397fn foo() {
4398  [1, 2]
4399}
4400";
4401        let ast = crate::parsing::top_level_parse(code).unwrap();
4402        let recasted = ast.recast_top(&FormatOptions::new(), 0);
4403        let expected = code;
4404        assert_eq!(recasted, expected);
4405    }
4406
4407    #[test]
4408    fn indented_name_expression() {
4409        let code = "\
4410fn foo() {
4411  x
4412}
4413";
4414        let ast = crate::parsing::top_level_parse(code).unwrap();
4415        let recasted = ast.recast_top(&FormatOptions::new(), 0);
4416        let expected = code;
4417        assert_eq!(recasted, expected);
4418    }
4419
4420    #[test]
4421    fn indented_member_assignment() {
4422        let code = "\
4423brakcetPlane = {
4424  origin = { x = length / 2 },
4425  origin = { x = length / 2 },
4426  origin = { x = length / 2 },
4427  origin = { x = length / 2 },
4428  origin = { x = length / 2 },
4429  origin = { x = length / 2 },
4430  origin = { x = length / 2 },
4431  origin = { x = length / 2 }
4432}
4433";
4434        let ast = crate::parsing::top_level_parse(code).unwrap();
4435        let recasted = ast.recast_top(&FormatOptions::new(), 0);
4436        let expected = code;
4437        assert_eq!(recasted, expected);
4438    }
4439
4440    #[test]
4441    fn badly_formatted_inline_calls() {
4442        let code = "\
4443return union([right, left])
4444  |> subtract(tools = [
4445       translate(axle(), y = pitchStabL + forkBaseL + wheelRGap + wheelR + addedLength),
4446       socket(rakeAngle = rearRake, xyTrans = [0, 12]),
4447       socket(
4448         rakeAngle = frontRake,
4449         xyTrans = [
4450           wheelW / 2 + wheelWGap + forkTineW / 2,
4451           40 + addedLength
4452         ],
4453       )
4454     ])
4455";
4456        let ast = crate::parsing::top_level_parse(code).unwrap();
4457        let recasted = ast.recast_top(&FormatOptions::new(), 0);
4458        let expected = code;
4459        assert_eq!(recasted, expected);
4460    }
4461
4462    #[test]
4463    fn fn_args_prefixed_with_spaces() {
4464        let code = "holeAt(
4465  [cube1, cube2],
4466  plane = XY,
4467  holeBottom =   hole::flat(),
4468  holeBody =   hole::blind(depth = 2, diameter = 1),
4469  holeType =   hole::counterbore(diameter = 1.4, depth = 1),
4470  cutAt = [1, 1],
4471)";
4472        let expected = "holeAt(
4473  [cube1, cube2],
4474  plane = XY,
4475  holeBottom = hole::flat(),
4476  holeBody = hole::blind(depth = 2, diameter = 1),
4477  holeType = hole::counterbore(diameter = 1.4, depth = 1),
4478  cutAt = [1, 1],
4479)
4480";
4481        let ast = crate::parsing::top_level_parse(code).unwrap();
4482        let recasted = ast.recast_top(&FormatOptions::new(), 0);
4483        assert_eq!(recasted, expected);
4484    }
4485
4486    #[test]
4487    fn some_fn_args_still_prefixed() {
4488        let code = "a
4489  |> b()
4490  |> subtract(
4491       tools =     startSketchOn(XY)
4492      |> circle(diameter = hubDiameter)
4493      |> extrude(length = hubThickness * 5, symmetric = true),
4494       tolerance,
4495     )
4496";
4497        let ast = crate::parsing::top_level_parse(code).unwrap();
4498        let actual_recasted = ast.recast_top(&FormatOptions::new(), 0);
4499        let expected_recasted = "a
4500  |> b()
4501  |> subtract(
4502       tools = startSketchOn(XY)
4503      |> circle(diameter = hubDiameter)
4504      |> extrude(length = hubThickness * 5, symmetric = true),
4505       tolerance,
4506     )
4507";
4508        assert_eq!(actual_recasted, expected_recasted);
4509    }
4510
4511    #[test]
4512    fn first_in_pipeline_indent() {
4513        // These code snippets are identical, except that one passes the gear into a clone,
4514        // and the other doesn't.
4515        let not_clone = "gear::helical(
4516  nTeeth = 12,
4517  module = 1.5,
4518  pressureAngle = 14deg,
4519  helixAngle = 25deg,
4520  gearHeight = 5,
4521)
4522";
4523        let yes_clone = "gear::helical(
4524  nTeeth = 12,
4525  module = 1.5,
4526  pressureAngle = 14deg,
4527  helixAngle = 25deg,
4528  gearHeight = 5,
4529)
4530|> clone()
4531";
4532        // Both these should format their gear::helical parameters the same.
4533        let not_clone_recasted = crate::parsing::top_level_parse(not_clone)
4534            .unwrap()
4535            .recast_top(&FormatOptions::new(), 0);
4536        let yes_clone_recasted = crate::parsing::top_level_parse(yes_clone)
4537            .unwrap()
4538            .recast_top(&FormatOptions::new(), 0);
4539        assert!(not_clone_recasted.contains("\n  nTeeth"));
4540        assert!(!yes_clone_recasted.contains("\n       nTeeth"));
4541        assert!(yes_clone_recasted.contains("\n  nTeeth"));
4542    }
4543}