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