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