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