1use std::fmt::Write;
2
3use crate::parsing::{
4 ast::types::{
5 Annotation, ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem,
6 CallExpression, CallExpressionKw, CommentStyle, DefaultParamVal, Expr, FormatOptions, FunctionExpression,
7 IfExpression, ImportSelector, ImportStatement, ItemVisibility, LabeledArg, Literal, LiteralIdentifier,
8 LiteralValue, MemberExpression, MemberObject, Node, NonCodeNode, NonCodeValue, ObjectExpression, Parameter,
9 PipeExpression, Program, TagDeclarator, Type, TypeDeclaration, UnaryExpression, VariableDeclaration,
10 VariableKind,
11 },
12 token::NumericSuffix,
13 PIPE_OPERATOR,
14};
15
16impl Program {
17 pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
18 let indentation = options.get_indentation(indentation_level);
19
20 let mut result = self
21 .shebang
22 .as_ref()
23 .map(|sh| format!("{}\n\n", sh.inner.content))
24 .unwrap_or_default();
25
26 for attr in &self.inner_attrs {
27 result.push_str(&attr.recast(options, indentation_level));
28 }
29 for start in &self.non_code_meta.start_nodes {
30 result.push_str(&start.recast(options, indentation_level));
31 }
32 let result = result; let result = self
35 .body
36 .iter()
37 .map(|body_item| {
38 let mut result = String::new();
39 for attr in body_item.get_attrs() {
40 result.push_str(&attr.recast(options, indentation_level));
41 }
42 result.push_str(&match body_item.clone() {
43 BodyItem::ImportStatement(stmt) => stmt.recast(options, indentation_level),
44 BodyItem::ExpressionStatement(expression_statement) => {
45 expression_statement
46 .expression
47 .recast(options, indentation_level, ExprContext::Other)
48 }
49 BodyItem::VariableDeclaration(variable_declaration) => {
50 variable_declaration.recast(options, indentation_level)
51 }
52 BodyItem::TypeDeclaration(ty_declaration) => ty_declaration.recast(),
53 BodyItem::ReturnStatement(return_statement) => {
54 format!(
55 "{}return {}",
56 indentation,
57 return_statement
58 .argument
59 .recast(options, indentation_level, ExprContext::Other)
60 .trim_start()
61 )
62 }
63 });
64 result
65 })
66 .enumerate()
67 .fold(result, |mut output, (index, recast_str)| {
68 let start_string =
69 if index == 0 && self.non_code_meta.start_nodes.is_empty() && self.inner_attrs.is_empty() {
70 indentation.to_string()
72 } else {
73 String::new()
75 };
76
77 let maybe_line_break: String = if index == self.body.len() - 1 && indentation_level == 0 {
80 String::new()
81 } else {
82 "\n".to_string()
83 };
84
85 let custom_white_space_or_comment = match self.non_code_meta.non_code_nodes.get(&index) {
86 Some(noncodes) => noncodes
87 .iter()
88 .enumerate()
89 .map(|(i, custom_white_space_or_comment)| {
90 let formatted = custom_white_space_or_comment.recast(options, indentation_level);
91 if i == 0 && !formatted.trim().is_empty() {
92 if let NonCodeValue::BlockComment { .. } = custom_white_space_or_comment.value {
93 format!("\n{}", formatted)
94 } else {
95 formatted
96 }
97 } else {
98 formatted
99 }
100 })
101 .collect::<String>(),
102 None => String::new(),
103 };
104 let end_string = if custom_white_space_or_comment.is_empty() {
105 maybe_line_break
106 } else {
107 custom_white_space_or_comment
108 };
109
110 let _ = write!(output, "{}{}{}", start_string, recast_str, end_string);
111 output
112 })
113 .trim()
114 .to_string();
115
116 if options.insert_final_newline && !result.is_empty() {
118 format!("{}\n", result)
119 } else {
120 result
121 }
122 }
123}
124
125impl NonCodeValue {
126 fn should_cause_array_newline(&self) -> bool {
127 match self {
128 Self::InlineComment { .. } => false,
129 Self::BlockComment { .. } | Self::NewLineBlockComment { .. } | Self::NewLine => true,
130 }
131 }
132}
133
134impl Node<NonCodeNode> {
135 fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
136 let indentation = options.get_indentation(indentation_level);
137 match &self.value {
138 NonCodeValue::InlineComment {
139 value,
140 style: CommentStyle::Line,
141 } => format!(" // {}\n", value),
142 NonCodeValue::InlineComment {
143 value,
144 style: CommentStyle::Block,
145 } => format!(" /* {} */", value),
146 NonCodeValue::BlockComment { value, style } => match style {
147 CommentStyle::Block => format!("{}/* {} */", indentation, value),
148 CommentStyle::Line => {
149 if value.trim().is_empty() {
150 format!("{}//\n", indentation)
151 } else {
152 format!("{}// {}\n", indentation, value.trim())
153 }
154 }
155 },
156 NonCodeValue::NewLineBlockComment { value, style } => {
157 let add_start_new_line = if self.start == 0 { "" } else { "\n\n" };
158 match style {
159 CommentStyle::Block => format!("{}{}/* {} */\n", add_start_new_line, indentation, value),
160 CommentStyle::Line => {
161 if value.trim().is_empty() {
162 format!("{}{}//\n", add_start_new_line, indentation)
163 } else {
164 format!("{}{}// {}\n", add_start_new_line, indentation, value.trim())
165 }
166 }
167 }
168 }
169 NonCodeValue::NewLine => "\n\n".to_string(),
170 }
171 }
172}
173
174impl Node<Annotation> {
175 fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
176 let mut result = "@".to_owned();
177 if let Some(name) = &self.name {
178 result.push_str(&name.name);
179 }
180 if let Some(properties) = &self.properties {
181 result.push('(');
182 result.push_str(
183 &properties
184 .iter()
185 .map(|prop| {
186 format!(
187 "{} = {}",
188 prop.key.name,
189 prop.value
190 .recast(options, indentation_level + 1, ExprContext::Other)
191 .trim()
192 )
193 })
194 .collect::<Vec<String>>()
195 .join(", "),
196 );
197 result.push(')');
198 result.push('\n');
199 }
200
201 result
202 }
203}
204
205impl ImportStatement {
206 pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
207 let indentation = options.get_indentation(indentation_level);
208 let vis = if self.visibility == ItemVisibility::Export {
209 "export "
210 } else {
211 ""
212 };
213 let mut string = format!("{}{}import ", vis, indentation);
214 match &self.selector {
215 ImportSelector::List { items } => {
216 for (i, item) in items.iter().enumerate() {
217 if i > 0 {
218 string.push_str(", ");
219 }
220 string.push_str(&item.name.name);
221 if let Some(alias) = &item.alias {
222 if item.name.name != alias.name {
224 string.push_str(&format!(" as {}", alias.name));
225 }
226 }
227 }
228 string.push_str(" from ");
229 }
230 ImportSelector::Glob(_) => string.push_str("* from "),
231 ImportSelector::None { .. } => {}
232 }
233 string.push_str(&format!("\"{}\"", self.path));
234
235 if let ImportSelector::None { alias: Some(alias) } = &self.selector {
236 string.push_str(" as ");
237 string.push_str(&alias.name);
238 }
239 string
240 }
241}
242
243#[derive(Copy, Clone, Debug, Eq, PartialEq)]
244pub(crate) enum ExprContext {
245 Pipe,
246 Decl,
247 Other,
248}
249
250impl Expr {
251 pub(crate) fn recast(&self, options: &FormatOptions, indentation_level: usize, mut ctxt: ExprContext) -> String {
252 let is_decl = matches!(ctxt, ExprContext::Decl);
253 if is_decl {
254 ctxt = ExprContext::Other;
258 }
259 match &self {
260 Expr::BinaryExpression(bin_exp) => bin_exp.recast(options),
261 Expr::ArrayExpression(array_exp) => array_exp.recast(options, indentation_level, ctxt),
262 Expr::ArrayRangeExpression(range_exp) => range_exp.recast(options, indentation_level, ctxt),
263 Expr::ObjectExpression(ref obj_exp) => obj_exp.recast(options, indentation_level, ctxt),
264 Expr::MemberExpression(mem_exp) => mem_exp.recast(),
265 Expr::Literal(literal) => literal.recast(),
266 Expr::FunctionExpression(func_exp) => {
267 let mut result = if is_decl { String::new() } else { "fn".to_owned() };
268 result += &func_exp.recast(options, indentation_level);
269 result
270 }
271 Expr::CallExpression(call_exp) => call_exp.recast(options, indentation_level, ctxt),
272 Expr::CallExpressionKw(call_exp) => call_exp.recast(options, indentation_level, ctxt),
273 Expr::Identifier(ident) => ident.name.to_string(),
274 Expr::TagDeclarator(tag) => tag.recast(),
275 Expr::PipeExpression(pipe_exp) => pipe_exp.recast(options, indentation_level),
276 Expr::UnaryExpression(unary_exp) => unary_exp.recast(options),
277 Expr::IfExpression(e) => e.recast(options, indentation_level, ctxt),
278 Expr::PipeSubstitution(_) => crate::parsing::PIPE_SUBSTITUTION_OPERATOR.to_string(),
279 Expr::LabelledExpression(e) => {
280 let mut result = e.expr.recast(options, indentation_level, ctxt);
281 result += " as ";
282 result += &e.label.name;
283 result
284 }
285 Expr::AscribedExpression(e) => {
286 let mut result = e.expr.recast(options, indentation_level, ctxt);
287 result += ": ";
288 result += &e.ty.recast(options, indentation_level);
289 result
290 }
291 Expr::None(_) => {
292 unimplemented!("there is no literal None, see https://github.com/KittyCAD/modeling-app/issues/1115")
293 }
294 }
295 }
296}
297
298impl BinaryPart {
299 fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
300 match &self {
301 BinaryPart::Literal(literal) => literal.recast(),
302 BinaryPart::Identifier(identifier) => identifier.name.to_string(),
303 BinaryPart::BinaryExpression(binary_expression) => binary_expression.recast(options),
304 BinaryPart::CallExpression(call_expression) => {
305 call_expression.recast(options, indentation_level, ExprContext::Other)
306 }
307 BinaryPart::CallExpressionKw(call_expression) => {
308 call_expression.recast(options, indentation_level, ExprContext::Other)
309 }
310 BinaryPart::UnaryExpression(unary_expression) => unary_expression.recast(options),
311 BinaryPart::MemberExpression(member_expression) => member_expression.recast(),
312 BinaryPart::IfExpression(e) => e.recast(options, indentation_level, ExprContext::Other),
313 }
314 }
315}
316
317impl CallExpression {
318 fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
319 format!(
320 "{}{}({})",
321 if ctxt == ExprContext::Pipe {
322 "".to_string()
323 } else {
324 options.get_indentation(indentation_level)
325 },
326 self.callee.name,
327 self.arguments
328 .iter()
329 .map(|arg| arg.recast(options, indentation_level, ctxt))
330 .collect::<Vec<String>>()
331 .join(", ")
332 )
333 }
334}
335
336impl CallExpressionKw {
337 fn recast_args(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> Vec<String> {
338 let mut arg_list = if let Some(first_arg) = &self.unlabeled {
339 vec![first_arg.recast(options, indentation_level, ctxt)]
340 } else {
341 Vec::with_capacity(self.arguments.len())
342 };
343 arg_list.extend(
344 self.arguments
345 .iter()
346 .map(|arg| arg.recast(options, indentation_level, ctxt)),
347 );
348 arg_list
349 }
350 fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
351 let indent = if ctxt == ExprContext::Pipe {
352 "".to_string()
353 } else {
354 options.get_indentation(indentation_level)
355 };
356 let name = &self.callee.name;
357 let arg_list = self.recast_args(options, indentation_level, ctxt);
358 let args = arg_list.clone().join(", ");
359 let has_lots_of_args = arg_list.len() >= 4;
360 let some_arg_is_already_multiline = arg_list.len() > 1 && arg_list.iter().any(|arg| arg.contains('\n'));
361 let multiline = has_lots_of_args || some_arg_is_already_multiline;
362 if multiline {
363 let next_indent = indentation_level + 1;
364 let inner_indentation = if ctxt == ExprContext::Pipe {
365 options.get_indentation_offset_pipe(next_indent)
366 } else {
367 options.get_indentation(next_indent)
368 };
369 let arg_list = self.recast_args(options, next_indent, ctxt);
370 let mut args = arg_list.join(&format!(",\n{inner_indentation}"));
371 args.push(',');
372 let args = args;
373 let end_indent = if ctxt == ExprContext::Pipe {
374 options.get_indentation_offset_pipe(indentation_level)
375 } else {
376 options.get_indentation(indentation_level)
377 };
378 format!("{indent}{name}(\n{inner_indentation}{args}\n{end_indent})")
379 } else {
380 format!("{indent}{name}({args})")
381 }
382 }
383}
384
385impl LabeledArg {
386 fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
387 let label = &self.label.name;
388 let arg = self.arg.recast(options, indentation_level, ctxt);
389 format!("{label} = {arg}")
390 }
391}
392
393impl VariableDeclaration {
394 pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
395 let indentation = options.get_indentation(indentation_level);
396 let mut output = match self.visibility {
397 ItemVisibility::Default => String::new(),
398 ItemVisibility::Export => "export ".to_owned(),
399 };
400
401 let (keyword, eq) = match self.kind {
402 VariableKind::Fn => ("fn ", ""),
403 VariableKind::Const => ("", " = "),
404 };
405 let _ = write!(
406 output,
407 "{}{keyword}{}{eq}{}",
408 indentation,
409 self.declaration.id.name,
410 self.declaration
411 .init
412 .recast(options, indentation_level, ExprContext::Decl)
413 .trim()
414 );
415 output
416 }
417}
418
419impl TypeDeclaration {
420 pub fn recast(&self) -> String {
421 let vis = match self.visibility {
422 ItemVisibility::Default => String::new(),
423 ItemVisibility::Export => "export ".to_owned(),
424 };
425
426 let mut arg_str = String::new();
427 if let Some(args) = &self.args {
428 arg_str.push('(');
429 for a in args {
430 if arg_str.len() > 1 {
431 arg_str.push_str(", ");
432 }
433 arg_str.push_str(&a.name);
434 }
435 arg_str.push(')');
436 }
437 format!("{}type {}{}", vis, self.name.name, arg_str)
438 }
439}
440
441pub fn format_number(value: f64, suffix: NumericSuffix) -> String {
443 format!("{value}{suffix}")
444}
445
446impl Literal {
447 fn recast(&self) -> String {
448 match self.value {
449 LiteralValue::Number { value, suffix } => {
450 if self.raw.contains('.') && value.fract() == 0.0 {
451 format!("{value:?}{suffix}")
452 } else {
453 format!("{}{suffix}", self.raw)
454 }
455 }
456 LiteralValue::String(ref s) => {
457 let quote = if self.raw.trim().starts_with('"') { '"' } else { '\'' };
458 format!("{quote}{s}{quote}")
459 }
460 LiteralValue::Bool(_) => self.raw.clone(),
461 }
462 }
463}
464
465impl TagDeclarator {
466 pub fn recast(&self) -> String {
467 format!("${}", self.name)
469 }
470}
471
472impl ArrayExpression {
473 fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
474 let num_items = self.elements.len() + self.non_code_meta.non_code_nodes_len();
478 let mut elems = self.elements.iter();
479 let mut found_line_comment = false;
480 let mut format_items: Vec<_> = (0..num_items)
481 .flat_map(|i| {
482 if let Some(noncode) = self.non_code_meta.non_code_nodes.get(&i) {
483 noncode
484 .iter()
485 .map(|nc| {
486 found_line_comment |= nc.value.should_cause_array_newline();
487 nc.recast(options, 0)
488 })
489 .collect::<Vec<_>>()
490 } else {
491 let el = elems.next().unwrap();
492 let s = format!("{}, ", el.recast(options, 0, ExprContext::Other));
493 vec![s]
494 }
495 })
496 .collect();
497
498 if let Some(item) = format_items.last_mut() {
500 if let Some(norm) = item.strip_suffix(", ") {
501 *item = norm.to_owned();
502 }
503 }
504 let format_items = format_items; let flat_recast = format!("[{}]", format_items.join(""));
506
507 let max_array_length = 40;
509 let multi_line = flat_recast.len() > max_array_length || found_line_comment;
510 if !multi_line {
511 return flat_recast;
512 }
513
514 let inner_indentation = if ctxt == ExprContext::Pipe {
516 options.get_indentation_offset_pipe(indentation_level + 1)
517 } else {
518 options.get_indentation(indentation_level + 1)
519 };
520 let formatted_array_lines = format_items
521 .iter()
522 .map(|s| {
523 format!(
524 "{inner_indentation}{}{}",
525 if let Some(x) = s.strip_suffix(" ") { x } else { s },
526 if s.ends_with('\n') { "" } else { "\n" }
527 )
528 })
529 .collect::<Vec<String>>()
530 .join("")
531 .to_owned();
532 let end_indent = if ctxt == ExprContext::Pipe {
533 options.get_indentation_offset_pipe(indentation_level)
534 } else {
535 options.get_indentation(indentation_level)
536 };
537 format!("[\n{formatted_array_lines}{end_indent}]")
538 }
539}
540
541fn expr_is_trivial(expr: &Expr) -> bool {
543 matches!(
544 expr,
545 Expr::Literal(_) | Expr::Identifier(_) | Expr::TagDeclarator(_) | Expr::PipeSubstitution(_) | Expr::None(_)
546 )
547}
548
549impl ArrayRangeExpression {
550 fn recast(&self, options: &FormatOptions, _: usize, _: ExprContext) -> String {
551 let s1 = self.start_element.recast(options, 0, ExprContext::Other);
552 let s2 = self.end_element.recast(options, 0, ExprContext::Other);
553
554 if expr_is_trivial(&self.start_element) && expr_is_trivial(&self.end_element) {
559 format!("[{s1}..{s2}]")
560 } else {
561 format!("[{s1} .. {s2}]")
562 }
563
564 }
566}
567
568impl ObjectExpression {
569 fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
570 if self
571 .non_code_meta
572 .non_code_nodes
573 .values()
574 .any(|nc| nc.iter().any(|nc| nc.value.should_cause_array_newline()))
575 {
576 return self.recast_multi_line(options, indentation_level, ctxt);
577 }
578 let flat_recast = format!(
579 "{{ {} }}",
580 self.properties
581 .iter()
582 .map(|prop| {
583 format!(
584 "{} = {}",
585 prop.key.name,
586 prop.value.recast(options, indentation_level + 1, ctxt).trim()
587 )
588 })
589 .collect::<Vec<String>>()
590 .join(", ")
591 );
592 let max_array_length = 40;
593 let needs_multiple_lines = flat_recast.len() > max_array_length;
594 if !needs_multiple_lines {
595 return flat_recast;
596 }
597 self.recast_multi_line(options, indentation_level, ctxt)
598 }
599
600 fn recast_multi_line(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
602 let inner_indentation = if ctxt == ExprContext::Pipe {
603 options.get_indentation_offset_pipe(indentation_level + 1)
604 } else {
605 options.get_indentation(indentation_level + 1)
606 };
607 let num_items = self.properties.len() + self.non_code_meta.non_code_nodes_len();
608 let mut props = self.properties.iter();
609 let format_items: Vec<_> = (0..num_items)
610 .flat_map(|i| {
611 if let Some(noncode) = self.non_code_meta.non_code_nodes.get(&i) {
612 noncode.iter().map(|nc| nc.recast(options, 0)).collect::<Vec<_>>()
613 } else {
614 let prop = props.next().unwrap();
615 let comma = if i == num_items - 1 { "" } else { ",\n" };
617 let s = format!(
618 "{} = {}{comma}",
619 prop.key.name,
620 prop.value.recast(options, indentation_level + 1, ctxt).trim()
621 );
622 vec![s]
623 }
624 })
625 .collect();
626 let end_indent = if ctxt == ExprContext::Pipe {
627 options.get_indentation_offset_pipe(indentation_level)
628 } else {
629 options.get_indentation(indentation_level)
630 };
631 format!(
632 "{{\n{inner_indentation}{}\n{end_indent}}}",
633 format_items.join(&inner_indentation),
634 )
635 }
636}
637
638impl MemberExpression {
639 fn recast(&self) -> String {
640 let key_str = match &self.property {
641 LiteralIdentifier::Identifier(identifier) => {
642 if self.computed {
643 format!("[{}]", &(*identifier.name))
644 } else {
645 format!(".{}", &(*identifier.name))
646 }
647 }
648 LiteralIdentifier::Literal(lit) => format!("[{}]", &(*lit.raw)),
649 };
650
651 match &self.object {
652 MemberObject::MemberExpression(member_exp) => member_exp.recast() + key_str.as_str(),
653 MemberObject::Identifier(identifier) => identifier.name.to_string() + key_str.as_str(),
654 }
655 }
656}
657
658impl BinaryExpression {
659 fn recast(&self, options: &FormatOptions) -> String {
660 let maybe_wrap_it = |a: String, doit: bool| -> String {
661 if doit {
662 format!("({})", a)
663 } else {
664 a
665 }
666 };
667
668 let should_wrap_right = match &self.right {
669 BinaryPart::BinaryExpression(bin_exp) => {
670 self.precedence() > bin_exp.precedence()
671 || self.operator == BinaryOperator::Sub
672 || self.operator == BinaryOperator::Div
673 }
674 _ => false,
675 };
676
677 let should_wrap_left = match &self.left {
678 BinaryPart::BinaryExpression(bin_exp) => self.precedence() > bin_exp.precedence(),
679 _ => false,
680 };
681
682 format!(
683 "{} {} {}",
684 maybe_wrap_it(self.left.recast(options, 0), should_wrap_left),
685 self.operator,
686 maybe_wrap_it(self.right.recast(options, 0), should_wrap_right)
687 )
688 }
689}
690
691impl UnaryExpression {
692 fn recast(&self, options: &FormatOptions) -> String {
693 match self.argument {
694 BinaryPart::Literal(_)
695 | BinaryPart::Identifier(_)
696 | BinaryPart::MemberExpression(_)
697 | BinaryPart::IfExpression(_)
698 | BinaryPart::CallExpressionKw(_)
699 | BinaryPart::CallExpression(_) => {
700 format!("{}{}", &self.operator, self.argument.recast(options, 0))
701 }
702 BinaryPart::BinaryExpression(_) | BinaryPart::UnaryExpression(_) => {
703 format!("{}({})", &self.operator, self.argument.recast(options, 0))
704 }
705 }
706 }
707}
708
709impl IfExpression {
710 fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
711 let n = 2 + (self.else_ifs.len() * 2) + 3;
714 let mut lines = Vec::with_capacity(n);
715
716 let cond = self.cond.recast(options, indentation_level, ctxt);
717 lines.push((0, format!("if {cond} {{")));
718 lines.push((1, self.then_val.recast(options, indentation_level + 1)));
719 for else_if in &self.else_ifs {
720 let cond = else_if.cond.recast(options, indentation_level, ctxt);
721 lines.push((0, format!("}} else if {cond} {{")));
722 lines.push((1, else_if.then_val.recast(options, indentation_level + 1)));
723 }
724 lines.push((0, "} else {".to_owned()));
725 lines.push((1, self.final_else.recast(options, indentation_level + 1)));
726 lines.push((0, "}".to_owned()));
727 lines
728 .into_iter()
729 .map(|(ind, line)| format!("{}{}", options.get_indentation(indentation_level + ind), line.trim()))
730 .collect::<Vec<_>>()
731 .join("\n")
732 }
733}
734
735impl Node<PipeExpression> {
736 fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
737 let pipe = self
738 .body
739 .iter()
740 .enumerate()
741 .map(|(index, statement)| {
742 let indentation = options.get_indentation(indentation_level + 1);
743 let mut s = statement.recast(options, indentation_level + 1, ExprContext::Pipe);
744 let non_code_meta = self.non_code_meta.clone();
745 if let Some(non_code_meta_value) = non_code_meta.non_code_nodes.get(&index) {
746 for val in non_code_meta_value {
747 let formatted = if val.end == self.end {
748 val.recast(options, indentation_level)
749 .trim_end_matches('\n')
750 .to_string()
751 } else {
752 val.recast(options, indentation_level + 1)
753 .trim_end_matches('\n')
754 .to_string()
755 };
756 if let NonCodeValue::BlockComment { .. } = val.value {
757 s += "\n";
758 s += &formatted;
759 } else {
760 s += &formatted;
761 }
762 }
763 }
764
765 if index != self.body.len() - 1 {
766 s += "\n";
767 s += &indentation;
768 s += PIPE_OPERATOR;
769 s += " ";
770 }
771 s
772 })
773 .collect::<String>();
774 format!("{}{}", options.get_indentation(indentation_level), pipe)
775 }
776}
777
778impl FunctionExpression {
779 pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
780 let mut new_options = options.clone();
782 new_options.insert_final_newline = false;
783 let param_list = self
784 .params
785 .iter()
786 .map(|param| param.recast(options, indentation_level))
787 .collect::<Vec<String>>()
788 .join(", ");
789 let tab0 = options.get_indentation(indentation_level);
790 let tab1 = options.get_indentation(indentation_level + 1);
791 let return_type = match &self.return_type {
792 Some(rt) => format!(": {}", rt.recast(&new_options, indentation_level)),
793 None => String::new(),
794 };
795 let body = self.body.recast(&new_options, indentation_level + 1);
796
797 format!("({param_list}){return_type} {{\n{tab1}{body}\n{tab0}}}")
798 }
799}
800
801impl Parameter {
802 pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
803 let at_sign = if self.labeled { "" } else { "@" };
804 let identifier = &self.identifier.name;
805 let question_mark = if self.default_value.is_some() { "?" } else { "" };
806 let mut result = format!("{at_sign}{identifier}{question_mark}");
807 if let Some(ty) = &self.type_ {
808 result += ": ";
809 result += &ty.recast(options, indentation_level);
810 }
811 if let Some(DefaultParamVal::Literal(ref literal)) = self.default_value {
812 let lit = literal.recast();
813 result.push_str(&format!(" = {lit}"));
814 };
815
816 result
817 }
818}
819
820impl Type {
821 pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
822 match self {
823 Type::Primitive(t) => t.to_string(),
824 Type::Array(t) => format!("{t}[]"),
825 Type::Object { properties } => {
826 let mut result = "{".to_owned();
827 for p in properties {
828 result += " ";
829 result += &p.recast(options, indentation_level);
830 result += ",";
831 }
832
833 if result.ends_with(',') {
834 result.pop();
835 result += " ";
836 }
837 result += "}";
838
839 result
840 }
841 }
842 }
843}
844
845#[cfg(test)]
846mod tests {
847 use pretty_assertions::assert_eq;
848
849 use super::*;
850 use crate::{parsing::ast::types::FormatOptions, ModuleId};
851
852 #[test]
853 fn test_recast_annotations_without_body_items() {
854 let input = r#"@settings(defaultLengthUnit = in)
855"#;
856 let program = crate::parsing::top_level_parse(input).unwrap();
857 let output = program.recast(&Default::default(), 0);
858 assert_eq!(output, input);
859 }
860
861 #[test]
862 fn test_recast_annotations_in_function_body() {
863 let input = r#"fn myFunc() {
864 @meta(yes = true)
865 x = 2
866}
867"#;
868 let program = crate::parsing::top_level_parse(input).unwrap();
869 let output = program.recast(&Default::default(), 0);
870 assert_eq!(output, input);
871 }
872
873 #[test]
874 fn test_recast_annotations_in_function_body_without_items() {
875 let input = r#"fn myFunc() {
876 @meta(yes = true)
877}
878"#;
879 let program = crate::parsing::top_level_parse(input).unwrap();
880 let output = program.recast(&Default::default(), 0);
881 assert_eq!(output, input);
882 }
883
884 #[test]
885 fn test_recast_if_else_if_same() {
886 let input = r#"b = if false {
887 3
888} else if true {
889 4
890} else {
891 5
892}
893"#;
894 let program = crate::parsing::top_level_parse(input).unwrap();
895 let output = program.recast(&Default::default(), 0);
896 assert_eq!(output, input);
897 }
898
899 #[test]
900 fn test_recast_if_same() {
901 let input = r#"b = if false {
902 3
903} else {
904 5
905}
906"#;
907 let program = crate::parsing::top_level_parse(input).unwrap();
908 let output = program.recast(&Default::default(), 0);
909 assert_eq!(output, input);
910 }
911
912 #[test]
913 fn test_recast_import() {
914 let input = r#"import a from "a.kcl"
915import a as aaa from "a.kcl"
916import a, b from "a.kcl"
917import a as aaa, b from "a.kcl"
918import a, b as bbb from "a.kcl"
919import a as aaa, b as bbb from "a.kcl"
920import "a_b.kcl"
921import "a-b.kcl" as b
922import * from "a.kcl"
923export import a as aaa from "a.kcl"
924export import a, b from "a.kcl"
925export import a as aaa, b from "a.kcl"
926export import a, b as bbb from "a.kcl"
927"#;
928 let program = crate::parsing::top_level_parse(input).unwrap();
929 let output = program.recast(&Default::default(), 0);
930 assert_eq!(output, input);
931 }
932
933 #[test]
934 fn test_recast_import_as_same_name() {
935 let input = r#"import a as a from "a.kcl"
936"#;
937 let program = crate::parsing::top_level_parse(input).unwrap();
938 let output = program.recast(&Default::default(), 0);
939 let expected = r#"import a from "a.kcl"
940"#;
941 assert_eq!(output, expected);
942 }
943
944 #[test]
945 fn test_recast_export_fn() {
946 let input = r#"export fn a() {
947 return 0
948}
949"#;
950 let program = crate::parsing::top_level_parse(input).unwrap();
951 let output = program.recast(&Default::default(), 0);
952 assert_eq!(output, input);
953 }
954
955 #[test]
956 fn test_recast_bug_fn_in_fn() {
957 let some_program_string = r#"// Start point (top left)
958zoo_x = -20
959zoo_y = 7
960// Scale
961s = 1 // s = 1 -> height of Z is 13.4mm
962// Depth
963d = 1
964
965fn rect(x, y, w, h) {
966 startSketchOn('XY')
967 |> startProfileAt([x, y], %)
968 |> xLine(w, %)
969 |> yLine(h, %)
970 |> xLine(-w, %)
971 |> close()
972 |> extrude(d, %)
973}
974
975fn quad(x1, y1, x2, y2, x3, y3, x4, y4) {
976 startSketchOn('XY')
977 |> startProfileAt([x1, y1], %)
978 |> line(endAbsolute = [x2, y2])
979 |> line(endAbsolute = [x3, y3])
980 |> line(endAbsolute = [x4, y4])
981 |> close()
982 |> extrude(d, %)
983}
984
985fn crosshair(x, y) {
986 startSketchOn('XY')
987 |> startProfileAt([x, y], %)
988 |> yLine(1, %)
989 |> yLine(-2, %)
990 |> yLine(1, %)
991 |> xLine(1, %)
992 |> xLine(-2, %)
993}
994
995fn z(z_x, z_y) {
996 z_end_w = s * 8.4
997 z_end_h = s * 3
998 z_corner = s * 2
999 z_w = z_end_w + 2 * z_corner
1000 z_h = z_w * 1.08130081300813
1001 rect(z_x, z_y, z_end_w, -z_end_h)
1002 rect(z_x + z_w, z_y, -z_corner, -z_corner)
1003 rect(z_x + z_w, z_y - z_h, -z_end_w, z_end_h)
1004 rect(z_x, z_y - z_h, z_corner, z_corner)
1005 quad(z_x, z_y - z_h + z_corner, z_x + z_w - z_corner, z_y, z_x + z_w, z_y - z_corner, z_x + z_corner, z_y - z_h)
1006}
1007
1008fn o(c_x, c_y) {
1009 // Outer and inner radii
1010 o_r = s * 6.95
1011 i_r = 0.5652173913043478 * o_r
1012
1013 // Angle offset for diagonal break
1014 a = 7
1015
1016 // Start point for the top sketch
1017 o_x1 = c_x + o_r * cos((45 + a) / 360 * tau())
1018 o_y1 = c_y + o_r * sin((45 + a) / 360 * tau())
1019
1020 // Start point for the bottom sketch
1021 o_x2 = c_x + o_r * cos((225 + a) / 360 * tau())
1022 o_y2 = c_y + o_r * sin((225 + a) / 360 * tau())
1023
1024 // End point for the bottom startSketch
1025 o_x3 = c_x + o_r * cos((45 - a) / 360 * tau())
1026 o_y3 = c_y + o_r * sin((45 - a) / 360 * tau())
1027
1028 // Where is the center?
1029 // crosshair(c_x, c_y)
1030
1031
1032 startSketchOn('XY')
1033 |> startProfileAt([o_x1, o_y1], %)
1034 |> arc({
1035 radius = o_r,
1036 angle_start = 45 + a,
1037 angle_end = 225 - a
1038 }, %)
1039 |> angledLine([45, o_r - i_r], %)
1040 |> arc({
1041 radius = i_r,
1042 angle_start = 225 - a,
1043 angle_end = 45 + a
1044 }, %)
1045 |> close()
1046 |> extrude(d, %)
1047
1048 startSketchOn('XY')
1049 |> startProfileAt([o_x2, o_y2], %)
1050 |> arc({
1051 radius = o_r,
1052 angle_start = 225 + a,
1053 angle_end = 360 + 45 - a
1054 }, %)
1055 |> angledLine([225, o_r - i_r], %)
1056 |> arc({
1057 radius = i_r,
1058 angle_start = 45 - a,
1059 angle_end = 225 + a - 360
1060 }, %)
1061 |> close()
1062 |> extrude(d, %)
1063}
1064
1065fn zoo(x0, y0) {
1066 z(x0, y0)
1067 o(x0 + s * 20, y0 - (s * 6.7))
1068 o(x0 + s * 35, y0 - (s * 6.7))
1069}
1070
1071zoo(zoo_x, zoo_y)
1072"#;
1073 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1074
1075 let recasted = program.recast(&Default::default(), 0);
1076 assert_eq!(recasted, some_program_string);
1077 }
1078
1079 #[test]
1080 fn test_recast_bug_extra_parens() {
1081 let some_program_string = r#"// Ball Bearing
1082// 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.
1083
1084// Define constants like ball diameter, inside diameter, overhange length, and thickness
1085sphereDia = 0.5
1086insideDia = 1
1087thickness = 0.25
1088overHangLength = .4
1089
1090// Sketch and revolve the inside bearing piece
1091insideRevolve = startSketchOn('XZ')
1092 |> startProfileAt([insideDia / 2, 0], %)
1093 |> line([0, thickness + sphereDia / 2], %)
1094 |> line([overHangLength, 0], %)
1095 |> line([0, -thickness], %)
1096 |> line([-overHangLength + thickness, 0], %)
1097 |> line([0, -sphereDia], %)
1098 |> line([overHangLength - thickness, 0], %)
1099 |> line([0, -thickness], %)
1100 |> line([-overHangLength, 0], %)
1101 |> close()
1102 |> revolve({ axis: 'y' }, %)
1103
1104// 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)
1105sphere = startSketchOn('XZ')
1106 |> startProfileAt([
1107 0.05 + insideDia / 2 + thickness,
1108 0 - 0.05
1109 ], %)
1110 |> line([sphereDia - 0.1, 0], %)
1111 |> arc({
1112 angle_start = 0,
1113 angle_end = -180,
1114 radius = sphereDia / 2 - 0.05
1115 }, %)
1116 |> close()
1117 |> revolve({ axis: 'x' }, %)
1118 |> patternCircular3d(
1119 axis = [0, 0, 1],
1120 center = [0, 0, 0],
1121 repetitions = 10,
1122 arcDegrees = 360,
1123 rotateDuplicates = true
1124 )
1125
1126// Sketch and revolve the outside bearing
1127outsideRevolve = startSketchOn('XZ')
1128 |> startProfileAt([
1129 insideDia / 2 + thickness + sphereDia,
1130 0
1131 ], %)
1132 |> line([0, sphereDia / 2], %)
1133 |> line([-overHangLength + thickness, 0], %)
1134 |> line([0, thickness], %)
1135 |> line([overHangLength, 0], %)
1136 |> line([0, -2 * thickness - sphereDia], %)
1137 |> line([-overHangLength, 0], %)
1138 |> line([0, thickness], %)
1139 |> line([overHangLength - thickness, 0], %)
1140 |> close()
1141 |> revolve({ axis: 'y' }, %)"#;
1142 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1143
1144 let recasted = program.recast(&Default::default(), 0);
1145 assert_eq!(
1146 recasted,
1147 r#"// Ball Bearing
1148// 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.
1149
1150
1151// Define constants like ball diameter, inside diameter, overhange length, and thickness
1152sphereDia = 0.5
1153insideDia = 1
1154thickness = 0.25
1155overHangLength = .4
1156
1157// Sketch and revolve the inside bearing piece
1158insideRevolve = startSketchOn('XZ')
1159 |> startProfileAt([insideDia / 2, 0], %)
1160 |> line([0, thickness + sphereDia / 2], %)
1161 |> line([overHangLength, 0], %)
1162 |> line([0, -thickness], %)
1163 |> line([-overHangLength + thickness, 0], %)
1164 |> line([0, -sphereDia], %)
1165 |> line([overHangLength - thickness, 0], %)
1166 |> line([0, -thickness], %)
1167 |> line([-overHangLength, 0], %)
1168 |> close()
1169 |> revolve({ axis = 'y' }, %)
1170
1171// 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)
1172sphere = startSketchOn('XZ')
1173 |> startProfileAt([
1174 0.05 + insideDia / 2 + thickness,
1175 0 - 0.05
1176 ], %)
1177 |> line([sphereDia - 0.1, 0], %)
1178 |> arc({
1179 angle_start = 0,
1180 angle_end = -180,
1181 radius = sphereDia / 2 - 0.05
1182 }, %)
1183 |> close()
1184 |> revolve({ axis = 'x' }, %)
1185 |> patternCircular3d(
1186 axis = [0, 0, 1],
1187 center = [0, 0, 0],
1188 repetitions = 10,
1189 arcDegrees = 360,
1190 rotateDuplicates = true,
1191 )
1192
1193// Sketch and revolve the outside bearing
1194outsideRevolve = startSketchOn('XZ')
1195 |> startProfileAt([
1196 insideDia / 2 + thickness + sphereDia,
1197 0
1198 ], %)
1199 |> line([0, sphereDia / 2], %)
1200 |> line([-overHangLength + thickness, 0], %)
1201 |> line([0, thickness], %)
1202 |> line([overHangLength, 0], %)
1203 |> line([0, -2 * thickness - sphereDia], %)
1204 |> line([-overHangLength, 0], %)
1205 |> line([0, thickness], %)
1206 |> line([overHangLength - thickness, 0], %)
1207 |> close()
1208 |> revolve({ axis = 'y' }, %)
1209"#
1210 );
1211 }
1212
1213 #[test]
1214 fn test_recast_fn_in_object() {
1215 let some_program_string = r#"bing = { yo = 55 }
1216myNestedVar = [{ prop = callExp(bing.yo) }]
1217"#;
1218 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1219
1220 let recasted = program.recast(&Default::default(), 0);
1221 assert_eq!(recasted, some_program_string);
1222 }
1223
1224 #[test]
1225 fn test_recast_fn_in_array() {
1226 let some_program_string = r#"bing = { yo = 55 }
1227myNestedVar = [callExp(bing.yo)]
1228"#;
1229 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1230
1231 let recasted = program.recast(&Default::default(), 0);
1232 assert_eq!(recasted, some_program_string);
1233 }
1234
1235 #[test]
1236 fn test_recast_ranges() {
1237 let some_program_string = r#"foo = [0..10]
1238ten = 10
1239bar = [0 + 1 .. ten]
1240"#;
1241 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1242
1243 let recasted = program.recast(&Default::default(), 0);
1244 assert_eq!(recasted, some_program_string);
1245 }
1246
1247 #[test]
1248 fn test_recast_space_in_fn_call() {
1249 let some_program_string = r#"fn thing = (x) => {
1250 return x + 1
1251}
1252
1253thing ( 1 )
1254"#;
1255 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1256
1257 let recasted = program.recast(&Default::default(), 0);
1258 assert_eq!(
1259 recasted,
1260 r#"fn thing(x) {
1261 return x + 1
1262}
1263
1264thing(1)
1265"#
1266 );
1267 }
1268
1269 #[test]
1270 fn test_recast_typed_fn() {
1271 let some_program_string = r#"fn thing(x: string, y: bool[]): number {
1272 return x + 1
1273}
1274"#;
1275 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1276
1277 let recasted = program.recast(&Default::default(), 0);
1278 assert_eq!(recasted, some_program_string);
1279 }
1280
1281 #[test]
1282 fn test_recast_object_fn_in_array_weird_bracket() {
1283 let some_program_string = r#"bing = { yo = 55 }
1284myNestedVar = [
1285 {
1286 prop: line([bing.yo, 21], sketch001)
1287}
1288]
1289"#;
1290 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1291
1292 let recasted = program.recast(&Default::default(), 0);
1293 assert_eq!(
1294 recasted,
1295 r#"bing = { yo = 55 }
1296myNestedVar = [
1297 {
1298 prop = line([bing.yo, 21], sketch001)
1299}
1300]
1301"#
1302 );
1303 }
1304
1305 #[test]
1306 fn test_recast_empty_file() {
1307 let some_program_string = r#""#;
1308 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1309
1310 let recasted = program.recast(&Default::default(), 0);
1311 assert_eq!(recasted, r#""#);
1313 }
1314
1315 #[test]
1316 fn test_recast_empty_file_new_line() {
1317 let some_program_string = r#"
1318"#;
1319 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1320
1321 let recasted = program.recast(&Default::default(), 0);
1322 assert_eq!(recasted, r#""#);
1324 }
1325
1326 #[test]
1327 fn test_recast_shebang() {
1328 let some_program_string = r#"#!/usr/local/env zoo kcl
1329part001 = startSketchOn('XY')
1330 |> startProfileAt([-10, -10], %)
1331 |> line([20, 0], %)
1332 |> line([0, 20], %)
1333 |> line([-20, 0], %)
1334 |> close()
1335"#;
1336
1337 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1338
1339 let recasted = program.recast(&Default::default(), 0);
1340 assert_eq!(
1341 recasted,
1342 r#"#!/usr/local/env zoo kcl
1343
1344part001 = startSketchOn('XY')
1345 |> startProfileAt([-10, -10], %)
1346 |> line([20, 0], %)
1347 |> line([0, 20], %)
1348 |> line([-20, 0], %)
1349 |> close()
1350"#
1351 );
1352 }
1353
1354 #[test]
1355 fn test_recast_shebang_new_lines() {
1356 let some_program_string = r#"#!/usr/local/env zoo kcl
1357
1358
1359
1360part001 = startSketchOn('XY')
1361 |> startProfileAt([-10, -10], %)
1362 |> line([20, 0], %)
1363 |> line([0, 20], %)
1364 |> line([-20, 0], %)
1365 |> close()
1366"#;
1367
1368 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1369
1370 let recasted = program.recast(&Default::default(), 0);
1371 assert_eq!(
1372 recasted,
1373 r#"#!/usr/local/env zoo kcl
1374
1375part001 = startSketchOn('XY')
1376 |> startProfileAt([-10, -10], %)
1377 |> line([20, 0], %)
1378 |> line([0, 20], %)
1379 |> line([-20, 0], %)
1380 |> close()
1381"#
1382 );
1383 }
1384
1385 #[test]
1386 fn test_recast_shebang_with_comments() {
1387 let some_program_string = r#"#!/usr/local/env zoo kcl
1388
1389// Yo yo my comments.
1390part001 = startSketchOn('XY')
1391 |> startProfileAt([-10, -10], %)
1392 |> line([20, 0], %)
1393 |> line([0, 20], %)
1394 |> line([-20, 0], %)
1395 |> close()
1396"#;
1397
1398 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1399
1400 let recasted = program.recast(&Default::default(), 0);
1401 assert_eq!(
1402 recasted,
1403 r#"#!/usr/local/env zoo kcl
1404
1405// Yo yo my comments.
1406part001 = startSketchOn('XY')
1407 |> startProfileAt([-10, -10], %)
1408 |> line([20, 0], %)
1409 |> line([0, 20], %)
1410 |> line([-20, 0], %)
1411 |> close()
1412"#
1413 );
1414 }
1415
1416 #[test]
1417 fn test_recast_empty_function_body_with_comments() {
1418 let input = r#"fn myFunc() {
1419 // Yo yo my comments.
1420}
1421"#;
1422
1423 let program = crate::parsing::top_level_parse(input).unwrap();
1424 let output = program.recast(&Default::default(), 0);
1425 assert_eq!(output, input);
1426 }
1427
1428 #[test]
1429 fn test_recast_large_file() {
1430 let some_program_string = r#"@settings(units=mm)
1431// define nts
1432radius = 6.0
1433width = 144.0
1434length = 83.0
1435depth = 45.0
1436thk = 5
1437hole_diam = 5
1438// define a rectangular shape func
1439fn rectShape = (pos, w, l) => {
1440 rr = startSketchOn('xy')
1441 |> startProfileAt([pos[0] - (w / 2), pos[1] - (l / 2)], %)
1442 |> line(endAbsolute = [pos[0] + w / 2, pos[1] - (l / 2)], tag = $edge1)
1443 |> line(endAbsolute = [pos[0] + w / 2, pos[1] + l / 2], tag = $edge2)
1444 |> line(endAbsolute = [pos[0] - (w / 2), pos[1] + l / 2], tag = $edge3)
1445 |> close($edge4)
1446 return rr
1447}
1448// build the body of the focusrite scarlett solo gen 4
1449// only used for visualization
1450scarlett_body = rectShape([0, 0], width, length)
1451 |> extrude(depth, %)
1452 |> fillet(
1453 radius = radius,
1454 tags = [
1455 edge2,
1456 edge4,
1457 getOppositeEdge(edge2),
1458 getOppositeEdge(edge4)
1459]
1460 )
1461 // build the bracket sketch around the body
1462fn bracketSketch = (w, d, t) => {
1463 s = startSketchOn({
1464 plane: {
1465 origin: { x = 0, y = length / 2 + thk, z = 0 },
1466 x_axis: { x = 1, y = 0, z = 0 },
1467 y_axis: { x = 0, y = 0, z = 1 },
1468 z_axis: { x = 0, y = 1, z = 0 }
1469}
1470 })
1471 |> startProfileAt([-w / 2 - t, d + t], %)
1472 |> line(endAbsolute = [-w / 2 - t, -t], tag = $edge1)
1473 |> line(endAbsolute = [w / 2 + t, -t], tag = $edge2)
1474 |> line(endAbsolute = [w / 2 + t, d + t], tag = $edge3)
1475 |> line(endAbsolute = [w / 2, d + t], tag = $edge4)
1476 |> line(endAbsolute = [w / 2, 0], tag = $edge5)
1477 |> line(endAbsolute = [-w / 2, 0], tag = $edge6)
1478 |> line(endAbsolute = [-w / 2, d + t], tag = $edge7)
1479 |> close($edge8)
1480 return s
1481}
1482// build the body of the bracket
1483bracket_body = bracketSketch(width, depth, thk)
1484 |> extrude(length + 10, %)
1485 |> fillet(
1486 radius = radius,
1487 tags = [
1488 getNextAdjacentEdge(edge7),
1489 getNextAdjacentEdge(edge2),
1490 getNextAdjacentEdge(edge3),
1491 getNextAdjacentEdge(edge6)
1492]
1493 )
1494 // build the tabs of the mounting bracket (right side)
1495tabs_r = startSketchOn({
1496 plane: {
1497 origin: { x = 0, y = 0, z = depth + thk },
1498 x_axis: { x = 1, y = 0, z = 0 },
1499 y_axis: { x = 0, y = 1, z = 0 },
1500 z_axis: { x = 0, y = 0, z = 1 }
1501}
1502 })
1503 |> startProfileAt([width / 2 + thk, length / 2 + thk], %)
1504 |> line([10, -5], %)
1505 |> line([0, -10], %)
1506 |> line([-10, -5], %)
1507 |> close()
1508 |> hole(circle(
1509 center = [
1510 width / 2 + thk + hole_diam,
1511 length / 2 - hole_diam
1512 ],
1513 radius = hole_diam / 2
1514 ), %)
1515 |> extrude(-thk, %)
1516 |> patternLinear3d(
1517 axis = [0, -1, 0],
1518 repetitions = 1,
1519 distance = length - 10
1520 )
1521 // build the tabs of the mounting bracket (left side)
1522tabs_l = startSketchOn({
1523 plane: {
1524 origin = { x = 0, y = 0, z = depth + thk },
1525 x_axis = { x = 1, y = 0, z = 0 },
1526 y_axis = { x = 0, y = 1, z = 0 },
1527 z_axis = { x = 0, y = 0, z = 1 }
1528}
1529 })
1530 |> startProfileAt([-width / 2 - thk, length / 2 + thk], %)
1531 |> line([-10, -5], %)
1532 |> line([0, -10], %)
1533 |> line([10, -5], %)
1534 |> close()
1535 |> hole(circle(
1536 center = [
1537 -width / 2 - thk - hole_diam,
1538 length / 2 - hole_diam
1539 ],
1540 radius = hole_diam / 2
1541 ), %)
1542 |> extrude(-thk, %)
1543 |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10)
1544"#;
1545 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1546
1547 let recasted = program.recast(&Default::default(), 0);
1548 assert_eq!(
1550 recasted,
1551 r#"@settings(units = mm)
1552// define nts
1553radius = 6.0
1554width = 144.0
1555length = 83.0
1556depth = 45.0
1557thk = 5
1558hole_diam = 5
1559// define a rectangular shape func
1560fn rectShape(pos, w, l) {
1561 rr = startSketchOn('xy')
1562 |> startProfileAt([pos[0] - (w / 2), pos[1] - (l / 2)], %)
1563 |> line(endAbsolute = [pos[0] + w / 2, pos[1] - (l / 2)], tag = $edge1)
1564 |> line(endAbsolute = [pos[0] + w / 2, pos[1] + l / 2], tag = $edge2)
1565 |> line(endAbsolute = [pos[0] - (w / 2), pos[1] + l / 2], tag = $edge3)
1566 |> close($edge4)
1567 return rr
1568}
1569// build the body of the focusrite scarlett solo gen 4
1570// only used for visualization
1571scarlett_body = rectShape([0, 0], width, length)
1572 |> extrude(depth, %)
1573 |> fillet(
1574 radius = radius,
1575 tags = [
1576 edge2,
1577 edge4,
1578 getOppositeEdge(edge2),
1579 getOppositeEdge(edge4)
1580 ],
1581 )
1582// build the bracket sketch around the body
1583fn bracketSketch(w, d, t) {
1584 s = startSketchOn({
1585 plane = {
1586 origin = { x = 0, y = length / 2 + thk, z = 0 },
1587 x_axis = { x = 1, y = 0, z = 0 },
1588 y_axis = { x = 0, y = 0, z = 1 },
1589 z_axis = { x = 0, y = 1, z = 0 }
1590 }
1591 })
1592 |> startProfileAt([-w / 2 - t, d + t], %)
1593 |> line(endAbsolute = [-w / 2 - t, -t], tag = $edge1)
1594 |> line(endAbsolute = [w / 2 + t, -t], tag = $edge2)
1595 |> line(endAbsolute = [w / 2 + t, d + t], tag = $edge3)
1596 |> line(endAbsolute = [w / 2, d + t], tag = $edge4)
1597 |> line(endAbsolute = [w / 2, 0], tag = $edge5)
1598 |> line(endAbsolute = [-w / 2, 0], tag = $edge6)
1599 |> line(endAbsolute = [-w / 2, d + t], tag = $edge7)
1600 |> close($edge8)
1601 return s
1602}
1603// build the body of the bracket
1604bracket_body = bracketSketch(width, depth, thk)
1605 |> extrude(length + 10, %)
1606 |> fillet(
1607 radius = radius,
1608 tags = [
1609 getNextAdjacentEdge(edge7),
1610 getNextAdjacentEdge(edge2),
1611 getNextAdjacentEdge(edge3),
1612 getNextAdjacentEdge(edge6)
1613 ],
1614 )
1615// build the tabs of the mounting bracket (right side)
1616tabs_r = startSketchOn({
1617 plane = {
1618 origin = { x = 0, y = 0, z = depth + thk },
1619 x_axis = { x = 1, y = 0, z = 0 },
1620 y_axis = { x = 0, y = 1, z = 0 },
1621 z_axis = { x = 0, y = 0, z = 1 }
1622 }
1623 })
1624 |> startProfileAt([width / 2 + thk, length / 2 + thk], %)
1625 |> line([10, -5], %)
1626 |> line([0, -10], %)
1627 |> line([-10, -5], %)
1628 |> close()
1629 |> hole(circle(
1630 center = [
1631 width / 2 + thk + hole_diam,
1632 length / 2 - hole_diam
1633 ],
1634 radius = hole_diam / 2,
1635 ), %)
1636 |> extrude(-thk, %)
1637 |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10)
1638// build the tabs of the mounting bracket (left side)
1639tabs_l = startSketchOn({
1640 plane = {
1641 origin = { x = 0, y = 0, z = depth + thk },
1642 x_axis = { x = 1, y = 0, z = 0 },
1643 y_axis = { x = 0, y = 1, z = 0 },
1644 z_axis = { x = 0, y = 0, z = 1 }
1645 }
1646 })
1647 |> startProfileAt([-width / 2 - thk, length / 2 + thk], %)
1648 |> line([-10, -5], %)
1649 |> line([0, -10], %)
1650 |> line([10, -5], %)
1651 |> close()
1652 |> hole(circle(
1653 center = [
1654 -width / 2 - thk - hole_diam,
1655 length / 2 - hole_diam
1656 ],
1657 radius = hole_diam / 2,
1658 ), %)
1659 |> extrude(-thk, %)
1660 |> patternLinear3d(axis = [0, -1, 0], repetitions = 1, distance = length - 10)
1661"#
1662 );
1663 }
1664
1665 #[test]
1666 fn test_recast_nested_var_declaration_in_fn_body() {
1667 let some_program_string = r#"fn cube = (pos, scale) => {
1668 sg = startSketchOn('XY')
1669 |> startProfileAt(pos, %)
1670 |> line([0, scale], %)
1671 |> line([scale, 0], %)
1672 |> line([0, -scale], %)
1673 |> close()
1674 |> extrude(scale, %)
1675}"#;
1676 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1677
1678 let recasted = program.recast(&Default::default(), 0);
1679 assert_eq!(
1680 recasted,
1681 r#"fn cube(pos, scale) {
1682 sg = startSketchOn('XY')
1683 |> startProfileAt(pos, %)
1684 |> line([0, scale], %)
1685 |> line([scale, 0], %)
1686 |> line([0, -scale], %)
1687 |> close()
1688 |> extrude(scale, %)
1689}
1690"#
1691 );
1692 }
1693
1694 #[test]
1695 fn test_as() {
1696 let some_program_string = r#"fn cube(pos, scale) {
1697 x = dfsfs + dfsfsd as y
1698
1699 sg = startSketchOn('XY')
1700 |> startProfileAt(pos, %) as foo
1701 |> line([0, scale], %)
1702 |> line([scale, 0], %) as bar
1703 |> line([0 as baz, -scale] as qux, %)
1704 |> close()
1705 |> extrude(scale, %)
1706}
1707
1708cube(0, 0) as cub
1709"#;
1710 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1711
1712 let recasted = program.recast(&Default::default(), 0);
1713 assert_eq!(recasted, some_program_string,);
1714 }
1715
1716 #[test]
1717 fn test_recast_with_bad_indentation() {
1718 let some_program_string = r#"part001 = startSketchOn('XY')
1719 |> startProfileAt([0.0, 5.0], %)
1720 |> line([0.4900857016, -0.0240763666], %)
1721 |> line([0.6804562304, 0.9087880491], %)"#;
1722 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1723
1724 let recasted = program.recast(&Default::default(), 0);
1725 assert_eq!(
1726 recasted,
1727 r#"part001 = startSketchOn('XY')
1728 |> startProfileAt([0.0, 5.0], %)
1729 |> line([0.4900857016, -0.0240763666], %)
1730 |> line([0.6804562304, 0.9087880491], %)
1731"#
1732 );
1733 }
1734
1735 #[test]
1736 fn test_recast_with_bad_indentation_and_inline_comment() {
1737 let some_program_string = r#"part001 = startSketchOn('XY')
1738 |> startProfileAt([0.0, 5.0], %)
1739 |> line([0.4900857016, -0.0240763666], %) // hello world
1740 |> line([0.6804562304, 0.9087880491], %)"#;
1741 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1742
1743 let recasted = program.recast(&Default::default(), 0);
1744 assert_eq!(
1745 recasted,
1746 r#"part001 = startSketchOn('XY')
1747 |> startProfileAt([0.0, 5.0], %)
1748 |> line([0.4900857016, -0.0240763666], %) // hello world
1749 |> line([0.6804562304, 0.9087880491], %)
1750"#
1751 );
1752 }
1753 #[test]
1754 fn test_recast_with_bad_indentation_and_line_comment() {
1755 let some_program_string = r#"part001 = startSketchOn('XY')
1756 |> startProfileAt([0.0, 5.0], %)
1757 |> line([0.4900857016, -0.0240763666], %)
1758 // hello world
1759 |> line([0.6804562304, 0.9087880491], %)"#;
1760 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1761
1762 let recasted = program.recast(&Default::default(), 0);
1763 assert_eq!(
1764 recasted,
1765 r#"part001 = startSketchOn('XY')
1766 |> startProfileAt([0.0, 5.0], %)
1767 |> line([0.4900857016, -0.0240763666], %)
1768 // hello world
1769 |> line([0.6804562304, 0.9087880491], %)
1770"#
1771 );
1772 }
1773
1774 #[test]
1775 fn test_recast_comment_in_a_fn_block() {
1776 let some_program_string = r#"fn myFn = () => {
1777 // this is a comment
1778 yo = { a = { b = { c = '123' } } } /* block
1779 comment */
1780
1781 key = 'c'
1782 // this is also a comment
1783 return things
1784}"#;
1785 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1786
1787 let recasted = program.recast(&Default::default(), 0);
1788 assert_eq!(
1789 recasted,
1790 r#"fn myFn() {
1791 // this is a comment
1792 yo = { a = { b = { c = '123' } } } /* block
1793 comment */
1794
1795 key = 'c'
1796 // this is also a comment
1797 return things
1798}
1799"#
1800 );
1801 }
1802
1803 #[test]
1804 fn test_recast_comment_under_variable() {
1805 let some_program_string = r#"key = 'c'
1806// this is also a comment
1807thing = 'foo'
1808"#;
1809 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1810
1811 let recasted = program.recast(&Default::default(), 0);
1812 assert_eq!(
1813 recasted,
1814 r#"key = 'c'
1815// this is also a comment
1816thing = 'foo'
1817"#
1818 );
1819 }
1820
1821 #[test]
1822 fn test_recast_multiline_comment_start_file() {
1823 let some_program_string = r#"// hello world
1824// I am a comment
1825key = 'c'
1826// this is also a comment
1827// hello
1828thing = 'foo'
1829"#;
1830 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1831
1832 let recasted = program.recast(&Default::default(), 0);
1833 assert_eq!(
1834 recasted,
1835 r#"// hello world
1836// I am a comment
1837key = 'c'
1838// this is also a comment
1839// hello
1840thing = 'foo'
1841"#
1842 );
1843 }
1844
1845 #[test]
1846 fn test_recast_empty_comment() {
1847 let some_program_string = r#"// hello world
1848//
1849// I am a comment
1850key = 'c'
1851
1852//
1853// I am a comment
1854thing = 'c'
1855
1856foo = 'bar' //
1857"#;
1858 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1859
1860 let recasted = program.recast(&Default::default(), 0);
1861 assert_eq!(
1862 recasted,
1863 r#"// hello world
1864//
1865// I am a comment
1866key = 'c'
1867
1868//
1869// I am a comment
1870thing = 'c'
1871
1872foo = 'bar' //
1873"#
1874 );
1875 }
1876
1877 #[test]
1878 fn test_recast_multiline_comment_under_variable() {
1879 let some_program_string = r#"key = 'c'
1880// this is also a comment
1881// hello
1882thing = 'foo'
1883"#;
1884 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1885
1886 let recasted = program.recast(&Default::default(), 0);
1887 assert_eq!(
1888 recasted,
1889 r#"key = 'c'
1890// this is also a comment
1891// hello
1892thing = 'foo'
1893"#
1894 );
1895 }
1896
1897 #[test]
1898 fn test_recast_comment_at_start() {
1899 let test_program = r#"
1900/* comment at start */
1901
1902mySk1 = startSketchOn(XY)
1903 |> startProfileAt([0, 0], %)"#;
1904 let program = crate::parsing::top_level_parse(test_program).unwrap();
1905
1906 let recasted = program.recast(&Default::default(), 0);
1907 assert_eq!(
1908 recasted,
1909 r#"/* comment at start */
1910
1911mySk1 = startSketchOn(XY)
1912 |> startProfileAt([0, 0], %)
1913"#
1914 );
1915 }
1916
1917 #[test]
1918 fn test_recast_lots_of_comments() {
1919 let some_program_string = r#"// comment at start
1920mySk1 = startSketchOn('XY')
1921 |> startProfileAt([0, 0], %)
1922 |> line(endAbsolute = [1, 1])
1923 // comment here
1924 |> line(endAbsolute = [0, 1], tag = $myTag)
1925 |> line(endAbsolute = [1, 1])
1926 /* and
1927 here
1928 */
1929 // a comment between pipe expression statements
1930 |> rx(90, %)
1931 // and another with just white space between others below
1932 |> ry(45, %)
1933 |> rx(45, %)
1934// one more for good measure"#;
1935 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1936
1937 let recasted = program.recast(&Default::default(), 0);
1938 assert_eq!(
1939 recasted,
1940 r#"// comment at start
1941mySk1 = startSketchOn('XY')
1942 |> startProfileAt([0, 0], %)
1943 |> line(endAbsolute = [1, 1])
1944 // comment here
1945 |> line(endAbsolute = [0, 1], tag = $myTag)
1946 |> line(endAbsolute = [1, 1])
1947 /* and
1948 here */
1949 // a comment between pipe expression statements
1950 |> rx(90, %)
1951 // and another with just white space between others below
1952 |> ry(45, %)
1953 |> rx(45, %)
1954// one more for good measure
1955"#
1956 );
1957 }
1958
1959 #[test]
1960 fn test_recast_multiline_object() {
1961 let some_program_string = r#"part001 = startSketchOn('XY')
1962 |> startProfileAt([-0.01, -0.08], %)
1963 |> line([0.62, 4.15], %, $seg01)
1964 |> line([2.77, -1.24], %)
1965 |> angledLineThatIntersects({
1966 angle = 201,
1967 offset = -1.35,
1968 intersectTag = seg01
1969 }, %)
1970 |> line([-0.42, -1.72], %)"#;
1971 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1972
1973 let recasted = program.recast(&Default::default(), 0);
1974 assert_eq!(recasted.trim(), some_program_string);
1975 }
1976
1977 #[test]
1978 fn test_recast_first_level_object() {
1979 let some_program_string = r#"three = 3
1980
1981yo = {
1982 aStr = 'str',
1983 anum = 2,
1984 identifier = three,
1985 binExp = 4 + 5
1986}
1987yo = [
1988 1,
1989 " 2,",
1990 "three",
1991 4 + 5,
1992 " hey oooooo really long long long"
1993]
1994"#;
1995 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
1996
1997 let recasted = program.recast(&Default::default(), 0);
1998 assert_eq!(recasted, some_program_string);
1999 }
2000
2001 #[test]
2002 fn test_recast_new_line_before_comment() {
2003 let some_program_string = r#"
2004// this is a comment
2005yo = { a = { b = { c = '123' } } }
2006
2007key = 'c'
2008things = "things"
2009
2010// this is also a comment"#;
2011 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2012
2013 let recasted = program.recast(&Default::default(), 0);
2014 let expected = some_program_string.trim();
2015 let actual = recasted.trim();
2017 assert_eq!(actual, expected);
2018 }
2019
2020 #[test]
2021 fn test_recast_comment_tokens_inside_strings() {
2022 let some_program_string = r#"b = {
2023 end = 141,
2024 start = 125,
2025 type_ = "NonCodeNode",
2026 value = "
2027 // a comment
2028 "
2029}"#;
2030 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2031
2032 let recasted = program.recast(&Default::default(), 0);
2033 assert_eq!(recasted.trim(), some_program_string.trim());
2034 }
2035
2036 #[test]
2037 fn test_recast_array_new_line_in_pipe() {
2038 let some_program_string = r#"myVar = 3
2039myVar2 = 5
2040myVar3 = 6
2041myAng = 40
2042myAng2 = 134
2043part001 = startSketchOn('XY')
2044 |> startProfileAt([0, 0], %)
2045 |> line([1, 3.82], %, $seg01) // ln-should-get-tag
2046 |> angledLineToX([
2047 -angleToMatchLengthX(seg01, myVar, %),
2048 myVar
2049 ], %) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
2050 |> angledLineToY([
2051 -angleToMatchLengthY(seg01, myVar, %),
2052 myVar
2053 ], %) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper"#;
2054 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2055
2056 let recasted = program.recast(&Default::default(), 0);
2057 assert_eq!(recasted.trim(), some_program_string);
2058 }
2059
2060 #[test]
2061 fn test_recast_array_new_line_in_pipe_custom() {
2062 let some_program_string = r#"myVar = 3
2063myVar2 = 5
2064myVar3 = 6
2065myAng = 40
2066myAng2 = 134
2067part001 = startSketchOn('XY')
2068 |> startProfileAt([0, 0], %)
2069 |> line([1, 3.82], %, $seg01) // ln-should-get-tag
2070 |> angledLineToX([
2071 -angleToMatchLengthX(seg01, myVar, %),
2072 myVar
2073 ], %) // ln-lineTo-xAbsolute should use angleToMatchLengthX helper
2074 |> angledLineToY([
2075 -angleToMatchLengthY(seg01, myVar, %),
2076 myVar
2077 ], %) // ln-lineTo-yAbsolute should use angleToMatchLengthY helper
2078"#;
2079 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2080
2081 let recasted = program.recast(
2082 &FormatOptions {
2083 tab_size: 3,
2084 use_tabs: false,
2085 insert_final_newline: true,
2086 },
2087 0,
2088 );
2089 assert_eq!(recasted, some_program_string);
2090 }
2091
2092 #[test]
2093 fn test_recast_after_rename_std() {
2094 let some_program_string = r#"part001 = startSketchOn('XY')
2095 |> startProfileAt([0.0000000000, 5.0000000000], %)
2096 |> line([0.4900857016, -0.0240763666], %)
2097
2098part002 = "part002"
2099things = [part001, 0.0]
2100blah = 1
2101foo = false
2102baz = {a: 1, part001: "thing"}
2103
2104fn ghi = (part001) => {
2105 return part001
2106}
2107"#;
2108 let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
2109 program.rename_symbol("mySuperCoolPart", 6);
2110
2111 let recasted = program.recast(&Default::default(), 0);
2112 assert_eq!(
2113 recasted,
2114 r#"mySuperCoolPart = startSketchOn('XY')
2115 |> startProfileAt([0.0, 5.0], %)
2116 |> line([0.4900857016, -0.0240763666], %)
2117
2118part002 = "part002"
2119things = [mySuperCoolPart, 0.0]
2120blah = 1
2121foo = false
2122baz = { a = 1, part001 = "thing" }
2123
2124fn ghi(part001) {
2125 return part001
2126}
2127"#
2128 );
2129 }
2130
2131 #[test]
2132 fn test_recast_after_rename_fn_args() {
2133 let some_program_string = r#"fn ghi = (x, y, z) => {
2134 return x
2135}"#;
2136 let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
2137 program.rename_symbol("newName", 10);
2138
2139 let recasted = program.recast(&Default::default(), 0);
2140 assert_eq!(
2141 recasted,
2142 r#"fn ghi(newName, y, z) {
2143 return newName
2144}
2145"#
2146 );
2147 }
2148
2149 #[test]
2150 fn test_recast_trailing_comma() {
2151 let some_program_string = r#"startSketchOn('XY')
2152 |> startProfileAt([0, 0], %)
2153 |> arc({
2154 radius = 1,
2155 angle_start = 0,
2156 angle_end = 180,
2157 }, %)"#;
2158 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2159
2160 let recasted = program.recast(&Default::default(), 0);
2161 assert_eq!(
2162 recasted,
2163 r#"startSketchOn('XY')
2164 |> startProfileAt([0, 0], %)
2165 |> arc({
2166 radius = 1,
2167 angle_start = 0,
2168 angle_end = 180
2169 }, %)
2170"#
2171 );
2172 }
2173
2174 #[test]
2175 fn test_recast_negative_var() {
2176 let some_program_string = r#"w = 20
2177l = 8
2178h = 10
2179
2180firstExtrude = startSketchOn('XY')
2181 |> startProfileAt([0,0], %)
2182 |> line([0, l], %)
2183 |> line([w, 0], %)
2184 |> line([0, -l], %)
2185 |> close()
2186 |> extrude(h, %)
2187"#;
2188 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2189
2190 let recasted = program.recast(&Default::default(), 0);
2191 assert_eq!(
2192 recasted,
2193 r#"w = 20
2194l = 8
2195h = 10
2196
2197firstExtrude = startSketchOn('XY')
2198 |> startProfileAt([0, 0], %)
2199 |> line([0, l], %)
2200 |> line([w, 0], %)
2201 |> line([0, -l], %)
2202 |> close()
2203 |> extrude(h, %)
2204"#
2205 );
2206 }
2207
2208 #[test]
2209 fn test_recast_multiline_comment() {
2210 let some_program_string = r#"w = 20
2211l = 8
2212h = 10
2213
2214// This is my comment
2215// It has multiple lines
2216// And it's really long
2217firstExtrude = startSketchOn('XY')
2218 |> startProfileAt([0,0], %)
2219 |> line([0, l], %)
2220 |> line([w, 0], %)
2221 |> line([0, -l], %)
2222 |> close()
2223 |> extrude(h, %)
2224"#;
2225 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2226
2227 let recasted = program.recast(&Default::default(), 0);
2228 assert_eq!(
2229 recasted,
2230 r#"w = 20
2231l = 8
2232h = 10
2233
2234// This is my comment
2235// It has multiple lines
2236// And it's really long
2237firstExtrude = startSketchOn('XY')
2238 |> startProfileAt([0, 0], %)
2239 |> line([0, l], %)
2240 |> line([w, 0], %)
2241 |> line([0, -l], %)
2242 |> close()
2243 |> extrude(h, %)
2244"#
2245 );
2246 }
2247
2248 #[test]
2249 fn test_recast_math_start_negative() {
2250 let some_program_string = r#"myVar = -5 + 6"#;
2251 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2252
2253 let recasted = program.recast(&Default::default(), 0);
2254 assert_eq!(recasted.trim(), some_program_string);
2255 }
2256
2257 #[test]
2258 fn test_recast_math_negate_parens() {
2259 let some_program_string = r#"wallMountL = 3.82
2260thickness = 0.5
2261
2262startSketchOn('XY')
2263 |> startProfileAt([0, 0], %)
2264 |> line([0, -(wallMountL - thickness)], %)
2265 |> line([0, -(5 - thickness)], %)
2266 |> line([0, -(5 - 1)], %)
2267 |> line([0, -(-5 - 1)], %)"#;
2268 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2269
2270 let recasted = program.recast(&Default::default(), 0);
2271 assert_eq!(recasted.trim(), some_program_string);
2272 }
2273
2274 #[test]
2275 fn test_recast_math_nested_parens() {
2276 let some_program_string = r#"distance = 5
2277p = 3: Plane
2278FOS = { a = 3, b = 42 }: Sketch
2279sigmaAllow = 8: number(mm)
2280width = 20
2281thickness = sqrt(distance * p * FOS * 6 / (sigmaAllow * width))"#;
2282 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2283
2284 let recasted = program.recast(&Default::default(), 0);
2285 assert_eq!(recasted.trim(), some_program_string);
2286 }
2287
2288 #[test]
2289 fn no_vardec_keyword() {
2290 let some_program_string = r#"distance = 5"#;
2291 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2292
2293 let recasted = program.recast(&Default::default(), 0);
2294 assert_eq!(recasted.trim(), some_program_string);
2295 }
2296
2297 #[test]
2298 fn recast_types() {
2299 let some_program_string = r#"type foo
2300
2301// A comment
2302@(impl = primitive)
2303export type bar(unit, baz)
2304"#;
2305 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2306 let recasted = program.recast(&Default::default(), 0);
2307 assert_eq!(recasted, some_program_string);
2308 }
2309
2310 #[test]
2311 fn recast_nested_fn() {
2312 let some_program_string = r#"fn f = () => {
2313 return fn() => {
2314 return 1
2315}
2316}"#;
2317 let program = crate::parsing::top_level_parse(some_program_string).unwrap();
2318 let recasted = program.recast(&Default::default(), 0);
2319 let expected = "\
2320fn f() {
2321 return fn() {
2322 return 1
2323 }
2324}";
2325 assert_eq!(recasted.trim(), expected);
2326 }
2327
2328 #[test]
2329 fn recast_literal() {
2330 use winnow::Parser;
2331 for (i, (raw, expected, reason)) in [
2332 (
2333 "5.0",
2334 "5.0",
2335 "fractional numbers should stay fractional, i.e. don't reformat this to '5'",
2336 ),
2337 (
2338 "5",
2339 "5",
2340 "integers should stay integral, i.e. don't reformat this to '5.0'",
2341 ),
2342 (
2343 "5.0000000",
2344 "5.0",
2345 "if the number is f64 but not fractional, use its canonical format",
2346 ),
2347 ("5.1", "5.1", "straightforward case works"),
2348 ]
2349 .into_iter()
2350 .enumerate()
2351 {
2352 let tokens = crate::parsing::token::lex(raw, ModuleId::default()).unwrap();
2353 let literal = crate::parsing::parser::unsigned_number_literal
2354 .parse(tokens.as_slice())
2355 .unwrap();
2356 assert_eq!(
2357 literal.recast(),
2358 expected,
2359 "failed test {i}, which is testing that {reason}"
2360 );
2361 }
2362 }
2363
2364 #[test]
2365 fn recast_objects_no_comments() {
2366 let input = r#"
2367sketch002 = startSketchOn({
2368 plane: {
2369 origin: { x = 1, y = 2, z = 3 },
2370 x_axis: { x = 4, y = 5, z = 6 },
2371 y_axis: { x = 7, y = 8, z = 9 },
2372 z_axis: { x = 10, y = 11, z = 12 }
2373 }
2374 })
2375"#;
2376 let expected = r#"sketch002 = startSketchOn({
2377 plane = {
2378 origin = { x = 1, y = 2, z = 3 },
2379 x_axis = { x = 4, y = 5, z = 6 },
2380 y_axis = { x = 7, y = 8, z = 9 },
2381 z_axis = { x = 10, y = 11, z = 12 }
2382 }
2383})
2384"#;
2385 let ast = crate::parsing::top_level_parse(input).unwrap();
2386 let actual = ast.recast(&FormatOptions::new(), 0);
2387 assert_eq!(actual, expected);
2388 }
2389
2390 #[test]
2391 fn unparse_fn_unnamed() {
2392 let input = r#"squares_out = reduce(arr, 0: number, fn(i, squares) {
2393 return 1
2394})
2395"#;
2396 let ast = crate::parsing::top_level_parse(input).unwrap();
2397 let actual = ast.recast(&FormatOptions::new(), 0);
2398 assert_eq!(actual, input);
2399 }
2400
2401 #[test]
2402 fn unparse_fn_named() {
2403 let input = r#"fn f(x) {
2404 return 1
2405}
2406"#;
2407 let ast = crate::parsing::top_level_parse(input).unwrap();
2408 let actual = ast.recast(&FormatOptions::new(), 0);
2409 assert_eq!(actual, input);
2410 }
2411
2412 #[test]
2413 fn recast_objects_with_comments() {
2414 use winnow::Parser;
2415 for (i, (input, expected, reason)) in [(
2416 "\
2417{
2418 a = 1,
2419 // b = 2,
2420 c = 3
2421}",
2422 "\
2423{
2424 a = 1,
2425 // b = 2,
2426 c = 3
2427}",
2428 "preserves comments",
2429 )]
2430 .into_iter()
2431 .enumerate()
2432 {
2433 let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
2434 crate::parsing::parser::print_tokens(tokens.as_slice());
2435 let expr = crate::parsing::parser::object.parse(tokens.as_slice()).unwrap();
2436 assert_eq!(
2437 expr.recast(&FormatOptions::new(), 0, ExprContext::Other),
2438 expected,
2439 "failed test {i}, which is testing that recasting {reason}"
2440 );
2441 }
2442 }
2443
2444 #[test]
2445 fn recast_array_with_comments() {
2446 use winnow::Parser;
2447 for (i, (input, expected, reason)) in [
2448 (
2449 "\
2450[
2451 1,
2452 2,
2453 3,
2454 4,
2455 5,
2456 6,
2457 7,
2458 8,
2459 9,
2460 10,
2461 11,
2462 12,
2463 13,
2464 14,
2465 15,
2466 16,
2467 17,
2468 18,
2469 19,
2470 20,
2471]",
2472 "\
2473[
2474 1,
2475 2,
2476 3,
2477 4,
2478 5,
2479 6,
2480 7,
2481 8,
2482 9,
2483 10,
2484 11,
2485 12,
2486 13,
2487 14,
2488 15,
2489 16,
2490 17,
2491 18,
2492 19,
2493 20
2494]",
2495 "preserves multi-line arrays",
2496 ),
2497 (
2498 "\
2499[
2500 1,
2501 // 2,
2502 3
2503]",
2504 "\
2505[
2506 1,
2507 // 2,
2508 3
2509]",
2510 "preserves comments",
2511 ),
2512 (
2513 "\
2514[
2515 1,
2516 2,
2517 // 3
2518]",
2519 "\
2520[
2521 1,
2522 2,
2523 // 3
2524]",
2525 "preserves comments at the end of the array",
2526 ),
2527 ]
2528 .into_iter()
2529 .enumerate()
2530 {
2531 let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap();
2532 let expr = crate::parsing::parser::array_elem_by_elem
2533 .parse(tokens.as_slice())
2534 .unwrap();
2535 assert_eq!(
2536 expr.recast(&FormatOptions::new(), 0, ExprContext::Other),
2537 expected,
2538 "failed test {i}, which is testing that recasting {reason}"
2539 );
2540 }
2541 }
2542}