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