Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

Select the types of activity you want to include in your feed.

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