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