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