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 if value.starts_with("0x") || value.starts_with("0b") || value.starts_with("0o") {
1464 return value.to_doc(arena);
1465 }
1466
1467 self.underscore_integer_string(arena, value)
1468 }
1469
1470 fn underscore_integer_string(
1471 &self,
1472 arena: &'doc DocumentArena<'a, 'doc>,
1473 value: &'a str,
1474 ) -> Document<'a, 'doc> {
1475 let underscore = '_';
1476 let minus = '-';
1477
1478 let len = value.len();
1479 let underscore_ch_cnt = value.matches(underscore).count();
1480 let reformat_watershed = if value.starts_with(minus) { 6 } else { 5 };
1481 let insert_underscores = (len - underscore_ch_cnt) >= reformat_watershed;
1482
1483 let mut new_value = String::new();
1484 let mut j = 0;
1485 for (i, character) in value.chars().rev().enumerate() {
1486 if character == underscore {
1487 continue;
1488 }
1489
1490 if insert_underscores && i != 0 && character != minus && i < len && j % 3 == 0 {
1491 new_value.push(underscore);
1492 }
1493 new_value.push(character);
1494
1495 j += 1;
1496 }
1497
1498 new_value.chars().rev().collect::<EcoString>().to_doc(arena)
1499 }
1500
1501 #[allow(clippy::too_many_arguments)]
1502 fn pattern_constructor(
1503 &mut self,
1504 arena: &'doc DocumentArena<'a, 'doc>,
1505 name: &'a str,
1506 arguments: &'a [CallArg<UntypedPattern>],
1507 module: &'a Option<(EcoString, SrcSpan)>,
1508 spread: Option<SrcSpan>,
1509 location: &SrcSpan,
1510 ) -> Document<'a, 'doc> {
1511 fn is_breakable(expression: &UntypedPattern) -> bool {
1512 match expression {
1513 Pattern::Tuple { .. } | Pattern::List { .. } | Pattern::BitArray { .. } => true,
1514 Pattern::Constructor { arguments, .. } => !arguments.is_empty(),
1515 Pattern::Int { .. }
1516 | Pattern::Float { .. }
1517 | Pattern::String { .. }
1518 | Pattern::Variable { .. }
1519 | Pattern::BitArraySize(_)
1520 | Pattern::Assign { .. }
1521 | Pattern::Discard { .. }
1522 | Pattern::StringPrefix { .. }
1523 | Pattern::Invalid { .. } => false,
1524 }
1525 }
1526
1527 let name = match module {
1528 Some((module, _)) => module
1529 .to_doc(arena)
1530 .append(arena, DOT_DOCUMENT)
1531 .append(arena, name),
1532 None => name.to_doc(arena),
1533 };
1534
1535 if arguments.is_empty() && spread.is_some() {
1536 name.append(arena, OPEN_PAREN_DOT_DOT_CLOSE_PAREN_DOCUMENT)
1537 } else if arguments.is_empty() {
1538 name
1539 } else if spread.is_some() {
1540 let arguments = arguments
1541 .iter()
1542 .map(|argument| self.pattern_call_arg(arena, argument))
1543 .collect_vec();
1544 name.append(
1545 arena,
1546 self.wrap_arguments_with_spread(arena, arguments, location.end),
1547 )
1548 .group(arena)
1549 } else {
1550 match arguments {
1551 [argument] if is_breakable(&argument.value) => name
1552 .append(arena, OPEN_PAREN_DOCUMENT)
1553 .append(arena, self.pattern_call_arg(arena, argument))
1554 .append(arena, CLOSE_PAREN_DOCUMENT)
1555 .group(arena),
1556
1557 _ => {
1558 let arguments = arguments
1559 .iter()
1560 .map(|argument| self.pattern_call_arg(arena, argument))
1561 .collect_vec();
1562 name.append(arena, self.wrap_arguments(arena, arguments, location.end))
1563 .group(arena)
1564 }
1565 }
1566 }
1567 }
1568
1569 fn call(
1570 &mut self,
1571 arena: &'doc DocumentArena<'a, 'doc>,
1572 function: &'a UntypedExpr,
1573 arguments: &'a [CallArg<UntypedExpr>],
1574 location: &SrcSpan,
1575 ) -> Document<'a, 'doc> {
1576 let expression = match function {
1577 UntypedExpr::PipeLine { .. } => break_block(arena, self.expr(arena, function)),
1578
1579 UntypedExpr::BinOp { .. }
1580 | UntypedExpr::Int { .. }
1581 | UntypedExpr::Float { .. }
1582 | UntypedExpr::String { .. }
1583 | UntypedExpr::Block { .. }
1584 | UntypedExpr::Var { .. }
1585 | UntypedExpr::Fn { .. }
1586 | UntypedExpr::List { .. }
1587 | UntypedExpr::Call { .. }
1588 | UntypedExpr::Case { .. }
1589 | UntypedExpr::FieldAccess { .. }
1590 | UntypedExpr::Tuple { .. }
1591 | UntypedExpr::TupleIndex { .. }
1592 | UntypedExpr::Todo { .. }
1593 | UntypedExpr::Echo { .. }
1594 | UntypedExpr::Panic { .. }
1595 | UntypedExpr::BitArray { .. }
1596 | UntypedExpr::RecordUpdate { .. }
1597 | UntypedExpr::NegateBool { .. }
1598 | UntypedExpr::NegateInt { .. } => self.expr(arena, function),
1599 };
1600
1601 let arity = arguments.len();
1602 self.append_inlinable_wrapped_arguments(
1603 arena,
1604 expression,
1605 arguments,
1606 location,
1607 |argument| is_breakable_argument(&argument.value, arguments.len()),
1608 |self_, argument| self_.call_arg(arena, argument, arity),
1609 )
1610 }
1611
1612 fn tuple(
1613 &mut self,
1614 arena: &'doc DocumentArena<'a, 'doc>,
1615 elements: &'a [UntypedExpr],
1616 location: &SrcSpan,
1617 ) -> Document<'a, 'doc> {
1618 if elements.is_empty() {
1619 // We take all comments that come _before_ the end of the tuple,
1620 // that is all comments that are inside "#(" and ")", if there's
1621 // any comment we want to put it inside the empty tuple!
1622 return match printed_comments(arena, self.pop_comments(location.end), false) {
1623 None => EMPTY_TUPLE_DOCUMENT,
1624 Some(comments) => OPEN_TUPLE_DOCUMENT
1625 .append(arena, EMPTY_BREAK_DOCUMENT.nest(arena, INDENT))
1626 .append(arena, comments)
1627 .append(arena, EMPTY_BREAK_DOCUMENT)
1628 .append(arena, CLOSE_PAREN_DOCUMENT)
1629 // vvv We want to make sure the comments are on a separate
1630 // line from the opening and closing parentheses so we
1631 // force the breaks to be split on newlines.
1632 .force_break(arena),
1633 };
1634 }
1635
1636 self.append_inlinable_wrapped_arguments(
1637 arena,
1638 HASHTAG_DOCUMENT,
1639 elements,
1640 location,
1641 |expression| {
1642 !expression.is_tuple() && is_breakable_argument(expression, elements.len())
1643 },
1644 |self_, expression| self_.comma_separated_item(arena, expression, elements.len()),
1645 )
1646 }
1647
1648 // Appends to the given docs a comma-separated list of documents wrapped by
1649 // parentheses. If the last item of the argument list is splittable the
1650 // resulting document will try to first split that before splitting all the
1651 // other arguments.
1652 // This is used for function calls and tuples.
1653 #[allow(clippy::too_many_arguments)]
1654 fn append_inlinable_wrapped_arguments<'b, T, Predicate, ToDoc>(
1655 &mut self,
1656 arena: &'doc DocumentArena<'a, 'doc>,
1657 doc: Document<'a, 'doc>,
1658 values: &'b [T],
1659 location: &SrcSpan,
1660 is_breakable_argument: Predicate,
1661 to_doc: ToDoc,
1662 ) -> Document<'a, 'doc>
1663 where
1664 T: HasLocation,
1665 T: std::fmt::Debug,
1666 Predicate: Fn(&T) -> bool,
1667 ToDoc: Fn(&mut Self, &'b T) -> Document<'a, 'doc>,
1668 {
1669 match init_and_last(values) {
1670 Some((initial_values, last_value))
1671 if is_breakable_argument(last_value)
1672 && !self.any_comments(last_value.location().start)
1673 && !self.any_comment_between(last_value.location().end, location.end) =>
1674 {
1675 let mut docs = initial_values
1676 .iter()
1677 .map(|value| to_doc(self, value))
1678 .collect_vec();
1679
1680 let last_value_doc = to_doc(self, last_value)
1681 .group(arena)
1682 .next_break_fits(arena, NextBreakFitsMode::Enabled);
1683
1684 docs.append(&mut vec![last_value_doc]);
1685
1686 doc.append(
1687 arena,
1688 self.wrap_function_call_arguments(arena, docs, location),
1689 )
1690 .next_break_fits(arena, NextBreakFitsMode::Disabled)
1691 .group(arena)
1692 }
1693
1694 Some(_) | None => {
1695 let docs = values.iter().map(|value| to_doc(self, value)).collect_vec();
1696 doc.append(
1697 arena,
1698 self.wrap_function_call_arguments(arena, docs, location),
1699 )
1700 .group(arena)
1701 }
1702 }
1703 }
1704
1705 pub fn case(
1706 &mut self,
1707 arena: &'doc DocumentArena<'a, 'doc>,
1708 subjects: &'a [UntypedExpr],
1709 clauses: &'a [UntypedClause],
1710 location: &'a SrcSpan,
1711 ) -> Document<'a, 'doc> {
1712 let subjects_doc = CASE_BREAK_DOCUMENT
1713 .append(
1714 arena,
1715 arena.join(
1716 subjects
1717 .iter()
1718 .map(|subject| self.expr(arena, subject).group(arena)),
1719 COMMA_BREAK_DOCUMENT,
1720 ),
1721 )
1722 .nest(arena, INDENT)
1723 .append(arena, BREAKABLE_SPACE_DOCUMENT)
1724 .append(arena, OPEN_CURLY_DOCUMENT)
1725 .next_break_fits(arena, NextBreakFitsMode::Disabled)
1726 .group(arena);
1727
1728 let clauses_doc = arena.concat(
1729 clauses
1730 .iter()
1731 .enumerate()
1732 .map(|(i, clause)| self.clause(arena, clause, i as u32).group(arena)),
1733 );
1734
1735 // We get all remaining comments that come before the case's closing
1736 // bracket. If there's any we add those before the closing bracket
1737 // instead of moving those out of the case expression.
1738 // Otherwise those would be moved out of the case expression.
1739 let comments = self.pop_comments(location.end);
1740 let closing_bracket = match printed_comments(arena, comments, false) {
1741 None => docvec![arena, LINE_DOCUMENT, CLOSE_CURLY_DOCUMENT],
1742 Some(comment) => docvec![arena, LINE_DOCUMENT, comment]
1743 .nest(arena, INDENT)
1744 .append(arena, LINE_DOCUMENT)
1745 .append(arena, CLOSE_CURLY_DOCUMENT),
1746 };
1747
1748 subjects_doc
1749 .append(
1750 arena,
1751 LINE_DOCUMENT.append(arena, clauses_doc).nest(arena, INDENT),
1752 )
1753 .append(arena, closing_bracket)
1754 .force_break(arena)
1755 }
1756
1757 pub fn record_update(
1758 &mut self,
1759 arena: &'doc DocumentArena<'a, 'doc>,
1760 constructor: &'a UntypedExpr,
1761 record: &'a RecordBeingUpdated<UntypedExpr>,
1762 arguments: &'a [UntypedRecordUpdateArg],
1763 location: &SrcSpan,
1764 ) -> Document<'a, 'doc> {
1765 let constructor_doc: Document<'a, 'doc> = self.expr(arena, constructor);
1766 let pieces = std::iter::once(UntypedRecordUpdatePiece::Record(record))
1767 .chain(arguments.iter().map(UntypedRecordUpdatePiece::Argument))
1768 .collect_vec();
1769
1770 self.append_inlinable_wrapped_arguments(
1771 arena,
1772 constructor_doc,
1773 &pieces,
1774 location,
1775 |argument| {
1776 let expression = match argument {
1777 UntypedRecordUpdatePiece::Argument(argument) => &argument.value,
1778 UntypedRecordUpdatePiece::Record(record) => &record.base,
1779 };
1780 is_breakable_argument(expression, pieces.len())
1781 },
1782 |this, argument| match argument {
1783 UntypedRecordUpdatePiece::Argument(arg) => this.record_update_arg(arena, arg),
1784 UntypedRecordUpdatePiece::Record(record) => {
1785 let comments = this.pop_comments(record.location.start);
1786 commented(
1787 arena,
1788 DOT_DOT_DOCUMENT.append(arena, this.expr(arena, &record.base)),
1789 comments,
1790 )
1791 }
1792 },
1793 )
1794 }
1795
1796 #[allow(clippy::too_many_arguments)]
1797 pub fn const_record_update<A>(
1798 &mut self,
1799 arena: &'doc DocumentArena<'a, 'doc>,
1800 module: &Option<(EcoString, SrcSpan)>,
1801 name: &'a EcoString,
1802 record: &'a RecordBeingUpdated<Constant<A>>,
1803 arguments: &'a [RecordUpdateArg<Constant<A>>],
1804 location: &SrcSpan,
1805 ) -> Document<'a, 'doc> {
1806 let constructor_doc = match module {
1807 Some((m, _)) => m
1808 .to_doc(arena)
1809 .append(arena, DOT_DOCUMENT)
1810 .append(arena, name.as_str()),
1811 None => name.to_doc(arena),
1812 };
1813
1814 let pieces = std::iter::once(RecordUpdatePiece::Record(record))
1815 .chain(arguments.iter().map(RecordUpdatePiece::Argument))
1816 .collect_vec();
1817
1818 let docs = pieces
1819 .iter()
1820 .map(|piece| match piece {
1821 RecordUpdatePiece::Argument(argument) => {
1822 let comments = self.pop_comments(argument.location.start);
1823 let doc = match argument {
1824 _ if argument.uses_label_shorthand() => argument
1825 .label
1826 .as_str()
1827 .to_doc(arena)
1828 .append(arena, COLON_DOCUMENT),
1829 _ => argument
1830 .label
1831 .as_str()
1832 .to_doc(arena)
1833 .append(arena, COLON_SPACE_DOCUMENT)
1834 .append(arena, self.const_expr(arena, &argument.value))
1835 .group(arena),
1836 };
1837 commented(arena, doc, comments)
1838 }
1839 RecordUpdatePiece::Record(record) => {
1840 let comments = self.pop_comments(record.location.start);
1841 commented(
1842 arena,
1843 DOT_DOT_DOCUMENT.append(arena, self.const_expr(arena, &record.base)),
1844 comments,
1845 )
1846 }
1847 })
1848 .collect_vec();
1849
1850 constructor_doc
1851 .append(arena, self.wrap_arguments(arena, docs, location.end))
1852 .group(arena)
1853 }
1854
1855 pub fn bin_op(
1856 &mut self,
1857 arena: &'doc DocumentArena<'a, 'doc>,
1858 name: &'a BinOp,
1859 left: &'a UntypedExpr,
1860 right: &'a UntypedExpr,
1861 nest_steps: bool,
1862 ) -> Document<'a, 'doc> {
1863 let left_side = self.bin_op_side(arena, name, left, nest_steps);
1864
1865 let comments = self.pop_comments(right.start_byte_index());
1866 let name_doc =
1867 BREAKABLE_SPACE_DOCUMENT.append(arena, commented(arena, binop(*name), comments));
1868
1869 let right_side = self.bin_op_side(arena, name, right, nest_steps);
1870
1871 left_side
1872 .append(
1873 arena,
1874 if nest_steps {
1875 name_doc.nest(arena, INDENT)
1876 } else {
1877 name_doc
1878 },
1879 )
1880 .append(arena, SPACE_DOCUMENT)
1881 .append(arena, right_side)
1882 }
1883
1884 fn bin_op_side(
1885 &mut self,
1886 arena: &'doc DocumentArena<'a, 'doc>,
1887 operator: &'a BinOp,
1888 side: &'a UntypedExpr,
1889 nest_steps: bool,
1890 ) -> Document<'a, 'doc> {
1891 let side_doc = match side {
1892 UntypedExpr::String { value, .. } => self.bin_op_string(arena, value),
1893 UntypedExpr::BinOp {
1894 operator,
1895 left,
1896 right,
1897 ..
1898 } => self.bin_op(arena, operator, left, right, nest_steps),
1899 UntypedExpr::Int { .. }
1900 | UntypedExpr::Float { .. }
1901 | UntypedExpr::Block { .. }
1902 | UntypedExpr::Var { .. }
1903 | UntypedExpr::Fn { .. }
1904 | UntypedExpr::List { .. }
1905 | UntypedExpr::Call { .. }
1906 | UntypedExpr::PipeLine { .. }
1907 | UntypedExpr::Case { .. }
1908 | UntypedExpr::FieldAccess { .. }
1909 | UntypedExpr::Tuple { .. }
1910 | UntypedExpr::TupleIndex { .. }
1911 | UntypedExpr::Todo { .. }
1912 | UntypedExpr::Panic { .. }
1913 | UntypedExpr::Echo { .. }
1914 | UntypedExpr::BitArray { .. }
1915 | UntypedExpr::RecordUpdate { .. }
1916 | UntypedExpr::NegateBool { .. }
1917 | UntypedExpr::NegateInt { .. } => self.expr(arena, side),
1918 };
1919 match side.bin_op_name() {
1920 // In case the other side is a binary operation as well and it can
1921 // be grouped together with the current binary operation, the two
1922 // docs are simply concatenated, so that they will end up in the
1923 // same group and the formatter will try to keep those on a single
1924 // line.
1925 Some(side_name) if side_name.can_be_grouped_with(operator) => side_doc,
1926 // In case the binary operations cannot be grouped together the
1927 // other side is treated as a group on its own so that it can be
1928 // broken independently of other pieces of the binary operations
1929 // chain.
1930 _ => self.operator_side(
1931 arena,
1932 side_doc.group(arena),
1933 operator.precedence(),
1934 side.bin_op_precedence(),
1935 ),
1936 }
1937 }
1938
1939 pub fn operator_side(
1940 &self,
1941 arena: &'doc DocumentArena<'a, 'doc>,
1942 doc: Document<'a, 'doc>,
1943 op: u8,
1944 side: u8,
1945 ) -> Document<'a, 'doc> {
1946 if op > side {
1947 wrap_block(arena, doc).group(arena)
1948 } else {
1949 doc
1950 }
1951 }
1952
1953 fn spans_multiple_lines(&self, start: u32, end: u32) -> bool {
1954 self.new_lines
1955 .binary_search_by(|newline| {
1956 if *newline <= start {
1957 Ordering::Less
1958 } else if *newline >= end {
1959 Ordering::Greater
1960 } else {
1961 // If the newline is in between the pipe start and end
1962 // then we've found it!
1963 Ordering::Equal
1964 }
1965 })
1966 // If we couldn't find any newline between the start and end of
1967 // the pipeline then we will try and keep it on a single line.
1968 .is_ok()
1969 }
1970
1971 /// Returns true if there's a trailing comma between `start` and `end`.
1972 ///
1973 fn has_trailing_comma(&self, start: u32, end: u32) -> bool {
1974 self.trailing_commas
1975 .binary_search_by(|comma| {
1976 if *comma < start {
1977 Ordering::Less
1978 } else if *comma > end {
1979 Ordering::Greater
1980 } else {
1981 Ordering::Equal
1982 }
1983 })
1984 .is_ok()
1985 }
1986
1987 fn pipeline(
1988 &mut self,
1989 arena: &'doc DocumentArena<'a, 'doc>,
1990 expressions: &'a Vec1<UntypedExpr>,
1991 nest_pipe: bool,
1992 ) -> Document<'a, 'doc> {
1993 // We start by producing the document for the first step of the pipeline.
1994 let first = expressions.first();
1995 let first_precedence = first.bin_op_precedence();
1996 let first = self.expr(arena, first).group(arena);
1997 let first_step = self.operator_side(arena, first, 5, first_precedence);
1998
1999 // We then produce a doc for each item in the rest of the pipeline.
2000 let pipeline_start = expressions.first().location().start;
2001 let pipeline_end = expressions.last().location().end;
2002 let try_to_keep_on_one_line = !self.spans_multiple_lines(pipeline_start, pipeline_end);
2003 let steps_separator = if try_to_keep_on_one_line {
2004 BREAKABLE_SPACE_DOCUMENT
2005 } else {
2006 LINE_DOCUMENT
2007 };
2008
2009 let other_steps = expressions.iter().skip(1).map(|expression| {
2010 // We start by producing the document for the pipe itself `|>`,
2011 // we also want to put comments before it!
2012 let comments = self.pop_comments(expression.location().start);
2013 let commented_pipe = commented(arena, PIPE_SPACE_DOCUMENT, comments);
2014 let mut pipe = docvec![arena, steps_separator, commented_pipe];
2015 if nest_pipe {
2016 pipe = pipe.nest(arena, INDENT);
2017 };
2018
2019 // We then produce the document for the expression being piped into.
2020 let expression_doc =
2021 self.expression_on_right_hand_side_of_pipe(arena, nest_pipe, expression);
2022 let expression_doc =
2023 self.operator_side(arena, expression_doc, 4, expression.bin_op_precedence());
2024
2025 // And finally piece everything together.
2026 pipe.append(arena, expression_doc)
2027 });
2028
2029 let stops = std::iter::once(first_step).chain(other_steps);
2030 if try_to_keep_on_one_line {
2031 arena.concat(stops)
2032 } else {
2033 arena.concat(stops).force_break(arena)
2034 }
2035 }
2036
2037 fn expression_on_right_hand_side_of_pipe(
2038 &mut self,
2039 arena: &'doc DocumentArena<'a, 'doc>,
2040 nest_pipe: bool,
2041 expression: &'a UntypedExpr,
2042 ) -> Document<'a, 'doc> {
2043 let expression = if let UntypedExpr::Fn { kind, body, .. } = expression
2044 && kind.is_capture()
2045 {
2046 self.fn_capture(arena, body, FnCapturePosition::RightHandSideOfPipe)
2047 } else {
2048 self.expr(arena, expression)
2049 };
2050
2051 if nest_pipe {
2052 expression.nest(arena, INDENT)
2053 } else {
2054 expression
2055 }
2056 }
2057
2058 fn fn_capture(
2059 &mut self,
2060 arena: &'doc DocumentArena<'a, 'doc>,
2061 call: &'a [UntypedStatement],
2062 position: FnCapturePosition,
2063 ) -> Document<'a, 'doc> {
2064 // The body of a capture being multiple statements shouldn't be possible...
2065 if call.len() != 1 {
2066 panic!("Function capture found not to have a single statement call");
2067 }
2068
2069 let Some(Statement::Expression(UntypedExpr::Call {
2070 fun,
2071 arguments,
2072 location,
2073 ..
2074 })) = call.first()
2075 else {
2076 // The body of a capture being not a fn shouldn't be possible...
2077 panic!("Function capture body found not to be a call in the formatter")
2078 };
2079
2080 match (position, arguments.as_slice()) {
2081 // The capture has a single unlabelled hole:
2082 //
2083 // wibble |> wobble(_)
2084 // list.map([], wobble(_))
2085 //
2086 // We want these to become:
2087 //
2088 // wibble |> wobble
2089 // list.map([], wobble)
2090 //
2091 (
2092 FnCapturePosition::RightHandSideOfPipe | FnCapturePosition::EverywhereElse,
2093 [argument],
2094 ) if argument.is_capture_hole() && argument.label.is_none() => self.expr(arena, fun),
2095
2096 // The capture is on the right hand side of a pipe and its first
2097 // argument it an unlabelled capture hole:
2098 //
2099 // wibble |> wobble(_, woo)
2100 //
2101 // We want it to become:
2102 //
2103 // wibble |> wobble(woo)
2104 //
2105 (FnCapturePosition::RightHandSideOfPipe, [argument, rest @ ..])
2106 if argument.is_capture_hole() && argument.label.is_none() =>
2107 {
2108 let expression = self.expr(arena, fun);
2109 let arity = rest.len();
2110 self.append_inlinable_wrapped_arguments(
2111 arena,
2112 expression,
2113 rest,
2114 location,
2115 |argument| is_breakable_argument(&argument.value, rest.len()),
2116 |self_, argument| self_.call_arg(arena, argument, arity),
2117 )
2118 }
2119
2120 // In all other cases we print it like a regular function call
2121 // without changing it.
2122 //
2123 (
2124 FnCapturePosition::RightHandSideOfPipe | FnCapturePosition::EverywhereElse,
2125 arguments,
2126 ) => {
2127 let expression = self.expr(arena, fun);
2128 let arity = arguments.len();
2129 self.append_inlinable_wrapped_arguments(
2130 arena,
2131 expression,
2132 arguments,
2133 location,
2134 |argument| is_breakable_argument(&argument.value, arguments.len()),
2135 |self_, argument| self_.call_arg(arena, argument, arity),
2136 )
2137 }
2138 }
2139 }
2140
2141 pub fn record_constructor<A>(
2142 &mut self,
2143 arena: &'doc DocumentArena<'a, 'doc>,
2144 constructor: &'a RecordConstructor<A>,
2145 ) -> Document<'a, 'doc> {
2146 let comments = self.pop_comments(constructor.location.start);
2147 let doc_comments = self.doc_comments(arena, constructor.location.start);
2148 let attributes = AttributesPrinter::new()
2149 .set_deprecation(&constructor.deprecation)
2150 .to_doc(arena);
2151
2152 let doc = if constructor.arguments.is_empty() {
2153 if self.any_comments(constructor.location.end) {
2154 attributes
2155 .append(arena, constructor.name.as_str().to_doc(arena))
2156 .append(
2157 arena,
2158 self.wrap_arguments(arena, vec![], constructor.location.end),
2159 )
2160 .group(arena)
2161 } else {
2162 attributes.append(arena, constructor.name.as_str().to_doc(arena))
2163 }
2164 } else {
2165 let arguments = constructor
2166 .arguments
2167 .iter()
2168 .map(
2169 |RecordConstructorArg {
2170 label,
2171 ast,
2172 location,
2173 ..
2174 }| {
2175 let arg_comments = self.pop_comments(location.start);
2176 let argument = match label {
2177 Some((_, label)) => label
2178 .to_doc(arena)
2179 .append(arena, COLON_SPACE_DOCUMENT)
2180 .append(arena, self.type_ast(arena, ast)),
2181 None => self.type_ast(arena, ast),
2182 };
2183
2184 commented(
2185 arena,
2186 self.doc_comments(arena, location.start)
2187 .append(arena, argument)
2188 .group(arena),
2189 arg_comments,
2190 )
2191 },
2192 )
2193 .collect_vec();
2194
2195 attributes
2196 .append(arena, constructor.name.as_str().to_doc(arena))
2197 .append(
2198 arena,
2199 self.wrap_arguments(arena, arguments, constructor.location.end)
2200 .group(arena),
2201 )
2202 };
2203
2204 commented(
2205 arena,
2206 doc_comments.append(arena, doc).group(arena),
2207 comments,
2208 )
2209 }
2210
2211 pub fn custom_type<A>(
2212 &mut self,
2213 arena: &'doc DocumentArena<'a, 'doc>,
2214 type_: &'a CustomType<A>,
2215 ) -> Document<'a, 'doc> {
2216 let CustomType {
2217 location,
2218 end_position,
2219 name,
2220 name_location: _,
2221 publicity,
2222 constructors,
2223 documentation: _,
2224 deprecation,
2225 opaque,
2226 parameters,
2227 typed_parameters: _,
2228 external_erlang,
2229 external_javascript,
2230 } = type_;
2231
2232 let _ = self.pop_empty_lines(location.end);
2233
2234 let attributes = AttributesPrinter::new()
2235 .set_deprecation(deprecation)
2236 .set_internal(*publicity)
2237 .set_external_erlang(external_erlang)
2238 .set_external_javascript(external_javascript)
2239 .to_doc(arena);
2240
2241 let doc = attributes
2242 .append(arena, pub_(*publicity))
2243 .append(
2244 arena,
2245 if *opaque {
2246 OPAQUE_TYPE_SPACE_DOCUMENT
2247 } else {
2248 TYPE_SPACE_DOCUMENT
2249 },
2250 )
2251 .append(
2252 arena,
2253 if parameters.is_empty() {
2254 name.clone().to_doc(arena)
2255 } else {
2256 let arguments = type_
2257 .parameters
2258 .iter()
2259 .map(|(_, e)| e.to_doc(arena))
2260 .collect_vec();
2261 type_
2262 .name
2263 .clone()
2264 .to_doc(arena)
2265 .append(arena, self.wrap_arguments(arena, arguments, location.end))
2266 .group(arena)
2267 },
2268 );
2269
2270 if constructors.is_empty() {
2271 return doc;
2272 }
2273 let doc = doc.append(arena, SPACE_OPEN_CURLY_DOCUMENT);
2274
2275 let inner = arena.concat(constructors.iter().map(|c| {
2276 if self.pop_empty_lines(c.location.start) {
2277 TWO_LINES_DOCUMENT
2278 } else {
2279 LINE_DOCUMENT
2280 }
2281 .append(arena, self.record_constructor(arena, c))
2282 }));
2283
2284 // Add any trailing comments
2285 let inner = match printed_comments(arena, self.pop_comments(*end_position), false) {
2286 Some(comments) => inner.append(arena, LINE_DOCUMENT).append(arena, comments),
2287 None => inner,
2288 }
2289 .nest(arena, INDENT)
2290 .group(arena);
2291
2292 doc.append(arena, inner)
2293 .append(arena, LINE_DOCUMENT)
2294 .append(arena, CLOSE_CURLY_DOCUMENT)
2295 }
2296
2297 fn call_arg(
2298 &mut self,
2299 arena: &'doc DocumentArena<'a, 'doc>,
2300 argument: &'a CallArg<UntypedExpr>,
2301 arity: usize,
2302 ) -> Document<'a, 'doc> {
2303 self.format_call_arg(arena, argument, expr_call_arg_formatting, |this, value| {
2304 this.comma_separated_item(arena, value, arity)
2305 })
2306 }
2307
2308 fn format_call_arg<A, F, G>(
2309 &mut self,
2310 arena: &'doc DocumentArena<'a, 'doc>,
2311 argument: &'a CallArg<A>,
2312 figure_formatting: F,
2313 format_value: G,
2314 ) -> Document<'a, 'doc>
2315 where
2316 F: Fn(&'a CallArg<A>) -> CallArgFormatting<'a, A>,
2317 G: Fn(&mut Self, &'a A) -> Document<'a, 'doc>,
2318 {
2319 match figure_formatting(argument) {
2320 CallArgFormatting::Unlabelled(value) => format_value(self, value),
2321 CallArgFormatting::ShorthandLabelled(label) => {
2322 let comments = self.pop_comments(argument.location.start);
2323 let label = label.as_ref().to_doc(arena).append(arena, COLON_DOCUMENT);
2324 commented(arena, label, comments)
2325 }
2326 CallArgFormatting::Labelled(label, value) => {
2327 let comments = self.pop_comments(argument.location.start);
2328 let label = label
2329 .as_ref()
2330 .to_doc(arena)
2331 .append(arena, COLON_SPACE_DOCUMENT);
2332 let value = format_value(self, value);
2333 commented(arena, label, comments).append(arena, value)
2334 }
2335 }
2336 }
2337
2338 fn record_update_arg(
2339 &mut self,
2340 arena: &'doc DocumentArena<'a, 'doc>,
2341 argument: &'a UntypedRecordUpdateArg,
2342 ) -> Document<'a, 'doc> {
2343 let comments = self.pop_comments(argument.location.start);
2344 match argument {
2345 // Argument supplied with a label shorthand.
2346 _ if argument.uses_label_shorthand() => commented(
2347 arena,
2348 argument
2349 .label
2350 .as_str()
2351 .to_doc(arena)
2352 .append(arena, COLON_DOCUMENT),
2353 comments,
2354 ),
2355 // Labelled argument.
2356 _ => {
2357 let doc = argument
2358 .label
2359 .as_str()
2360 .to_doc(arena)
2361 .append(arena, COLON_SPACE_DOCUMENT)
2362 .append(arena, self.expr(arena, &argument.value))
2363 .group(arena);
2364
2365 if argument.value.is_binop() || argument.value.is_pipeline() {
2366 commented(arena, doc, comments).nest(arena, INDENT)
2367 } else {
2368 commented(arena, doc, comments)
2369 }
2370 }
2371 }
2372 }
2373
2374 fn tuple_index(
2375 &mut self,
2376 arena: &'doc DocumentArena<'a, 'doc>,
2377 tuple: &'a UntypedExpr,
2378 index: u64,
2379 ) -> Document<'a, 'doc> {
2380 // In case we have a block with a single variable tuple access we
2381 // remove that redundant wrapper:
2382 //
2383 // {tuple.1}.0 becomes
2384 // tuple.1.0
2385 //
2386 if let UntypedExpr::Block { statements, .. } = tuple {
2387 match statements.as_slice() {
2388 [Statement::Expression(tuple @ UntypedExpr::TupleIndex { tuple: inner, .. })]
2389 // We can't apply this change if the inner thing is a
2390 // literal tuple because the compiler cannot currently parse
2391 // it: `#(1, #(2, 3)).1.0` is a syntax error at the moment.
2392 if !inner.is_tuple() =>
2393 {
2394 self.expr(arena,tuple)
2395 }
2396 _ => self.expr(arena,tuple),
2397 }
2398 } else {
2399 self.expr(arena, tuple)
2400 }
2401 .append(arena, DOT_DOCUMENT)
2402 .append(arena, index)
2403 }
2404
2405 fn case_clause_value(
2406 &mut self,
2407 arena: &'doc DocumentArena<'a, 'doc>,
2408 expression: &'a UntypedExpr,
2409 ) -> Document<'a, 'doc> {
2410 match expression {
2411 UntypedExpr::Fn { .. }
2412 | UntypedExpr::List { .. }
2413 | UntypedExpr::Tuple { .. }
2414 | UntypedExpr::BitArray { .. } => {
2415 let expression_comments = self.pop_comments(expression.location().start);
2416 let expression_doc = self.expr(arena, expression);
2417 match printed_comments(arena, expression_comments, true) {
2418 Some(comments) => LINE_DOCUMENT
2419 .append(arena, comments)
2420 .append(arena, expression_doc)
2421 .nest(arena, INDENT),
2422 None => SPACE_DOCUMENT.append(arena, expression_doc),
2423 }
2424 }
2425
2426 UntypedExpr::Case { .. } => LINE_DOCUMENT
2427 .append(arena, self.expr(arena, expression))
2428 .nest(arena, INDENT),
2429
2430 UntypedExpr::Block {
2431 statements,
2432 location,
2433 ..
2434 } => SPACE_DOCUMENT.append(arena, self.block(arena, location, statements, true)),
2435
2436 UntypedExpr::Int { .. }
2437 | UntypedExpr::Float { .. }
2438 | UntypedExpr::String { .. }
2439 | UntypedExpr::Var { .. }
2440 | UntypedExpr::Call { .. }
2441 | UntypedExpr::BinOp { .. }
2442 | UntypedExpr::PipeLine { .. }
2443 | UntypedExpr::FieldAccess { .. }
2444 | UntypedExpr::TupleIndex { .. }
2445 | UntypedExpr::Todo { .. }
2446 | UntypedExpr::Panic { .. }
2447 | UntypedExpr::Echo { .. }
2448 | UntypedExpr::RecordUpdate { .. }
2449 | UntypedExpr::NegateBool { .. }
2450 | UntypedExpr::NegateInt { .. } => BREAKABLE_SPACE_DOCUMENT
2451 .append(arena, self.expr(arena, expression).group(arena))
2452 .nest(arena, INDENT),
2453 }
2454 .next_break_fits(arena, NextBreakFitsMode::Disabled)
2455 .group(arena)
2456 }
2457
2458 fn assigned_value(
2459 &mut self,
2460 arena: &'doc DocumentArena<'a, 'doc>,
2461 expression: &'a UntypedExpr,
2462 ) -> Document<'a, 'doc> {
2463 match expression {
2464 UntypedExpr::Case { .. } => SPACE_DOCUMENT
2465 .append(arena, self.expr(arena, expression))
2466 .group(arena),
2467 UntypedExpr::Int { .. }
2468 | UntypedExpr::Float { .. }
2469 | UntypedExpr::String { .. }
2470 | UntypedExpr::Block { .. }
2471 | UntypedExpr::Var { .. }
2472 | UntypedExpr::Fn { .. }
2473 | UntypedExpr::List { .. }
2474 | UntypedExpr::Call { .. }
2475 | UntypedExpr::BinOp { .. }
2476 | UntypedExpr::PipeLine { .. }
2477 | UntypedExpr::FieldAccess { .. }
2478 | UntypedExpr::Tuple { .. }
2479 | UntypedExpr::TupleIndex { .. }
2480 | UntypedExpr::Todo { .. }
2481 | UntypedExpr::Panic { .. }
2482 | UntypedExpr::Echo { .. }
2483 | UntypedExpr::BitArray { .. }
2484 | UntypedExpr::RecordUpdate { .. }
2485 | UntypedExpr::NegateBool { .. }
2486 | UntypedExpr::NegateInt { .. } => self.case_clause_value(arena, expression),
2487 }
2488 }
2489
2490 fn clause(
2491 &mut self,
2492 arena: &'doc DocumentArena<'a, 'doc>,
2493 clause: &'a UntypedClause,
2494 index: u32,
2495 ) -> Document<'a, 'doc> {
2496 let space_before = self.pop_empty_lines(clause.location.start);
2497 let comments = self.pop_comments(clause.location.start);
2498
2499 let clause_doc = match &clause.guard {
2500 None => self.alternative_patterns(arena, clause),
2501 Some(guard) => self
2502 .alternative_patterns(arena, clause)
2503 .append(arena, BREAKABLE_SPACE_DOCUMENT.nest(arena, INDENT))
2504 .append(arena, IF_SPACE_DOCUMENT)
2505 .append(
2506 arena,
2507 self.clause_guard(arena, guard)
2508 .group(arena)
2509 .nest(arena, INDENT),
2510 ),
2511 };
2512
2513 // In case there's a guard or multiple subjects, if we decide to break
2514 // the patterns on multiple lines we also want the arrow to end up on
2515 // its own line to improve legibility.
2516 //
2517 // This looks like this:
2518 // ```gleam
2519 // case wibble, wobble {
2520 // Wibble(_), // pretend this goes over the line limit
2521 // Wobble(_)
2522 // -> todo
2523 // // Notice how the arrow is broken on its own line, the same goes
2524 // // for patterns with `if` guards.
2525 // }
2526 // ```
2527
2528 let has_guard = clause.guard.is_some();
2529 let has_multiple_subjects = clause.pattern.len() > 1;
2530 let arrow_break = if has_guard || has_multiple_subjects {
2531 BREAKABLE_SPACE_DOCUMENT
2532 } else {
2533 SPACE_DOCUMENT
2534 };
2535
2536 let clause_doc = docvec![arena, clause_doc, arrow_break, RIGHT_ARROW_DOCUMENT]
2537 .group(arena)
2538 .append(arena, self.case_clause_value(arena, &clause.then))
2539 .group(arena);
2540
2541 let clause_doc = match printed_comments(arena, comments, false) {
2542 Some(comments) => comments
2543 .append(arena, LINE_DOCUMENT)
2544 .append(arena, clause_doc),
2545 None => clause_doc,
2546 };
2547
2548 if index == 0 {
2549 clause_doc
2550 } else if space_before {
2551 TWO_LINES_DOCUMENT.append(arena, clause_doc)
2552 } else {
2553 LINE_DOCUMENT.append(arena, clause_doc)
2554 }
2555 }
2556
2557 fn alternative_patterns(
2558 &mut self,
2559 arena: &'doc DocumentArena<'a, 'doc>,
2560 clause: &'a UntypedClause,
2561 ) -> Document<'a, 'doc> {
2562 let has_guard = clause.guard.is_some();
2563 let has_multiple_subjects = clause.pattern.len() > 1;
2564
2565 // In case there's an `if` guard but no multiple subjects we want to add
2566 // additional indentation before the vartical bar separating alternative
2567 // patterns `|`.
2568 // We're not adding the indentation if there's multiple subjects as that
2569 // would make things harder to read, aligning the vertical bar with the
2570 // different subjects:
2571 // ```
2572 // case wibble, wobble {
2573 // Wibble,
2574 // Wobble
2575 // | Wibble, // <- we don't want this indentation!
2576 // Wobble -> todo
2577 // }
2578 // ```
2579 let alternatives_separator = if has_guard && !has_multiple_subjects {
2580 BREAKABLE_SPACE_DOCUMENT
2581 .nest(arena, INDENT)
2582 .append(arena, VERTICAL_BAR_SPACE_DOCUMENT)
2583 } else {
2584 BREAKABLE_SPACE_DOCUMENT.append(arena, VERTICAL_BAR_SPACE_DOCUMENT)
2585 };
2586
2587 let alternative_patterns = std::iter::once(&clause.pattern)
2588 .chain(&clause.alternative_patterns)
2589 .enumerate()
2590 .map(|(alternative_index, patterns)| {
2591 // Here `p` is a single pattern that can be comprised of
2592 // multiple subjects.
2593 // ```gleam
2594 // case wibble, wobble {
2595 // True, False
2596 // //^^^^^^^^^^^ This is a single pattern with multiple subjects
2597 // | _, _ -> todo
2598 // }
2599 // ```
2600 let is_first_alternative = alternative_index == 0;
2601 let pattern_docs = patterns.iter().enumerate().map(|(pattern_index, pattern)| {
2602 // There's a small catch in turning each subject into a document.
2603 // Sadly we can't simply call `self.pattern` on each subject and
2604 // then nest each one in case it gets broken.
2605 // The first ever pattern that appears in a case clause (that is
2606 // the first subject of the first alternative) must not be nested
2607 // further; otherwise, when broken, it would have 2 extra spaces
2608 // of indentation: https://github.com/gleam-lang/gleam/issues/2940.
2609 let is_first_pattern = pattern_index == 0;
2610 let is_first_pattern_of_clause = is_first_pattern && is_first_alternative;
2611 let pattern_doc = self.pattern(arena, pattern);
2612 if is_first_pattern_of_clause {
2613 pattern_doc
2614 } else {
2615 pattern_doc.nest(arena, INDENT)
2616 }
2617 });
2618 // We join all subjects with a breakable comma (that's also
2619 // going to be nested) and make the subjects into a group to
2620 // make sure the formatter tries to keep them on a single line.
2621 arena
2622 .join(pattern_docs, COMMA_BREAK_DOCUMENT.nest(arena, INDENT))
2623 .group(arena)
2624 });
2625 arena.join(alternative_patterns, alternatives_separator)
2626 }
2627
2628 fn list(
2629 &mut self,
2630 arena: &'doc DocumentArena<'a, 'doc>,
2631 elements: &'a [UntypedExpr],
2632 tail: Option<&'a UntypedExpr>,
2633 location: &SrcSpan,
2634 ) -> Document<'a, 'doc> {
2635 if elements.is_empty() {
2636 return match tail {
2637 Some(tail) => self.expr(arena, tail),
2638 // We take all comments that come _before_ the end of the list,
2639 // that is all comments that are inside "[" and "]", if there's
2640 // any comment we want to put it inside the empty list!
2641 None => {
2642 let comments = self.pop_comments(location.end);
2643 match printed_comments(arena, comments, false) {
2644 None => OPEN_CLOSE_SQUARE_DOCUMENT,
2645 Some(comments) => OPEN_SQUARE_DOCUMENT
2646 .append(arena, EMPTY_BREAK_DOCUMENT.nest(arena, INDENT))
2647 .append(arena, comments)
2648 .append(arena, EMPTY_BREAK_DOCUMENT)
2649 .append(arena, CLOSE_SQUARE_DOCUMENT)
2650 // vvv We want to make sure the comments are on a separate
2651 // line from the opening and closing brackets so we
2652 // force the breaks to be split on newlines.
2653 .force_break(arena),
2654 }
2655 }
2656 };
2657 }
2658
2659 let list_packing = self.items_sequence_packing(
2660 elements,
2661 tail,
2662 UntypedExpr::can_have_multiple_per_line,
2663 *location,
2664 );
2665
2666 let comma = match list_packing {
2667 ItemsPacking::FitMultiplePerLine => FLEX_COMMA_DOCUMENT,
2668 ItemsPacking::FitOnePerLine | ItemsPacking::BreakOnePerLine => COMMA_BREAK_DOCUMENT,
2669 };
2670
2671 let list_size = elements.len()
2672 + match tail {
2673 Some(_) => 1,
2674 None => 0,
2675 };
2676
2677 let mut is_empty = true;
2678 let mut elements_doc = EMPTY_DOCUMENT;
2679 for element in elements {
2680 let empty_lines = self.pop_empty_lines(element.location().start);
2681 let element_doc = self.comma_separated_item(arena, element, list_size);
2682
2683 elements_doc = if is_empty {
2684 is_empty = false;
2685 element_doc
2686 } else if empty_lines {
2687 // If there's empty lines before the list item we want to add an
2688 // empty line here. Notice how we're making sure no nesting is
2689 // added after the comma, otherwise we would be adding needless
2690 // whitespace in the empty line!
2691 docvec![
2692 arena,
2693 elements_doc,
2694 comma.set_nesting(arena, 0),
2695 LINE_DOCUMENT,
2696 element_doc
2697 ]
2698 } else {
2699 docvec![arena, elements_doc, comma, element_doc]
2700 };
2701 }
2702 elements_doc = elements_doc.next_break_fits(arena, NextBreakFitsMode::Disabled);
2703
2704 let doc = OPEN_SQUARE_BREAK_DOCUMENT.append(arena, elements_doc);
2705 // We need to keep the last break aside and do not add it immediately
2706 // because in case there's a final comment before the closing square
2707 // bracket we want to add indentation (to just that break). Otherwise,
2708 // the final comment would be less indented than list's elements.
2709 let (doc, last_break) = match tail {
2710 None => (doc.nest(arena, INDENT), TRAILING_COMMA_BREAK_DOCUMENT),
2711
2712 Some(tail) => {
2713 let comments = self.pop_comments(tail.location().start);
2714 let tail = commented(
2715 arena,
2716 docvec![arena, DOT_DOT_DOCUMENT, self.expr(arena, tail)],
2717 comments,
2718 );
2719 (
2720 doc.append(arena, COMMA_BREAK_DOCUMENT)
2721 .append(arena, tail)
2722 .nest(arena, INDENT),
2723 EMPTY_BREAK_DOCUMENT,
2724 )
2725 }
2726 };
2727
2728 // We get all remaining comments that come before the list's closing
2729 // square bracket.
2730 // If there's any we add those before the closing square bracket instead
2731 // of moving those out of the list.
2732 // Otherwise those would be moved out of the list.
2733 let comments = self.pop_comments(location.end);
2734 let doc = match printed_comments(arena, comments, false) {
2735 None => doc
2736 .append(arena, last_break)
2737 .append(arena, CLOSE_SQUARE_DOCUMENT),
2738 Some(comment) => doc
2739 .append(arena, last_break.nest(arena, INDENT))
2740 // ^ See how here we're adding the missing indentation to the
2741 // final break so that the final comment is as indented as the
2742 // list's items.
2743 .append(arena, comment)
2744 .append(arena, LINE_DOCUMENT)
2745 .append(arena, CLOSE_SQUARE_DOCUMENT)
2746 .force_break(arena),
2747 };
2748
2749 match list_packing {
2750 ItemsPacking::FitOnePerLine | ItemsPacking::FitMultiplePerLine => doc.group(arena),
2751 ItemsPacking::BreakOnePerLine => doc.force_break(arena),
2752 }
2753 }
2754
2755 fn items_sequence_packing<T: HasLocation>(
2756 &self,
2757 items: &'a [T],
2758 tail: Option<&'a T>,
2759 can_have_multiple_per_line: impl Fn(&'a T) -> bool,
2760 list_location: SrcSpan,
2761 ) -> ItemsPacking {
2762 let ends_with_trailing_comma = tail
2763 .map(|tail| tail.location().end)
2764 .or_else(|| items.last().map(|last| last.location().end))
2765 .is_some_and(|last_element_end| {
2766 self.has_trailing_comma(last_element_end, list_location.end)
2767 });
2768
2769 let has_multiple_elements_per_line =
2770 self.has_items_on_the_same_line(items.iter().chain(tail));
2771
2772 let has_empty_lines_between_elements = match (items.first(), items.last().or(tail)) {
2773 (Some(first), Some(last)) => self.empty_lines.first().is_some_and(|empty_line| {
2774 *empty_line >= first.location().end && *empty_line < last.location().start
2775 }),
2776 _ => false,
2777 };
2778
2779 if has_empty_lines_between_elements {
2780 // If there's any empty line between elements we want to force each
2781 // item onto its own line to preserve the empty lines that were
2782 // intentionally added.
2783 ItemsPacking::BreakOnePerLine
2784 } else if !ends_with_trailing_comma {
2785 // If the list doesn't end with a trailing comma we try and pack it in
2786 // a single line; if we can't we'll put one item per line, no matter
2787 // the content of the list.
2788 ItemsPacking::FitOnePerLine
2789 } else if tail.is_none()
2790 && items.iter().all(can_have_multiple_per_line)
2791 && has_multiple_elements_per_line
2792 && self.spans_multiple_lines(list_location.start, list_location.end)
2793 {
2794 // If there's a trailing comma, we can have multiple items per line,
2795 // and there's already multiple items per line, we try and pack as
2796 // many items as possible on each line.
2797 //
2798 // Note how we only ever try and pack lists where all items are
2799 // unbreakable primitives. To pack a list we need to use put
2800 // `flex_break`s between each item.
2801 // If the items themselves had breaks we could end up in a situation
2802 // where an item gets broken making it span multiple lines and the
2803 // spaces are not, for example:
2804 //
2805 // ```gleam
2806 // [Constructor("wibble", "lorem ipsum dolor sit amet something something"), Other(1)]
2807 // ```
2808 //
2809 // If we used flex breaks here the list would be formatted as:
2810 //
2811 // ```gleam
2812 // [
2813 // Constructor(
2814 // "wibble",
2815 // "lorem ipsum dolor sit amet something something",
2816 // ), Other(1)
2817 // ]
2818 // ```
2819 //
2820 // The first item is broken, meaning that once we get to the flex
2821 // space separating it from the following one the formatter is not
2822 // going to break it since there's enough space in the current line!
2823 ItemsPacking::FitMultiplePerLine
2824 } else {
2825 // If it ends with a trailing comma we will force the list on
2826 // multiple lines, with one item per line.
2827 ItemsPacking::BreakOnePerLine
2828 }
2829 }
2830
2831 fn has_items_on_the_same_line<L: HasLocation + 'a, T: Iterator<Item = &'a L>>(
2832 &self,
2833 items: T,
2834 ) -> bool {
2835 let mut previous: Option<SrcSpan> = None;
2836 for item in items {
2837 let item_location = item.location();
2838 // A list has multiple items on the same line if two consecutive
2839 // ones do not span multiple lines.
2840 if let Some(previous) = previous
2841 && !self.spans_multiple_lines(previous.end, item_location.start)
2842 {
2843 return true;
2844 }
2845 previous = Some(item_location);
2846 }
2847 false
2848 }
2849
2850 /// Pretty prints an expression to be used in a comma separated list; for
2851 /// example as a list item, a tuple item or as an argument of a function call.
2852 fn comma_separated_item(
2853 &mut self,
2854 arena: &'doc DocumentArena<'a, 'doc>,
2855 expression: &'a UntypedExpr,
2856 siblings: usize,
2857 ) -> Document<'a, 'doc> {
2858 // If there's more than one item in the comma separated list and there's a
2859 // pipeline or long binary chain, we want to indent those to make it
2860 // easier to tell where one item ends and the other starts.
2861 // Othewise we just print the expression as a normal expr.
2862 match expression {
2863 UntypedExpr::BinOp {
2864 operator,
2865 left,
2866 right,
2867 ..
2868 } if siblings > 1 => {
2869 let comments = self.pop_comments(expression.start_byte_index());
2870 let doc = self.bin_op(arena, operator, left, right, true).group(arena);
2871 commented(arena, doc, comments)
2872 }
2873 UntypedExpr::PipeLine { expressions } if siblings > 1 => {
2874 let comments = self.pop_comments(expression.start_byte_index());
2875 let doc = self.pipeline(arena, expressions, true).group(arena);
2876 commented(arena, doc, comments)
2877 }
2878 UntypedExpr::Int { .. }
2879 | UntypedExpr::Float { .. }
2880 | UntypedExpr::String { .. }
2881 | UntypedExpr::Block { .. }
2882 | UntypedExpr::Var { .. }
2883 | UntypedExpr::Fn { .. }
2884 | UntypedExpr::List { .. }
2885 | UntypedExpr::Call { .. }
2886 | UntypedExpr::BinOp { .. }
2887 | UntypedExpr::PipeLine { .. }
2888 | UntypedExpr::Case { .. }
2889 | UntypedExpr::FieldAccess { .. }
2890 | UntypedExpr::Tuple { .. }
2891 | UntypedExpr::TupleIndex { .. }
2892 | UntypedExpr::Todo { .. }
2893 | UntypedExpr::Panic { .. }
2894 | UntypedExpr::Echo { .. }
2895 | UntypedExpr::BitArray { .. }
2896 | UntypedExpr::RecordUpdate { .. }
2897 | UntypedExpr::NegateBool { .. }
2898 | UntypedExpr::NegateInt { .. } => self.expr(arena, expression).group(arena),
2899 }
2900 }
2901
2902 fn pattern(
2903 &mut self,
2904 arena: &'doc DocumentArena<'a, 'doc>,
2905 pattern: &'a UntypedPattern,
2906 ) -> Document<'a, 'doc> {
2907 let comments = self.pop_comments(pattern.location().start);
2908 let doc = match pattern {
2909 Pattern::Int { value, .. } => self.int(arena, value),
2910
2911 Pattern::Float { value, .. } => self.float(arena, value),
2912
2913 Pattern::String { value, .. } => self.string(arena, value),
2914
2915 Pattern::Variable { name, .. } => name.to_doc(arena),
2916
2917 Pattern::BitArraySize(size) => self.bit_array_size(arena, size),
2918
2919 Pattern::Assign { name, pattern, .. } => {
2920 if pattern.is_discard() {
2921 name.to_doc(arena)
2922 } else {
2923 self.pattern(arena, pattern)
2924 .append(arena, SPACE_AS_SPACE_DOCUMENT)
2925 .append(arena, name.as_str())
2926 }
2927 }
2928
2929 Pattern::Discard { name, .. } => name.to_doc(arena),
2930
2931 Pattern::List { elements, tail, .. } => self.list_pattern(arena, elements, tail),
2932
2933 Pattern::Constructor {
2934 name,
2935 arguments,
2936 module,
2937 spread,
2938 location,
2939 ..
2940 } => self.pattern_constructor(arena, name, arguments, module, *spread, location),
2941
2942 Pattern::Tuple {
2943 elements, location, ..
2944 } => {
2945 let arguments = elements
2946 .iter()
2947 .map(|element| self.pattern(arena, element))
2948 .collect_vec();
2949 "#".to_doc(arena)
2950 .append(arena, self.wrap_arguments(arena, arguments, location.end))
2951 .group(arena)
2952 }
2953
2954 Pattern::BitArray {
2955 segments, location, ..
2956 } => {
2957 let segment_docs = segments
2958 .iter()
2959 .map(|segment| {
2960 bit_array_segment(arena, segment, |pattern| self.pattern(arena, pattern))
2961 })
2962 .collect_vec();
2963
2964 self.bit_array(arena, segment_docs, ItemsPacking::FitOnePerLine, location)
2965 }
2966
2967 Pattern::StringPrefix {
2968 left_side_string: left,
2969 right_side_assignment: right,
2970 left_side_assignment: left_assign,
2971 ..
2972 } => {
2973 let left = self.string(arena, left);
2974 let right = match right {
2975 AssignName::Variable(name) => name.to_doc(arena),
2976 AssignName::Discard(name) => name.to_doc(arena),
2977 };
2978 match left_assign {
2979 Some((name, _)) => {
2980 docvec![
2981 arena,
2982 left,
2983 SPACE_AS_SPACE_DOCUMENT,
2984 name,
2985 SPACE_CONCAT_SPACE_DOCUMENT,
2986 right
2987 ]
2988 }
2989 None => docvec![arena, left, SPACE_CONCAT_SPACE_DOCUMENT, right],
2990 }
2991 }
2992
2993 Pattern::Invalid { .. } => panic!("invalid patterns can not be in an untyped ast"),
2994 };
2995 commented(arena, doc, comments)
2996 }
2997
2998 fn bit_array_size(
2999 &mut self,
3000 arena: &'doc DocumentArena<'a, 'doc>,
3001 size: &'a BitArraySize<()>,
3002 ) -> Document<'a, 'doc> {
3003 match size {
3004 BitArraySize::Int { value, .. } => self.int(arena, value),
3005 BitArraySize::Variable { name, .. } => name.to_doc(arena),
3006 BitArraySize::BinaryOperator {
3007 left,
3008 right,
3009 operator,
3010 ..
3011 } => {
3012 let operator = match operator {
3013 IntOperator::Add => SPACE_PLUS_SPACE_DOCUMENT,
3014 IntOperator::Subtract => SPACE_MINUS_SPACE_DOCUMENT,
3015 IntOperator::Multiply => SPACE_TIMES_SPACE_DOCUMENT,
3016 IntOperator::Divide => SPACE_SLASH_SPACE_DOCUMENT,
3017 IntOperator::Remainder => SPACE_MODULE_SPACE_DOCUMENT,
3018 };
3019
3020 docvec![
3021 arena,
3022 self.bit_array_size(arena, left),
3023 operator,
3024 self.bit_array_size(arena, right)
3025 ]
3026 }
3027 BitArraySize::Block { inner, .. } => self.bit_array_size(arena, inner).surround(
3028 arena,
3029 OPEN_CURLY_SPACE_DOCUMENT,
3030 SPACE_CLOSE_CURLY_DOCUMENT,
3031 ),
3032 }
3033 }
3034
3035 fn list_pattern(
3036 &mut self,
3037 arena: &'doc DocumentArena<'a, 'doc>,
3038 elements: &'a [UntypedPattern],
3039 tail: &'a Option<Box<UntypedTailPattern>>,
3040 ) -> Document<'a, 'doc> {
3041 if elements.is_empty() {
3042 return match tail {
3043 Some(tail) => self.pattern(arena, &tail.pattern),
3044 None => OPEN_CLOSE_SQUARE_DOCUMENT,
3045 };
3046 }
3047 let elements = arena.join(
3048 elements.iter().map(|element| self.pattern(arena, element)),
3049 COMMA_BREAK_DOCUMENT,
3050 );
3051 let doc = OPEN_SQUARE_BREAK_DOCUMENT.append(arena, elements);
3052 match tail {
3053 None => doc
3054 .nest(arena, INDENT)
3055 .append(arena, TRAILING_COMMA_BREAK_DOCUMENT),
3056
3057 Some(tail) => {
3058 let tail = &tail.pattern;
3059 let comments = self.pop_comments(tail.location().start);
3060 let tail = if tail.is_discard() {
3061 DOT_DOT_DOCUMENT
3062 } else {
3063 docvec![arena, DOT_DOT_DOCUMENT, self.pattern(arena, tail)]
3064 };
3065 let tail = commented(arena, tail, comments);
3066 doc.append(arena, COMMA_BREAK_DOCUMENT)
3067 .append(arena, tail)
3068 .nest(arena, INDENT)
3069 .append(arena, EMPTY_BREAK_DOCUMENT)
3070 }
3071 }
3072 .append(arena, "]")
3073 .group(arena)
3074 }
3075
3076 fn pattern_call_arg(
3077 &mut self,
3078 arena: &'doc DocumentArena<'a, 'doc>,
3079 argument: &'a CallArg<UntypedPattern>,
3080 ) -> Document<'a, 'doc> {
3081 self.format_call_arg(
3082 arena,
3083 argument,
3084 pattern_call_arg_formatting,
3085 |this, value| this.pattern(arena, value),
3086 )
3087 }
3088
3089 pub fn clause_guard_bin_op(
3090 &mut self,
3091 arena: &'doc DocumentArena<'a, 'doc>,
3092 name: &'a BinOp,
3093 left: &'a UntypedClauseGuard,
3094 right: &'a UntypedClauseGuard,
3095 ) -> Document<'a, 'doc> {
3096 self.clause_guard_bin_op_side(arena, name, left, left.precedence())
3097 .append(arena, BREAKABLE_SPACE_DOCUMENT)
3098 .append(arena, binop(*name))
3099 .append(arena, SPACE_DOCUMENT)
3100 .append(
3101 arena,
3102 self.clause_guard_bin_op_side(arena, name, right, right.precedence() - 1),
3103 )
3104 }
3105
3106 fn clause_guard_bin_op_side(
3107 &mut self,
3108 arena: &'doc DocumentArena<'a, 'doc>,
3109 name: &BinOp,
3110 side: &'a UntypedClauseGuard,
3111 // As opposed to `bin_op_side`, here we take the side precedence as an
3112 // argument instead of computing it ourselves. That's because
3113 // `clause_guard_bin_op` will reduce the precedence of any right side to
3114 // make sure the formatter doesn't remove any needed curly bracket.
3115 side_precedence: u8,
3116 ) -> Document<'a, 'doc> {
3117 let side_doc = self.clause_guard(arena, side);
3118 match side.bin_op_name() {
3119 // In case the other side is a binary operation as well and it can
3120 // be grouped together with the current binary operation, the two
3121 // docs are simply concatenated, so that they will end up in the
3122 // same group and the formatter will try to keep those on a single
3123 // line.
3124 Some(side_name) if side_name.can_be_grouped_with(name) => {
3125 self.operator_side(arena, side_doc, name.precedence(), side_precedence)
3126 }
3127 // In case the binary operations cannot be grouped together the
3128 // other side is treated as a group on its own so that it can be
3129 // broken independently of other pieces of the binary operations
3130 // chain.
3131 _ => self.operator_side(
3132 arena,
3133 side_doc.group(arena),
3134 name.precedence(),
3135 side_precedence,
3136 ),
3137 }
3138 }
3139
3140 fn clause_guard(
3141 &mut self,
3142 arena: &'doc DocumentArena<'a, 'doc>,
3143 clause_guard: &'a UntypedClauseGuard,
3144 ) -> Document<'a, 'doc> {
3145 match clause_guard {
3146 ClauseGuard::Invalid { .. } => unreachable!("invalid guard made it to formatting"),
3147
3148 ClauseGuard::BinaryOperator {
3149 operator,
3150 left,
3151 right,
3152 ..
3153 } => self.clause_guard_bin_op(arena, operator, left, right),
3154
3155 ClauseGuard::Var { name, .. } => name.to_doc(arena),
3156
3157 ClauseGuard::TupleIndex { tuple, index, .. } => self
3158 .clause_guard(arena, tuple)
3159 .append(arena, DOT_DOCUMENT)
3160 .append(arena, *index)
3161 .to_doc(arena),
3162
3163 ClauseGuard::FieldAccess {
3164 container, label, ..
3165 } => self
3166 .clause_guard(arena, container)
3167 .append(arena, DOT_DOCUMENT)
3168 .append(arena, label)
3169 .to_doc(arena),
3170
3171 ClauseGuard::ModuleSelect {
3172 module_name, label, ..
3173 } => module_name
3174 .to_doc(arena)
3175 .append(arena, DOT_DOCUMENT)
3176 .append(arena, label)
3177 .to_doc(arena),
3178
3179 ClauseGuard::Constant(constant) => self.const_expr(arena, constant),
3180
3181 ClauseGuard::Not { expression, .. } => {
3182 docvec![
3183 arena,
3184 EXCLAMATION_MARK_DOCUMENT,
3185 self.clause_guard(arena, expression)
3186 ]
3187 }
3188
3189 ClauseGuard::Block { value, .. } => {
3190 wrap_block(arena, self.clause_guard(arena, value)).group(arena)
3191 }
3192 }
3193 }
3194
3195 fn constant_call_arg<A>(
3196 &mut self,
3197 arena: &'doc DocumentArena<'a, 'doc>,
3198 argument: &'a CallArg<Constant<A>>,
3199 ) -> Document<'a, 'doc> {
3200 self.format_call_arg(
3201 arena,
3202 argument,
3203 constant_call_arg_formatting,
3204 |this, value| this.const_expr(arena, value),
3205 )
3206 }
3207
3208 fn negate_bool(
3209 &mut self,
3210 arena: &'doc DocumentArena<'a, 'doc>,
3211 expression: &'a UntypedExpr,
3212 ) -> Document<'a, 'doc> {
3213 match expression {
3214 UntypedExpr::NegateBool { value, .. } => self.expr(arena, value),
3215 UntypedExpr::BinOp { .. } => {
3216 let doc = self.expr(arena, expression);
3217
3218 EXCLAMATION_MARK_DOCUMENT.append(arena, wrap_block(arena, doc))
3219 }
3220 UntypedExpr::Int { .. }
3221 | UntypedExpr::Float { .. }
3222 | UntypedExpr::String { .. }
3223 | UntypedExpr::Block { .. }
3224 | UntypedExpr::Var { .. }
3225 | UntypedExpr::Fn { .. }
3226 | UntypedExpr::List { .. }
3227 | UntypedExpr::Call { .. }
3228 | UntypedExpr::PipeLine { .. }
3229 | UntypedExpr::Case { .. }
3230 | UntypedExpr::FieldAccess { .. }
3231 | UntypedExpr::Tuple { .. }
3232 | UntypedExpr::TupleIndex { .. }
3233 | UntypedExpr::Todo { .. }
3234 | UntypedExpr::Panic { .. }
3235 | UntypedExpr::Echo { .. }
3236 | UntypedExpr::BitArray { .. }
3237 | UntypedExpr::RecordUpdate { .. }
3238 | UntypedExpr::NegateInt { .. } => {
3239 docvec![
3240 arena,
3241 EXCLAMATION_MARK_DOCUMENT,
3242 self.expr(arena, expression)
3243 ]
3244 }
3245 }
3246 }
3247
3248 fn negate_int(
3249 &mut self,
3250 arena: &'doc DocumentArena<'a, 'doc>,
3251 expression: &'a UntypedExpr,
3252 ) -> Document<'a, 'doc> {
3253 match expression {
3254 UntypedExpr::NegateInt { value, .. } => self.expr(arena, value),
3255 UntypedExpr::Int { value, .. } if value.starts_with('-') => {
3256 self.int(arena, &value[1..])
3257 }
3258 UntypedExpr::BinOp { .. } => {
3259 MINUS_SPACE_DOCUMENT.append(arena, self.expr(arena, expression))
3260 }
3261
3262 UntypedExpr::Int { .. }
3263 | UntypedExpr::Float { .. }
3264 | UntypedExpr::String { .. }
3265 | UntypedExpr::Block { .. }
3266 | UntypedExpr::Var { .. }
3267 | UntypedExpr::Fn { .. }
3268 | UntypedExpr::List { .. }
3269 | UntypedExpr::Call { .. }
3270 | UntypedExpr::PipeLine { .. }
3271 | UntypedExpr::Case { .. }
3272 | UntypedExpr::FieldAccess { .. }
3273 | UntypedExpr::Tuple { .. }
3274 | UntypedExpr::TupleIndex { .. }
3275 | UntypedExpr::Todo { .. }
3276 | UntypedExpr::Panic { .. }
3277 | UntypedExpr::Echo { .. }
3278 | UntypedExpr::BitArray { .. }
3279 | UntypedExpr::RecordUpdate { .. }
3280 | UntypedExpr::NegateBool { .. } => {
3281 docvec![arena, SUB_INT_DOCUMENT, self.expr(arena, expression)]
3282 }
3283 }
3284 }
3285
3286 fn use_(
3287 &mut self,
3288 arena: &'doc DocumentArena<'a, 'doc>,
3289 use_: &'a UntypedUse,
3290 ) -> Document<'a, 'doc> {
3291 let comments = self.pop_comments(use_.location.start);
3292
3293 let call = if use_.call.is_call() {
3294 docvec![arena, SPACE_DOCUMENT, self.expr(arena, &use_.call)]
3295 } else {
3296 docvec![
3297 arena,
3298 BREAKABLE_SPACE_DOCUMENT,
3299 self.expr(arena, &use_.call)
3300 ]
3301 .nest(arena, INDENT)
3302 }
3303 .group(arena);
3304
3305 let doc = if use_.assignments.is_empty() {
3306 docvec![arena, USE_AND_ARROW_DOCUMENT, call]
3307 } else {
3308 let assignments = use_.assignments.iter().map(|use_assignment| {
3309 let pattern = self.pattern(arena, &use_assignment.pattern);
3310 let annotation = use_assignment
3311 .annotation
3312 .as_ref()
3313 .map(|annotation| {
3314 COLON_SPACE_DOCUMENT.append(arena, self.type_ast(arena, annotation))
3315 })
3316 .unwrap_or(EMPTY_DOCUMENT);
3317
3318 pattern.append(arena, annotation).group(arena)
3319 });
3320 let assignments = Itertools::intersperse(assignments, COMMA_BREAK_DOCUMENT);
3321 let left = [USE_DOCUMENT, BREAKABLE_SPACE_DOCUMENT]
3322 .into_iter()
3323 .chain(assignments);
3324 let left = arena
3325 .concat(left)
3326 .nest(arena, INDENT)
3327 .append(arena, BREAKABLE_SPACE_DOCUMENT)
3328 .group(arena);
3329 docvec![arena, left, LEFT_ARROW_DOCUMENT, call].group(arena)
3330 };
3331
3332 commented(arena, doc, comments)
3333 }
3334
3335 fn assert(
3336 &mut self,
3337 arena: &'doc DocumentArena<'a, 'doc>,
3338 assert: &'a UntypedAssert,
3339 ) -> Document<'a, 'doc> {
3340 let comments = self.pop_comments(assert.location.start);
3341
3342 let expression = if assert.value.is_binop() || assert.value.is_pipeline() {
3343 self.expr(arena, &assert.value).nest(arena, INDENT)
3344 } else {
3345 self.expr(arena, &assert.value)
3346 };
3347
3348 let doc = self.append_as_message_expression(
3349 arena,
3350 expression,
3351 PrecedingAs::Expression,
3352 assert.message.as_ref(),
3353 );
3354 commented(arena, docvec![arena, ASSERT_SPACE_DOCUMENT, doc], comments)
3355 }
3356
3357 fn bit_array(
3358 &mut self,
3359 arena: &'doc DocumentArena<'a, 'doc>,
3360 segments: Vec<Document<'a, 'doc>>,
3361 packing: ItemsPacking,
3362 location: &SrcSpan,
3363 ) -> Document<'a, 'doc> {
3364 let comments = self.pop_comments(location.end);
3365 let comments_doc = printed_comments(arena, comments, false);
3366
3367 // Avoid adding illegal comma in empty bit array by explicitly handling it
3368 if segments.is_empty() {
3369 // We take all comments that come _before_ the end of the bit array,
3370 // that is all comments that are inside "<<" and ">>", if there's
3371 // any comment we want to put it inside the empty bit array!
3372 // Refer to the `list` function for a similar procedure.
3373 return match comments_doc {
3374 None => EMPTY_BIT_ARRAY_DOCUMENT,
3375 Some(comments) => OPEN_BIT_ARRAY_DOCUMENT
3376 .append(arena, EMPTY_BREAK_DOCUMENT.nest(arena, INDENT))
3377 .append(arena, comments)
3378 .append(arena, EMPTY_BREAK_DOCUMENT)
3379 .append(arena, CLOSE_BIT_ARRAY_DOCUMENT)
3380 // vvv We want to make sure the comments are on a separate
3381 // line from the opening and closing angle brackets so
3382 // we force the breaks to be split on newlines.
3383 .force_break(arena),
3384 };
3385 }
3386
3387 let comma = match packing {
3388 ItemsPacking::FitMultiplePerLine => FLEX_COMMA_DOCUMENT,
3389 ItemsPacking::FitOnePerLine | ItemsPacking::BreakOnePerLine => COMMA_BREAK_DOCUMENT,
3390 };
3391
3392 let last_break = TRAILING_COMMA_BREAK_DOCUMENT;
3393 let doc = OPEN_BIT_ARRAY_BREAK_DOCUMENT
3394 .append(arena, arena.join(segments, comma))
3395 .nest(arena, INDENT);
3396
3397 let doc = match comments_doc {
3398 None => doc
3399 .append(arena, last_break)
3400 .append(arena, CLOSE_BIT_ARRAY_DOCUMENT),
3401 Some(comments) => doc
3402 .append(arena, last_break.nest(arena, INDENT))
3403 // ^ Notice how in this case we nest the final break before
3404 // adding it: this way the comments are going to be as
3405 // indented as the bit array items.
3406 .append(arena, comments.nest(arena, INDENT))
3407 .append(arena, LINE_DOCUMENT)
3408 .append(arena, CLOSE_BIT_ARRAY_DOCUMENT)
3409 .force_break(arena),
3410 };
3411
3412 match packing {
3413 ItemsPacking::FitOnePerLine | ItemsPacking::FitMultiplePerLine => doc.group(arena),
3414 ItemsPacking::BreakOnePerLine => doc.force_break(arena),
3415 }
3416 }
3417
3418 fn bit_array_segment_expr(
3419 &mut self,
3420 arena: &'doc DocumentArena<'a, 'doc>,
3421 expression: &'a UntypedExpr,
3422 ) -> Document<'a, 'doc> {
3423 match expression {
3424 UntypedExpr::BinOp { .. } => EMPTY_BREAK_DOCUMENT
3425 .append(arena, self.expr(arena, expression))
3426 .nest_if_broken(arena, INDENT)
3427 .append(arena, EMPTY_BREAK_DOCUMENT),
3428
3429 UntypedExpr::Int { .. }
3430 | UntypedExpr::Float { .. }
3431 | UntypedExpr::String { .. }
3432 | UntypedExpr::Var { .. }
3433 | UntypedExpr::Fn { .. }
3434 | UntypedExpr::List { .. }
3435 | UntypedExpr::Call { .. }
3436 | UntypedExpr::PipeLine { .. }
3437 | UntypedExpr::Case { .. }
3438 | UntypedExpr::FieldAccess { .. }
3439 | UntypedExpr::Tuple { .. }
3440 | UntypedExpr::TupleIndex { .. }
3441 | UntypedExpr::Todo { .. }
3442 | UntypedExpr::Panic { .. }
3443 | UntypedExpr::Echo { .. }
3444 | UntypedExpr::BitArray { .. }
3445 | UntypedExpr::RecordUpdate { .. }
3446 | UntypedExpr::NegateBool { .. }
3447 | UntypedExpr::NegateInt { .. }
3448 | UntypedExpr::Block { .. } => self.expr(arena, expression),
3449 }
3450 }
3451
3452 fn statement(
3453 &mut self,
3454 arena: &'doc DocumentArena<'a, 'doc>,
3455 statement: &'a UntypedStatement,
3456 ) -> Document<'a, 'doc> {
3457 match statement {
3458 Statement::Expression(expression) => self.expr(arena, expression),
3459 Statement::Assignment(assignment) => self.assignment(arena, assignment),
3460 Statement::Use(use_) => self.use_(arena, use_),
3461 Statement::Assert(assert) => self.assert(arena, assert),
3462 }
3463 }
3464
3465 fn block(
3466 &mut self,
3467 arena: &'doc DocumentArena<'a, 'doc>,
3468 location: &SrcSpan,
3469 statements: &'a Vec1<UntypedStatement>,
3470 force_breaks: bool,
3471 ) -> Document<'a, 'doc> {
3472 let statements_doc = docvec![
3473 arena,
3474 BREAKABLE_SPACE_DOCUMENT,
3475 self.statements(arena, statements.as_vec())
3476 ]
3477 .nest(arena, INDENT);
3478 let trailing_comments = self.pop_comments(location.end);
3479 let trailing_comments = printed_comments(arena, trailing_comments, false);
3480 let block_doc = match trailing_comments {
3481 Some(trailing_comments_doc) => docvec![
3482 arena,
3483 OPEN_CURLY_DOCUMENT,
3484 statements_doc,
3485 LINE_DOCUMENT.nest(arena, INDENT),
3486 trailing_comments_doc.nest(arena, INDENT),
3487 LINE_DOCUMENT,
3488 CLOSE_CURLY_DOCUMENT
3489 ]
3490 .force_break(arena),
3491 None => docvec![
3492 arena,
3493 OPEN_CURLY_DOCUMENT,
3494 statements_doc,
3495 BREAKABLE_SPACE_DOCUMENT,
3496 CLOSE_CURLY_DOCUMENT
3497 ],
3498 };
3499
3500 if force_breaks {
3501 block_doc.force_break(arena).group(arena)
3502 } else {
3503 block_doc.group(arena)
3504 }
3505 }
3506
3507 pub fn wrap_function_call_arguments<I>(
3508 &mut self,
3509 arena: &'doc DocumentArena<'a, 'doc>,
3510 arguments: I,
3511 location: &SrcSpan,
3512 ) -> Document<'a, 'doc>
3513 where
3514 I: IntoIterator<Item = Document<'a, 'doc>>,
3515 {
3516 let mut arguments = arguments.into_iter().peekable();
3517 if arguments.peek().is_none() {
3518 return OPEN_CLOSE_PAREN_DOCUMENT;
3519 }
3520
3521 let arguments_doc = EMPTY_BREAK_DOCUMENT
3522 .append(arena, arena.join(arguments, COMMA_BREAK_DOCUMENT))
3523 .nest_if_broken(arena, INDENT);
3524
3525 // We get all remaining comments that come before the call's closing
3526 // parenthesis.
3527 // If there's any we add those before the closing parenthesis instead
3528 // of moving those out of the call.
3529 // Otherwise those would be moved out of the call.
3530 let comments = self.pop_comments(location.end);
3531 let closing_parens = match printed_comments(arena, comments, false) {
3532 None => docvec![arena, TRAILING_COMMA_BREAK_DOCUMENT, CLOSE_PAREN_DOCUMENT],
3533 Some(comment) => docvec![
3534 arena,
3535 TRAILING_COMMA_BREAK_DOCUMENT.nest(arena, INDENT),
3536 comment,
3537 LINE_DOCUMENT,
3538 CLOSE_PAREN_DOCUMENT
3539 ]
3540 .force_break(arena),
3541 };
3542
3543 OPEN_PAREN_DOCUMENT
3544 .append(arena, arguments_doc)
3545 .append(arena, closing_parens)
3546 .group(arena)
3547 }
3548
3549 pub fn wrap_arguments<I>(
3550 &mut self,
3551 arena: &'doc DocumentArena<'a, 'doc>,
3552 arguments: I,
3553 comments_limit: u32,
3554 ) -> Document<'a, 'doc>
3555 where
3556 I: IntoIterator<Item = Document<'a, 'doc>>,
3557 {
3558 let mut arguments = arguments.into_iter().peekable();
3559 if arguments.peek().is_none() {
3560 let comments = self.pop_comments(comments_limit);
3561 return match printed_comments(arena, comments, false) {
3562 Some(comments) => OPEN_PAREN_DOCUMENT
3563 .to_doc(arena)
3564 .append(arena, EMPTY_BREAK_DOCUMENT)
3565 .append(arena, comments)
3566 .nest_if_broken(arena, INDENT)
3567 .force_break(arena)
3568 .append(arena, EMPTY_BREAK_DOCUMENT)
3569 .append(arena, CLOSE_PAREN_DOCUMENT),
3570 None => OPEN_CLOSE_PAREN_DOCUMENT,
3571 };
3572 }
3573 let doc =
3574 OPEN_PAREN_BREAK_DOCUMENT.append(arena, arena.join(arguments, COMMA_BREAK_DOCUMENT));
3575
3576 // Include trailing comments if there are any
3577 let comments = self.pop_comments(comments_limit);
3578 match printed_comments(arena, comments, false) {
3579 Some(comments) => doc
3580 .append(arena, TRAILING_COMMA_BREAK_DOCUMENT)
3581 .append(arena, comments)
3582 .nest_if_broken(arena, INDENT)
3583 .force_break(arena)
3584 .append(arena, EMPTY_BREAK_DOCUMENT)
3585 .append(arena, CLOSE_PAREN_DOCUMENT),
3586 None => doc
3587 .nest_if_broken(arena, INDENT)
3588 .append(arena, TRAILING_COMMA_BREAK_DOCUMENT)
3589 .append(arena, CLOSE_PAREN_DOCUMENT),
3590 }
3591 }
3592
3593 pub fn wrap_arguments_with_spread<I>(
3594 &mut self,
3595 arena: &'doc DocumentArena<'a, 'doc>,
3596 arguments: I,
3597 comments_limit: u32,
3598 ) -> Document<'a, 'doc>
3599 where
3600 I: IntoIterator<Item = Document<'a, 'doc>>,
3601 {
3602 let mut arguments = arguments.into_iter().peekable();
3603 if arguments.peek().is_none() {
3604 return self.wrap_arguments(arena, arguments, comments_limit);
3605 }
3606 let doc = OPEN_PAREN_BREAK_DOCUMENT
3607 .append(arena, arena.join(arguments, COMMA_BREAK_DOCUMENT))
3608 .append(arena, COMMA_BREAK_DOCUMENT)
3609 .append(arena, DOT_DOT_DOCUMENT);
3610
3611 // Include trailing comments if there are any
3612 let comments = self.pop_comments(comments_limit);
3613 match printed_comments(arena, comments, false) {
3614 Some(comments) => doc
3615 .append(arena, TRAILING_COMMA_BREAK_DOCUMENT)
3616 .append(arena, comments)
3617 .nest_if_broken(arena, INDENT)
3618 .force_break(arena)
3619 .append(arena, EMPTY_BREAK_DOCUMENT)
3620 .append(arena, CLOSE_PAREN_DOCUMENT),
3621 None => doc
3622 .nest_if_broken(arena, INDENT)
3623 .append(arena, TRAILING_COMMA_BREAK_DOCUMENT)
3624 .append(arena, CLOSE_PAREN_DOCUMENT),
3625 }
3626 }
3627
3628 /// Given some regular comments it pretty prints those with any respective
3629 /// doc comment that might be preceding those.
3630 /// For example:
3631 ///
3632 /// ```gleam
3633 /// /// Doc
3634 /// // comment
3635 ///
3636 /// /// Doc
3637 /// pub fn wibble() {}
3638 /// ```
3639 ///
3640 /// We don't want the first doc comment to be merged together with
3641 /// `wibble`'s doc comment, so when we run into comments like `// comment`
3642 /// we need to first print all documentation comments that come before it.
3643 ///
3644 fn printed_documented_comments<'b>(
3645 &mut self,
3646 arena: &'doc DocumentArena<'a, 'doc>,
3647 comments: impl IntoIterator<Item = (u32, Option<&'b str>)>,
3648 ) -> Option<Document<'a, 'doc>> {
3649 let mut comments = comments.into_iter().peekable();
3650 let _ = comments.peek()?;
3651
3652 let mut doc = Vec::new();
3653 while let Some(comment) = comments.next() {
3654 let (is_doc_commented, comment) = match comment {
3655 (comment_start, Some(c)) => {
3656 let doc_comment = self.doc_comments(arena, comment_start);
3657 let is_doc_commented = !doc_comment.is_empty();
3658 doc.push(doc_comment);
3659 (is_doc_commented, c)
3660 }
3661 (_, None) => continue,
3662 };
3663 doc.push(
3664 COMMENT_DOCUMENT.append(arena, arena.zero_width_string(EcoString::from(comment))),
3665 );
3666 match comments.peek() {
3667 // Next line is a comment
3668 Some((_, Some(_))) => doc.push(LINE_DOCUMENT),
3669 // Next line is empty
3670 Some((_, None)) => {
3671 let _ = comments.next();
3672 doc.push(TWO_LINES_DOCUMENT);
3673 }
3674 // We've reached the end, there are no more lines
3675 None => {
3676 if is_doc_commented {
3677 doc.push(TWO_LINES_DOCUMENT);
3678 } else {
3679 doc.push(LINE_DOCUMENT);
3680 }
3681 }
3682 }
3683 }
3684 let doc = arena.concat(doc);
3685 Some(doc.force_break(arena))
3686 }
3687
3688 fn append_as_message_expression(
3689 &mut self,
3690 arena: &'doc DocumentArena<'a, 'doc>,
3691 doc: Document<'a, 'doc>,
3692 preceding_as: PrecedingAs,
3693 message: Option<&'a UntypedExpr>,
3694 ) -> Document<'a, 'doc> {
3695 let Some(message) = message else { return doc };
3696
3697 let comments = self.pop_comments(message.location().start);
3698 let comments = printed_comments(arena, comments, false);
3699
3700 let as_ = match preceding_as {
3701 PrecedingAs::Keyword => SPACE_AS_DOCUMENT,
3702 PrecedingAs::Expression => {
3703 docvec![arena, BREAKABLE_SPACE_DOCUMENT, AS_DOCUMENT].nest(arena, INDENT)
3704 }
3705 };
3706
3707 let doc = match comments {
3708 // If there's comments between the document and the message we want
3709 // the `as` bit to be on the same line as the original document and
3710 // go on a new indented line with the message and comments:
3711 // ```gleam
3712 // todo as
3713 // // comment!
3714 // "wibble"
3715 // ```
3716 Some(comments) => docvec![
3717 arena,
3718 doc.group(arena),
3719 as_,
3720 docvec![
3721 arena,
3722 LINE_DOCUMENT,
3723 comments,
3724 LINE_DOCUMENT,
3725 self.expr(arena, message).group(arena)
3726 ]
3727 .nest(arena, INDENT)
3728 ],
3729
3730 None => {
3731 let message = match (preceding_as, message) {
3732 // If we have `as` preceded by a keyword (like with `panic` and `todo`)
3733 // and the message is a block, we don't want to nest it any further. That is,
3734 // we want it to look like this:
3735 // ```gleam
3736 // panic as {
3737 // wibble wobble
3738 // }
3739 // ```
3740 // instead of this:
3741 // ```gleam
3742 // panic as {
3743 // wibble wobble
3744 // }
3745 // ```
3746 (PrecedingAs::Keyword, UntypedExpr::Block { .. }) => {
3747 self.expr(arena, message).group(arena)
3748 }
3749 _ => self.expr(arena, message).group(arena).nest(arena, INDENT),
3750 };
3751 docvec![arena, doc.group(arena), as_, SPACE_DOCUMENT, message]
3752 }
3753 };
3754
3755 doc.group(arena)
3756 }
3757
3758 fn append_as_message_constant<A>(
3759 &mut self,
3760 arena: &'doc DocumentArena<'a, 'doc>,
3761 doc: Document<'a, 'doc>,
3762 message: Option<&'a Constant<A>>,
3763 ) -> Document<'a, 'doc> {
3764 let Some(message) = message else { return doc };
3765
3766 let comments = self.pop_comments(message.location().start);
3767 let comments = printed_comments(arena, comments, false);
3768
3769 let doc = match comments {
3770 // If there's comments between the document and the message we want
3771 // the `as` bit to be on the same line as the original document and
3772 // go on a new indented line with the message and comments:
3773 // ```gleam
3774 // todo as
3775 // // comment!
3776 // "wibble"
3777 // ```
3778 Some(comments) => docvec![
3779 arena,
3780 doc.group(arena),
3781 SPACE_AS_DOCUMENT,
3782 docvec![
3783 arena,
3784 LINE_DOCUMENT,
3785 comments,
3786 LINE_DOCUMENT,
3787 self.const_expr(arena, message).group(arena)
3788 ]
3789 .nest(arena, INDENT)
3790 ],
3791
3792 None => {
3793 let message = self
3794 .const_expr(arena, message)
3795 .group(arena)
3796 .nest(arena, INDENT);
3797 docvec![arena, doc.group(arena), SPACE_AS_SPACE_DOCUMENT, message]
3798 }
3799 };
3800
3801 doc.group(arena)
3802 }
3803
3804 fn echo(
3805 &mut self,
3806 arena: &'doc DocumentArena<'a, 'doc>,
3807 expression: &'a Option<Box<UntypedExpr>>,
3808 message: &'a Option<Box<UntypedExpr>>,
3809 ) -> Document<'a, 'doc> {
3810 let Some(expression) = expression else {
3811 return self.append_as_message_expression(
3812 arena,
3813 ECHO_DOCUMENT,
3814 PrecedingAs::Keyword,
3815 message.as_deref(),
3816 );
3817 };
3818
3819 // When a binary expression gets broken on multiple lines we don't want
3820 // it to be on the same line as echo, or it would look confusing;
3821 // instead it's nested onto a new line:
3822 //
3823 // ```gleam
3824 // echo first
3825 // |> wobble
3826 // |> wibble
3827 // ```
3828 //
3829 // So it's easier to see echo is printing the whole thing. Otherwise,
3830 // it would look like echo is printing just the first item:
3831 //
3832 // ```gleam
3833 // echo first
3834 // |> wobble
3835 // |> wibble
3836 // ```
3837 //
3838 let doc = self.expr(arena, expression);
3839 if expression.is_binop() || expression.is_pipeline() {
3840 let doc = self.append_as_message_expression(
3841 arena,
3842 doc.nest(arena, INDENT),
3843 PrecedingAs::Expression,
3844 message.as_deref(),
3845 );
3846 docvec![arena, ECHO_SPACE_DOCUMENT, doc]
3847 } else {
3848 docvec![
3849 arena,
3850 ECHO_SPACE_DOCUMENT,
3851 self.append_as_message_expression(
3852 arena,
3853 doc,
3854 PrecedingAs::Expression,
3855 message.as_deref()
3856 )
3857 ]
3858 }
3859 }
3860}
3861
3862/// This is used to describe the kind of things that might preceding an `as`
3863/// message that can be added to various places: `panic`, `echo`, `let assert`,
3864/// `assert`, `todo`.
3865///
3866/// It might be preceded by a keyword, like with `echo` and `panic`, or by
3867/// an expression, like in `assert` or `let assert`.
3868///
3869enum PrecedingAs {
3870 /// An expression is preceding the `as` message:
3871 /// ```gleam
3872 /// echo 1 as "message"
3873 /// assert 1 == 2 as "message"
3874 /// let assert Ok(_) = result as "message"
3875 /// ```
3876 ///
3877 Expression,
3878
3879 /// A keyword is preceding the `as` message:
3880 /// ```gleam
3881 /// 1 |> echo as "message"
3882 /// panic as "message"
3883 /// todo as "message"
3884 /// ```
3885 ///
3886 Keyword,
3887}
3888
3889fn init_and_last<T>(vec: &[T]) -> Option<(&[T], &T)> {
3890 match vec {
3891 [] => None,
3892 _ => match vec.split_at(vec.len() - 1) {
3893 (init, [last]) => Some((init, last)),
3894 _ => panic!("unreachable"),
3895 },
3896 }
3897}
3898
3899fn binop<'a, 'doc>(binop: BinOp) -> Document<'a, 'doc> {
3900 match binop {
3901 BinOp::And => AND_DOCUMENT,
3902 BinOp::Or => OR_DOCUMENT,
3903 BinOp::LtInt => LT_INT_DOCUMENT,
3904 BinOp::LtEqInt => LT_EQ_INT_DOCUMENT,
3905 BinOp::LtFloat => LT_FLOAT_DOCUMENT,
3906 BinOp::LtEqFloat => LT_EQ_FLOAT_DOCUMENT,
3907 BinOp::Eq => EQ_DOCUMENT,
3908 BinOp::NotEq => NOT_EQ_DOCUMENT,
3909 BinOp::GtEqInt => GT_EQ_INT_DOCUMENT,
3910 BinOp::GtInt => GT_INT_DOCUMENT,
3911 BinOp::GtEqFloat => GT_EQ_FLOAT_DOCUMENT,
3912 BinOp::GtFloat => GT_FLOAT_DOCUMENT,
3913 BinOp::AddInt => ADD_INT_DOCUMENT,
3914 BinOp::AddFloat => ADD_FLOAT_DOCUMENT,
3915 BinOp::SubInt => SUB_INT_DOCUMENT,
3916 BinOp::SubFloat => SUB_FLOAT_DOCUMENT,
3917 BinOp::MultInt => MULT_INT_DOCUMENT,
3918 BinOp::MultFloat => MULT_FLOAT_DOCUMENT,
3919 BinOp::DivInt => DIV_INT_DOCUMENT,
3920 BinOp::DivFloat => DIV_FLOAT_DOCUMENT,
3921 BinOp::RemainderInt => REMAINDER_INT_DOCUMENT,
3922 BinOp::Concatenate => CONCAT_DOCUMENT,
3923 }
3924}
3925
3926#[allow(clippy::enum_variant_names)]
3927#[derive(Debug)]
3928/// This is used to determine how to fit the items of a list, or the segments of
3929/// a bit array in a line.
3930///
3931enum ItemsPacking {
3932 /// Try and fit everything on a single line; if the items don't fit, break
3933 /// the list putting each item into its own line.
3934 ///
3935 /// ```gleam
3936 /// // unbroken
3937 /// [1, 2, 3]
3938 ///
3939 /// // broken
3940 /// [
3941 /// 1,
3942 /// 2,
3943 /// 3,
3944 /// ]
3945 /// ```
3946 ///
3947 FitOnePerLine,
3948
3949 /// Try and fit everything on a single line; if the items don't fit, break
3950 /// the list putting as many items as possible in a single line.
3951 ///
3952 /// ```gleam
3953 /// // unbroken
3954 /// [1, 2, 3]
3955 ///
3956 /// // broken
3957 /// [
3958 /// 1, 2, 3, ...
3959 /// 4, 100,
3960 /// ]
3961 /// ```
3962 ///
3963 FitMultiplePerLine,
3964
3965 /// Always break the list, putting each item into its own line:
3966 ///
3967 /// ```gleam
3968 /// [
3969 /// 1,
3970 /// 2,
3971 /// 3,
3972 /// ]
3973 /// ```
3974 ///
3975 BreakOnePerLine,
3976}
3977
3978pub fn comments_before<'a>(
3979 comments: &'a [Comment<'a>],
3980 empty_lines: &'a [u32],
3981 limit: u32,
3982 retain_empty_lines: bool,
3983) -> (
3984 impl Iterator<Item = (u32, Option<&'a str>)>,
3985 &'a [Comment<'a>],
3986 &'a [u32],
3987) {
3988 let end_comments = comments
3989 .iter()
3990 .position(|comment| comment.start > limit)
3991 .unwrap_or(comments.len());
3992 let end_empty_lines = empty_lines
3993 .iter()
3994 .position(|empty_line| *empty_line > limit)
3995 .unwrap_or(empty_lines.len());
3996 let popped_comments = comments
3997 .get(0..end_comments)
3998 .expect("0..end_comments is guaranteed to be in bounds")
3999 .iter()
4000 .map(|comment| (comment.start, Some(comment.content)));
4001 let popped_empty_lines = if retain_empty_lines { empty_lines } else { &[] }
4002 .get(0..end_empty_lines)
4003 .unwrap_or(&[])
4004 .iter()
4005 .map(|i| (i, i))
4006 // compact consecutive empty lines into a single line
4007 .coalesce(|(a_start, a_end), (b_start, b_end)| {
4008 if *a_end + 1 == *b_start {
4009 Ok((a_start, b_end))
4010 } else {
4011 Err(((a_start, a_end), (b_start, b_end)))
4012 }
4013 })
4014 .map(|l| (*l.0, None));
4015 let popped = popped_comments
4016 .merge_by(popped_empty_lines, |(a, _), (b, _)| a < b)
4017 .skip_while(|(_, comment_or_line)| comment_or_line.is_none());
4018 (
4019 popped,
4020 comments.get(end_comments..).expect("in bounds"),
4021 empty_lines.get(end_empty_lines..).expect("in bounds"),
4022 )
4023}
4024
4025fn is_breakable_argument(expression: &UntypedExpr, arity: usize) -> bool {
4026 match expression {
4027 // A call is only breakable if it is the only argument
4028 UntypedExpr::Call { .. } => arity == 1,
4029
4030 UntypedExpr::Fn { .. }
4031 | UntypedExpr::Block { .. }
4032 | UntypedExpr::Case { .. }
4033 | UntypedExpr::List { .. }
4034 | UntypedExpr::Tuple { .. }
4035 | UntypedExpr::BitArray { .. } => true,
4036
4037 UntypedExpr::Int { .. }
4038 | UntypedExpr::Float { .. }
4039 | UntypedExpr::String { .. }
4040 | UntypedExpr::Var { .. }
4041 | UntypedExpr::BinOp { .. }
4042 | UntypedExpr::PipeLine { .. }
4043 | UntypedExpr::FieldAccess { .. }
4044 | UntypedExpr::TupleIndex { .. }
4045 | UntypedExpr::Todo { .. }
4046 | UntypedExpr::Panic { .. }
4047 | UntypedExpr::Echo { .. }
4048 | UntypedExpr::RecordUpdate { .. }
4049 | UntypedExpr::NegateBool { .. }
4050 | UntypedExpr::NegateInt { .. } => false,
4051 }
4052}
4053
4054enum CallArgFormatting<'a, A> {
4055 ShorthandLabelled(&'a EcoString),
4056 Unlabelled(&'a A),
4057 Labelled(&'a EcoString, &'a A),
4058}
4059
4060fn expr_call_arg_formatting(argument: &CallArg<UntypedExpr>) -> CallArgFormatting<'_, UntypedExpr> {
4061 match argument {
4062 // An argument supplied using label shorthand syntax.
4063 _ if argument.uses_label_shorthand() => CallArgFormatting::ShorthandLabelled(
4064 argument
4065 .label
4066 .as_ref()
4067 .expect("label shorthand with no label"),
4068 ),
4069 // A labelled argument.
4070 CallArg {
4071 label: Some(label),
4072 value,
4073 ..
4074 } => CallArgFormatting::Labelled(label, value),
4075 // An unlabelled argument.
4076 CallArg { value, .. } => CallArgFormatting::Unlabelled(value),
4077 }
4078}
4079
4080fn pattern_call_arg_formatting(
4081 argument: &CallArg<UntypedPattern>,
4082) -> CallArgFormatting<'_, UntypedPattern> {
4083 match argument {
4084 // An argument supplied using label shorthand syntax.
4085 _ if argument.uses_label_shorthand() => CallArgFormatting::ShorthandLabelled(
4086 argument
4087 .label
4088 .as_ref()
4089 .expect("label shorthand with no label"),
4090 ),
4091 // A labelled argument.
4092 CallArg {
4093 label: Some(label),
4094 value,
4095 ..
4096 } => CallArgFormatting::Labelled(label, value),
4097 // An unlabelled argument.
4098 CallArg { value, .. } => CallArgFormatting::Unlabelled(value),
4099 }
4100}
4101
4102fn constant_call_arg_formatting<A>(
4103 argument: &CallArg<Constant<A>>,
4104) -> CallArgFormatting<'_, Constant<A>> {
4105 match argument {
4106 // An argument supplied using label shorthand syntax.
4107 _ if argument.uses_label_shorthand() => CallArgFormatting::ShorthandLabelled(
4108 argument
4109 .label
4110 .as_ref()
4111 .expect("label shorthand with no label"),
4112 ),
4113 // A labelled argument.
4114 CallArg {
4115 label: Some(label),
4116 value,
4117 ..
4118 } => CallArgFormatting::Labelled(label, value),
4119 // An unlabelled argument.
4120 CallArg { value, .. } => CallArgFormatting::Unlabelled(value),
4121 }
4122}
4123
4124struct AttributesPrinter<'a> {
4125 external_erlang: &'a Option<(EcoString, EcoString, SrcSpan)>,
4126 external_javascript: &'a Option<(EcoString, EcoString, SrcSpan)>,
4127 deprecation: &'a Deprecation,
4128 internal: bool,
4129}
4130
4131impl<'a> AttributesPrinter<'a> {
4132 pub fn new() -> Self {
4133 Self {
4134 external_erlang: &None,
4135 external_javascript: &None,
4136 deprecation: &Deprecation::NotDeprecated,
4137 internal: false,
4138 }
4139 }
4140
4141 pub fn set_external_erlang(
4142 mut self,
4143 external: &'a Option<(EcoString, EcoString, SrcSpan)>,
4144 ) -> Self {
4145 self.external_erlang = external;
4146 self
4147 }
4148
4149 pub fn set_external_javascript(
4150 mut self,
4151 external: &'a Option<(EcoString, EcoString, SrcSpan)>,
4152 ) -> Self {
4153 self.external_javascript = external;
4154 self
4155 }
4156
4157 pub fn set_internal(mut self, publicity: Publicity) -> Self {
4158 self.internal = publicity.is_internal();
4159 self
4160 }
4161
4162 pub fn set_deprecation(mut self, deprecation: &'a Deprecation) -> Self {
4163 self.deprecation = deprecation;
4164 self
4165 }
4166}
4167
4168impl<'a, 'doc> AttributesPrinter<'a> {
4169 fn to_doc(&self, arena: &'doc DocumentArena<'a, 'doc>) -> Document<'a, 'doc> {
4170 let mut attributes = vec![];
4171
4172 // @deprecated attribute
4173 if let Deprecation::Deprecated { message } = self.deprecation {
4174 attributes.push(docvec![
4175 arena,
4176 DEPRECATED_ATTRIBUTE_QUOTE_DOCUMENT,
4177 message,
4178 QUOTE_CLOSE_PAREN_DOCUMENT
4179 ])
4180 };
4181
4182 // @external attributes
4183 if let Some((module, function, _)) = self.external_erlang {
4184 attributes.push(docvec![
4185 arena,
4186 EXTERNAL_ERLANG_QUOTE_DOCUMENT,
4187 module,
4188 QUOTE_COMMA_SPACE_QUOTE_DOCUMENT,
4189 function,
4190 QUOTE_CLOSE_PAREN_DOCUMENT,
4191 ])
4192 };
4193
4194 if let Some((module, function, _)) = self.external_javascript {
4195 attributes.push(docvec![
4196 arena,
4197 EXTERNAL_JAVASCRIPT_QUOTE_DOCUMENT,
4198 module,
4199 QUOTE_COMMA_SPACE_QUOTE_DOCUMENT,
4200 function,
4201 QUOTE_CLOSE_PAREN_DOCUMENT
4202 ])
4203 };
4204
4205 // @internal attribute
4206 if self.internal {
4207 attributes.push(INTERNAL_ATTRIBUTE_DOCUMENT);
4208 };
4209
4210 if attributes.is_empty() {
4211 EMPTY_DOCUMENT
4212 } else {
4213 arena
4214 .join(attributes, arena.line())
4215 .append(arena, arena.line())
4216 }
4217 }
4218}
4219
4220pub fn break_block<'a, 'doc>(
4221 arena: &'doc DocumentArena<'a, 'doc>,
4222 doc: Document<'a, 'doc>,
4223) -> Document<'a, 'doc> {
4224 OPEN_CURLY_DOCUMENT
4225 .append(arena, LINE_DOCUMENT.append(arena, doc).nest(arena, INDENT))
4226 .append(arena, LINE_DOCUMENT)
4227 .append(arena, CLOSE_CURLY_DOCUMENT)
4228 .force_break(arena)
4229}
4230
4231pub fn wrap_block<'a, 'doc>(
4232 arena: &'doc DocumentArena<'a, 'doc>,
4233 doc: Document<'a, 'doc>,
4234) -> Document<'a, 'doc> {
4235 OPEN_CURLY_BREAK_DOCUMENT
4236 .append(arena, doc)
4237 .nest(arena, INDENT)
4238 .append(arena, BREAKABLE_SPACE_DOCUMENT)
4239 .append(arena, CLOSE_CURLY_DOCUMENT)
4240}
4241
4242fn printed_comments<'a, 'doc>(
4243 arena: &'doc DocumentArena<'a, 'doc>,
4244 comments: impl IntoIterator<Item = Option<&'a str>>,
4245 trailing_newline: bool,
4246) -> Option<Document<'a, 'doc>> {
4247 let mut comments = comments.into_iter().peekable();
4248 let _ = comments.peek()?;
4249
4250 let mut doc = Vec::new();
4251 while let Some(comment) = comments.next() {
4252 let Some(comment) = comment else { continue };
4253
4254 // The comment is turned into a zero width string rather than a regular
4255 // string document: comment lines are never touched by the formatter and
4256 // we don't need to know how long each one is.
4257 // So we can do this to avoid counting the graphemes of each one, which
4258 // is a lot of wasted work.
4259 let comment = arena.zero_width_str(comment);
4260
4261 doc.push(COMMENT_DOCUMENT.append(arena, comment));
4262 match comments.peek() {
4263 // Next line is a comment
4264 Some(Some(_)) => doc.push(LINE_DOCUMENT),
4265 // Next line is empty
4266 Some(None) => {
4267 let _ = comments.next();
4268 match comments.peek() {
4269 Some(_) => doc.push(TWO_LINES_DOCUMENT),
4270 None => {
4271 if trailing_newline {
4272 doc.push(TWO_LINES_DOCUMENT);
4273 }
4274 }
4275 }
4276 }
4277 // We've reached the end, there are no more lines
4278 None => {
4279 if trailing_newline {
4280 doc.push(LINE_DOCUMENT);
4281 }
4282 }
4283 }
4284 }
4285 let doc = arena.concat(doc);
4286 if trailing_newline {
4287 Some(doc.force_break(arena))
4288 } else {
4289 Some(doc)
4290 }
4291}
4292
4293fn commented<'a, 'doc>(
4294 arena: &'doc DocumentArena<'a, 'doc>,
4295 doc: Document<'a, 'doc>,
4296 comments: impl IntoIterator<Item = Option<&'a str>>,
4297) -> Document<'a, 'doc> {
4298 match printed_comments(arena, comments, true) {
4299 Some(comments) => comments.append(arena, doc.group(arena)),
4300 None => doc,
4301 }
4302}
4303
4304fn bit_array_segment<'a, 'doc, Value, Type, ToDoc>(
4305 arena: &'doc DocumentArena<'a, 'doc>,
4306 segment: &'a BitArraySegment<Value, Type>,
4307 mut to_doc: ToDoc,
4308) -> Document<'a, 'doc>
4309where
4310 ToDoc: FnMut(&'a Value) -> Document<'a, 'doc>,
4311{
4312 match segment {
4313 BitArraySegment { value, options, .. } if options.is_empty() => to_doc(value),
4314
4315 BitArraySegment { value, options, .. } => {
4316 to_doc(value).append(arena, COLON_DOCUMENT).append(
4317 arena,
4318 arena.join(
4319 options
4320 .iter()
4321 .map(|option| segment_option(arena, option, &mut to_doc)),
4322 SUB_INT_DOCUMENT,
4323 ),
4324 )
4325 }
4326 }
4327}
4328
4329fn segment_option<'a, 'doc, ToDoc, Value>(
4330 arena: &'doc DocumentArena<'a, 'doc>,
4331 option: &'a BitArrayOption<Value>,
4332 mut to_doc: ToDoc,
4333) -> Document<'a, 'doc>
4334where
4335 ToDoc: FnMut(&'a Value) -> Document<'a, 'doc>,
4336{
4337 match option {
4338 BitArrayOption::Bytes { .. } => BYTES_DOCUMENT,
4339 BitArrayOption::Bits { .. } => BITS_DOCUMENT,
4340 BitArrayOption::Int { .. } => INT_DOCUMENT,
4341 BitArrayOption::Float { .. } => FLOAT_DOCUMENT,
4342 BitArrayOption::Utf8 { .. } => UTF8_DOCUMENT,
4343 BitArrayOption::Utf16 { .. } => UTF16_DOCUMENT,
4344 BitArrayOption::Utf32 { .. } => UTF32_DOCUMENT,
4345 BitArrayOption::Utf8Codepoint { .. } => UTF8_CODEPOINT_DOCUMENT,
4346 BitArrayOption::Utf16Codepoint { .. } => UTF16_CODEPOINT_DOCUMENT,
4347 BitArrayOption::Utf32Codepoint { .. } => UTF32_CODEPOINT_DOCUMENT,
4348 BitArrayOption::Signed { .. } => SIGNED_DOCUMENT,
4349 BitArrayOption::Unsigned { .. } => UNSIGNED_DOCUMENT,
4350 BitArrayOption::Big { .. } => BIG_DOCUMENT,
4351 BitArrayOption::Little { .. } => LITTLE_DOCUMENT,
4352 BitArrayOption::Native { .. } => NATIVE_DOCUMENT,
4353
4354 BitArrayOption::Size {
4355 value,
4356 short_form: false,
4357 ..
4358 } => SIZE_DOCUMENT
4359 .append(arena, OPEN_PAREN_DOCUMENT)
4360 .append(arena, to_doc(value))
4361 .append(arena, CLOSE_PAREN_DOCUMENT),
4362
4363 BitArrayOption::Size {
4364 value,
4365 short_form: true,
4366 ..
4367 } => to_doc(value),
4368
4369 BitArrayOption::Unit { value, .. } => UNIT_DOCUMENT
4370 .append(arena, OPEN_PAREN_DOCUMENT)
4371 .append(arena, eco_format!("{value}"))
4372 .append(arena, CLOSE_PAREN_DOCUMENT),
4373 }
4374}
4375
4376fn pub_<'a, 'doc>(publicity: Publicity) -> Document<'a, 'doc> {
4377 match publicity {
4378 Publicity::Public | Publicity::Internal { .. } => PUB_SPACE_DOCUMENT,
4379 Publicity::Private => EMPTY_DOCUMENT,
4380 }
4381}