Fork of daniellemaywood.uk/gleam — Wasm codegen work
133 kB
3710 lines
1#[cfg(test)]
2mod tests;
3
4use crate::{
5 Error, Result,
6 ast::{
7 CustomType, Import, ModuleConstant, TypeAlias, TypeAstConstructor, TypeAstFn, TypeAstHole,
8 TypeAstTuple, TypeAstVar, *,
9 },
10 build::Target,
11 docvec,
12 io::Utf8Writer,
13 parse::extra::{Comment, ModuleExtra},
14 pretty::{self, *},
15 warning::WarningEmitter,
16};
17use ecow::{EcoString, eco_format};
18use itertools::Itertools;
19use std::cmp::Ordering;
20use vec1::Vec1;
21
22use crate::type_::Deprecation;
23use camino::Utf8Path;
24
25const INDENT: isize = 2;
26
27pub fn pretty(writer: &mut impl Utf8Writer, src: &EcoString, path: &Utf8Path) -> Result<()> {
28 let parsed = crate::parse::parse_module(path.to_owned(), src, &WarningEmitter::null())
29 .map_err(|error| Error::Parse {
30 path: path.to_path_buf(),
31 src: src.clone(),
32 error: Box::new(error),
33 })?;
34 let intermediate = Intermediate::from_extra(&parsed.extra, src);
35 Formatter::with_comments(&intermediate)
36 .module(&parsed.module)
37 .pretty_print(80, writer)
38}
39
40pub(crate) struct Intermediate<'a> {
41 comments: Vec<Comment<'a>>,
42 doc_comments: Vec<Comment<'a>>,
43 module_comments: Vec<Comment<'a>>,
44 empty_lines: &'a [u32],
45 new_lines: &'a [u32],
46 trailing_commas: &'a [u32],
47}
48
49impl<'a> Intermediate<'a> {
50 pub fn from_extra(extra: &'a ModuleExtra, src: &'a EcoString) -> Intermediate<'a> {
51 Intermediate {
52 comments: extra
53 .comments
54 .iter()
55 .map(|span| Comment::from((span, src)))
56 .collect(),
57 doc_comments: extra
58 .doc_comments
59 .iter()
60 .map(|span| Comment::from((span, src)))
61 .collect(),
62 empty_lines: &extra.empty_lines,
63 module_comments: extra
64 .module_comments
65 .iter()
66 .map(|span| Comment::from((span, src)))
67 .collect(),
68 new_lines: &extra.new_lines,
69 trailing_commas: &extra.trailing_commas,
70 }
71 }
72}
73
74#[derive(Debug)]
75enum FnCapturePosition {
76 RightHandSideOfPipe,
77 EverywhereElse,
78}
79
80#[derive(Debug)]
81/// One of the pieces making a record update arg list: it could be the starting
82/// record being updated, or one of the subsequent arguments.
83///
84enum RecordUpdatePiece<'a, A> {
85 Record(&'a RecordBeingUpdated<A>),
86 Argument(&'a RecordUpdateArg<A>),
87}
88
89impl<A> HasLocation for RecordUpdatePiece<'_, A> {
90 fn location(&self) -> SrcSpan {
91 match self {
92 RecordUpdatePiece::Record(record) => record.location,
93 RecordUpdatePiece::Argument(arg) => arg.location,
94 }
95 }
96}
97
98type UntypedRecordUpdatePiece<'a> = RecordUpdatePiece<'a, UntypedExpr>;
99
100/// Hayleigh's bane
101#[derive(Debug, Clone, Default)]
102pub struct Formatter<'a> {
103 comments: &'a [Comment<'a>],
104 doc_comments: &'a [Comment<'a>],
105 module_comments: &'a [Comment<'a>],
106 empty_lines: &'a [u32],
107 new_lines: &'a [u32],
108 trailing_commas: &'a [u32],
109}
110
111impl<'comments> Formatter<'comments> {
112 pub fn new() -> Self {
113 Default::default()
114 }
115
116 pub(crate) fn with_comments(extra: &'comments Intermediate<'comments>) -> Self {
117 Self {
118 comments: &extra.comments,
119 doc_comments: &extra.doc_comments,
120 module_comments: &extra.module_comments,
121 empty_lines: extra.empty_lines,
122 new_lines: extra.new_lines,
123 trailing_commas: extra.trailing_commas,
124 }
125 }
126
127 /// Returns true if there's any comment that comes before the given
128 /// position.
129 ///
130 fn any_comments(&self, limit: u32) -> bool {
131 self.comments
132 .first()
133 .is_some_and(|comment| comment.start < limit)
134 }
135
136 /// Returns true if there's any comment that appears inside the given span.
137 ///
138 fn any_comment_between(&self, start: u32, end: u32) -> bool {
139 self.comments
140 .binary_search_by(|comment| {
141 if comment.start < start {
142 Ordering::Less
143 } else if comment.start > end {
144 Ordering::Greater
145 } else {
146 Ordering::Equal
147 }
148 })
149 .is_ok()
150 }
151
152 fn any_empty_lines(&self, limit: u32) -> bool {
153 self.empty_lines.first().is_some_and(|line| *line < limit)
154 }
155
156 /// Pop comments that occur before a byte-index in the source, consuming
157 /// and retaining any empty lines contained within.
158 /// Returns an iterator of comments with their start position.
159 fn pop_comments_with_position(
160 &mut self,
161 limit: u32,
162 ) -> impl Iterator<Item = (u32, Option<&'comments str>)> + use<'comments> {
163 let (popped, rest, empty_lines) =
164 comments_before(self.comments, self.empty_lines, limit, true);
165 self.comments = rest;
166 self.empty_lines = empty_lines;
167 popped
168 }
169
170 /// Pop comments that occur before a byte-index in the source, consuming
171 /// and retaining any empty lines contained within.
172 fn pop_comments(
173 &mut self,
174 limit: u32,
175 ) -> impl Iterator<Item = Option<&'comments str>> + use<'comments> {
176 self.pop_comments_with_position(limit)
177 .map(|(_position, comment)| comment)
178 }
179
180 /// Pop doc comments that occur before a byte-index in the source, consuming
181 /// and dropping any empty lines contained within.
182 fn pop_doc_comments(
183 &mut self,
184 limit: u32,
185 ) -> impl Iterator<Item = Option<&'comments str>> + use<'comments> {
186 let (popped, rest, empty_lines) =
187 comments_before(self.doc_comments, self.empty_lines, limit, false);
188 self.doc_comments = rest;
189 self.empty_lines = empty_lines;
190 popped.map(|(_position, comment)| comment)
191 }
192
193 /// Remove between 0 and `limit` empty lines following the current position,
194 /// returning true if any empty lines were removed.
195 fn pop_empty_lines(&mut self, limit: u32) -> bool {
196 let mut end = 0;
197 for (i, &position) in self.empty_lines.iter().enumerate() {
198 if position > limit {
199 break;
200 }
201 end = i + 1;
202 }
203
204 self.empty_lines = self
205 .empty_lines
206 .get(end..)
207 .expect("Pop empty lines slicing");
208 end != 0
209 }
210
211 fn targeted_definition<'a>(&mut self, definition: &'a TargetedDefinition) -> Document<'a> {
212 let target = definition.target;
213 let definition = &definition.definition;
214 let start = definition.location().start;
215
216 let comments = self.pop_comments_with_position(start);
217 let comments = self.printed_documented_comments(comments);
218 let document = self.documented_definition(definition);
219 let document = match target {
220 None => document,
221 Some(Target::Erlang) => docvec!["@target(erlang)", line(), document],
222 Some(Target::JavaScript) => docvec!["@target(javascript)", line(), document],
223 };
224
225 comments.to_doc().append(document.group())
226 }
227
228 pub(crate) fn module<'a>(&mut self, module: &'a UntypedModule) -> Document<'a> {
229 let mut documents = vec![];
230 let mut previous_was_a_definition = false;
231
232 // Here we take consecutive groups of imports so that they can be sorted
233 // alphabetically.
234 for (is_import_group, definitions) in &module
235 .definitions
236 .iter()
237 .chunk_by(|definition| definition.definition.is_import())
238 {
239 if is_import_group {
240 if previous_was_a_definition {
241 documents.push(lines(2));
242 }
243 documents.append(&mut self.imports(definitions.collect_vec()));
244 previous_was_a_definition = false;
245 } else {
246 for definition in definitions {
247 if !documents.is_empty() {
248 documents.push(lines(2));
249 }
250 documents.push(self.targeted_definition(definition));
251 }
252 previous_was_a_definition = true;
253 }
254 }
255
256 let definitions = concat(documents);
257
258 // Now that definitions has been collected, only freestanding comments (//)
259 // and doc comments (///) remain. Freestanding comments aren't associated
260 // with any statement, and are moved to the bottom of the module.
261 let doc_comments = join(
262 self.doc_comments
263 .iter()
264 .map(|comment| "///".to_doc().append(EcoString::from(comment.content))),
265 line(),
266 );
267
268 let comments = match printed_comments(self.pop_comments(u32::MAX), false) {
269 Some(comments) => comments,
270 None => nil(),
271 };
272
273 let module_comments = if !self.module_comments.is_empty() {
274 let comments = self
275 .module_comments
276 .iter()
277 .map(|s| "////".to_doc().append(EcoString::from(s.content)));
278 join(comments, line()).append(line())
279 } else {
280 nil()
281 };
282
283 let non_empty = vec![module_comments, definitions, doc_comments, comments]
284 .into_iter()
285 .filter(|doc| !doc.is_empty());
286
287 join(non_empty, line()).append(line())
288 }
289
290 /// Separates the imports in groups delimited by comments or empty lines and
291 /// sorts each group alphabetically.
292 ///
293 /// The formatter needs to play nicely with import groups defined by the
294 /// programmer. If one puts a comment before an import then that's a clue
295 /// for the formatter that it has run into a gorup of related imports.
296 ///
297 /// So we can't just sort `imports` and format each one, we have to be a
298 /// bit smarter and see if each import is preceded by a comment.
299 /// Once we find a comment we know we're done with the current import
300 /// group and a new one has started.
301 ///
302 /// ```gleam
303 /// // This is an import group.
304 /// import gleam/int
305 /// import gleam/string
306 ///
307 /// // This marks the beginning of a new import group that can't
308 /// // be mushed together with the previous one!
309 /// import wibble
310 /// import wobble
311 /// ```
312 fn imports<'a>(&mut self, imports: Vec<&'a TargetedDefinition>) -> Vec<Document<'a>> {
313 let mut import_groups_docs = vec![];
314 let mut current_group = vec![];
315 let mut current_group_delimiter = nil();
316
317 for import in imports {
318 let start = import.definition.location().start;
319
320 // We need to start a new group if the `import` is preceded by one or
321 // more empty lines or a `//` comment.
322 let start_new_group = self.any_comments(start) || self.any_empty_lines(start);
323 if start_new_group {
324 // First we print the previous group and clear it out to start a
325 // new empty group containing the import we've just ran into.
326 if !current_group.is_empty() {
327 import_groups_docs.push(docvec![
328 current_group_delimiter,
329 self.sorted_import_group(¤t_group)
330 ]);
331 current_group.clear();
332 }
333
334 // Now that we've taken care of the previous group we can start
335 // the new one. We know it's preceded either by an empty line or
336 // some comments se we have to be a bit more precise and save the
337 // actual delimiter that we're going to put at the top of this
338 // group.
339
340 let comments = self.pop_comments(start);
341 let _ = self.pop_empty_lines(start);
342 current_group_delimiter = printed_comments(comments, true).unwrap_or(nil());
343 }
344 // Lastly we add the import to the group.
345 current_group.push(import);
346 }
347
348 // Let's not forget about the last import group!
349 if !current_group.is_empty() {
350 import_groups_docs.push(docvec![
351 current_group_delimiter,
352 self.sorted_import_group(¤t_group)
353 ]);
354 }
355
356 // We want all consecutive import groups to be separated by an empty line.
357 // This should really be `.intersperse(line())` but I can't do that
358 // because of https://github.com/rust-lang/rust/issues/48919.
359 Itertools::intersperse(import_groups_docs.into_iter(), lines(2)).collect_vec()
360 }
361
362 /// Prints the imports as a single sorted group of import statements.
363 ///
364 fn sorted_import_group<'a>(&mut self, imports: &[&'a TargetedDefinition]) -> Document<'a> {
365 let imports = imports
366 .iter()
367 .sorted_by(|one, other| match (&one.definition, &other.definition) {
368 (Definition::Import(one), Definition::Import(other)) => {
369 one.module.cmp(&other.module)
370 }
371 // It shouldn't really be possible for a non import to be here so
372 // we just return a default value.
373 _ => Ordering::Equal,
374 })
375 .map(|import| self.targeted_definition(import));
376
377 // This should really be `.intersperse(line())` but I can't do that
378 // because of https://github.com/rust-lang/rust/issues/48919.
379 Itertools::intersperse(imports, line())
380 .collect_vec()
381 .to_doc()
382 }
383
384 fn definition<'a>(&mut self, statement: &'a UntypedDefinition) -> Document<'a> {
385 match statement {
386 Definition::Function(function) => self.statement_fn(function),
387
388 Definition::TypeAlias(alias) => self.type_alias(alias),
389
390 Definition::CustomType(ct) => self.custom_type(ct),
391
392 Definition::Import(Import {
393 module,
394 as_name,
395 unqualified_values,
396 unqualified_types,
397 ..
398 }) => {
399 let second = if unqualified_values.is_empty() && unqualified_types.is_empty() {
400 nil()
401 } else {
402 let unqualified_types = unqualified_types
403 .iter()
404 .sorted_by(|a, b| a.name.cmp(&b.name))
405 .map(|type_| docvec!["type ", type_]);
406 let unqualified_values = unqualified_values
407 .iter()
408 .sorted_by(|a, b| a.name.cmp(&b.name))
409 .map(|value| value.to_doc());
410 let unqualified = join(
411 unqualified_types.chain(unqualified_values),
412 flex_break(",", ", "),
413 );
414 let unqualified = break_("", "")
415 .append(unqualified)
416 .nest(INDENT)
417 .append(break_(",", ""))
418 .group();
419 ".{".to_doc().append(unqualified).append("}")
420 };
421
422 let doc = docvec!["import ", module.as_str(), second];
423 let default_module_access_name = module.split('/').next_back().map(EcoString::from);
424 match (default_module_access_name, as_name) {
425 // If the `as name` is the same as the module name that would be
426 // used anyways we won't render it. For example:
427 // ```gleam
428 // import gleam/int as int
429 // ^^^^^^ this is redundant and removed
430 // ```
431 (Some(module_name), Some((AssignName::Variable(name), _)))
432 if &module_name == name =>
433 {
434 doc
435 }
436 (_, None) => doc,
437 (_, Some((AssignName::Variable(name) | AssignName::Discard(name), _))) => {
438 doc.append(" as ").append(name)
439 }
440 }
441 }
442
443 Definition::ModuleConstant(ModuleConstant {
444 publicity,
445 name,
446 annotation,
447 value,
448 deprecation,
449 documentation: _,
450 location: _,
451 name_location: _,
452 type_: _,
453 implementations: _,
454 }) => {
455 let attributes = AttributesPrinter::new()
456 .set_internal(*publicity)
457 .set_deprecation(deprecation)
458 .to_doc();
459 let head = attributes
460 .append(pub_(*publicity))
461 .append("const ")
462 .append(name.as_str());
463 let head = match annotation {
464 None => head,
465 Some(t) => head.append(": ").append(self.type_ast(t)),
466 };
467 head.append(" = ").append(self.const_expr(value).group())
468 }
469 }
470 }
471
472 fn const_expr<'a, A, B>(&mut self, value: &'a Constant<A, B>) -> Document<'a> {
473 let comments = self.pop_comments(value.location().start);
474 let document = match value {
475 Constant::Int { value, .. } => self.int(value),
476
477 Constant::Float { value, .. } => self.float(value),
478
479 Constant::String { value, .. } => self.string(value),
480
481 Constant::List {
482 elements,
483 location,
484 tail,
485 ..
486 } => self.const_list(elements, location, tail),
487
488 Constant::Tuple {
489 elements, location, ..
490 } => self.const_tuple(elements, location),
491
492 Constant::BitArray {
493 segments, location, ..
494 } => {
495 let segment_docs = segments
496 .iter()
497 .map(|segment| bit_array_segment(segment, |e| self.const_expr(e)))
498 .collect_vec();
499
500 let packing = self.items_sequence_packing(
501 segments,
502 None,
503 |segment| segment.value.can_have_multiple_per_line(),
504 *location,
505 );
506
507 self.bit_array(segment_docs, packing, location)
508 }
509
510 Constant::Record {
511 name,
512 arguments,
513 module: None,
514 ..
515 } if arguments.is_empty() => name.to_doc(),
516
517 Constant::Record {
518 name,
519 arguments,
520 module: Some((m, _)),
521 ..
522 } if arguments.is_empty() => m.to_doc().append(".").append(name.as_str()),
523
524 Constant::Record {
525 name,
526 arguments,
527 module: None,
528 location,
529 ..
530 } => {
531 let arguments = arguments
532 .iter()
533 .map(|argument| self.constant_call_arg(argument))
534 .collect_vec();
535 name.to_doc()
536 .append(self.wrap_arguments(arguments, location.end))
537 .group()
538 }
539
540 Constant::Record {
541 name,
542 arguments,
543 module: Some((m, _)),
544 location,
545 ..
546 } => {
547 let arguments = arguments
548 .iter()
549 .map(|argument| self.constant_call_arg(argument))
550 .collect_vec();
551 m.to_doc()
552 .append(".")
553 .append(name.as_str())
554 .append(self.wrap_arguments(arguments, location.end))
555 .group()
556 }
557
558 Constant::Var {
559 name, module: None, ..
560 } => name.to_doc(),
561
562 Constant::Var {
563 name,
564 module: Some((module, _)),
565 ..
566 } => docvec![module, ".", name],
567
568 Constant::StringConcatenation { left, right, .. } => self
569 .const_expr(left)
570 .append(break_("", " ").append("<>".to_doc()))
571 .nest(INDENT)
572 .append(" ")
573 .append(self.const_expr(right)),
574
575 Constant::RecordUpdate {
576 module,
577 name,
578 record,
579 arguments,
580 location,
581 ..
582 } => self.const_record_update(module, name, record, arguments, location),
583
584 Constant::Invalid { .. } => panic!("invalid constants can not be in an untyped ast"),
585 };
586 commented(document, comments)
587 }
588
589 fn const_list<'a, A, B>(
590 &mut self,
591 elements: &'a [Constant<A, B>],
592 location: &SrcSpan,
593 tail: &'a Option<Box<Constant<A, B>>>,
594 ) -> Document<'a> {
595 if elements.is_empty() {
596 // We take all comments that come _before_ the end of the list,
597 // that is all comments that are inside "[" and "]", if there's
598 // any comment we want to put it inside the empty list!
599 return match printed_comments(self.pop_comments(location.end), false) {
600 None => "[]".to_doc(),
601 Some(comments) => "["
602 .to_doc()
603 .append(break_("", "").nest(INDENT))
604 .append(comments)
605 .append(break_("", ""))
606 .append("]")
607 // vvv We want to make sure the comments are on a separate
608 // line from the opening and closing brackets so we
609 // force the breaks to be split on newlines.
610 .force_break(),
611 };
612 }
613
614 let list_packing = self.items_sequence_packing(
615 elements,
616 tail.as_deref(),
617 |element| element.can_have_multiple_per_line(),
618 *location,
619 );
620 let comma = match list_packing {
621 ItemsPacking::FitMultiplePerLine => flex_break(",", ", "),
622 ItemsPacking::FitOnePerLine | ItemsPacking::BreakOnePerLine => break_(",", ", "),
623 };
624
625 let mut elements_doc = nil();
626 for element in elements.iter() {
627 let empty_lines = self.pop_empty_lines(element.location().start);
628 let element_doc = self.const_expr(element);
629
630 elements_doc = if elements_doc.is_empty() {
631 element_doc
632 } else if empty_lines {
633 // If there's empty lines before the list item we want to add an
634 // empty line here. Notice how we're making sure no nesting is
635 // added after the comma, otherwise we would be adding needless
636 // whitespace in the empty line!
637 docvec![
638 elements_doc,
639 comma.clone().set_nesting(0),
640 line(),
641 element_doc
642 ]
643 } else {
644 docvec![elements_doc, comma.clone(), element_doc]
645 };
646 }
647 elements_doc = elements_doc.next_break_fits(NextBreakFitsMode::Disabled);
648
649 let doc = break_("[", "[").append(elements_doc);
650 let (doc, final_break) = match tail {
651 None => (doc.nest(INDENT), break_(",", "")),
652 Some(tail) => {
653 let comments = self.pop_comments(tail.location().start);
654
655 let tail = commented(docvec!["..", self.const_expr(tail)], comments);
656 (
657 doc.append(break_(",", ", ")).append(tail).nest(INDENT),
658 break_("", ""),
659 )
660 }
661 };
662
663 // We get all remaining comments that come before the list's closing
664 // square bracket.
665 // If there's any we add those before the closing square bracket instead
666 // of moving those out of the list.
667 // Otherwise those would be moved out of the list.
668 let comments = self.pop_comments(location.end);
669 let doc = match printed_comments(comments, false) {
670 None => doc.append(final_break).append("]"),
671 Some(comment) => doc
672 .append(final_break.nest(INDENT))
673 // ^ See how here we're adding the missing indentation to the
674 // final break so that the final comment is as indented as the
675 // list's items.
676 .append(comment)
677 .append(line())
678 .append("]")
679 .force_break(),
680 };
681
682 match list_packing {
683 ItemsPacking::FitOnePerLine | ItemsPacking::FitMultiplePerLine => doc.group(),
684 ItemsPacking::BreakOnePerLine => doc.force_break(),
685 }
686 }
687
688 pub fn const_tuple<'a, A, B>(
689 &mut self,
690 elements: &'a [Constant<A, B>],
691 location: &SrcSpan,
692 ) -> Document<'a> {
693 if elements.is_empty() {
694 // We take all comments that come _before_ the end of the tuple,
695 // that is all comments that are inside "#(" and ")", if there's
696 // any comment we want to put it inside the empty list!
697 return match printed_comments(self.pop_comments(location.end), false) {
698 None => "#()".to_doc(),
699 Some(comments) => "#("
700 .to_doc()
701 .append(break_("", "").nest(INDENT))
702 .append(comments)
703 .append(break_("", ""))
704 .append(")")
705 // vvv We want to make sure the comments are on a separate
706 // line from the opening and closing parentheses so we
707 // force the breaks to be split on newlines.
708 .force_break(),
709 };
710 }
711
712 let arguments_docs = elements.iter().map(|element| self.const_expr(element));
713 let tuple_doc = break_("#(", "#(")
714 .append(
715 join(arguments_docs, break_(",", ", "))
716 .next_break_fits(NextBreakFitsMode::Disabled),
717 )
718 .nest(INDENT);
719
720 let comments = self.pop_comments(location.end);
721 match printed_comments(comments, false) {
722 None => tuple_doc.append(break_(",", "")).append(")").group(),
723 Some(comments) => tuple_doc
724 .append(break_(",", "").nest(INDENT))
725 .append(comments)
726 .append(line())
727 .append(")")
728 .force_break(),
729 }
730 }
731
732 fn documented_definition<'a>(&mut self, s: &'a UntypedDefinition) -> Document<'a> {
733 let comments = self.doc_comments(s.location().start);
734 comments.append(self.definition(s).group()).group()
735 }
736
737 fn doc_comments<'a>(&mut self, limit: u32) -> Document<'a> {
738 let mut comments = self.pop_doc_comments(limit).peekable();
739 match comments.peek() {
740 None => nil(),
741 Some(_) => join(
742 comments.map(|c| match c {
743 Some(c) => "///".to_doc().append(EcoString::from(c)),
744 None => unreachable!("empty lines dropped by pop_doc_comments"),
745 }),
746 line(),
747 )
748 .append(line())
749 .force_break(),
750 }
751 }
752
753 fn type_ast_constructor<'a>(
754 &mut self,
755 name: &'a TypeAstConstructorName,
756 arguments: &'a [TypeAst],
757 location: &SrcSpan,
758 ) -> Document<'a> {
759 let head = match name {
760 TypeAstConstructorName::Unqualified { name, .. } => name.to_doc(),
761 TypeAstConstructorName::Qualified { module, name, .. } => {
762 module.to_doc().append(".").append(
763 name.as_ref()
764 .map_or(nil(), |(name, _name_location)| name.to_doc()),
765 )
766 }
767 };
768
769 if arguments.is_empty() {
770 head
771 } else {
772 head.append(self.type_arguments(arguments, location))
773 }
774 }
775
776 fn type_ast<'a>(&mut self, type_: &'a TypeAst) -> Document<'a> {
777 let comments = self.pop_comments(type_.location().start);
778
779 let type_ = match type_ {
780 TypeAst::Hole(TypeAstHole { name, .. }) => name.to_doc(),
781
782 TypeAst::Constructor(TypeAstConstructor {
783 name,
784 arguments,
785 location,
786 start_parentheses: _,
787 }) => self.type_ast_constructor(name, arguments, location),
788
789 TypeAst::Fn(TypeAstFn {
790 arguments,
791 return_,
792 location,
793 }) => "fn"
794 .to_doc()
795 .append(self.type_arguments(arguments, location))
796 .group()
797 .append(" ->")
798 .append(
799 break_("", " ")
800 .append(self.type_ast(return_))
801 .group()
802 .nest(INDENT),
803 ),
804
805 TypeAst::Var(TypeAstVar { name, .. }) => name.to_doc(),
806
807 TypeAst::Tuple(TypeAstTuple { elements, location }) => {
808 "#".to_doc().append(self.type_arguments(elements, location))
809 }
810 };
811
812 commented(type_.group(), comments)
813 }
814
815 fn type_arguments<'a>(&mut self, arguments: &'a [TypeAst], location: &SrcSpan) -> Document<'a> {
816 let arguments = arguments
817 .iter()
818 .map(|type_| self.type_ast(type_))
819 .collect_vec();
820 self.wrap_arguments(arguments, location.end)
821 }
822
823 pub fn type_alias<'a, A>(&mut self, alias: &'a TypeAlias<A>) -> Document<'a> {
824 let TypeAlias {
825 alias: name,
826 parameters: arguments,
827 type_ast: type_,
828 publicity,
829 deprecation,
830 location,
831 name_location: _,
832 type_: _,
833 documentation: _,
834 } = alias;
835
836 let attributes = AttributesPrinter::new()
837 .set_deprecation(deprecation)
838 .set_internal(*publicity)
839 .to_doc();
840
841 let head = docvec![attributes, pub_(*publicity), "type ", name];
842 let head = if arguments.is_empty() {
843 head
844 } else {
845 let arguments = arguments.iter().map(|(_, e)| e.to_doc()).collect_vec();
846 head.append(self.wrap_arguments(arguments, location.end).group())
847 };
848
849 head.append(" =")
850 .append(line().append(self.type_ast(type_)).group().nest(INDENT))
851 }
852
853 fn fn_arg<'a, A>(&mut self, argument: &'a Arg<A>) -> Document<'a> {
854 let comments = self.pop_comments(argument.location.start);
855 let doc = match &argument.annotation {
856 None => argument.names.to_doc(),
857 Some(a) => argument
858 .names
859 .to_doc()
860 .append(": ")
861 .append(self.type_ast(a)),
862 }
863 .group();
864 commented(doc, comments)
865 }
866
867 fn statement_fn<'a>(&mut self, function: &'a UntypedFunction) -> Document<'a> {
868 let Function {
869 location,
870 body_start: _,
871 end_position,
872 name,
873 arguments,
874 body,
875 publicity,
876 deprecation,
877 return_annotation,
878 return_type: _,
879 documentation: _,
880 external_erlang,
881 external_javascript,
882 implementations: _,
883 purity: _,
884 } = function;
885
886 let attributes = AttributesPrinter::new()
887 .set_deprecation(deprecation)
888 .set_internal(*publicity)
889 .set_external_erlang(external_erlang)
890 .set_external_javascript(external_javascript)
891 .to_doc();
892
893 // Fn name and args
894 let arguments = arguments
895 .iter()
896 .map(|argument| self.fn_arg(argument))
897 .collect_vec();
898 let signature = pub_(*publicity)
899 .append("fn ")
900 .append(
901 &name
902 .as_ref()
903 .expect("Function in a statement must be named")
904 .1,
905 )
906 .append(
907 self.wrap_arguments(
908 arguments,
909 // Calculate end location of arguments to not consume comments in
910 // return annotation
911 return_annotation
912 .as_ref()
913 .map_or(location.end, |ann| ann.location().start),
914 ),
915 );
916
917 // Add return annotation
918 let signature = match &return_annotation {
919 Some(anno) => signature.append(" -> ").append(self.type_ast(anno)),
920 None => signature,
921 }
922 .group();
923
924 if body.is_empty() {
925 return attributes.append(signature);
926 }
927
928 let head = attributes.append(signature);
929
930 // Format body
931
932 let body = self.statements(body);
933
934 // Add any trailing comments
935 let body = match printed_comments(self.pop_comments(*end_position), false) {
936 Some(comments) => body.append(line()).append(comments),
937 None => body,
938 };
939
940 // Stick it all together
941 head.append(" {")
942 .append(line().append(body).nest(INDENT).group())
943 .append(line())
944 .append("}")
945 }
946
947 fn expr_fn<'a>(
948 &mut self,
949 arguments: &'a [UntypedArg],
950 return_annotation: Option<&'a TypeAst>,
951 body: &'a Vec1<UntypedStatement>,
952 location: &SrcSpan,
953 end_of_head_byte_index: &u32,
954 ) -> Document<'a> {
955 let arguments_docs = arguments
956 .iter()
957 .map(|argument| self.fn_arg(argument))
958 .collect_vec();
959 let arguments = self
960 .wrap_arguments(arguments_docs, *end_of_head_byte_index)
961 .group()
962 .next_break_fits(NextBreakFitsMode::Disabled);
963 // ^^^ We add this so that when an expression function is passed as
964 // the last argument of a function and it goes over the line
965 // limit with just its arguments we don't get some strange
966 // splitting.
967 // See https://github.com/gleam-lang/gleam/issues/2571
968 //
969 // There's many ways we could be smarter than this. For example:
970 // - still split the arguments like it did in the example shown in the
971 // issue if the expr_fn has more than one argument
972 // - make sure that an anonymous function whose body is made up of a
973 // single expression doesn't get split (I think that could boil down
974 // to wrapping the body with a `next_break_fits(Disabled)`)
975 //
976 // These are some of the ways we could tweak the look of expression
977 // functions in the future if people are not satisfied with it.
978
979 let header = "fn".to_doc().append(arguments);
980
981 let header = match return_annotation {
982 None => header,
983 Some(t) => header.append(" -> ").append(self.type_ast(t)),
984 };
985
986 let statements = self.statements(body.as_vec());
987 let body = match printed_comments(self.pop_comments(location.end), false) {
988 None => statements,
989 Some(comments) => statements.append(line()).append(comments).force_break(),
990 };
991
992 header.append(" ").append(wrap_block(body)).group()
993 }
994
995 fn statements<'a>(&mut self, statements: &'a [UntypedStatement]) -> Document<'a> {
996 let mut previous_position = 0;
997 let count = statements.len();
998 let mut documents = Vec::with_capacity(count * 2);
999 for (i, statement) in statements.iter().enumerate() {
1000 let preceding_newline = self.pop_empty_lines(previous_position + 1);
1001 if i != 0 && preceding_newline {
1002 documents.push(lines(2));
1003 } else if i != 0 {
1004 documents.push(line());
1005 }
1006 previous_position = statement.location().end;
1007 documents.push(self.statement(statement).group());
1008
1009 // If the last statement is a use we make sure it's followed by a
1010 // todo to make it explicit it has an unimplemented callback.
1011 if statement.is_use() && i == count - 1 {
1012 documents.push(line());
1013 documents.push("todo".to_doc());
1014 }
1015 }
1016
1017 if count == 1
1018 && statements
1019 .first()
1020 .is_some_and(|statement| statement.is_expression())
1021 {
1022 documents.to_doc()
1023 } else {
1024 documents.to_doc().force_break()
1025 }
1026 }
1027
1028 fn assignment<'a>(&mut self, assignment: &'a UntypedAssignment) -> Document<'a> {
1029 let comments = self.pop_comments(assignment.location.start);
1030 let Assignment {
1031 pattern,
1032 value,
1033 kind,
1034 annotation,
1035 ..
1036 } = assignment;
1037
1038 let _ = self.pop_empty_lines(pattern.location().end);
1039
1040 let (keyword, message) = match kind {
1041 AssignmentKind::Let | AssignmentKind::Generated => ("let ", None),
1042 AssignmentKind::Assert { message, .. } => ("let assert ", message.as_ref()),
1043 };
1044
1045 let pattern = self.pattern(pattern);
1046
1047 let annotation = annotation
1048 .as_ref()
1049 .map(|a| ": ".to_doc().append(self.type_ast(a)));
1050
1051 let doc = keyword
1052 .to_doc()
1053 .append(pattern.append(annotation).group())
1054 .append(" =")
1055 .append(self.assigned_value(value));
1056
1057 commented(
1058 self.append_as_message(doc, PrecedingAs::Expression, message),
1059 comments,
1060 )
1061 }
1062
1063 fn expr<'a>(&mut self, expression: &'a UntypedExpr) -> Document<'a> {
1064 let comments = self.pop_comments(expression.start_byte_index());
1065
1066 let document = match expression {
1067 UntypedExpr::Panic { message, .. } => {
1068 self.append_as_message("panic".to_doc(), PrecedingAs::Keyword, message.as_deref())
1069 }
1070
1071 UntypedExpr::Todo { message, .. } => {
1072 self.append_as_message("todo".to_doc(), PrecedingAs::Keyword, message.as_deref())
1073 }
1074
1075 UntypedExpr::Echo {
1076 expression,
1077 location: _,
1078 keyword_end: _,
1079 message,
1080 } => self.echo(expression, message),
1081
1082 UntypedExpr::PipeLine { expressions, .. } => self.pipeline(expressions, false),
1083
1084 UntypedExpr::Int { value, .. } => self.int(value),
1085
1086 UntypedExpr::Float { value, .. } => self.float(value),
1087
1088 UntypedExpr::String { value, .. } => self.string(value),
1089
1090 UntypedExpr::Block {
1091 statements,
1092 location,
1093 ..
1094 } => self.block(location, statements, false),
1095
1096 UntypedExpr::Var { name, .. } if name == CAPTURE_VARIABLE => "_".to_doc(),
1097
1098 UntypedExpr::Var { name, .. } => name.to_doc(),
1099
1100 UntypedExpr::TupleIndex { tuple, index, .. } => self.tuple_index(tuple, *index),
1101
1102 UntypedExpr::NegateInt { value, .. } => self.negate_int(value),
1103
1104 UntypedExpr::NegateBool { value, .. } => self.negate_bool(value),
1105
1106 UntypedExpr::Fn { kind, body, .. } if kind.is_capture() => {
1107 self.fn_capture(body, FnCapturePosition::EverywhereElse)
1108 }
1109
1110 UntypedExpr::Fn {
1111 return_annotation,
1112 arguments,
1113 body,
1114 location,
1115 end_of_head_byte_index,
1116 ..
1117 } => self.expr_fn(
1118 arguments,
1119 return_annotation.as_ref(),
1120 body,
1121 location,
1122 end_of_head_byte_index,
1123 ),
1124
1125 UntypedExpr::List {
1126 elements,
1127 tail,
1128 location,
1129 } => self.list(elements, tail.as_deref(), location),
1130
1131 UntypedExpr::Call {
1132 fun,
1133 arguments,
1134 location,
1135 ..
1136 } => self.call(fun, arguments, location),
1137
1138 UntypedExpr::BinOp {
1139 name, left, right, ..
1140 } => self.bin_op(name, left, right, false),
1141
1142 UntypedExpr::Case {
1143 subjects,
1144 clauses,
1145 location,
1146 } => self.case(subjects, clauses.as_deref().unwrap_or_default(), location),
1147
1148 UntypedExpr::FieldAccess {
1149 label, container, ..
1150 } => self.expr(container).append(".").append(label.as_str()),
1151
1152 UntypedExpr::Tuple { elements, location } => self.tuple(elements, location),
1153
1154 UntypedExpr::BitArray {
1155 segments, location, ..
1156 } => {
1157 let segment_docs = segments
1158 .iter()
1159 .map(|segment| bit_array_segment(segment, |e| self.bit_array_segment_expr(e)))
1160 .collect_vec();
1161
1162 let packing = self.items_sequence_packing(
1163 segments,
1164 None,
1165 |segment| segment.value.can_have_multiple_per_line(),
1166 *location,
1167 );
1168
1169 self.bit_array(segment_docs, packing, location)
1170 }
1171 UntypedExpr::RecordUpdate {
1172 constructor,
1173 record,
1174 arguments,
1175 location,
1176 ..
1177 } => self.record_update(constructor, record, arguments, location),
1178 };
1179 commented(document, comments)
1180 }
1181
1182 fn string<'a>(&self, string: &'a EcoString) -> Document<'a> {
1183 let doc = string.to_doc().surround("\"", "\"");
1184 if string.contains('\n') {
1185 doc.force_break()
1186 } else {
1187 doc
1188 }
1189 }
1190
1191 fn bin_op_string<'a>(&self, string: &'a EcoString) -> Document<'a> {
1192 let lines = string.split('\n').collect_vec();
1193 match lines.as_slice() {
1194 [] | [_] => string.to_doc().surround("\"", "\""),
1195 [first_line, lines @ ..] => {
1196 let mut doc = docvec!["\"", first_line];
1197 for line in lines {
1198 doc = doc
1199 .append(pretty::line().set_nesting(0))
1200 .append(line.to_doc())
1201 }
1202 doc.append("\"".to_doc()).group()
1203 }
1204 }
1205 }
1206
1207 fn float<'a>(&self, value: &'a str) -> Document<'a> {
1208 // Create parts
1209 let mut parts = value.split('.');
1210 let integer_part = parts.next().unwrap_or_default();
1211 // floating point part
1212 let fp_part = parts.next().unwrap_or_default();
1213 let integer_doc = self.underscore_integer_string(integer_part);
1214 let dot_doc = ".".to_doc();
1215
1216 // Split fp_part into a regular fractional and maybe a scientific part
1217 let (fp_part_fractional, fp_part_scientific) = fp_part.split_at(
1218 fp_part
1219 .chars()
1220 .position(|ch| ch == 'e')
1221 .unwrap_or(fp_part.len()),
1222 );
1223
1224 // Trim right any consequtive '0's
1225 let mut fp_part_fractional = fp_part_fractional.trim_end_matches('0').to_string();
1226 // If there is no fractional part left, add a '0', thus that 1. becomes 1.0 etc.
1227 if fp_part_fractional.is_empty() {
1228 fp_part_fractional.push('0');
1229 }
1230 let fp_doc = fp_part_fractional.chars().collect::<EcoString>();
1231
1232 integer_doc
1233 .append(dot_doc)
1234 .append(fp_doc)
1235 .append(fp_part_scientific)
1236 }
1237
1238 fn int<'a>(&self, value: &'a str) -> Document<'a> {
1239 if value.starts_with("0x") || value.starts_with("0b") || value.starts_with("0o") {
1240 return value.to_doc();
1241 }
1242
1243 self.underscore_integer_string(value)
1244 }
1245
1246 fn underscore_integer_string<'a>(&self, value: &'a str) -> Document<'a> {
1247 let underscore_ch = '_';
1248 let minus_ch = '-';
1249
1250 let len = value.len();
1251 let underscore_ch_cnt = value.matches(underscore_ch).count();
1252 let reformat_watershed = if value.starts_with(minus_ch) { 6 } else { 5 };
1253 let insert_underscores = (len - underscore_ch_cnt) >= reformat_watershed;
1254
1255 let mut new_value = String::new();
1256 let mut j = 0;
1257 for (i, ch) in value.chars().rev().enumerate() {
1258 if ch == '_' {
1259 continue;
1260 }
1261
1262 if insert_underscores && i != 0 && ch != minus_ch && i < len && j % 3 == 0 {
1263 new_value.push(underscore_ch);
1264 }
1265 new_value.push(ch);
1266
1267 j += 1;
1268 }
1269
1270 new_value.chars().rev().collect::<EcoString>().to_doc()
1271 }
1272
1273 fn pattern_constructor<'a>(
1274 &mut self,
1275 name: &'a str,
1276 arguments: &'a [CallArg<UntypedPattern>],
1277 module: &'a Option<(EcoString, SrcSpan)>,
1278 spread: Option<SrcSpan>,
1279 location: &SrcSpan,
1280 ) -> Document<'a> {
1281 fn is_breakable(expression: &UntypedPattern) -> bool {
1282 match expression {
1283 Pattern::Tuple { .. } | Pattern::List { .. } | Pattern::BitArray { .. } => true,
1284 Pattern::Constructor { arguments, .. } => !arguments.is_empty(),
1285 Pattern::Int { .. }
1286 | Pattern::Float { .. }
1287 | Pattern::String { .. }
1288 | Pattern::Variable { .. }
1289 | Pattern::BitArraySize(_)
1290 | Pattern::Assign { .. }
1291 | Pattern::Discard { .. }
1292 | Pattern::StringPrefix { .. }
1293 | Pattern::Invalid { .. } => false,
1294 }
1295 }
1296
1297 let name = match module {
1298 Some((m, _)) => m.to_doc().append(".").append(name),
1299 None => name.to_doc(),
1300 };
1301
1302 if arguments.is_empty() && spread.is_some() {
1303 name.append("(..)")
1304 } else if arguments.is_empty() {
1305 name
1306 } else if spread.is_some() {
1307 let arguments = arguments
1308 .iter()
1309 .map(|argument| self.pattern_call_arg(argument))
1310 .collect_vec();
1311 name.append(self.wrap_arguments_with_spread(arguments, location.end))
1312 .group()
1313 } else {
1314 match arguments {
1315 [argument] if is_breakable(&argument.value) => name
1316 .append("(")
1317 .append(self.pattern_call_arg(argument))
1318 .append(")")
1319 .group(),
1320
1321 _ => {
1322 let arguments = arguments
1323 .iter()
1324 .map(|argument| self.pattern_call_arg(argument))
1325 .collect_vec();
1326 name.append(self.wrap_arguments(arguments, location.end))
1327 .group()
1328 }
1329 }
1330 }
1331 }
1332
1333 fn call<'a>(
1334 &mut self,
1335 fun: &'a UntypedExpr,
1336 arguments: &'a [CallArg<UntypedExpr>],
1337 location: &SrcSpan,
1338 ) -> Document<'a> {
1339 let expression = match fun {
1340 UntypedExpr::PipeLine { .. } => break_block(self.expr(fun)),
1341
1342 UntypedExpr::BinOp { .. }
1343 | UntypedExpr::Int { .. }
1344 | UntypedExpr::Float { .. }
1345 | UntypedExpr::String { .. }
1346 | UntypedExpr::Block { .. }
1347 | UntypedExpr::Var { .. }
1348 | UntypedExpr::Fn { .. }
1349 | UntypedExpr::List { .. }
1350 | UntypedExpr::Call { .. }
1351 | UntypedExpr::Case { .. }
1352 | UntypedExpr::FieldAccess { .. }
1353 | UntypedExpr::Tuple { .. }
1354 | UntypedExpr::TupleIndex { .. }
1355 | UntypedExpr::Todo { .. }
1356 | UntypedExpr::Echo { .. }
1357 | UntypedExpr::Panic { .. }
1358 | UntypedExpr::BitArray { .. }
1359 | UntypedExpr::RecordUpdate { .. }
1360 | UntypedExpr::NegateBool { .. }
1361 | UntypedExpr::NegateInt { .. } => self.expr(fun),
1362 };
1363
1364 let arity = arguments.len();
1365 self.append_inlinable_wrapped_arguments(
1366 expression,
1367 arguments,
1368 location,
1369 |argument| is_breakable_argument(&argument.value, arguments.len()),
1370 |self_, argument| self_.call_arg(argument, arity),
1371 )
1372 }
1373
1374 fn tuple<'a>(&mut self, elements: &'a [UntypedExpr], location: &SrcSpan) -> Document<'a> {
1375 if elements.is_empty() {
1376 // We take all comments that come _before_ the end of the tuple,
1377 // that is all comments that are inside "#(" and ")", if there's
1378 // any comment we want to put it inside the empty tuple!
1379 return match printed_comments(self.pop_comments(location.end), false) {
1380 None => "#()".to_doc(),
1381 Some(comments) => "#("
1382 .to_doc()
1383 .append(break_("", "").nest(INDENT))
1384 .append(comments)
1385 .append(break_("", ""))
1386 .append(")")
1387 // vvv We want to make sure the comments are on a separate
1388 // line from the opening and closing parentheses so we
1389 // force the breaks to be split on newlines.
1390 .force_break(),
1391 };
1392 }
1393
1394 self.append_inlinable_wrapped_arguments(
1395 "#".to_doc(),
1396 elements,
1397 location,
1398 |expression| {
1399 !expression.is_tuple() && is_breakable_argument(expression, elements.len())
1400 },
1401 |self_, expression| self_.comma_separated_item(expression, elements.len()),
1402 )
1403 }
1404
1405 // Appends to the given docs a comma-separated list of documents wrapped by
1406 // parentheses. If the last item of the argument list is splittable the
1407 // resulting document will try to first split that before splitting all the
1408 // other arguments.
1409 // This is used for function calls and tuples.
1410 fn append_inlinable_wrapped_arguments<'a, 'b, T, Predicate, ToDoc>(
1411 &mut self,
1412 doc: Document<'a>,
1413 values: &'b [T],
1414 location: &SrcSpan,
1415 is_breakable_argument: Predicate,
1416 to_doc: ToDoc,
1417 ) -> Document<'a>
1418 where
1419 T: HasLocation,
1420 T: std::fmt::Debug,
1421 Predicate: Fn(&T) -> bool,
1422 ToDoc: Fn(&mut Self, &'b T) -> Document<'a>,
1423 {
1424 match init_and_last(values) {
1425 Some((initial_values, last_value))
1426 if is_breakable_argument(last_value)
1427 && !self.any_comments(last_value.location().start)
1428 && !self.any_comment_between(last_value.location().end, location.end) =>
1429 {
1430 let mut docs = initial_values
1431 .iter()
1432 .map(|value| to_doc(self, value))
1433 .collect_vec();
1434
1435 let last_value_doc = to_doc(self, last_value)
1436 .group()
1437 .next_break_fits(NextBreakFitsMode::Enabled);
1438
1439 docs.append(&mut vec![last_value_doc]);
1440
1441 doc.append(self.wrap_function_call_arguments(docs, location))
1442 .next_break_fits(NextBreakFitsMode::Disabled)
1443 .group()
1444 }
1445
1446 Some(_) | None => {
1447 let docs = values.iter().map(|value| to_doc(self, value)).collect_vec();
1448 doc.append(self.wrap_function_call_arguments(docs, location))
1449 .group()
1450 }
1451 }
1452 }
1453
1454 pub fn case<'a>(
1455 &mut self,
1456 subjects: &'a [UntypedExpr],
1457 clauses: &'a [UntypedClause],
1458 location: &'a SrcSpan,
1459 ) -> Document<'a> {
1460 let subjects_doc = break_("case", "case ")
1461 .append(join(
1462 subjects.iter().map(|s| self.expr(s).group()),
1463 break_(",", ", "),
1464 ))
1465 .nest(INDENT)
1466 .append(break_("", " "))
1467 .append("{")
1468 .next_break_fits(NextBreakFitsMode::Disabled)
1469 .group();
1470
1471 let clauses_doc = concat(
1472 clauses
1473 .iter()
1474 .enumerate()
1475 .map(|(i, c)| self.clause(c, i as u32).group()),
1476 );
1477
1478 // We get all remaining comments that come before the case's closing
1479 // bracket. If there's any we add those before the closing bracket
1480 // instead of moving those out of the case expression.
1481 // Otherwise those would be moved out of the case expression.
1482 let comments = self.pop_comments(location.end);
1483 let closing_bracket = match printed_comments(comments, false) {
1484 None => docvec![line(), "}"],
1485 Some(comment) => docvec![line(), comment]
1486 .nest(INDENT)
1487 .append(line())
1488 .append("}"),
1489 };
1490
1491 subjects_doc
1492 .append(line().append(clauses_doc).nest(INDENT))
1493 .append(closing_bracket)
1494 .force_break()
1495 }
1496
1497 pub fn record_update<'a>(
1498 &mut self,
1499 constructor: &'a UntypedExpr,
1500 record: &'a RecordBeingUpdated<UntypedExpr>,
1501 arguments: &'a [UntypedRecordUpdateArg],
1502 location: &SrcSpan,
1503 ) -> Document<'a> {
1504 let constructor_doc: Document<'a> = self.expr(constructor);
1505 let pieces = std::iter::once(UntypedRecordUpdatePiece::Record(record))
1506 .chain(arguments.iter().map(UntypedRecordUpdatePiece::Argument))
1507 .collect_vec();
1508
1509 self.append_inlinable_wrapped_arguments(
1510 constructor_doc,
1511 &pieces,
1512 location,
1513 |argument| {
1514 let expression = match argument {
1515 UntypedRecordUpdatePiece::Argument(argument) => &argument.value,
1516 UntypedRecordUpdatePiece::Record(record) => &record.base,
1517 };
1518 is_breakable_argument(expression, pieces.len())
1519 },
1520 |this, argument| match argument {
1521 UntypedRecordUpdatePiece::Argument(arg) => this.record_update_arg(arg),
1522 UntypedRecordUpdatePiece::Record(record) => {
1523 let comments = this.pop_comments(record.location.start);
1524 commented("..".to_doc().append(this.expr(&record.base)), comments)
1525 }
1526 },
1527 )
1528 }
1529
1530 pub fn const_record_update<'a, A, B>(
1531 &mut self,
1532 module: &Option<(EcoString, SrcSpan)>,
1533 name: &'a EcoString,
1534 record: &'a RecordBeingUpdated<Constant<A, B>>,
1535 arguments: &'a [RecordUpdateArg<Constant<A, B>>],
1536 location: &SrcSpan,
1537 ) -> Document<'a> {
1538 let constructor_doc = match module {
1539 Some((m, _)) => m.to_doc().append(".").append(name.as_str()),
1540 None => name.to_doc(),
1541 };
1542
1543 let pieces = std::iter::once(RecordUpdatePiece::Record(record))
1544 .chain(arguments.iter().map(RecordUpdatePiece::Argument))
1545 .collect_vec();
1546
1547 let docs = pieces
1548 .iter()
1549 .map(|piece| match piece {
1550 RecordUpdatePiece::Argument(argument) => {
1551 let comments = self.pop_comments(argument.location.start);
1552 let doc = match argument {
1553 _ if argument.uses_label_shorthand() => {
1554 argument.label.as_str().to_doc().append(":")
1555 }
1556 _ => argument
1557 .label
1558 .as_str()
1559 .to_doc()
1560 .append(": ")
1561 .append(self.const_expr(&argument.value))
1562 .group(),
1563 };
1564 commented(doc, comments)
1565 }
1566 RecordUpdatePiece::Record(record) => {
1567 let comments = self.pop_comments(record.location.start);
1568 commented(
1569 "..".to_doc().append(self.const_expr(&record.base)),
1570 comments,
1571 )
1572 }
1573 })
1574 .collect_vec();
1575
1576 constructor_doc
1577 .append(self.wrap_arguments(docs, location.end))
1578 .group()
1579 }
1580
1581 pub fn bin_op<'a>(
1582 &mut self,
1583 name: &'a BinOp,
1584 left: &'a UntypedExpr,
1585 right: &'a UntypedExpr,
1586 nest_steps: bool,
1587 ) -> Document<'a> {
1588 let left_side = self.bin_op_side(name, left, nest_steps);
1589
1590 let comments = self.pop_comments(right.start_byte_index());
1591 let name_doc = break_("", " ").append(commented(name.to_doc(), comments));
1592
1593 let right_side = self.bin_op_side(name, right, nest_steps);
1594
1595 left_side
1596 .append(if nest_steps {
1597 name_doc.nest(INDENT)
1598 } else {
1599 name_doc
1600 })
1601 .append(" ")
1602 .append(right_side)
1603 }
1604
1605 fn bin_op_side<'a>(
1606 &mut self,
1607 operator: &'a BinOp,
1608 side: &'a UntypedExpr,
1609 nest_steps: bool,
1610 ) -> Document<'a> {
1611 let side_doc = match side {
1612 UntypedExpr::String { value, .. } => self.bin_op_string(value),
1613 UntypedExpr::BinOp {
1614 name, left, right, ..
1615 } => self.bin_op(name, left, right, nest_steps),
1616 UntypedExpr::Int { .. }
1617 | UntypedExpr::Float { .. }
1618 | UntypedExpr::Block { .. }
1619 | UntypedExpr::Var { .. }
1620 | UntypedExpr::Fn { .. }
1621 | UntypedExpr::List { .. }
1622 | UntypedExpr::Call { .. }
1623 | UntypedExpr::PipeLine { .. }
1624 | UntypedExpr::Case { .. }
1625 | UntypedExpr::FieldAccess { .. }
1626 | UntypedExpr::Tuple { .. }
1627 | UntypedExpr::TupleIndex { .. }
1628 | UntypedExpr::Todo { .. }
1629 | UntypedExpr::Panic { .. }
1630 | UntypedExpr::Echo { .. }
1631 | UntypedExpr::BitArray { .. }
1632 | UntypedExpr::RecordUpdate { .. }
1633 | UntypedExpr::NegateBool { .. }
1634 | UntypedExpr::NegateInt { .. } => self.expr(side),
1635 };
1636 match side.bin_op_name() {
1637 // In case the other side is a binary operation as well and it can
1638 // be grouped together with the current binary operation, the two
1639 // docs are simply concatenated, so that they will end up in the
1640 // same group and the formatter will try to keep those on a single
1641 // line.
1642 Some(side_name) if side_name.can_be_grouped_with(operator) => side_doc,
1643 // In case the binary operations cannot be grouped together the
1644 // other side is treated as a group on its own so that it can be
1645 // broken independently of other pieces of the binary operations
1646 // chain.
1647 _ => self.operator_side(
1648 side_doc.group(),
1649 operator.precedence(),
1650 side.bin_op_precedence(),
1651 ),
1652 }
1653 }
1654
1655 pub fn operator_side<'a>(&self, doc: Document<'a>, op: u8, side: u8) -> Document<'a> {
1656 if op > side {
1657 wrap_block(doc).group()
1658 } else {
1659 doc
1660 }
1661 }
1662
1663 fn spans_multiple_lines(&self, start: u32, end: u32) -> bool {
1664 self.new_lines
1665 .binary_search_by(|newline| {
1666 if *newline <= start {
1667 Ordering::Less
1668 } else if *newline >= end {
1669 Ordering::Greater
1670 } else {
1671 // If the newline is in between the pipe start and end
1672 // then we've found it!
1673 Ordering::Equal
1674 }
1675 })
1676 // If we couldn't find any newline between the start and end of
1677 // the pipeline then we will try and keep it on a single line.
1678 .is_ok()
1679 }
1680
1681 /// Returns true if there's a trailing comma between `start` and `end`.
1682 ///
1683 fn has_trailing_comma(&self, start: u32, end: u32) -> bool {
1684 self.trailing_commas
1685 .binary_search_by(|comma| {
1686 if *comma < start {
1687 Ordering::Less
1688 } else if *comma > end {
1689 Ordering::Greater
1690 } else {
1691 Ordering::Equal
1692 }
1693 })
1694 .is_ok()
1695 }
1696
1697 fn pipeline<'a>(
1698 &mut self,
1699 expressions: &'a Vec1<UntypedExpr>,
1700 nest_pipe: bool,
1701 ) -> Document<'a> {
1702 let mut docs = Vec::with_capacity(expressions.len() * 3);
1703 let first = expressions.first();
1704 let first_precedence = first.bin_op_precedence();
1705 let first = self.expr(first).group();
1706 docs.push(self.operator_side(first, 5, first_precedence));
1707
1708 let pipeline_start = expressions.first().location().start;
1709 let pipeline_end = expressions.last().location().end;
1710 let try_to_keep_on_one_line = !self.spans_multiple_lines(pipeline_start, pipeline_end);
1711
1712 for expression in expressions.iter().skip(1) {
1713 let comments = self.pop_comments(expression.location().start);
1714 let doc = if let UntypedExpr::Fn { kind, body, .. } = expression
1715 && kind.is_capture()
1716 {
1717 self.fn_capture(body, FnCapturePosition::RightHandSideOfPipe)
1718 } else {
1719 self.expr(expression)
1720 };
1721 let doc = if nest_pipe { doc.nest(INDENT) } else { doc };
1722 let space = if try_to_keep_on_one_line {
1723 break_("", " ")
1724 } else {
1725 line()
1726 };
1727 let pipe = space.append(commented("|> ".to_doc(), comments));
1728 let pipe = if nest_pipe { pipe.nest(INDENT) } else { pipe };
1729 docs.push(pipe);
1730 docs.push(self.operator_side(doc, 4, expression.bin_op_precedence()));
1731 }
1732
1733 if try_to_keep_on_one_line {
1734 docs.to_doc()
1735 } else {
1736 docs.to_doc().force_break()
1737 }
1738 }
1739
1740 fn fn_capture<'a>(
1741 &mut self,
1742 call: &'a [UntypedStatement],
1743 position: FnCapturePosition,
1744 ) -> Document<'a> {
1745 // The body of a capture being multiple statements shouldn't be possible...
1746 if call.len() != 1 {
1747 panic!("Function capture found not to have a single statement call");
1748 }
1749
1750 let Some(Statement::Expression(UntypedExpr::Call {
1751 fun,
1752 arguments,
1753 location,
1754 })) = call.first()
1755 else {
1756 // The body of a capture being not a fn shouldn't be possible...
1757 panic!("Function capture body found not to be a call in the formatter")
1758 };
1759
1760 match (position, arguments.as_slice()) {
1761 // The capture has a single unlabelled hole:
1762 //
1763 // wibble |> wobble(_)
1764 // list.map([], wobble(_))
1765 //
1766 // We want these to become:
1767 //
1768 // wibble |> wobble
1769 // list.map([], wobble)
1770 //
1771 (
1772 FnCapturePosition::RightHandSideOfPipe | FnCapturePosition::EverywhereElse,
1773 [argument],
1774 ) if argument.is_capture_hole() && argument.label.is_none() => self.expr(fun),
1775
1776 // The capture is on the right hand side of a pipe and its first
1777 // argument it an unlabelled capture hole:
1778 //
1779 // wibble |> wobble(_, woo)
1780 //
1781 // We want it to become:
1782 //
1783 // wibble |> wobble(woo)
1784 //
1785 (FnCapturePosition::RightHandSideOfPipe, [argument, rest @ ..])
1786 if argument.is_capture_hole() && argument.label.is_none() =>
1787 {
1788 let expression = self.expr(fun);
1789 let arity = rest.len();
1790 self.append_inlinable_wrapped_arguments(
1791 expression,
1792 rest,
1793 location,
1794 |argument| is_breakable_argument(&argument.value, rest.len()),
1795 |self_, argument| self_.call_arg(argument, arity),
1796 )
1797 }
1798
1799 // In all other cases we print it like a regular function call
1800 // without changing it.
1801 //
1802 (
1803 FnCapturePosition::RightHandSideOfPipe | FnCapturePosition::EverywhereElse,
1804 arguments,
1805 ) => {
1806 let expression = self.expr(fun);
1807 let arity = arguments.len();
1808 self.append_inlinable_wrapped_arguments(
1809 expression,
1810 arguments,
1811 location,
1812 |argument| is_breakable_argument(&argument.value, arguments.len()),
1813 |self_, argument| self_.call_arg(argument, arity),
1814 )
1815 }
1816 }
1817 }
1818
1819 pub fn record_constructor<'a, A>(
1820 &mut self,
1821 constructor: &'a RecordConstructor<A>,
1822 ) -> Document<'a> {
1823 let comments = self.pop_comments(constructor.location.start);
1824 let doc_comments = self.doc_comments(constructor.location.start);
1825 let attributes = AttributesPrinter::new()
1826 .set_deprecation(&constructor.deprecation)
1827 .to_doc();
1828
1829 let doc = if constructor.arguments.is_empty() {
1830 if self.any_comments(constructor.location.end) {
1831 attributes
1832 .append(constructor.name.as_str().to_doc())
1833 .append(self.wrap_arguments(vec![], constructor.location.end))
1834 .group()
1835 } else {
1836 attributes.append(constructor.name.as_str().to_doc())
1837 }
1838 } else {
1839 let arguments = constructor
1840 .arguments
1841 .iter()
1842 .map(
1843 |RecordConstructorArg {
1844 label,
1845 ast,
1846 location,
1847 ..
1848 }| {
1849 let arg_comments = self.pop_comments(location.start);
1850 let argument = match label {
1851 Some((_, l)) => l.to_doc().append(": ").append(self.type_ast(ast)),
1852 None => self.type_ast(ast),
1853 };
1854
1855 commented(
1856 self.doc_comments(location.start).append(argument).group(),
1857 arg_comments,
1858 )
1859 },
1860 )
1861 .collect_vec();
1862
1863 attributes
1864 .append(constructor.name.as_str().to_doc())
1865 .append(
1866 self.wrap_arguments(arguments, constructor.location.end)
1867 .group(),
1868 )
1869 };
1870
1871 commented(doc_comments.append(doc).group(), comments)
1872 }
1873
1874 pub fn custom_type<'a, A>(&mut self, type_: &'a CustomType<A>) -> Document<'a> {
1875 let CustomType {
1876 location,
1877 end_position,
1878 name,
1879 name_location: _,
1880 publicity,
1881 constructors,
1882 documentation: _,
1883 deprecation,
1884 opaque,
1885 parameters,
1886 typed_parameters: _,
1887 external_erlang,
1888 external_javascript,
1889 } = type_;
1890
1891 let _ = self.pop_empty_lines(location.end);
1892
1893 let attributes = AttributesPrinter::new()
1894 .set_deprecation(deprecation)
1895 .set_internal(*publicity)
1896 .set_external_erlang(external_erlang)
1897 .set_external_javascript(external_javascript)
1898 .to_doc();
1899
1900 let doc = attributes
1901 .append(pub_(*publicity))
1902 .append(if *opaque { "opaque type " } else { "type " })
1903 .append(if parameters.is_empty() {
1904 name.clone().to_doc()
1905 } else {
1906 let arguments = type_
1907 .parameters
1908 .iter()
1909 .map(|(_, e)| e.to_doc())
1910 .collect_vec();
1911 type_
1912 .name
1913 .clone()
1914 .to_doc()
1915 .append(self.wrap_arguments(arguments, location.end))
1916 .group()
1917 });
1918
1919 if constructors.is_empty() {
1920 return doc;
1921 }
1922 let doc = doc.append(" {");
1923
1924 let inner = concat(constructors.iter().map(|c| {
1925 if self.pop_empty_lines(c.location.start) {
1926 lines(2)
1927 } else {
1928 line()
1929 }
1930 .append(self.record_constructor(c))
1931 }));
1932
1933 // Add any trailing comments
1934 let inner = match printed_comments(self.pop_comments(*end_position), false) {
1935 Some(comments) => inner.append(line()).append(comments),
1936 None => inner,
1937 }
1938 .nest(INDENT)
1939 .group();
1940
1941 doc.append(inner).append(line()).append("}")
1942 }
1943
1944 fn call_arg<'a>(&mut self, argument: &'a CallArg<UntypedExpr>, arity: usize) -> Document<'a> {
1945 self.format_call_arg(argument, expr_call_arg_formatting, |this, value| {
1946 this.comma_separated_item(value, arity)
1947 })
1948 }
1949
1950 fn format_call_arg<'a, A, F, G>(
1951 &mut self,
1952 argument: &'a CallArg<A>,
1953 figure_formatting: F,
1954 format_value: G,
1955 ) -> Document<'a>
1956 where
1957 F: Fn(&'a CallArg<A>) -> CallArgFormatting<'a, A>,
1958 G: Fn(&mut Self, &'a A) -> Document<'a>,
1959 {
1960 match figure_formatting(argument) {
1961 CallArgFormatting::Unlabelled(value) => format_value(self, value),
1962 CallArgFormatting::ShorthandLabelled(label) => {
1963 let comments = self.pop_comments(argument.location.start);
1964 let label = label.as_ref().to_doc().append(":");
1965 commented(label, comments)
1966 }
1967 CallArgFormatting::Labelled(label, value) => {
1968 let comments = self.pop_comments(argument.location.start);
1969 let label = label.as_ref().to_doc().append(": ");
1970 let value = format_value(self, value);
1971 commented(label, comments).append(value)
1972 }
1973 }
1974 }
1975
1976 fn record_update_arg<'a>(&mut self, argument: &'a UntypedRecordUpdateArg) -> Document<'a> {
1977 let comments = self.pop_comments(argument.location.start);
1978 match argument {
1979 // Argument supplied with a label shorthand.
1980 _ if argument.uses_label_shorthand() => {
1981 commented(argument.label.as_str().to_doc().append(":"), comments)
1982 }
1983 // Labelled argument.
1984 _ => {
1985 let doc = argument
1986 .label
1987 .as_str()
1988 .to_doc()
1989 .append(": ")
1990 .append(self.expr(&argument.value))
1991 .group();
1992
1993 if argument.value.is_binop() || argument.value.is_pipeline() {
1994 commented(doc, comments).nest(INDENT)
1995 } else {
1996 commented(doc, comments)
1997 }
1998 }
1999 }
2000 }
2001
2002 fn tuple_index<'a>(&mut self, tuple: &'a UntypedExpr, index: u64) -> Document<'a> {
2003 // In case we have a block with a single variable tuple access we
2004 // remove that redundant wrapper:
2005 //
2006 // {tuple.1}.0 becomes
2007 // tuple.1.0
2008 //
2009 if let UntypedExpr::Block { statements, .. } = tuple {
2010 match statements.as_slice() {
2011 [Statement::Expression(tuple @ UntypedExpr::TupleIndex { tuple: inner, .. })]
2012 // We can't apply this change if the inner thing is a
2013 // literal tuple because the compiler cannot currently parse
2014 // it: `#(1, #(2, 3)).1.0` is a syntax error at the moment.
2015 if !inner.is_tuple() =>
2016 {
2017 self.expr(tuple)
2018 }
2019 _ => self.expr(tuple),
2020 }
2021 } else {
2022 self.expr(tuple)
2023 }
2024 .append(".")
2025 .append(index)
2026 }
2027
2028 fn case_clause_value<'a>(&mut self, expression: &'a UntypedExpr) -> Document<'a> {
2029 match expression {
2030 UntypedExpr::Fn { .. }
2031 | UntypedExpr::List { .. }
2032 | UntypedExpr::Tuple { .. }
2033 | UntypedExpr::BitArray { .. } => {
2034 let expression_comments = self.pop_comments(expression.location().start);
2035 let expression_doc = self.expr(expression);
2036 match printed_comments(expression_comments, true) {
2037 Some(comments) => line().append(comments).append(expression_doc).nest(INDENT),
2038 None => " ".to_doc().append(expression_doc),
2039 }
2040 }
2041
2042 UntypedExpr::Case { .. } => line().append(self.expr(expression)).nest(INDENT),
2043
2044 UntypedExpr::Block {
2045 statements,
2046 location,
2047 ..
2048 } => " ".to_doc().append(self.block(location, statements, true)),
2049
2050 UntypedExpr::Int { .. }
2051 | UntypedExpr::Float { .. }
2052 | UntypedExpr::String { .. }
2053 | UntypedExpr::Var { .. }
2054 | UntypedExpr::Call { .. }
2055 | UntypedExpr::BinOp { .. }
2056 | UntypedExpr::PipeLine { .. }
2057 | UntypedExpr::FieldAccess { .. }
2058 | UntypedExpr::TupleIndex { .. }
2059 | UntypedExpr::Todo { .. }
2060 | UntypedExpr::Panic { .. }
2061 | UntypedExpr::Echo { .. }
2062 | UntypedExpr::RecordUpdate { .. }
2063 | UntypedExpr::NegateBool { .. }
2064 | UntypedExpr::NegateInt { .. } => break_("", " ")
2065 .append(self.expr(expression).group())
2066 .nest(INDENT),
2067 }
2068 .next_break_fits(NextBreakFitsMode::Disabled)
2069 .group()
2070 }
2071
2072 fn assigned_value<'a>(&mut self, expression: &'a UntypedExpr) -> Document<'a> {
2073 match expression {
2074 UntypedExpr::Case { .. } => " ".to_doc().append(self.expr(expression)).group(),
2075 UntypedExpr::Int { .. }
2076 | UntypedExpr::Float { .. }
2077 | UntypedExpr::String { .. }
2078 | UntypedExpr::Block { .. }
2079 | UntypedExpr::Var { .. }
2080 | UntypedExpr::Fn { .. }
2081 | UntypedExpr::List { .. }
2082 | UntypedExpr::Call { .. }
2083 | UntypedExpr::BinOp { .. }
2084 | UntypedExpr::PipeLine { .. }
2085 | UntypedExpr::FieldAccess { .. }
2086 | UntypedExpr::Tuple { .. }
2087 | UntypedExpr::TupleIndex { .. }
2088 | UntypedExpr::Todo { .. }
2089 | UntypedExpr::Panic { .. }
2090 | UntypedExpr::Echo { .. }
2091 | UntypedExpr::BitArray { .. }
2092 | UntypedExpr::RecordUpdate { .. }
2093 | UntypedExpr::NegateBool { .. }
2094 | UntypedExpr::NegateInt { .. } => self.case_clause_value(expression),
2095 }
2096 }
2097
2098 fn clause<'a>(&mut self, clause: &'a UntypedClause, index: u32) -> Document<'a> {
2099 let space_before = self.pop_empty_lines(clause.location.start);
2100 let comments = self.pop_comments(clause.location.start);
2101
2102 let clause_doc = match &clause.guard {
2103 None => self.alternative_patterns(clause),
2104 Some(guard) => self
2105 .alternative_patterns(clause)
2106 .append(break_("", " ").nest(INDENT))
2107 .append("if ")
2108 .append(self.clause_guard(guard).group().nest(INDENT)),
2109 };
2110
2111 // In case there's a guard or multiple subjects, if we decide to break
2112 // the patterns on multiple lines we also want the arrow to end up on
2113 // its own line to improve legibility.
2114 //
2115 // This looks like this:
2116 // ```gleam
2117 // case wibble, wobble {
2118 // Wibble(_), // pretend this goes over the line limit
2119 // Wobble(_)
2120 // -> todo
2121 // // Notice how the arrow is broken on its own line, the same goes
2122 // // for patterns with `if` guards.
2123 // }
2124 // ```
2125
2126 let has_guard = clause.guard.is_some();
2127 let has_multiple_subjects = clause.pattern.len() > 1;
2128 let arrow_break = if has_guard || has_multiple_subjects {
2129 break_("", " ")
2130 } else {
2131 " ".to_doc()
2132 };
2133
2134 let clause_doc = clause_doc
2135 .append(arrow_break)
2136 .group()
2137 .append("->")
2138 .append(self.case_clause_value(&clause.then).group())
2139 .group();
2140
2141 let clause_doc = match printed_comments(comments, false) {
2142 Some(comments) => comments.append(line()).append(clause_doc),
2143 None => clause_doc,
2144 };
2145
2146 if index == 0 {
2147 clause_doc
2148 } else if space_before {
2149 lines(2).append(clause_doc)
2150 } else {
2151 line().append(clause_doc)
2152 }
2153 }
2154
2155 fn alternative_patterns<'a>(&mut self, clause: &'a UntypedClause) -> Document<'a> {
2156 let has_guard = clause.guard.is_some();
2157 let has_multiple_subjects = clause.pattern.len() > 1;
2158
2159 // In case there's an `if` guard but no multiple subjects we want to add
2160 // additional indentation before the vartical bar separating alternative
2161 // patterns `|`.
2162 // We're not adding the indentation if there's multiple subjects as that
2163 // would make things harder to read, aligning the vertical bar with the
2164 // different subjects:
2165 // ```
2166 // case wibble, wobble {
2167 // Wibble,
2168 // Wobble
2169 // | Wibble, // <- we don't want this indentation!
2170 // Wobble -> todo
2171 // }
2172 // ```
2173 let alternatives_separator = if has_guard && !has_multiple_subjects {
2174 break_("", " ").nest(INDENT).append("| ")
2175 } else {
2176 break_("", " ").append("| ")
2177 };
2178
2179 let alternative_patterns = std::iter::once(&clause.pattern)
2180 .chain(&clause.alternative_patterns)
2181 .enumerate()
2182 .map(|(alternative_index, p)| {
2183 // Here `p` is a single pattern that can be comprised of
2184 // multiple subjects.
2185 // ```gleam
2186 // case wibble, wobble {
2187 // True, False
2188 // //^^^^^^^^^^^ This is a single pattern with multiple subjects
2189 // | _, _ -> todo
2190 // }
2191 // ```
2192 let is_first_alternative = alternative_index == 0;
2193 let subject_docs = p.iter().enumerate().map(|(subject_index, subject)| {
2194 // There's a small catch in turning each subject into a document.
2195 // Sadly we can't simply call `self.pattern` on each subject and
2196 // then nest each one in case it gets broken.
2197 // The first ever pattern that appears in a case clause (that is
2198 // the first subject of the first alternative) must not be nested
2199 // further; otherwise, when broken, it would have 2 extra spaces
2200 // of indentation: https://github.com/gleam-lang/gleam/issues/2940.
2201 let is_first_subject = subject_index == 0;
2202 let is_first_pattern_of_clause = is_first_subject && is_first_alternative;
2203 let subject_doc = self.pattern(subject);
2204 if is_first_pattern_of_clause {
2205 subject_doc
2206 } else {
2207 subject_doc.nest(INDENT)
2208 }
2209 });
2210 // We join all subjects with a breakable comma (that's also
2211 // going to be nested) and make the subjects into a group to
2212 // make sure the formatter tries to keep them on a single line.
2213 join(subject_docs, break_(",", ", ").nest(INDENT)).group()
2214 });
2215 // Last, we make sure that the formatter tries to keep each
2216 // alternative on a single line by making it a group!
2217 join(alternative_patterns, alternatives_separator).group()
2218 }
2219
2220 fn list<'a>(
2221 &mut self,
2222 elements: &'a [UntypedExpr],
2223 tail: Option<&'a UntypedExpr>,
2224 location: &SrcSpan,
2225 ) -> Document<'a> {
2226 if elements.is_empty() {
2227 return match tail {
2228 Some(tail) => self.expr(tail),
2229 // We take all comments that come _before_ the end of the list,
2230 // that is all comments that are inside "[" and "]", if there's
2231 // any comment we want to put it inside the empty list!
2232 None => match printed_comments(self.pop_comments(location.end), false) {
2233 None => "[]".to_doc(),
2234 Some(comments) => "["
2235 .to_doc()
2236 .append(break_("", "").nest(INDENT))
2237 .append(comments)
2238 .append(break_("", ""))
2239 .append("]")
2240 // vvv We want to make sure the comments are on a separate
2241 // line from the opening and closing brackets so we
2242 // force the breaks to be split on newlines.
2243 .force_break(),
2244 },
2245 };
2246 }
2247
2248 let list_packing = self.items_sequence_packing(
2249 elements,
2250 tail,
2251 UntypedExpr::can_have_multiple_per_line,
2252 *location,
2253 );
2254
2255 let comma = match list_packing {
2256 ItemsPacking::FitMultiplePerLine => flex_break(",", ", "),
2257 ItemsPacking::FitOnePerLine | ItemsPacking::BreakOnePerLine => break_(",", ", "),
2258 };
2259
2260 let list_size = elements.len()
2261 + match tail {
2262 Some(_) => 1,
2263 None => 0,
2264 };
2265
2266 let mut elements_doc = nil();
2267 for element in elements.iter() {
2268 let empty_lines = self.pop_empty_lines(element.location().start);
2269 let element_doc = self.comma_separated_item(element, list_size);
2270
2271 elements_doc = if elements_doc.is_empty() {
2272 element_doc
2273 } else if empty_lines {
2274 // If there's empty lines before the list item we want to add an
2275 // empty line here. Notice how we're making sure no nesting is
2276 // added after the comma, otherwise we would be adding needless
2277 // whitespace in the empty line!
2278 docvec![
2279 elements_doc,
2280 comma.clone().set_nesting(0),
2281 line(),
2282 element_doc
2283 ]
2284 } else {
2285 docvec![elements_doc, comma.clone(), element_doc]
2286 };
2287 }
2288 elements_doc = elements_doc.next_break_fits(NextBreakFitsMode::Disabled);
2289
2290 let doc = break_("[", "[").append(elements_doc);
2291 // We need to keep the last break aside and do not add it immediately
2292 // because in case there's a final comment before the closing square
2293 // bracket we want to add indentation (to just that break). Otherwise,
2294 // the final comment would be less indented than list's elements.
2295 let (doc, last_break) = match tail {
2296 None => (doc.nest(INDENT), break_(",", "")),
2297
2298 Some(tail) => {
2299 let comments = self.pop_comments(tail.location().start);
2300 let tail = commented(docvec!["..", self.expr(tail)], comments);
2301 (
2302 doc.append(break_(",", ", ")).append(tail).nest(INDENT),
2303 break_("", ""),
2304 )
2305 }
2306 };
2307
2308 // We get all remaining comments that come before the list's closing
2309 // square bracket.
2310 // If there's any we add those before the closing square bracket instead
2311 // of moving those out of the list.
2312 // Otherwise those would be moved out of the list.
2313 let comments = self.pop_comments(location.end);
2314 let doc = match printed_comments(comments, false) {
2315 None => doc.append(last_break).append("]"),
2316 Some(comment) => doc
2317 .append(last_break.nest(INDENT))
2318 // ^ See how here we're adding the missing indentation to the
2319 // final break so that the final comment is as indented as the
2320 // list's items.
2321 .append(comment)
2322 .append(line())
2323 .append("]")
2324 .force_break(),
2325 };
2326
2327 match list_packing {
2328 ItemsPacking::FitOnePerLine | ItemsPacking::FitMultiplePerLine => doc.group(),
2329 ItemsPacking::BreakOnePerLine => doc.force_break(),
2330 }
2331 }
2332
2333 fn items_sequence_packing<'a, T: HasLocation>(
2334 &self,
2335 items: &'a [T],
2336 tail: Option<&'a T>,
2337 can_have_multiple_per_line: impl Fn(&'a T) -> bool,
2338 list_location: SrcSpan,
2339 ) -> ItemsPacking {
2340 let ends_with_trailing_comma = tail
2341 .map(|tail| tail.location().end)
2342 .or_else(|| items.last().map(|last| last.location().end))
2343 .is_some_and(|last_element_end| {
2344 self.has_trailing_comma(last_element_end, list_location.end)
2345 });
2346
2347 let has_multiple_elements_per_line =
2348 self.has_items_on_the_same_line(items.iter().chain(tail));
2349
2350 let has_empty_lines_between_elements = match (items.first(), items.last().or(tail)) {
2351 (Some(first), Some(last)) => self.empty_lines.first().is_some_and(|empty_line| {
2352 *empty_line >= first.location().end && *empty_line < last.location().start
2353 }),
2354 _ => false,
2355 };
2356
2357 if has_empty_lines_between_elements {
2358 // If there's any empty line between elements we want to force each
2359 // item onto its own line to preserve the empty lines that were
2360 // intentionally added.
2361 ItemsPacking::BreakOnePerLine
2362 } else if !ends_with_trailing_comma {
2363 // If the list doesn't end with a trailing comma we try and pack it in
2364 // a single line; if we can't we'll put one item per line, no matter
2365 // the content of the list.
2366 ItemsPacking::FitOnePerLine
2367 } else if tail.is_none()
2368 && items.iter().all(can_have_multiple_per_line)
2369 && has_multiple_elements_per_line
2370 && self.spans_multiple_lines(list_location.start, list_location.end)
2371 {
2372 // If there's a trailing comma, we can have multiple items per line,
2373 // and there's already multiple items per line, we try and pack as
2374 // many items as possible on each line.
2375 //
2376 // Note how we only ever try and pack lists where all items are
2377 // unbreakable primitives. To pack a list we need to use put
2378 // `flex_break`s between each item.
2379 // If the items themselves had breaks we could end up in a situation
2380 // where an item gets broken making it span multiple lines and the
2381 // spaces are not, for example:
2382 //
2383 // ```gleam
2384 // [Constructor("wibble", "lorem ipsum dolor sit amet something something"), Other(1)]
2385 // ```
2386 //
2387 // If we used flex breaks here the list would be formatted as:
2388 //
2389 // ```gleam
2390 // [
2391 // Constructor(
2392 // "wibble",
2393 // "lorem ipsum dolor sit amet something something",
2394 // ), Other(1)
2395 // ]
2396 // ```
2397 //
2398 // The first item is broken, meaning that once we get to the flex
2399 // space separating it from the following one the formatter is not
2400 // going to break it since there's enough space in the current line!
2401 ItemsPacking::FitMultiplePerLine
2402 } else {
2403 // If it ends with a trailing comma we will force the list on
2404 // multiple lines, with one item per line.
2405 ItemsPacking::BreakOnePerLine
2406 }
2407 }
2408
2409 fn has_items_on_the_same_line<'a, L: HasLocation + 'a, T: Iterator<Item = &'a L>>(
2410 &self,
2411 items: T,
2412 ) -> bool {
2413 let mut previous: Option<SrcSpan> = None;
2414 for item in items {
2415 let item_location = item.location();
2416 // A list has multiple items on the same line if two consecutive
2417 // ones do not span multiple lines.
2418 if let Some(previous) = previous
2419 && !self.spans_multiple_lines(previous.end, item_location.start)
2420 {
2421 return true;
2422 }
2423 previous = Some(item_location);
2424 }
2425 false
2426 }
2427
2428 /// Pretty prints an expression to be used in a comma separated list; for
2429 /// example as a list item, a tuple item or as an argument of a function call.
2430 fn comma_separated_item<'a>(
2431 &mut self,
2432 expression: &'a UntypedExpr,
2433 siblings: usize,
2434 ) -> Document<'a> {
2435 // If there's more than one item in the comma separated list and there's a
2436 // pipeline or long binary chain, we want to indent those to make it
2437 // easier to tell where one item ends and the other starts.
2438 // Othewise we just print the expression as a normal expr.
2439 match expression {
2440 UntypedExpr::BinOp {
2441 name, left, right, ..
2442 } if siblings > 1 => {
2443 let comments = self.pop_comments(expression.start_byte_index());
2444 let doc = self.bin_op(name, left, right, true).group();
2445 commented(doc, comments)
2446 }
2447 UntypedExpr::PipeLine { expressions } if siblings > 1 => {
2448 let comments = self.pop_comments(expression.start_byte_index());
2449 let doc = self.pipeline(expressions, true).group();
2450 commented(doc, comments)
2451 }
2452 UntypedExpr::Int { .. }
2453 | UntypedExpr::Float { .. }
2454 | UntypedExpr::String { .. }
2455 | UntypedExpr::Block { .. }
2456 | UntypedExpr::Var { .. }
2457 | UntypedExpr::Fn { .. }
2458 | UntypedExpr::List { .. }
2459 | UntypedExpr::Call { .. }
2460 | UntypedExpr::BinOp { .. }
2461 | UntypedExpr::PipeLine { .. }
2462 | UntypedExpr::Case { .. }
2463 | UntypedExpr::FieldAccess { .. }
2464 | UntypedExpr::Tuple { .. }
2465 | UntypedExpr::TupleIndex { .. }
2466 | UntypedExpr::Todo { .. }
2467 | UntypedExpr::Panic { .. }
2468 | UntypedExpr::Echo { .. }
2469 | UntypedExpr::BitArray { .. }
2470 | UntypedExpr::RecordUpdate { .. }
2471 | UntypedExpr::NegateBool { .. }
2472 | UntypedExpr::NegateInt { .. } => self.expr(expression).group(),
2473 }
2474 }
2475
2476 fn pattern<'a>(&mut self, pattern: &'a UntypedPattern) -> Document<'a> {
2477 let comments = self.pop_comments(pattern.location().start);
2478 let doc = match pattern {
2479 Pattern::Int { value, .. } => self.int(value),
2480
2481 Pattern::Float { value, .. } => self.float(value),
2482
2483 Pattern::String { value, .. } => self.string(value),
2484
2485 Pattern::Variable { name, .. } => name.to_doc(),
2486
2487 Pattern::BitArraySize(size) => self.bit_array_size(size),
2488
2489 Pattern::Assign { name, pattern, .. } => {
2490 if pattern.is_discard() {
2491 name.to_doc()
2492 } else {
2493 self.pattern(pattern).append(" as ").append(name.as_str())
2494 }
2495 }
2496
2497 Pattern::Discard { name, .. } => name.to_doc(),
2498
2499 Pattern::List { elements, tail, .. } => self.list_pattern(elements, tail),
2500
2501 Pattern::Constructor {
2502 name,
2503 arguments,
2504 module,
2505 spread,
2506 location,
2507 ..
2508 } => self.pattern_constructor(name, arguments, module, *spread, location),
2509
2510 Pattern::Tuple {
2511 elements, location, ..
2512 } => {
2513 let arguments = elements
2514 .iter()
2515 .map(|element| self.pattern(element))
2516 .collect_vec();
2517 "#".to_doc()
2518 .append(self.wrap_arguments(arguments, location.end))
2519 .group()
2520 }
2521
2522 Pattern::BitArray {
2523 segments, location, ..
2524 } => {
2525 let segment_docs = segments
2526 .iter()
2527 .map(|segment| bit_array_segment(segment, |pattern| self.pattern(pattern)))
2528 .collect_vec();
2529
2530 self.bit_array(segment_docs, ItemsPacking::FitOnePerLine, location)
2531 }
2532
2533 Pattern::StringPrefix {
2534 left_side_string: left,
2535 right_side_assignment: right,
2536 left_side_assignment: left_assign,
2537 ..
2538 } => {
2539 let left = self.string(left);
2540 let right = match right {
2541 AssignName::Variable(name) => name.to_doc(),
2542 AssignName::Discard(name) => name.to_doc(),
2543 };
2544 match left_assign {
2545 Some((name, _)) => docvec![left, " as ", name, " <> ", right],
2546 None => docvec![left, " <> ", right],
2547 }
2548 }
2549
2550 Pattern::Invalid { .. } => panic!("invalid patterns can not be in an untyped ast"),
2551 };
2552 commented(doc, comments)
2553 }
2554
2555 fn bit_array_size<'a>(&mut self, size: &'a BitArraySize<()>) -> Document<'a> {
2556 match size {
2557 BitArraySize::Int { value, .. } => self.int(value),
2558 BitArraySize::Variable { name, .. } => name.to_doc(),
2559 BitArraySize::BinaryOperator {
2560 left,
2561 right,
2562 operator,
2563 ..
2564 } => {
2565 let operator = match operator {
2566 IntOperator::Add => " + ",
2567 IntOperator::Subtract => " - ",
2568 IntOperator::Multiply => " * ",
2569 IntOperator::Divide => " / ",
2570 IntOperator::Remainder => " % ",
2571 };
2572
2573 docvec![
2574 self.bit_array_size(left),
2575 operator,
2576 self.bit_array_size(right)
2577 ]
2578 }
2579 BitArraySize::Block { inner, .. } => self.bit_array_size(inner).surround("{ ", " }"),
2580 }
2581 }
2582
2583 fn list_pattern<'a>(
2584 &mut self,
2585 elements: &'a [UntypedPattern],
2586 tail: &'a Option<Box<UntypedTailPattern>>,
2587 ) -> Document<'a> {
2588 if elements.is_empty() {
2589 return match tail {
2590 Some(tail) => self.pattern(&tail.pattern),
2591 None => "[]".to_doc(),
2592 };
2593 }
2594 let elements = join(
2595 elements.iter().map(|element| self.pattern(element)),
2596 break_(",", ", "),
2597 );
2598 let doc = break_("[", "[").append(elements);
2599 match tail {
2600 None => doc.nest(INDENT).append(break_(",", "")),
2601
2602 Some(tail) => {
2603 let tail = &tail.pattern;
2604 let comments = self.pop_comments(tail.location().start);
2605 let tail = if tail.is_discard() {
2606 "..".to_doc()
2607 } else {
2608 docvec!["..", self.pattern(tail)]
2609 };
2610 let tail = commented(tail, comments);
2611 doc.append(break_(",", ", "))
2612 .append(tail)
2613 .nest(INDENT)
2614 .append(break_("", ""))
2615 }
2616 }
2617 .append("]")
2618 .group()
2619 }
2620
2621 fn pattern_call_arg<'a>(&mut self, argument: &'a CallArg<UntypedPattern>) -> Document<'a> {
2622 self.format_call_arg(argument, pattern_call_arg_formatting, |this, value| {
2623 this.pattern(value)
2624 })
2625 }
2626
2627 pub fn clause_guard_bin_op<'a>(
2628 &mut self,
2629 name: &'a BinOp,
2630 left: &'a UntypedClauseGuard,
2631 right: &'a UntypedClauseGuard,
2632 ) -> Document<'a> {
2633 self.clause_guard_bin_op_side(name, left, left.precedence())
2634 .append(break_("", " "))
2635 .append(name.to_doc())
2636 .append(" ")
2637 .append(self.clause_guard_bin_op_side(name, right, right.precedence() - 1))
2638 }
2639
2640 fn clause_guard_bin_op_side<'a>(
2641 &mut self,
2642 name: &BinOp,
2643 side: &'a UntypedClauseGuard,
2644 // As opposed to `bin_op_side`, here we take the side precedence as an
2645 // argument instead of computing it ourselves. That's because
2646 // `clause_guard_bin_op` will reduce the precedence of any right side to
2647 // make sure the formatter doesn't remove any needed curly bracket.
2648 side_precedence: u8,
2649 ) -> Document<'a> {
2650 let side_doc = self.clause_guard(side);
2651 match side.bin_op_name() {
2652 // In case the other side is a binary operation as well and it can
2653 // be grouped together with the current binary operation, the two
2654 // docs are simply concatenated, so that they will end up in the
2655 // same group and the formatter will try to keep those on a single
2656 // line.
2657 Some(side_name) if side_name.can_be_grouped_with(name) => {
2658 self.operator_side(side_doc, name.precedence(), side_precedence)
2659 }
2660 // In case the binary operations cannot be grouped together the
2661 // other side is treated as a group on its own so that it can be
2662 // broken independently of other pieces of the binary operations
2663 // chain.
2664 _ => self.operator_side(side_doc.group(), name.precedence(), side_precedence),
2665 }
2666 }
2667
2668 fn clause_guard<'a>(&mut self, clause_guard: &'a UntypedClauseGuard) -> Document<'a> {
2669 match clause_guard {
2670 ClauseGuard::BinaryOperator {
2671 operator,
2672 left,
2673 right,
2674 ..
2675 } => self.clause_guard_bin_op(operator, left, right),
2676
2677 ClauseGuard::Var { name, .. } => name.to_doc(),
2678
2679 ClauseGuard::TupleIndex { tuple, index, .. } => {
2680 self.clause_guard(tuple).append(".").append(*index).to_doc()
2681 }
2682
2683 ClauseGuard::FieldAccess {
2684 container, label, ..
2685 } => self
2686 .clause_guard(container)
2687 .append(".")
2688 .append(label)
2689 .to_doc(),
2690
2691 ClauseGuard::ModuleSelect {
2692 module_name, label, ..
2693 } => module_name.to_doc().append(".").append(label).to_doc(),
2694
2695 ClauseGuard::Constant(constant) => self.const_expr(constant),
2696
2697 ClauseGuard::Not { expression, .. } => docvec!["!", self.clause_guard(expression)],
2698
2699 ClauseGuard::Block { value, .. } => wrap_block(self.clause_guard(value)).group(),
2700 }
2701 }
2702
2703 fn constant_call_arg<'a, A, B>(
2704 &mut self,
2705 argument: &'a CallArg<Constant<A, B>>,
2706 ) -> Document<'a> {
2707 self.format_call_arg(argument, constant_call_arg_formatting, |this, value| {
2708 this.const_expr(value)
2709 })
2710 }
2711
2712 fn negate_bool<'a>(&mut self, expression: &'a UntypedExpr) -> Document<'a> {
2713 match expression {
2714 UntypedExpr::NegateBool { value, .. } => self.expr(value),
2715 UntypedExpr::BinOp { .. } => "!".to_doc().append(wrap_block(self.expr(expression))),
2716 UntypedExpr::Int { .. }
2717 | UntypedExpr::Float { .. }
2718 | UntypedExpr::String { .. }
2719 | UntypedExpr::Block { .. }
2720 | UntypedExpr::Var { .. }
2721 | UntypedExpr::Fn { .. }
2722 | UntypedExpr::List { .. }
2723 | UntypedExpr::Call { .. }
2724 | UntypedExpr::PipeLine { .. }
2725 | UntypedExpr::Case { .. }
2726 | UntypedExpr::FieldAccess { .. }
2727 | UntypedExpr::Tuple { .. }
2728 | UntypedExpr::TupleIndex { .. }
2729 | UntypedExpr::Todo { .. }
2730 | UntypedExpr::Panic { .. }
2731 | UntypedExpr::Echo { .. }
2732 | UntypedExpr::BitArray { .. }
2733 | UntypedExpr::RecordUpdate { .. }
2734 | UntypedExpr::NegateInt { .. } => docvec!["!", self.expr(expression)],
2735 }
2736 }
2737
2738 fn negate_int<'a>(&mut self, expression: &'a UntypedExpr) -> Document<'a> {
2739 match expression {
2740 UntypedExpr::NegateInt { value, .. } => self.expr(value),
2741 UntypedExpr::Int { value, .. } if value.starts_with('-') => self.int(&value[1..]),
2742 UntypedExpr::BinOp { .. } => "- ".to_doc().append(self.expr(expression)),
2743
2744 UntypedExpr::Int { .. }
2745 | UntypedExpr::Float { .. }
2746 | UntypedExpr::String { .. }
2747 | UntypedExpr::Block { .. }
2748 | UntypedExpr::Var { .. }
2749 | UntypedExpr::Fn { .. }
2750 | UntypedExpr::List { .. }
2751 | UntypedExpr::Call { .. }
2752 | UntypedExpr::PipeLine { .. }
2753 | UntypedExpr::Case { .. }
2754 | UntypedExpr::FieldAccess { .. }
2755 | UntypedExpr::Tuple { .. }
2756 | UntypedExpr::TupleIndex { .. }
2757 | UntypedExpr::Todo { .. }
2758 | UntypedExpr::Panic { .. }
2759 | UntypedExpr::Echo { .. }
2760 | UntypedExpr::BitArray { .. }
2761 | UntypedExpr::RecordUpdate { .. }
2762 | UntypedExpr::NegateBool { .. } => docvec!["-", self.expr(expression)],
2763 }
2764 }
2765
2766 fn use_<'a>(&mut self, use_: &'a UntypedUse) -> Document<'a> {
2767 let comments = self.pop_comments(use_.location.start);
2768
2769 let call = if use_.call.is_call() {
2770 docvec![" ", self.expr(&use_.call)]
2771 } else {
2772 docvec![break_("", " "), self.expr(&use_.call)].nest(INDENT)
2773 }
2774 .group();
2775
2776 let doc = if use_.assignments.is_empty() {
2777 docvec!["use <-", call]
2778 } else {
2779 let assignments = use_.assignments.iter().map(|use_assignment| {
2780 let pattern = self.pattern(&use_assignment.pattern);
2781 let annotation = use_assignment
2782 .annotation
2783 .as_ref()
2784 .map(|a| ": ".to_doc().append(self.type_ast(a)));
2785
2786 pattern.append(annotation).group()
2787 });
2788 let assignments = Itertools::intersperse(assignments, break_(",", ", "));
2789 let left = ["use".to_doc(), break_("", " ")]
2790 .into_iter()
2791 .chain(assignments);
2792 let left = concat(left).nest(INDENT).append(break_("", " ")).group();
2793 docvec![left, "<-", call].group()
2794 };
2795
2796 commented(doc, comments)
2797 }
2798
2799 fn assert<'a>(&mut self, assert: &'a UntypedAssert) -> Document<'a> {
2800 let comments = self.pop_comments(assert.location.start);
2801
2802 let expression = if assert.value.is_binop() || assert.value.is_pipeline() {
2803 self.expr(&assert.value).nest(INDENT)
2804 } else {
2805 self.expr(&assert.value)
2806 };
2807
2808 let doc =
2809 self.append_as_message(expression, PrecedingAs::Expression, assert.message.as_ref());
2810 commented(docvec!["assert ", doc], comments)
2811 }
2812
2813 fn bit_array<'a>(
2814 &mut self,
2815 segments: Vec<Document<'a>>,
2816 packing: ItemsPacking,
2817 location: &SrcSpan,
2818 ) -> Document<'a> {
2819 let comments = self.pop_comments(location.end);
2820 let comments_doc = printed_comments(comments, false);
2821
2822 // Avoid adding illegal comma in empty bit array by explicitly handling it
2823 if segments.is_empty() {
2824 // We take all comments that come _before_ the end of the bit array,
2825 // that is all comments that are inside "<<" and ">>", if there's
2826 // any comment we want to put it inside the empty bit array!
2827 // Refer to the `list` function for a similar procedure.
2828 return match comments_doc {
2829 None => "<<>>".to_doc(),
2830 Some(comments) => "<<"
2831 .to_doc()
2832 .append(break_("", "").nest(INDENT))
2833 .append(comments)
2834 .append(break_("", ""))
2835 .append(">>")
2836 // vvv We want to make sure the comments are on a separate
2837 // line from the opening and closing angle brackets so
2838 // we force the breaks to be split on newlines.
2839 .force_break(),
2840 };
2841 }
2842
2843 let comma = match packing {
2844 ItemsPacking::FitMultiplePerLine => flex_break(",", ", "),
2845 ItemsPacking::FitOnePerLine | ItemsPacking::BreakOnePerLine => break_(",", ", "),
2846 };
2847
2848 let last_break = break_(",", "");
2849 let doc = break_("<<", "<<")
2850 .append(join(segments, comma))
2851 .nest(INDENT);
2852
2853 let doc = match comments_doc {
2854 None => doc.append(last_break).append(">>"),
2855 Some(comments) => doc
2856 .append(last_break.nest(INDENT))
2857 // ^ Notice how in this case we nest the final break before
2858 // adding it: this way the comments are going to be as
2859 // indented as the bit array items.
2860 .append(comments.nest(INDENT))
2861 .append(line())
2862 .append(">>")
2863 .force_break(),
2864 };
2865
2866 match packing {
2867 ItemsPacking::FitOnePerLine | ItemsPacking::FitMultiplePerLine => doc.group(),
2868 ItemsPacking::BreakOnePerLine => doc.force_break(),
2869 }
2870 }
2871
2872 fn bit_array_segment_expr<'a>(&mut self, expression: &'a UntypedExpr) -> Document<'a> {
2873 match expression {
2874 UntypedExpr::BinOp { .. } => wrap_block(self.expr(expression)),
2875
2876 UntypedExpr::Int { .. }
2877 | UntypedExpr::Float { .. }
2878 | UntypedExpr::String { .. }
2879 | UntypedExpr::Var { .. }
2880 | UntypedExpr::Fn { .. }
2881 | UntypedExpr::List { .. }
2882 | UntypedExpr::Call { .. }
2883 | UntypedExpr::PipeLine { .. }
2884 | UntypedExpr::Case { .. }
2885 | UntypedExpr::FieldAccess { .. }
2886 | UntypedExpr::Tuple { .. }
2887 | UntypedExpr::TupleIndex { .. }
2888 | UntypedExpr::Todo { .. }
2889 | UntypedExpr::Panic { .. }
2890 | UntypedExpr::Echo { .. }
2891 | UntypedExpr::BitArray { .. }
2892 | UntypedExpr::RecordUpdate { .. }
2893 | UntypedExpr::NegateBool { .. }
2894 | UntypedExpr::NegateInt { .. }
2895 | UntypedExpr::Block { .. } => self.expr(expression),
2896 }
2897 }
2898
2899 fn statement<'a>(&mut self, statement: &'a UntypedStatement) -> Document<'a> {
2900 match statement {
2901 Statement::Expression(expression) => self.expr(expression),
2902 Statement::Assignment(assignment) => self.assignment(assignment),
2903 Statement::Use(use_) => self.use_(use_),
2904 Statement::Assert(assert) => self.assert(assert),
2905 }
2906 }
2907
2908 fn block<'a>(
2909 &mut self,
2910 location: &SrcSpan,
2911 statements: &'a Vec1<UntypedStatement>,
2912 force_breaks: bool,
2913 ) -> Document<'a> {
2914 let statements_doc =
2915 docvec![break_("", " "), self.statements(statements.as_vec())].nest(INDENT);
2916 let trailing_comments = self.pop_comments(location.end);
2917 let trailing_comments = printed_comments(trailing_comments, false);
2918 let block_doc = match trailing_comments {
2919 Some(trailing_comments_doc) => docvec![
2920 "{",
2921 statements_doc,
2922 line().nest(INDENT),
2923 trailing_comments_doc.nest(INDENT),
2924 line(),
2925 "}"
2926 ]
2927 .force_break(),
2928 None => docvec!["{", statements_doc, break_("", " "), "}"],
2929 };
2930
2931 if force_breaks {
2932 block_doc.force_break().group()
2933 } else {
2934 block_doc.group()
2935 }
2936 }
2937
2938 pub fn wrap_function_call_arguments<'a, I>(
2939 &mut self,
2940 arguments: I,
2941 location: &SrcSpan,
2942 ) -> Document<'a>
2943 where
2944 I: IntoIterator<Item = Document<'a>>,
2945 {
2946 let mut arguments = arguments.into_iter().peekable();
2947 if arguments.peek().is_none() {
2948 return "()".to_doc();
2949 }
2950
2951 let arguments_doc = break_("", "")
2952 .append(join(arguments, break_(",", ", ")))
2953 .nest_if_broken(INDENT);
2954
2955 // We get all remaining comments that come before the call's closing
2956 // parenthesis.
2957 // If there's any we add those before the closing parenthesis instead
2958 // of moving those out of the call.
2959 // Otherwise those would be moved out of the call.
2960 let comments = self.pop_comments(location.end);
2961 let closing_parens = match printed_comments(comments, false) {
2962 None => docvec![break_(",", ""), ")"],
2963 Some(comment) => {
2964 docvec![break_(",", "").nest(INDENT), comment, line(), ")"].force_break()
2965 }
2966 };
2967
2968 "(".to_doc()
2969 .append(arguments_doc)
2970 .append(closing_parens)
2971 .group()
2972 }
2973
2974 pub fn wrap_arguments<'a, I>(&mut self, arguments: I, comments_limit: u32) -> Document<'a>
2975 where
2976 I: IntoIterator<Item = Document<'a>>,
2977 {
2978 let mut arguments = arguments.into_iter().peekable();
2979 if arguments.peek().is_none() {
2980 let comments = self.pop_comments(comments_limit);
2981 return match printed_comments(comments, false) {
2982 Some(comments) => "("
2983 .to_doc()
2984 .append(break_("", ""))
2985 .append(comments)
2986 .nest_if_broken(INDENT)
2987 .force_break()
2988 .append(break_("", ""))
2989 .append(")"),
2990 None => "()".to_doc(),
2991 };
2992 }
2993 let doc = break_("(", "(").append(join(arguments, break_(",", ", ")));
2994
2995 // Include trailing comments if there are any
2996 let comments = self.pop_comments(comments_limit);
2997 match printed_comments(comments, false) {
2998 Some(comments) => doc
2999 .append(break_(",", ""))
3000 .append(comments)
3001 .nest_if_broken(INDENT)
3002 .force_break()
3003 .append(break_("", ""))
3004 .append(")"),
3005 None => doc
3006 .nest_if_broken(INDENT)
3007 .append(break_(",", ""))
3008 .append(")"),
3009 }
3010 }
3011
3012 pub fn wrap_arguments_with_spread<'a, I>(
3013 &mut self,
3014 arguments: I,
3015 comments_limit: u32,
3016 ) -> Document<'a>
3017 where
3018 I: IntoIterator<Item = Document<'a>>,
3019 {
3020 let mut arguments = arguments.into_iter().peekable();
3021 if arguments.peek().is_none() {
3022 return self.wrap_arguments(arguments, comments_limit);
3023 }
3024 let doc = break_("(", "(")
3025 .append(join(arguments, break_(",", ", ")))
3026 .append(break_(",", ", "))
3027 .append("..");
3028
3029 // Include trailing comments if there are any
3030 let comments = self.pop_comments(comments_limit);
3031 match printed_comments(comments, false) {
3032 Some(comments) => doc
3033 .append(break_(",", ""))
3034 .append(comments)
3035 .nest_if_broken(INDENT)
3036 .force_break()
3037 .append(break_("", ""))
3038 .append(")"),
3039 None => doc
3040 .nest_if_broken(INDENT)
3041 .append(break_(",", ""))
3042 .append(")"),
3043 }
3044 }
3045
3046 /// Given some regular comments it pretty prints those with any respective
3047 /// doc comment that might be preceding those.
3048 /// For example:
3049 ///
3050 /// ```gleam
3051 /// /// Doc
3052 /// // comment
3053 ///
3054 /// /// Doc
3055 /// pub fn wibble() {}
3056 /// ```
3057 ///
3058 /// We don't want the first doc comment to be merged together with
3059 /// `wibble`'s doc comment, so when we run into comments like `// comment`
3060 /// we need to first print all documentation comments that come before it.
3061 ///
3062 fn printed_documented_comments<'a, 'b>(
3063 &mut self,
3064 comments: impl IntoIterator<Item = (u32, Option<&'b str>)>,
3065 ) -> Option<Document<'a>> {
3066 let mut comments = comments.into_iter().peekable();
3067 let _ = comments.peek()?;
3068
3069 let mut doc = Vec::new();
3070 while let Some(c) = comments.next() {
3071 let (is_doc_commented, c) = match c {
3072 (comment_start, Some(c)) => {
3073 let doc_comment = self.doc_comments(comment_start);
3074 let is_doc_commented = !doc_comment.is_empty();
3075 doc.push(doc_comment);
3076 (is_doc_commented, c)
3077 }
3078 (_, None) => continue,
3079 };
3080 doc.push("//".to_doc().append(EcoString::from(c)));
3081 match comments.peek() {
3082 // Next line is a comment
3083 Some((_, Some(_))) => doc.push(line()),
3084 // Next line is empty
3085 Some((_, None)) => {
3086 let _ = comments.next();
3087 doc.push(lines(2));
3088 }
3089 // We've reached the end, there are no more lines
3090 None => {
3091 if is_doc_commented {
3092 doc.push(lines(2));
3093 } else {
3094 doc.push(line());
3095 }
3096 }
3097 }
3098 }
3099 let doc = concat(doc);
3100 Some(doc.force_break())
3101 }
3102
3103 fn append_as_message<'a>(
3104 &mut self,
3105 doc: Document<'a>,
3106 preceding_as: PrecedingAs,
3107 message: Option<&'a UntypedExpr>,
3108 ) -> Document<'a> {
3109 let Some(message) = message else { return doc };
3110
3111 let comments = self.pop_comments(message.location().start);
3112 let comments = printed_comments(comments, false);
3113
3114 let as_ = match preceding_as {
3115 PrecedingAs::Keyword => " as".to_doc(),
3116 PrecedingAs::Expression => docvec![break_("", " "), "as"].nest(INDENT),
3117 };
3118
3119 let doc = match comments {
3120 // If there's comments between the document and the message we want
3121 // the `as` bit to be on the same line as the original document and
3122 // go on a new indented line with the message and comments:
3123 // ```gleam
3124 // todo as
3125 // // comment!
3126 // "wibble"
3127 // ```
3128 Some(comments) => docvec![
3129 doc.group(),
3130 as_,
3131 docvec![line(), comments, line(), self.expr(message).group()].nest(INDENT)
3132 ],
3133
3134 None => {
3135 let message = match (preceding_as, message) {
3136 // If we have `as` preceded by a keyword (like with `panic` and `todo`)
3137 // and the message is a block, we don't want to nest it any further. That is,
3138 // we want it to look like this:
3139 // ```gleam
3140 // panic as {
3141 // wibble wobble
3142 // }
3143 // ```
3144 // instead of this:
3145 // ```gleam
3146 // panic as {
3147 // wibble wobble
3148 // }
3149 // ```
3150 (PrecedingAs::Keyword, UntypedExpr::Block { .. }) => self.expr(message).group(),
3151 _ => self.expr(message).group().nest(INDENT),
3152 };
3153 docvec![doc.group(), as_, " ", message]
3154 }
3155 };
3156
3157 doc.group()
3158 }
3159
3160 fn echo<'a>(
3161 &mut self,
3162 expression: &'a Option<Box<UntypedExpr>>,
3163 message: &'a Option<Box<UntypedExpr>>,
3164 ) -> Document<'a> {
3165 let Some(expression) = expression else {
3166 return self.append_as_message(
3167 "echo".to_doc(),
3168 PrecedingAs::Keyword,
3169 message.as_deref(),
3170 );
3171 };
3172
3173 // When a binary expression gets broken on multiple lines we don't want
3174 // it to be on the same line as echo, or it would look confusing;
3175 // instead it's nested onto a new line:
3176 //
3177 // ```gleam
3178 // echo first
3179 // |> wobble
3180 // |> wibble
3181 // ```
3182 //
3183 // So it's easier to see echo is printing the whole thing. Otherwise,
3184 // it would look like echo is printing just the first item:
3185 //
3186 // ```gleam
3187 // echo first
3188 // |> wobble
3189 // |> wibble
3190 // ```
3191 //
3192 let doc = self.expr(expression);
3193 if expression.is_binop() || expression.is_pipeline() {
3194 let doc = self.append_as_message(
3195 doc.nest(INDENT),
3196 PrecedingAs::Expression,
3197 message.as_deref(),
3198 );
3199 docvec!["echo ", doc]
3200 } else {
3201 docvec![
3202 "echo ",
3203 self.append_as_message(doc, PrecedingAs::Expression, message.as_deref())
3204 ]
3205 }
3206 }
3207}
3208
3209/// This is used to describe the kind of things that might preceding an `as`
3210/// message that can be added to various places: `panic`, `echo`, `let assert`,
3211/// `assert`, `todo`.
3212///
3213/// It might be preceded by a keyword, like with `echo` and `panic`, or by
3214/// an expression, like in `assert` or `let assert`.
3215///
3216enum PrecedingAs {
3217 /// An expression is preceding the `as` message:
3218 /// ```gleam
3219 /// echo 1 as "message"
3220 /// assert 1 == 2 as "message"
3221 /// let assert Ok(_) = result as "message"
3222 /// ```
3223 ///
3224 Expression,
3225
3226 /// A keyword is preceding the `as` message:
3227 /// ```gleam
3228 /// 1 |> echo as "message"
3229 /// panic as "message"
3230 /// todo as "message"
3231 /// ```
3232 ///
3233 Keyword,
3234}
3235
3236fn init_and_last<T>(vec: &[T]) -> Option<(&[T], &T)> {
3237 match vec {
3238 [] => None,
3239 _ => match vec.split_at(vec.len() - 1) {
3240 (init, [last]) => Some((init, last)),
3241 _ => panic!("unreachable"),
3242 },
3243 }
3244}
3245
3246impl<'a> Documentable<'a> for &'a ArgNames {
3247 fn to_doc(self) -> Document<'a> {
3248 match self {
3249 ArgNames::Named { name, .. } | ArgNames::Discard { name, .. } => name.to_doc(),
3250 ArgNames::LabelledDiscard { label, name, .. }
3251 | ArgNames::NamedLabelled { label, name, .. } => {
3252 docvec![label, " ", name]
3253 }
3254 }
3255 }
3256}
3257
3258fn pub_(publicity: Publicity) -> Document<'static> {
3259 match publicity {
3260 Publicity::Public | Publicity::Internal { .. } => "pub ".to_doc(),
3261 Publicity::Private => nil(),
3262 }
3263}
3264
3265impl<'a> Documentable<'a> for &'a UnqualifiedImport {
3266 fn to_doc(self) -> Document<'a> {
3267 self.name.as_str().to_doc().append(match &self.as_name {
3268 None => nil(),
3269 Some(s) => " as ".to_doc().append(s.as_str()),
3270 })
3271 }
3272}
3273
3274impl<'a> Documentable<'a> for &'a BinOp {
3275 fn to_doc(self) -> Document<'a> {
3276 match self {
3277 BinOp::And => "&&",
3278 BinOp::Or => "||",
3279 BinOp::LtInt => "<",
3280 BinOp::LtEqInt => "<=",
3281 BinOp::LtFloat => "<.",
3282 BinOp::LtEqFloat => "<=.",
3283 BinOp::Eq => "==",
3284 BinOp::NotEq => "!=",
3285 BinOp::GtEqInt => ">=",
3286 BinOp::GtInt => ">",
3287 BinOp::GtEqFloat => ">=.",
3288 BinOp::GtFloat => ">.",
3289 BinOp::AddInt => "+",
3290 BinOp::AddFloat => "+.",
3291 BinOp::SubInt => "-",
3292 BinOp::SubFloat => "-.",
3293 BinOp::MultInt => "*",
3294 BinOp::MultFloat => "*.",
3295 BinOp::DivInt => "/",
3296 BinOp::DivFloat => "/.",
3297 BinOp::RemainderInt => "%",
3298 BinOp::Concatenate => "<>",
3299 }
3300 .to_doc()
3301 }
3302}
3303
3304#[allow(clippy::enum_variant_names)]
3305#[derive(Debug)]
3306/// This is used to determine how to fit the items of a list, or the segments of
3307/// a bit array in a line.
3308///
3309enum ItemsPacking {
3310 /// Try and fit everything on a single line; if the items don't fit, break
3311 /// the list putting each item into its own line.
3312 ///
3313 /// ```gleam
3314 /// // unbroken
3315 /// [1, 2, 3]
3316 ///
3317 /// // broken
3318 /// [
3319 /// 1,
3320 /// 2,
3321 /// 3,
3322 /// ]
3323 /// ```
3324 ///
3325 FitOnePerLine,
3326
3327 /// Try and fit everything on a single line; if the items don't fit, break
3328 /// the list putting as many items as possible in a single line.
3329 ///
3330 /// ```gleam
3331 /// // unbroken
3332 /// [1, 2, 3]
3333 ///
3334 /// // broken
3335 /// [
3336 /// 1, 2, 3, ...
3337 /// 4, 100,
3338 /// ]
3339 /// ```
3340 ///
3341 FitMultiplePerLine,
3342
3343 /// Always break the list, putting each item into its own line:
3344 ///
3345 /// ```gleam
3346 /// [
3347 /// 1,
3348 /// 2,
3349 /// 3,
3350 /// ]
3351 /// ```
3352 ///
3353 BreakOnePerLine,
3354}
3355
3356pub fn break_block(doc: Document<'_>) -> Document<'_> {
3357 "{".to_doc()
3358 .append(line().append(doc).nest(INDENT))
3359 .append(line())
3360 .append("}")
3361 .force_break()
3362}
3363
3364pub fn wrap_block(doc: Document<'_>) -> Document<'_> {
3365 break_("{", "{ ")
3366 .append(doc)
3367 .nest(INDENT)
3368 .append(break_("", " "))
3369 .append("}")
3370}
3371
3372fn printed_comments<'a, 'comments>(
3373 comments: impl IntoIterator<Item = Option<&'comments str>>,
3374 trailing_newline: bool,
3375) -> Option<Document<'a>> {
3376 let mut comments = comments.into_iter().peekable();
3377 let _ = comments.peek()?;
3378
3379 let mut doc = Vec::new();
3380 while let Some(c) = comments.next() {
3381 let c = match c {
3382 Some(c) => c,
3383 None => continue,
3384 };
3385 doc.push("//".to_doc().append(EcoString::from(c)));
3386 match comments.peek() {
3387 // Next line is a comment
3388 Some(Some(_)) => doc.push(line()),
3389 // Next line is empty
3390 Some(None) => {
3391 let _ = comments.next();
3392 match comments.peek() {
3393 Some(_) => doc.push(lines(2)),
3394 None => {
3395 if trailing_newline {
3396 doc.push(lines(2));
3397 }
3398 }
3399 }
3400 }
3401 // We've reached the end, there are no more lines
3402 None => {
3403 if trailing_newline {
3404 doc.push(line());
3405 }
3406 }
3407 }
3408 }
3409 let doc = concat(doc);
3410 if trailing_newline {
3411 Some(doc.force_break())
3412 } else {
3413 Some(doc)
3414 }
3415}
3416
3417fn commented<'a, 'comments>(
3418 doc: Document<'a>,
3419 comments: impl IntoIterator<Item = Option<&'comments str>>,
3420) -> Document<'a> {
3421 match printed_comments(comments, true) {
3422 Some(comments) => comments.append(doc.group()),
3423 None => doc,
3424 }
3425}
3426
3427fn bit_array_segment<Value, Type, ToDoc>(
3428 segment: &BitArraySegment<Value, Type>,
3429 mut to_doc: ToDoc,
3430) -> Document<'_>
3431where
3432 ToDoc: FnMut(&Value) -> Document<'_>,
3433{
3434 match segment {
3435 BitArraySegment { value, options, .. } if options.is_empty() => to_doc(value),
3436
3437 BitArraySegment { value, options, .. } => to_doc(value).append(":").append(join(
3438 options
3439 .iter()
3440 .map(|option| segment_option(option, |value| to_doc(value))),
3441 "-".to_doc(),
3442 )),
3443 }
3444}
3445
3446fn segment_option<ToDoc, Value>(option: &BitArrayOption<Value>, mut to_doc: ToDoc) -> Document<'_>
3447where
3448 ToDoc: FnMut(&Value) -> Document<'_>,
3449{
3450 match option {
3451 BitArrayOption::Bytes { .. } => "bytes".to_doc(),
3452 BitArrayOption::Bits { .. } => "bits".to_doc(),
3453 BitArrayOption::Int { .. } => "int".to_doc(),
3454 BitArrayOption::Float { .. } => "float".to_doc(),
3455 BitArrayOption::Utf8 { .. } => "utf8".to_doc(),
3456 BitArrayOption::Utf16 { .. } => "utf16".to_doc(),
3457 BitArrayOption::Utf32 { .. } => "utf32".to_doc(),
3458 BitArrayOption::Utf8Codepoint { .. } => "utf8_codepoint".to_doc(),
3459 BitArrayOption::Utf16Codepoint { .. } => "utf16_codepoint".to_doc(),
3460 BitArrayOption::Utf32Codepoint { .. } => "utf32_codepoint".to_doc(),
3461 BitArrayOption::Signed { .. } => "signed".to_doc(),
3462 BitArrayOption::Unsigned { .. } => "unsigned".to_doc(),
3463 BitArrayOption::Big { .. } => "big".to_doc(),
3464 BitArrayOption::Little { .. } => "little".to_doc(),
3465 BitArrayOption::Native { .. } => "native".to_doc(),
3466
3467 BitArrayOption::Size {
3468 value,
3469 short_form: false,
3470 ..
3471 } => "size"
3472 .to_doc()
3473 .append("(")
3474 .append(to_doc(value))
3475 .append(")"),
3476
3477 BitArrayOption::Size {
3478 value,
3479 short_form: true,
3480 ..
3481 } => to_doc(value),
3482
3483 BitArrayOption::Unit { value, .. } => "unit"
3484 .to_doc()
3485 .append("(")
3486 .append(eco_format!("{value}"))
3487 .append(")"),
3488 }
3489}
3490
3491pub fn comments_before<'a>(
3492 comments: &'a [Comment<'a>],
3493 empty_lines: &'a [u32],
3494 limit: u32,
3495 retain_empty_lines: bool,
3496) -> (
3497 impl Iterator<Item = (u32, Option<&'a str>)>,
3498 &'a [Comment<'a>],
3499 &'a [u32],
3500) {
3501 let end_comments = comments
3502 .iter()
3503 .position(|c| c.start > limit)
3504 .unwrap_or(comments.len());
3505 let end_empty_lines = empty_lines
3506 .iter()
3507 .position(|l| *l > limit)
3508 .unwrap_or(empty_lines.len());
3509 let popped_comments = comments
3510 .get(0..end_comments)
3511 .expect("0..end_comments is guaranteed to be in bounds")
3512 .iter()
3513 .map(|c| (c.start, Some(c.content)));
3514 let popped_empty_lines = if retain_empty_lines { empty_lines } else { &[] }
3515 .get(0..end_empty_lines)
3516 .unwrap_or(&[])
3517 .iter()
3518 .map(|i| (i, i))
3519 // compact consecutive empty lines into a single line
3520 .coalesce(|(a_start, a_end), (b_start, b_end)| {
3521 if *a_end + 1 == *b_start {
3522 Ok((a_start, b_end))
3523 } else {
3524 Err(((a_start, a_end), (b_start, b_end)))
3525 }
3526 })
3527 .map(|l| (*l.0, None));
3528 let popped = popped_comments
3529 .merge_by(popped_empty_lines, |(a, _), (b, _)| a < b)
3530 .skip_while(|(_, comment_or_line)| comment_or_line.is_none());
3531 (
3532 popped,
3533 comments.get(end_comments..).expect("in bounds"),
3534 empty_lines.get(end_empty_lines..).expect("in bounds"),
3535 )
3536}
3537
3538fn is_breakable_argument(expression: &UntypedExpr, arity: usize) -> bool {
3539 match expression {
3540 // A call is only breakable if it is the only argument
3541 UntypedExpr::Call { .. } => arity == 1,
3542
3543 UntypedExpr::Fn { .. }
3544 | UntypedExpr::Block { .. }
3545 | UntypedExpr::Case { .. }
3546 | UntypedExpr::List { .. }
3547 | UntypedExpr::Tuple { .. }
3548 | UntypedExpr::BitArray { .. } => true,
3549
3550 UntypedExpr::Int { .. }
3551 | UntypedExpr::Float { .. }
3552 | UntypedExpr::String { .. }
3553 | UntypedExpr::Var { .. }
3554 | UntypedExpr::BinOp { .. }
3555 | UntypedExpr::PipeLine { .. }
3556 | UntypedExpr::FieldAccess { .. }
3557 | UntypedExpr::TupleIndex { .. }
3558 | UntypedExpr::Todo { .. }
3559 | UntypedExpr::Panic { .. }
3560 | UntypedExpr::Echo { .. }
3561 | UntypedExpr::RecordUpdate { .. }
3562 | UntypedExpr::NegateBool { .. }
3563 | UntypedExpr::NegateInt { .. } => false,
3564 }
3565}
3566
3567enum CallArgFormatting<'a, A> {
3568 ShorthandLabelled(&'a EcoString),
3569 Unlabelled(&'a A),
3570 Labelled(&'a EcoString, &'a A),
3571}
3572
3573fn expr_call_arg_formatting(argument: &CallArg<UntypedExpr>) -> CallArgFormatting<'_, UntypedExpr> {
3574 match argument {
3575 // An argument supplied using label shorthand syntax.
3576 _ if argument.uses_label_shorthand() => CallArgFormatting::ShorthandLabelled(
3577 argument
3578 .label
3579 .as_ref()
3580 .expect("label shorthand with no label"),
3581 ),
3582 // A labelled argument.
3583 CallArg {
3584 label: Some(label),
3585 value,
3586 ..
3587 } => CallArgFormatting::Labelled(label, value),
3588 // An unlabelled argument.
3589 CallArg { value, .. } => CallArgFormatting::Unlabelled(value),
3590 }
3591}
3592
3593fn pattern_call_arg_formatting(
3594 argument: &CallArg<UntypedPattern>,
3595) -> CallArgFormatting<'_, UntypedPattern> {
3596 match argument {
3597 // An argument supplied using label shorthand syntax.
3598 _ if argument.uses_label_shorthand() => CallArgFormatting::ShorthandLabelled(
3599 argument
3600 .label
3601 .as_ref()
3602 .expect("label shorthand with no label"),
3603 ),
3604 // A labelled argument.
3605 CallArg {
3606 label: Some(label),
3607 value,
3608 ..
3609 } => CallArgFormatting::Labelled(label, value),
3610 // An unlabelled argument.
3611 CallArg { value, .. } => CallArgFormatting::Unlabelled(value),
3612 }
3613}
3614
3615fn constant_call_arg_formatting<A, B>(
3616 argument: &CallArg<Constant<A, B>>,
3617) -> CallArgFormatting<'_, Constant<A, B>> {
3618 match argument {
3619 // An argument supplied using label shorthand syntax.
3620 _ if argument.uses_label_shorthand() => CallArgFormatting::ShorthandLabelled(
3621 argument
3622 .label
3623 .as_ref()
3624 .expect("label shorthand with no label"),
3625 ),
3626 // A labelled argument.
3627 CallArg {
3628 label: Some(label),
3629 value,
3630 ..
3631 } => CallArgFormatting::Labelled(label, value),
3632 // An unlabelled argument.
3633 CallArg { value, .. } => CallArgFormatting::Unlabelled(value),
3634 }
3635}
3636
3637struct AttributesPrinter<'a> {
3638 external_erlang: &'a Option<(EcoString, EcoString, SrcSpan)>,
3639 external_javascript: &'a Option<(EcoString, EcoString, SrcSpan)>,
3640 deprecation: &'a Deprecation,
3641 internal: bool,
3642}
3643
3644impl<'a> AttributesPrinter<'a> {
3645 pub fn new() -> Self {
3646 Self {
3647 external_erlang: &None,
3648 external_javascript: &None,
3649 deprecation: &Deprecation::NotDeprecated,
3650 internal: false,
3651 }
3652 }
3653
3654 pub fn set_external_erlang(
3655 mut self,
3656 external: &'a Option<(EcoString, EcoString, SrcSpan)>,
3657 ) -> Self {
3658 self.external_erlang = external;
3659 self
3660 }
3661
3662 pub fn set_external_javascript(
3663 mut self,
3664 external: &'a Option<(EcoString, EcoString, SrcSpan)>,
3665 ) -> Self {
3666 self.external_javascript = external;
3667 self
3668 }
3669
3670 pub fn set_internal(mut self, publicity: Publicity) -> Self {
3671 self.internal = publicity.is_internal();
3672 self
3673 }
3674
3675 pub fn set_deprecation(mut self, deprecation: &'a Deprecation) -> Self {
3676 self.deprecation = deprecation;
3677 self
3678 }
3679}
3680
3681impl<'a> Documentable<'a> for AttributesPrinter<'a> {
3682 fn to_doc(self) -> Document<'a> {
3683 let mut attributes = vec![];
3684
3685 // @deprecated attribute
3686 if let Deprecation::Deprecated { message } = self.deprecation {
3687 attributes.push(docvec!["@deprecated(\"", message, "\")"])
3688 };
3689
3690 // @external attributes
3691 if let Some((m, f, _)) = self.external_erlang {
3692 attributes.push(docvec!["@external(erlang, \"", m, "\", \"", f, "\")"])
3693 };
3694
3695 if let Some((m, f, _)) = self.external_javascript {
3696 attributes.push(docvec!["@external(javascript, \"", m, "\", \"", f, "\")"])
3697 };
3698
3699 // @internal attribute
3700 if self.internal {
3701 attributes.push("@internal".to_doc());
3702 };
3703
3704 if attributes.is_empty() {
3705 nil()
3706 } else {
3707 join(attributes, line()).append(line())
3708 }
3709 }
3710}