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

Configure Feed

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

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