// SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2020 The Gleam contributors #[cfg(test)] mod tests; use crate::{ Error, Result, ast::{ CustomType, Import, ModuleConstant, TypeAlias, TypeAstConstructor, TypeAstFn, TypeAstHole, TypeAstTuple, TypeAstVar, *, }, build::Target, io::Utf8Writer, parse::extra::{Comment, ModuleExtra}, warning::WarningEmitter, }; use ecow::{EcoString, eco_format}; use itertools::Itertools; use pretty_arena::*; use std::cmp::Ordering; use vec1::Vec1; use crate::type_::Deprecation; use camino::Utf8Path; const INDENT: isize = 2; pub fn pretty(writer: &mut impl Utf8Writer, src: &EcoString, path: &Utf8Path) -> Result<()> { let parsed = crate::parse::parse_module(path.to_owned(), src, &WarningEmitter::null()) .map_err(|error| Error::Parse { path: path.to_path_buf(), src: src.clone(), error: Box::new(error), })?; let intermediate = Intermediate::from_extra(&parsed.extra, src); let arena = DocumentArena::new(); Formatter::with_comments(&intermediate) .module(&arena, &parsed.module) .pretty_print(80, writer) .map_err(|error| writer.convert_err(error)) } pub(crate) struct Intermediate<'a> { comments: Vec>, doc_comments: Vec>, module_comments: Vec>, empty_lines: &'a [u32], new_lines: &'a [u32], trailing_commas: &'a [u32], } impl<'a> Intermediate<'a> { pub fn from_extra(extra: &'a ModuleExtra, src: &'a EcoString) -> Intermediate<'a> { Intermediate { comments: extra .comments .iter() .map(|span| Comment::from((span, src))) .collect(), doc_comments: extra .doc_comments .iter() .map(|span| Comment::from((span, src))) .collect(), empty_lines: &extra.empty_lines, module_comments: extra .module_comments .iter() .map(|span| Comment::from((span, src))) .collect(), new_lines: &extra.new_lines, trailing_commas: &extra.trailing_commas, } } } #[derive(Debug)] enum FnCapturePosition { RightHandSideOfPipe, EverywhereElse, } #[derive(Debug)] /// One of the pieces making a record update arg list: it could be the starting /// record being updated, or one of the subsequent arguments. /// enum RecordUpdatePiece<'a, A> { Record(&'a RecordBeingUpdated), Argument(&'a RecordUpdateArg), } impl HasLocation for RecordUpdatePiece<'_, A> { fn location(&self) -> SrcSpan { match self { RecordUpdatePiece::Record(record) => record.location, RecordUpdatePiece::Argument(arg) => arg.location, } } } type UntypedRecordUpdatePiece<'a> = RecordUpdatePiece<'a, UntypedExpr>; /// Hayleigh's bane #[derive(Debug)] pub struct Formatter<'a> { comments: &'a [Comment<'a>], doc_comments: &'a [Comment<'a>], module_comments: &'a [Comment<'a>], empty_lines: &'a [u32], new_lines: &'a [u32], trailing_commas: &'a [u32], } impl<'a, 'doc> Formatter<'a> { pub(crate) fn with_comments(extra: &'a Intermediate<'a>) -> Self { Self { comments: &extra.comments, doc_comments: &extra.doc_comments, module_comments: &extra.module_comments, empty_lines: extra.empty_lines, new_lines: extra.new_lines, trailing_commas: extra.trailing_commas, } } /// Returns true if there's any comment that comes before the given /// position. /// fn any_comments(&self, limit: u32) -> bool { self.comments .first() .is_some_and(|comment| comment.start < limit) } /// Returns true if there's any comment that appears inside the given span. /// fn any_comment_between(&self, start: u32, end: u32) -> bool { self.comments .binary_search_by(|comment| { if comment.start < start { Ordering::Less } else if comment.start > end { Ordering::Greater } else { Ordering::Equal } }) .is_ok() } fn any_empty_lines(&self, limit: u32) -> bool { self.empty_lines.first().is_some_and(|line| *line < limit) } /// Pop comments that occur before a byte-index in the source, consuming /// and retaining any empty lines contained within. /// Returns an iterator of comments with their start position. fn pop_comments_with_position( &mut self, limit: u32, ) -> impl Iterator)> + use<'a> { let (popped, rest, empty_lines) = comments_before(self.comments, self.empty_lines, limit, true); self.comments = rest; self.empty_lines = empty_lines; popped } /// Pop comments that occur before a byte-index in the source, consuming /// and retaining any empty lines contained within. fn pop_comments(&mut self, limit: u32) -> impl Iterator> + use<'a> { self.pop_comments_with_position(limit) .map(|(_position, comment)| comment) } /// Pop doc comments that occur before a byte-index in the source, consuming /// and dropping any empty lines contained within. fn pop_doc_comments(&mut self, limit: u32) -> impl Iterator> + use<'a> { let (popped, rest, empty_lines) = comments_before(self.doc_comments, self.empty_lines, limit, false); self.doc_comments = rest; self.empty_lines = empty_lines; popped.map(|(_position, comment)| comment) } /// Remove between 0 and `limit` empty lines following the current position, /// returning true if any empty lines were removed. fn pop_empty_lines(&mut self, limit: u32) -> bool { let mut end = 0; for (i, &position) in self.empty_lines.iter().enumerate() { if position > limit { break; } end = i + 1; } self.empty_lines = self .empty_lines .get(end..) .expect("Pop empty lines slicing"); end != 0 } fn targeted_definition( &mut self, arena: &'doc DocumentArena<'a, 'doc>, definition: &'a TargetedDefinition, ) -> Document<'a, 'doc> { let target = definition.target; let definition = &definition.definition; let start = definition.location().start; let comments = self.pop_comments_with_position(start); let comments = self.printed_documented_comments(arena, comments); let document = self.documented_definition(arena, definition); let document = match target { None => document, Some(Target::Erlang) => { docvec![arena, "@target(erlang)", LINE_DOCUMENT, document] } Some(Target::JavaScript) => { docvec![arena, "@target(javascript)", LINE_DOCUMENT, document] } }; let document = document.group(arena); match comments { Some(comments) => comments.append(arena, document), None => document, } } pub(crate) fn module( &mut self, arena: &'doc DocumentArena<'a, 'doc>, module: &'a UntypedModule, ) -> Document<'a, 'doc> { let mut documents = vec![]; let mut previous_was_a_definition = false; // Here we take consecutive groups of imports so that they can be sorted // alphabetically. for (is_import_group, definitions) in &module .definitions .iter() .chunk_by(|definition| definition.definition.is_import()) { if is_import_group { if previous_was_a_definition { documents.push(TWO_LINES_DOCUMENT); } documents.append(&mut self.imports(arena, definitions.collect_vec())); previous_was_a_definition = false; } else { for definition in definitions { if !documents.is_empty() { documents.push(TWO_LINES_DOCUMENT); } documents.push(self.targeted_definition(arena, definition)); } previous_was_a_definition = true; } } let definitions = arena.concat(documents); // Now that definitions has been collected, only freestanding comments (//) // and doc comments (///) remain. Freestanding comments aren't associated // with any statement, and are moved to the bottom of the module. let doc_comments = arena.join( self.doc_comments.iter().map(|comment| { DOC_COMMENT_DOCUMENT .to_doc(arena) .append(arena, EcoString::from(comment.content)) }), LINE_DOCUMENT, ); let comments = self.pop_comments(u32::MAX); let comments = match printed_comments(arena, comments, false) { Some(comments) => comments, None => EMPTY_DOCUMENT, }; let module_comments = if !self.module_comments.is_empty() { let comments = self.module_comments.iter().map(|s| { MODULE_COMMENT_DOCUMENT .to_doc(arena) .append(arena, EcoString::from(s.content)) }); arena .join(comments, LINE_DOCUMENT) .append(arena, LINE_DOCUMENT) } else { EMPTY_DOCUMENT }; let non_empty = vec![module_comments, definitions, doc_comments, comments] .into_iter() .filter(|doc| !doc.is_empty()); arena .join(non_empty, LINE_DOCUMENT) .append(arena, LINE_DOCUMENT) } /// Separates the imports in groups delimited by comments or empty lines and /// sorts each group alphabetically. /// /// The formatter needs to play nicely with import groups defined by the /// programmer. If one puts a comment before an import then that's a clue /// for the formatter that it has run into a gorup of related imports. /// /// So we can't just sort `imports` and format each one, we have to be a /// bit smarter and see if each import is preceded by a comment. /// Once we find a comment we know we're done with the current import /// group and a new one has started. /// /// ```gleam /// // This is an import group. /// import gleam/int /// import gleam/string /// /// // This marks the beginning of a new import group that can't /// // be mushed together with the previous one! /// import wibble /// import wobble /// ``` fn imports( &mut self, arena: &'doc DocumentArena<'a, 'doc>, imports: Vec<&'a TargetedDefinition>, ) -> Vec> { let mut import_groups_docs = vec![]; let mut current_group = vec![]; let mut current_group_delimiter = EMPTY_DOCUMENT; for import in imports { let start = import.definition.location().start; // We need to start a new group if the `import` is preceded by one or // more empty lines or a `//` comment. let start_new_group = self.any_comments(start) || self.any_empty_lines(start); if start_new_group { // First we print the previous group and clear it out to start a // new empty group containing the import we've just ran into. if !current_group.is_empty() { import_groups_docs.push(docvec![ arena, current_group_delimiter, self.sorted_import_group(arena, ¤t_group) ]); current_group.clear(); } // Now that we've taken care of the previous group we can start // the new one. We know it's preceded either by an empty line or // some comments se we have to be a bit more precise and save the // actual delimiter that we're going to put at the top of this // group. let comments = self.pop_comments(start); let _ = self.pop_empty_lines(start); current_group_delimiter = printed_comments(arena, comments, true).unwrap_or(EMPTY_DOCUMENT); } // Lastly we add the import to the group. current_group.push(import); } // Let's not forget about the last import group! if !current_group.is_empty() { import_groups_docs.push(docvec![ arena, current_group_delimiter, self.sorted_import_group(arena, ¤t_group) ]); } // We want all consecutive import groups to be separated by an empty line. // This should really be `.intersperse(LINE_DOCUMENT)` but I can't do that // because of https://github.com/rust-lang/rust/issues/48919. Itertools::intersperse(import_groups_docs.into_iter(), TWO_LINES_DOCUMENT).collect_vec() } /// Prints the imports as a single sorted group of import statements. /// fn sorted_import_group( &mut self, arena: &'doc DocumentArena<'a, 'doc>, imports: &[&'a TargetedDefinition], ) -> Document<'a, 'doc> { let imports = imports .iter() .sorted_by(|one, other| match (&one.definition, &other.definition) { (Definition::Import(one), Definition::Import(other)) => { one.module.cmp(&other.module) } // It shouldn't really be possible for a non import to be here so // we just return a default value. _ => Ordering::Equal, }) .map(|import| self.targeted_definition(arena, import)); arena.join(imports, LINE_DOCUMENT) } fn definition( &mut self, arena: &'doc DocumentArena<'a, 'doc>, statement: &'a UntypedDefinition, ) -> Document<'a, 'doc> { match statement { Definition::Function(function) => self.statement_fn(arena, function), Definition::TypeAlias(alias) => self.type_alias(arena, alias), Definition::CustomType(custom_type) => self.custom_type(arena, custom_type), Definition::Import(Import { module, as_name, unqualified_values, unqualified_types, .. }) => { let second = if unqualified_values.is_empty() && unqualified_types.is_empty() { EMPTY_DOCUMENT } else { let unqualified_types = unqualified_types .iter() .sorted_by(|a, b| a.name.cmp(&b.name)) .map(|type_| docvec![arena, TYPE_SPACE_DOCUMENT, type_]); let unqualified_values = unqualified_values .iter() .sorted_by(|a, b| a.name.cmp(&b.name)) .map(|value| value.to_doc(arena)); let unqualified = arena.join( unqualified_types.chain(unqualified_values), FLEX_COMMA_DOCUMENT, ); let unqualified = EMPTY_BREAK_DOCUMENT .append(arena, unqualified) .nest(arena, INDENT) .append(arena, TRAILING_COMMA_BREAK_DOCUMENT) .group(arena); ".{".to_doc(arena) .append(arena, unqualified) .append(arena, CLOSE_CURLY_DOCUMENT) }; let doc = docvec![arena, IMPORT_SPACE_DOCUMENT, module.as_str(), second]; let default_module_access_name = module.split('/').next_back().map(EcoString::from); match (default_module_access_name, as_name) { // If the `as name` is the same as the module name that would be // used anyways we won't render it. For example: // ```gleam // import gleam/int as int // ^^^^^^ this is redundant and removed // ``` (Some(module_name), Some((AssignName::Variable(name), _))) if &module_name == name => { doc } (_, None) => doc, (_, Some((AssignName::Variable(name) | AssignName::Discard(name), _))) => doc .append(arena, SPACE_AS_SPACE_DOCUMENT) .append(arena, name), } } Definition::ModuleConstant(ModuleConstant { publicity, name, annotation, value, deprecation, documentation: _, location: _, name_location: _, type_: _, implementations: _, }) => { let attributes = AttributesPrinter::new() .set_internal(*publicity) .set_deprecation(deprecation) .to_doc(arena); let head = attributes .append(arena, pub_(*publicity)) .append(arena, CONST_SPACE_DOCUMENT) .append(arena, name.as_str()); let head = match annotation { None => head, Some(type_) => head .append(arena, COLON_SPACE_DOCUMENT) .append(arena, self.type_ast(arena, type_)), }; head.append(arena, SPACE_EQUAL_SPACE_DOCUMENT) .append(arena, self.const_expr(arena, value).group(arena)) } } } fn const_expr( &mut self, arena: &'doc DocumentArena<'a, 'doc>, value: &'a Constant, ) -> Document<'a, 'doc> { let comments = self.pop_comments(value.location().start); let document = match value { Constant::Todo { message, .. } => { self.append_as_message_constant(arena, TODO_DOCUMENT, message.as_deref()) } Constant::Int { value, .. } => self.int(arena, value), Constant::Float { value, .. } => self.float(arena, value), Constant::String { value, .. } => self.string(arena, value), Constant::List { elements, location, tail, .. } => self.const_list(arena, elements, location, tail), Constant::Tuple { elements, location, .. } => self.const_tuple(arena, elements, location), Constant::BitArray { segments, location, .. } => { let segment_docs = segments .iter() .map(|segment| { bit_array_segment(arena, segment, |expression| { self.const_expr(arena, expression) }) }) .collect_vec(); let packing = self.items_sequence_packing( segments, None, |segment| segment.value.can_have_multiple_per_line(), *location, ); self.bit_array(arena, segment_docs, packing, location) } Constant::Record { name, arguments: None, module: None, .. } => name.to_doc(arena), Constant::Record { name, arguments: None, module: Some((module, _)), .. } => module .to_doc(arena) .append(arena, DOT_DOCUMENT) .append(arena, name.as_str()), Constant::Record { name, arguments: Some(arguments), module: None, location, .. } => { let arguments = arguments .iter() .map(|argument| self.constant_call_arg(arena, argument)) .collect_vec(); name.to_doc(arena) .append(arena, self.wrap_arguments(arena, arguments, location.end)) .group(arena) } Constant::Record { name, arguments: Some(arguments), module: Some((module, _)), location, .. } => { let arguments = arguments .iter() .map(|argument| self.constant_call_arg(arena, argument)) .collect_vec(); module .to_doc(arena) .append(arena, DOT_DOCUMENT) .append(arena, name.as_str()) .append(arena, self.wrap_arguments(arena, arguments, location.end)) .group(arena) } Constant::Var { name, module: None, .. } => name.to_doc(arena), Constant::Var { name, module: Some((module, _)), .. } => docvec![arena, module, DOT_DOCUMENT, name], Constant::StringConcatenation { left, right, .. } => self .const_expr(arena, left) .append( arena, BREAKABLE_SPACE_DOCUMENT.append(arena, CONCAT_DOCUMENT), ) .nest(arena, INDENT) .append(arena, SPACE_DOCUMENT) .append(arena, self.const_expr(arena, right)), Constant::RecordUpdate { module, name, record, arguments, location, .. } => self.const_record_update(arena, module, name, record, arguments, location), Constant::Invalid { .. } => panic!("invalid constants can not be in an untyped ast"), }; commented(arena, document, comments) } fn const_list( &mut self, arena: &'doc DocumentArena<'a, 'doc>, elements: &'a [Constant], location: &SrcSpan, tail: &'a Option>>, ) -> Document<'a, 'doc> { if elements.is_empty() { // We take all comments that come _before_ the end of the list, // that is all comments that are inside "[" and "]", if there's // any comment we want to put it inside the empty list! let comments = self.pop_comments(location.end); return match printed_comments(arena, comments, false) { None => OPEN_CLOSE_SQUARE_DOCUMENT, Some(comments) => OPEN_SQUARE_DOCUMENT .append(arena, EMPTY_BREAK_DOCUMENT.nest(arena, INDENT)) .append(arena, comments) .append(arena, EMPTY_BREAK_DOCUMENT) .append(arena, CLOSE_SQUARE_DOCUMENT) // vvv We want to make sure the comments are on a separate // line from the opening and closing brackets so we // force the breaks to be split on newlines. .force_break(arena), }; } let list_packing = self.items_sequence_packing( elements, tail.as_deref(), |element| element.can_have_multiple_per_line(), *location, ); let comma = match list_packing { ItemsPacking::FitMultiplePerLine => FLEX_COMMA_DOCUMENT, ItemsPacking::FitOnePerLine | ItemsPacking::BreakOnePerLine => COMMA_BREAK_DOCUMENT, }; let mut is_empty = true; let mut elements_doc = EMPTY_DOCUMENT; for element in elements.iter() { let empty_lines = self.pop_empty_lines(element.location().start); let element_doc = self.const_expr(arena, element); elements_doc = if is_empty { is_empty = false; element_doc } else if empty_lines { // If there's empty lines before the list item we want to add an // empty line here. Notice how we're making sure no nesting is // added after the comma, otherwise we would be adding needless // whitespace in the empty line! docvec![ arena, elements_doc, comma.set_nesting(arena, 0), LINE_DOCUMENT, element_doc ] } else { docvec![arena, elements_doc, comma, element_doc] }; } elements_doc = elements_doc.next_break_fits(arena, NextBreakFitsMode::Disabled); let doc = OPEN_SQUARE_BREAK_DOCUMENT.append(arena, elements_doc); let (doc, final_break) = match tail { None => (doc.nest(arena, INDENT), TRAILING_COMMA_BREAK_DOCUMENT), Some(tail) => { let comments = self.pop_comments(tail.location().start); let tail = commented( arena, docvec![arena, DOT_DOT_DOCUMENT, self.const_expr(arena, tail)], comments, ); ( doc.append(arena, COMMA_BREAK_DOCUMENT) .append(arena, tail) .nest(arena, INDENT), EMPTY_BREAK_DOCUMENT, ) } }; // We get all remaining comments that come before the list's closing // square bracket. // If there's any we add those before the closing square bracket instead // of moving those out of the list. // Otherwise those would be moved out of the list. let comments = self.pop_comments(location.end); let doc = match printed_comments(arena, comments, false) { None => doc .append(arena, final_break) .append(arena, CLOSE_SQUARE_DOCUMENT), Some(comment) => doc .append(arena, final_break.nest(arena, INDENT)) // ^ See how here we're adding the missing indentation to the // final break so that the final comment is as indented as the // list's items. .append(arena, comment) .append(arena, LINE_DOCUMENT) .append(arena, CLOSE_SQUARE_DOCUMENT) .force_break(arena), }; match list_packing { ItemsPacking::FitOnePerLine | ItemsPacking::FitMultiplePerLine => doc.group(arena), ItemsPacking::BreakOnePerLine => doc.force_break(arena), } } pub fn const_tuple( &mut self, arena: &'doc DocumentArena<'a, 'doc>, elements: &'a [Constant], location: &SrcSpan, ) -> Document<'a, 'doc> { if elements.is_empty() { // We take all comments that come _before_ the end of the tuple, // that is all comments that are inside "#(" and ")", if there's // any comment we want to put it inside the empty list! let comments = self.pop_comments(location.end); return match printed_comments(arena, comments, false) { None => EMPTY_TUPLE_DOCUMENT, Some(comments) => OPEN_TUPLE_DOCUMENT .append(arena, EMPTY_BREAK_DOCUMENT.nest(arena, INDENT)) .append(arena, comments) .append(arena, EMPTY_BREAK_DOCUMENT) .append(arena, CLOSE_PAREN_DOCUMENT) // vvv We want to make sure the comments are on a separate // line from the opening and closing parentheses so we // force the breaks to be split on newlines. .force_break(arena), }; } let arguments_docs = elements .iter() .map(|element| self.const_expr(arena, element)); let tuple_doc = OPEN_TUPLE_BREAK_DOCUMENT .append( arena, arena .join(arguments_docs, COMMA_BREAK_DOCUMENT) .next_break_fits(arena, NextBreakFitsMode::Disabled), ) .nest(arena, INDENT); let comments = self.pop_comments(location.end); match printed_comments(arena, comments, false) { None => tuple_doc .append(arena, TRAILING_COMMA_BREAK_DOCUMENT) .append(arena, CLOSE_PAREN_DOCUMENT) .group(arena), Some(comments) => tuple_doc .append(arena, TRAILING_COMMA_BREAK_DOCUMENT.nest(arena, INDENT)) .append(arena, comments) .append(arena, LINE_DOCUMENT) .append(arena, CLOSE_PAREN_DOCUMENT) .force_break(arena), } } fn documented_definition( &mut self, arena: &'doc DocumentArena<'a, 'doc>, definition: &'a UntypedDefinition, ) -> Document<'a, 'doc> { let comments = self.doc_comments(arena, definition.location().start); comments .append(arena, self.definition(arena, definition).group(arena)) .group(arena) } fn doc_comments( &mut self, arena: &'doc DocumentArena<'a, 'doc>, limit: u32, ) -> Document<'a, 'doc> { let mut comments = self.pop_doc_comments(limit).peekable(); match comments.peek() { None => EMPTY_DOCUMENT, Some(_) => arena .join( comments.map(|comment| match comment { Some(comment) => { DOC_COMMENT_DOCUMENT.append(arena, EcoString::from(comment)) } None => unreachable!("empty lines dropped by pop_doc_comments"), }), LINE_DOCUMENT, ) .append(arena, LINE_DOCUMENT) .force_break(arena), } } fn type_ast_constructor( &mut self, arena: &'doc DocumentArena<'a, 'doc>, name: &'a TypeAstConstructorName, arguments: &'a [TypeAst], location: &SrcSpan, ) -> Document<'a, 'doc> { let head = match name { TypeAstConstructorName::Unqualified { name, .. } => name.to_doc(arena), TypeAstConstructorName::Qualified { module, name, .. } => { module.to_doc(arena).append(arena, DOT_DOCUMENT).append( arena, name.as_ref() .map_or(EMPTY_DOCUMENT, |(name, _name_location)| name.to_doc(arena)), ) } }; if arguments.is_empty() { head } else { head.append(arena, self.type_arguments(arena, arguments, location)) } } fn type_ast( &mut self, arena: &'doc DocumentArena<'a, 'doc>, type_: &'a TypeAst, ) -> Document<'a, 'doc> { let comments = self.pop_comments(type_.location().start); let type_ = match type_ { TypeAst::Hole(TypeAstHole { name, .. }) => name.to_doc(arena), TypeAst::Constructor(TypeAstConstructor { name, arguments, location, start_parentheses: _, }) => self.type_ast_constructor(arena, name, arguments, location), TypeAst::Fn(TypeAstFn { arguments, return_, location, }) => FN_DOCUMENT .append(arena, self.type_arguments(arena, arguments, location)) .group(arena) .append(arena, SPACE_RIGHT_ARROW_DOCUMENT) .append( arena, BREAKABLE_SPACE_DOCUMENT .append(arena, self.type_ast(arena, return_)) .group(arena) .nest(arena, INDENT), ), TypeAst::Var(TypeAstVar { name, .. }) => name.to_doc(arena), TypeAst::Tuple(TypeAstTuple { elements, location }) => { HASHTAG_DOCUMENT.append(arena, self.type_arguments(arena, elements, location)) } }; commented(arena, type_.group(arena), comments) } fn type_arguments( &mut self, arena: &'doc DocumentArena<'a, 'doc>, arguments: &'a [TypeAst], location: &SrcSpan, ) -> Document<'a, 'doc> { let arguments = arguments .iter() .map(|type_| self.type_ast(arena, type_)) .collect_vec(); self.wrap_arguments(arena, arguments, location.end) } pub fn type_alias( &mut self, arena: &'doc DocumentArena<'a, 'doc>, alias: &'a TypeAlias, ) -> Document<'a, 'doc> { let TypeAlias { alias: name, parameters: arguments, type_ast: type_, publicity, deprecation, location, name_location: _, type_: _, documentation: _, } = alias; let attributes = AttributesPrinter::new() .set_deprecation(deprecation) .set_internal(*publicity) .to_doc(arena); let head = docvec![ arena, attributes, pub_(*publicity), TYPE_SPACE_DOCUMENT, name ]; let head = if arguments.is_empty() { head } else { let arguments = arguments.iter().map(|(_, e)| e.to_doc(arena)).collect_vec(); head.append( arena, self.wrap_arguments(arena, arguments, location.end) .group(arena), ) }; head.append(arena, SPACE_EQUAL_DOCUMENT).append( arena, LINE_DOCUMENT .append(arena, self.type_ast(arena, type_)) .group(arena) .nest(arena, INDENT), ) } fn fn_arg( &mut self, arena: &'doc DocumentArena<'a, 'doc>, argument: &'a Arg, ) -> Document<'a, 'doc> { let comments = self.pop_comments(argument.location.start); let doc = match &argument.annotation { None => argument.names.to_doc(arena), Some(type_) => argument .names .to_doc(arena) .append(arena, COLON_SPACE_DOCUMENT) .append(arena, self.type_ast(arena, type_)), } .group(arena); commented(arena, doc, comments) } fn statement_fn( &mut self, arena: &'doc DocumentArena<'a, 'doc>, function: &'a UntypedFunction, ) -> Document<'a, 'doc> { let Function { location, body_start: _, end_position, name, arguments, body, publicity, deprecation, return_annotation, return_type: _, documentation: _, external_erlang, external_javascript, implementations: _, purity: _, } = function; let attributes = AttributesPrinter::new() .set_deprecation(deprecation) .set_internal(*publicity) .set_external_erlang(external_erlang) .set_external_javascript(external_javascript) .to_doc(arena); // Fn name and args let arguments = arguments .iter() .map(|argument| self.fn_arg(arena, argument)) .collect_vec(); let signature = pub_(*publicity) .append(arena, FN_SPACE_DOCUMENT) .append( arena, &name .as_ref() .expect("Function in a statement must be named") .1, ) .append( arena, self.wrap_arguments( arena, arguments, // Calculate end location of arguments to not consume comments in // return annotation return_annotation .as_ref() .map_or(location.end, |ann| ann.location().start), ), ); // Add return annotation let signature = match &return_annotation { Some(annotation) => signature .append(arena, SPACE_RIGHT_ARROW_SPACE_DOCUMENT) .append(arena, self.type_ast(arena, annotation)), None => signature, }; if body.is_empty() { return docvec![arena, attributes, signature.group(arena)]; } // Format body and add any trailing comments let body = self.statements(arena, body); let comments = self.pop_comments(*end_position); let body = match printed_comments(arena, comments, false) { Some(comments) => body.append(arena, LINE_DOCUMENT).append(arena, comments), None => body, }; // Stick it all together let function = signature .append(arena, SPACE_OPEN_CURLY_DOCUMENT) .group(arena) .append( arena, LINE_DOCUMENT .append(arena, body) .nest(arena, INDENT) .group(arena), ) .append(arena, LINE_DOCUMENT) .append(arena, CLOSE_CURLY_DOCUMENT); docvec![arena, attributes, function] } #[allow(clippy::too_many_arguments)] fn expr_fn( &mut self, arena: &'doc DocumentArena<'a, 'doc>, arguments: &'a [UntypedArg], return_annotation: Option<&'a TypeAst>, body: &'a Vec1, location: &SrcSpan, end_of_head_byte_index: &u32, ) -> Document<'a, 'doc> { let arguments_docs = arguments .iter() .map(|argument| self.fn_arg(arena, argument)) .collect_vec(); let arguments = self .wrap_arguments(arena, arguments_docs, *end_of_head_byte_index) .group(arena) .next_break_fits(arena, NextBreakFitsMode::Disabled); // ^^^ We add this so that when an expression function is passed as // the last argument of a function and it goes over the line // limit with just its arguments we don't get some strange // splitting. // See https://github.com/gleam-lang/gleam/issues/2571 // // There's many ways we could be smarter than this. For example: // - still split the arguments like it did in the example shown in the // issue if the expr_fn has more than one argument // - make sure that an anonymous function whose body is made up of a // single expression doesn't get split (I think that could boil down // to wrapping the body with a `next_break_fits(Disabled)`) // // These are some of the ways we could tweak the look of expression // functions in the future if people are not satisfied with it. let header = "fn".to_doc(arena).append(arena, arguments); let header = match return_annotation { None => header, Some(return_annotation) => header .append(arena, SPACE_RIGHT_ARROW_SPACE_DOCUMENT) .append(arena, self.type_ast(arena, return_annotation)), }; let statements = self.statements(arena, body.as_vec()); let body = match printed_comments(arena, self.pop_comments(location.end), false) { None => statements, Some(comments) => statements .append(arena, LINE_DOCUMENT) .append(arena, comments) .force_break(arena), }; header .append(arena, SPACE_DOCUMENT) .append(arena, wrap_block(arena, body)) .group(arena) } fn statements( &mut self, arena: &'doc DocumentArena<'a, 'doc>, statements: &'a [UntypedStatement], ) -> Document<'a, 'doc> { let mut previous_position = 0; let count = statements.len(); let mut documents = Vec::with_capacity(count * 2); for (i, statement) in statements.iter().enumerate() { let preceding_newline = self.pop_empty_lines(previous_position + 1); if i != 0 && preceding_newline { documents.push(TWO_LINES_DOCUMENT); } else if i != 0 { documents.push(LINE_DOCUMENT); } previous_position = statement.location().end; documents.push(self.statement(arena, statement).group(arena)); // If the last statement is a use we make sure it's followed by a // todo to make it explicit it has an unimplemented callback. if statement.is_use() && i == count - 1 { documents.push(LINE_DOCUMENT); documents.push(TODO_DOCUMENT); } } if count == 1 && statements .first() .is_some_and(|statement| statement.is_expression()) { arena.concat(documents) } else { arena.concat(documents).force_break(arena) } } fn assignment( &mut self, arena: &'doc DocumentArena<'a, 'doc>, assignment: &'a UntypedAssignment, ) -> Document<'a, 'doc> { let comments = self.pop_comments(assignment.location.start); let Assignment { pattern, value, kind, annotation, .. } = assignment; let _ = self.pop_empty_lines(pattern.location().end); let (keyword, message) = match kind { AssignmentKind::Let | AssignmentKind::Generated => (LET_SPACE_DOCUMENT, None), AssignmentKind::Assert { message, .. } => (LET_ASSERT_SPACE_DOCUMENT, message.as_ref()), }; let pattern = self.pattern(arena, pattern); let annotation = annotation .as_ref() .map(|annotation| COLON_SPACE_DOCUMENT.append(arena, self.type_ast(arena, annotation))) .unwrap_or(EMPTY_DOCUMENT); let doc = keyword .to_doc(arena) .append(arena, pattern.append(arena, annotation).group(arena)) .append(arena, SPACE_EQUAL_DOCUMENT) .append(arena, self.assigned_value(arena, value)); commented( arena, self.append_as_message_expression(arena, doc, PrecedingAs::Expression, message), comments, ) } fn expr( &mut self, arena: &'doc DocumentArena<'a, 'doc>, expression: &'a UntypedExpr, ) -> Document<'a, 'doc> { let comments = self.pop_comments(expression.start_byte_index()); let document = match expression { UntypedExpr::Panic { message, .. } => self.append_as_message_expression( arena, PANIC_DOCUMENT, PrecedingAs::Keyword, message.as_deref(), ), UntypedExpr::Todo { message, .. } => self.append_as_message_expression( arena, TODO_DOCUMENT, PrecedingAs::Keyword, message.as_deref(), ), UntypedExpr::Echo { expression, location: _, keyword_end: _, message, } => self.echo(arena, expression, message), UntypedExpr::PipeLine { expressions, .. } => self.pipeline(arena, expressions, false), UntypedExpr::Int { value, .. } => self.int(arena, value), UntypedExpr::Float { value, .. } => self.float(arena, value), UntypedExpr::String { value, .. } => self.string(arena, value), UntypedExpr::Block { statements, location, .. } => self.block(arena, location, statements, false), UntypedExpr::Var { name, .. } if name == CAPTURE_VARIABLE => UNDERSCORE_DOCUMENT, UntypedExpr::Var { name, .. } => name.to_doc(arena), UntypedExpr::TupleIndex { tuple, index, .. } => self.tuple_index(arena, tuple, *index), UntypedExpr::NegateInt { value, .. } => self.negate_int(arena, value), UntypedExpr::NegateBool { value, .. } => self.negate_bool(arena, value), UntypedExpr::Fn { kind, body, .. } if kind.is_capture() => { self.fn_capture(arena, body, FnCapturePosition::EverywhereElse) } UntypedExpr::Fn { return_annotation, arguments, body, location, end_of_head_byte_index, .. } => self.expr_fn( arena, arguments, return_annotation.as_ref(), body, location, end_of_head_byte_index, ), UntypedExpr::List { elements, tail, location, } => self.list(arena, elements, tail.as_deref(), location), UntypedExpr::Call { fun, arguments, location, .. } => self.call(arena, fun, arguments, location), UntypedExpr::BinOp { operator, left, right, .. } => self.bin_op(arena, operator, left, right, false), UntypedExpr::Case { subjects, clauses, location, } => self.case( arena, subjects, clauses.as_deref().unwrap_or_default(), location, ), UntypedExpr::FieldAccess { label, container, .. } => self .expr(arena, container) .append(arena, DOT_DOCUMENT) .append(arena, label.as_str()), UntypedExpr::Tuple { elements, location } => self.tuple(arena, elements, location), UntypedExpr::BitArray { segments, location, .. } => { let segment_docs = segments .iter() .map(|segment| { bit_array_segment(arena, segment, |e| self.bit_array_segment_expr(arena, e)) }) .collect_vec(); let packing = self.items_sequence_packing( segments, None, |segment| segment.value.can_have_multiple_per_line(), *location, ); self.bit_array(arena, segment_docs, packing, location) } UntypedExpr::RecordUpdate { constructor, record, arguments, location, .. } => self.record_update(arena, constructor, record, arguments, location), }; commented(arena, document, comments) } fn string( &self, arena: &'doc DocumentArena<'a, 'doc>, string: &'a EcoString, ) -> Document<'a, 'doc> { let doc = string .to_doc(arena) .surround(arena, DOUBLE_QUOTE_DOCUMENT, DOUBLE_QUOTE_DOCUMENT); if string.contains('\n') { doc.force_break(arena) } else { doc } } fn bin_op_string( &self, arena: &'doc DocumentArena<'a, 'doc>, string: &'a EcoString, ) -> Document<'a, 'doc> { let lines = string.split('\n').collect_vec(); match lines.as_slice() { [] | [_] => { string .to_doc(arena) .surround(arena, DOUBLE_QUOTE_DOCUMENT, DOUBLE_QUOTE_DOCUMENT) } [first_line, lines @ ..] => { let mut doc = docvec![arena, DOUBLE_QUOTE_DOCUMENT, *first_line]; for line in lines { doc = doc .append(arena, LINE_DOCUMENT.set_nesting(arena, 0)) .append(arena, line.to_doc(arena)) } doc.append(arena, DOUBLE_QUOTE_DOCUMENT).group(arena) } } } fn float(&self, arena: &'doc DocumentArena<'a, 'doc>, value: &'a str) -> Document<'a, 'doc> { // Create parts let mut parts = value.split('.'); let integer_part = parts.next().unwrap_or_default(); let floating_part = parts.next().unwrap_or_default(); let integer_doc = self.underscore_integer_string(arena, integer_part); // Split fp_part into a regular fractional and maybe a scientific part let (fractional_part, scientific_part) = floating_part.split_at( floating_part .chars() .position(|character| character == 'e') .unwrap_or(floating_part.len()), ); // Trim right any consequtive '0's let mut fractional_part = fractional_part.trim_end_matches('0').to_string(); // If there is no fractional part left, add a '0', thus that 1. becomes 1.0 etc. if fractional_part.is_empty() { fractional_part.push('0'); } let float_doc = fractional_part.chars().collect::(); integer_doc .append(arena, DOT_DOCUMENT) .append(arena, float_doc) .append(arena, scientific_part) } fn int(&self, arena: &'doc DocumentArena<'a, 'doc>, value: &'a str) -> Document<'a, 'doc> { if value.starts_with("0x") || value.starts_with("0b") || value.starts_with("0o") { return value.to_doc(arena); } self.underscore_integer_string(arena, value) } fn underscore_integer_string( &self, arena: &'doc DocumentArena<'a, 'doc>, value: &'a str, ) -> Document<'a, 'doc> { let underscore = '_'; let minus = '-'; let len = value.len(); let underscore_ch_cnt = value.matches(underscore).count(); let reformat_watershed = if value.starts_with(minus) { 6 } else { 5 }; let insert_underscores = (len - underscore_ch_cnt) >= reformat_watershed; let mut new_value = String::new(); let mut j = 0; for (i, character) in value.chars().rev().enumerate() { if character == underscore { continue; } if insert_underscores && i != 0 && character != minus && i < len && j % 3 == 0 { new_value.push(underscore); } new_value.push(character); j += 1; } new_value.chars().rev().collect::().to_doc(arena) } #[allow(clippy::too_many_arguments)] fn pattern_constructor( &mut self, arena: &'doc DocumentArena<'a, 'doc>, name: &'a str, arguments: &'a [CallArg], module: &'a Option<(EcoString, SrcSpan)>, spread: Option, location: &SrcSpan, ) -> Document<'a, 'doc> { fn is_breakable(expression: &UntypedPattern) -> bool { match expression { Pattern::Tuple { .. } | Pattern::List { .. } | Pattern::BitArray { .. } => true, Pattern::Constructor { arguments, .. } => !arguments.is_empty(), Pattern::Int { .. } | Pattern::Float { .. } | Pattern::String { .. } | Pattern::Variable { .. } | Pattern::BitArraySize(_) | Pattern::Assign { .. } | Pattern::Discard { .. } | Pattern::StringPrefix { .. } | Pattern::Invalid { .. } => false, } } let name = match module { Some((module, _)) => module .to_doc(arena) .append(arena, DOT_DOCUMENT) .append(arena, name), None => name.to_doc(arena), }; if arguments.is_empty() && spread.is_some() { name.append(arena, OPEN_PAREN_DOT_DOT_CLOSE_PAREN_DOCUMENT) } else if arguments.is_empty() { name } else if spread.is_some() { let arguments = arguments .iter() .map(|argument| self.pattern_call_arg(arena, argument)) .collect_vec(); name.append( arena, self.wrap_arguments_with_spread(arena, arguments, location.end), ) .group(arena) } else { match arguments { [argument] if is_breakable(&argument.value) => name .append(arena, OPEN_PAREN_DOCUMENT) .append(arena, self.pattern_call_arg(arena, argument)) .append(arena, CLOSE_PAREN_DOCUMENT) .group(arena), _ => { let arguments = arguments .iter() .map(|argument| self.pattern_call_arg(arena, argument)) .collect_vec(); name.append(arena, self.wrap_arguments(arena, arguments, location.end)) .group(arena) } } } } fn call( &mut self, arena: &'doc DocumentArena<'a, 'doc>, function: &'a UntypedExpr, arguments: &'a [CallArg], location: &SrcSpan, ) -> Document<'a, 'doc> { let expression = match function { UntypedExpr::PipeLine { .. } => break_block(arena, self.expr(arena, function)), UntypedExpr::BinOp { .. } | UntypedExpr::Int { .. } | UntypedExpr::Float { .. } | UntypedExpr::String { .. } | UntypedExpr::Block { .. } | UntypedExpr::Var { .. } | UntypedExpr::Fn { .. } | UntypedExpr::List { .. } | UntypedExpr::Call { .. } | UntypedExpr::Case { .. } | UntypedExpr::FieldAccess { .. } | UntypedExpr::Tuple { .. } | UntypedExpr::TupleIndex { .. } | UntypedExpr::Todo { .. } | UntypedExpr::Echo { .. } | UntypedExpr::Panic { .. } | UntypedExpr::BitArray { .. } | UntypedExpr::RecordUpdate { .. } | UntypedExpr::NegateBool { .. } | UntypedExpr::NegateInt { .. } => self.expr(arena, function), }; let arity = arguments.len(); self.append_inlinable_wrapped_arguments( arena, expression, arguments, location, |argument| is_breakable_argument(&argument.value, arguments.len()), |self_, argument| self_.call_arg(arena, argument, arity), ) } fn tuple( &mut self, arena: &'doc DocumentArena<'a, 'doc>, elements: &'a [UntypedExpr], location: &SrcSpan, ) -> Document<'a, 'doc> { if elements.is_empty() { // We take all comments that come _before_ the end of the tuple, // that is all comments that are inside "#(" and ")", if there's // any comment we want to put it inside the empty tuple! return match printed_comments(arena, self.pop_comments(location.end), false) { None => EMPTY_TUPLE_DOCUMENT, Some(comments) => OPEN_TUPLE_DOCUMENT .append(arena, EMPTY_BREAK_DOCUMENT.nest(arena, INDENT)) .append(arena, comments) .append(arena, EMPTY_BREAK_DOCUMENT) .append(arena, CLOSE_PAREN_DOCUMENT) // vvv We want to make sure the comments are on a separate // line from the opening and closing parentheses so we // force the breaks to be split on newlines. .force_break(arena), }; } self.append_inlinable_wrapped_arguments( arena, HASHTAG_DOCUMENT, elements, location, |expression| { !expression.is_tuple() && is_breakable_argument(expression, elements.len()) }, |self_, expression| self_.comma_separated_item(arena, expression, elements.len()), ) } // Appends to the given docs a comma-separated list of documents wrapped by // parentheses. If the last item of the argument list is splittable the // resulting document will try to first split that before splitting all the // other arguments. // This is used for function calls and tuples. #[allow(clippy::too_many_arguments)] fn append_inlinable_wrapped_arguments<'b, T, Predicate, ToDoc>( &mut self, arena: &'doc DocumentArena<'a, 'doc>, doc: Document<'a, 'doc>, values: &'b [T], location: &SrcSpan, is_breakable_argument: Predicate, to_doc: ToDoc, ) -> Document<'a, 'doc> where T: HasLocation, T: std::fmt::Debug, Predicate: Fn(&T) -> bool, ToDoc: Fn(&mut Self, &'b T) -> Document<'a, 'doc>, { match init_and_last(values) { Some((initial_values, last_value)) if is_breakable_argument(last_value) && !self.any_comments(last_value.location().start) && !self.any_comment_between(last_value.location().end, location.end) => { let mut docs = initial_values .iter() .map(|value| to_doc(self, value)) .collect_vec(); let last_value_doc = to_doc(self, last_value) .group(arena) .next_break_fits(arena, NextBreakFitsMode::Enabled); docs.append(&mut vec![last_value_doc]); doc.append( arena, self.wrap_function_call_arguments(arena, docs, location), ) .next_break_fits(arena, NextBreakFitsMode::Disabled) .group(arena) } Some(_) | None => { let docs = values.iter().map(|value| to_doc(self, value)).collect_vec(); doc.append( arena, self.wrap_function_call_arguments(arena, docs, location), ) .group(arena) } } } pub fn case( &mut self, arena: &'doc DocumentArena<'a, 'doc>, subjects: &'a [UntypedExpr], clauses: &'a [UntypedClause], location: &'a SrcSpan, ) -> Document<'a, 'doc> { let subjects_doc = CASE_BREAK_DOCUMENT .append( arena, arena.join( subjects .iter() .map(|subject| self.expr(arena, subject).group(arena)), COMMA_BREAK_DOCUMENT, ), ) .nest(arena, INDENT) .append(arena, BREAKABLE_SPACE_DOCUMENT) .append(arena, OPEN_CURLY_DOCUMENT) .next_break_fits(arena, NextBreakFitsMode::Disabled) .group(arena); let clauses_doc = arena.concat( clauses .iter() .enumerate() .map(|(i, clause)| self.clause(arena, clause, i as u32).group(arena)), ); // We get all remaining comments that come before the case's closing // bracket. If there's any we add those before the closing bracket // instead of moving those out of the case expression. // Otherwise those would be moved out of the case expression. let comments = self.pop_comments(location.end); let closing_bracket = match printed_comments(arena, comments, false) { None => docvec![arena, LINE_DOCUMENT, CLOSE_CURLY_DOCUMENT], Some(comment) => docvec![arena, LINE_DOCUMENT, comment] .nest(arena, INDENT) .append(arena, LINE_DOCUMENT) .append(arena, CLOSE_CURLY_DOCUMENT), }; subjects_doc .append( arena, LINE_DOCUMENT.append(arena, clauses_doc).nest(arena, INDENT), ) .append(arena, closing_bracket) .force_break(arena) } pub fn record_update( &mut self, arena: &'doc DocumentArena<'a, 'doc>, constructor: &'a UntypedExpr, record: &'a RecordBeingUpdated, arguments: &'a [UntypedRecordUpdateArg], location: &SrcSpan, ) -> Document<'a, 'doc> { let constructor_doc: Document<'a, 'doc> = self.expr(arena, constructor); let pieces = std::iter::once(UntypedRecordUpdatePiece::Record(record)) .chain(arguments.iter().map(UntypedRecordUpdatePiece::Argument)) .collect_vec(); self.append_inlinable_wrapped_arguments( arena, constructor_doc, &pieces, location, |argument| { let expression = match argument { UntypedRecordUpdatePiece::Argument(argument) => &argument.value, UntypedRecordUpdatePiece::Record(record) => &record.base, }; is_breakable_argument(expression, pieces.len()) }, |this, argument| match argument { UntypedRecordUpdatePiece::Argument(arg) => this.record_update_arg(arena, arg), UntypedRecordUpdatePiece::Record(record) => { let comments = this.pop_comments(record.location.start); commented( arena, DOT_DOT_DOCUMENT.append(arena, this.expr(arena, &record.base)), comments, ) } }, ) } #[allow(clippy::too_many_arguments)] pub fn const_record_update( &mut self, arena: &'doc DocumentArena<'a, 'doc>, module: &Option<(EcoString, SrcSpan)>, name: &'a EcoString, record: &'a RecordBeingUpdated>, arguments: &'a [RecordUpdateArg>], location: &SrcSpan, ) -> Document<'a, 'doc> { let constructor_doc = match module { Some((m, _)) => m .to_doc(arena) .append(arena, DOT_DOCUMENT) .append(arena, name.as_str()), None => name.to_doc(arena), }; let pieces = std::iter::once(RecordUpdatePiece::Record(record)) .chain(arguments.iter().map(RecordUpdatePiece::Argument)) .collect_vec(); let docs = pieces .iter() .map(|piece| match piece { RecordUpdatePiece::Argument(argument) => { let comments = self.pop_comments(argument.location.start); let doc = match argument { _ if argument.uses_label_shorthand() => argument .label .as_str() .to_doc(arena) .append(arena, COLON_DOCUMENT), _ => argument .label .as_str() .to_doc(arena) .append(arena, COLON_SPACE_DOCUMENT) .append(arena, self.const_expr(arena, &argument.value)) .group(arena), }; commented(arena, doc, comments) } RecordUpdatePiece::Record(record) => { let comments = self.pop_comments(record.location.start); commented( arena, DOT_DOT_DOCUMENT.append(arena, self.const_expr(arena, &record.base)), comments, ) } }) .collect_vec(); constructor_doc .append(arena, self.wrap_arguments(arena, docs, location.end)) .group(arena) } pub fn bin_op( &mut self, arena: &'doc DocumentArena<'a, 'doc>, name: &'a BinOp, left: &'a UntypedExpr, right: &'a UntypedExpr, nest_steps: bool, ) -> Document<'a, 'doc> { let left_side = self.bin_op_side(arena, name, left, nest_steps); let comments = self.pop_comments(right.start_byte_index()); let name_doc = BREAKABLE_SPACE_DOCUMENT.append(arena, commented(arena, binop(*name), comments)); let right_side = self.bin_op_side(arena, name, right, nest_steps); left_side .append( arena, if nest_steps { name_doc.nest(arena, INDENT) } else { name_doc }, ) .append(arena, SPACE_DOCUMENT) .append(arena, right_side) } fn bin_op_side( &mut self, arena: &'doc DocumentArena<'a, 'doc>, operator: &'a BinOp, side: &'a UntypedExpr, nest_steps: bool, ) -> Document<'a, 'doc> { let side_doc = match side { UntypedExpr::String { value, .. } => self.bin_op_string(arena, value), UntypedExpr::BinOp { operator, left, right, .. } => self.bin_op(arena, operator, left, right, nest_steps), UntypedExpr::Int { .. } | UntypedExpr::Float { .. } | UntypedExpr::Block { .. } | UntypedExpr::Var { .. } | UntypedExpr::Fn { .. } | UntypedExpr::List { .. } | UntypedExpr::Call { .. } | UntypedExpr::PipeLine { .. } | UntypedExpr::Case { .. } | UntypedExpr::FieldAccess { .. } | UntypedExpr::Tuple { .. } | UntypedExpr::TupleIndex { .. } | UntypedExpr::Todo { .. } | UntypedExpr::Panic { .. } | UntypedExpr::Echo { .. } | UntypedExpr::BitArray { .. } | UntypedExpr::RecordUpdate { .. } | UntypedExpr::NegateBool { .. } | UntypedExpr::NegateInt { .. } => self.expr(arena, side), }; match side.bin_op_name() { // In case the other side is a binary operation as well and it can // be grouped together with the current binary operation, the two // docs are simply concatenated, so that they will end up in the // same group and the formatter will try to keep those on a single // line. Some(side_name) if side_name.can_be_grouped_with(operator) => side_doc, // In case the binary operations cannot be grouped together the // other side is treated as a group on its own so that it can be // broken independently of other pieces of the binary operations // chain. _ => self.operator_side( arena, side_doc.group(arena), operator.precedence(), side.bin_op_precedence(), ), } } pub fn operator_side( &self, arena: &'doc DocumentArena<'a, 'doc>, doc: Document<'a, 'doc>, op: u8, side: u8, ) -> Document<'a, 'doc> { if op > side { wrap_block(arena, doc).group(arena) } else { doc } } fn spans_multiple_lines(&self, start: u32, end: u32) -> bool { self.new_lines .binary_search_by(|newline| { if *newline <= start { Ordering::Less } else if *newline >= end { Ordering::Greater } else { // If the newline is in between the pipe start and end // then we've found it! Ordering::Equal } }) // If we couldn't find any newline between the start and end of // the pipeline then we will try and keep it on a single line. .is_ok() } /// Returns true if there's a trailing comma between `start` and `end`. /// fn has_trailing_comma(&self, start: u32, end: u32) -> bool { self.trailing_commas .binary_search_by(|comma| { if *comma < start { Ordering::Less } else if *comma > end { Ordering::Greater } else { Ordering::Equal } }) .is_ok() } fn pipeline( &mut self, arena: &'doc DocumentArena<'a, 'doc>, expressions: &'a Vec1, nest_pipe: bool, ) -> Document<'a, 'doc> { // We start by producing the document for the first step of the pipeline. let first = expressions.first(); let first_precedence = first.bin_op_precedence(); let first = self.expr(arena, first).group(arena); let first_step = self.operator_side(arena, first, 5, first_precedence); // We then produce a doc for each item in the rest of the pipeline. let pipeline_start = expressions.first().location().start; let pipeline_end = expressions.last().location().end; let try_to_keep_on_one_line = !self.spans_multiple_lines(pipeline_start, pipeline_end); let steps_separator = if try_to_keep_on_one_line { BREAKABLE_SPACE_DOCUMENT } else { LINE_DOCUMENT }; let other_steps = expressions.iter().skip(1).map(|expression| { // We start by producing the document for the pipe itself `|>`, // we also want to put comments before it! let comments = self.pop_comments(expression.location().start); let commented_pipe = commented(arena, PIPE_SPACE_DOCUMENT, comments); let mut pipe = docvec![arena, steps_separator, commented_pipe]; if nest_pipe { pipe = pipe.nest(arena, INDENT); }; // We then produce the document for the expression being piped into. let expression_doc = self.expression_on_right_hand_side_of_pipe(arena, nest_pipe, expression); let expression_doc = self.operator_side(arena, expression_doc, 4, expression.bin_op_precedence()); // And finally piece everything together. pipe.append(arena, expression_doc) }); let stops = std::iter::once(first_step).chain(other_steps); if try_to_keep_on_one_line { arena.concat(stops) } else { arena.concat(stops).force_break(arena) } } fn expression_on_right_hand_side_of_pipe( &mut self, arena: &'doc DocumentArena<'a, 'doc>, nest_pipe: bool, expression: &'a UntypedExpr, ) -> Document<'a, 'doc> { let expression = if let UntypedExpr::Fn { kind, body, .. } = expression && kind.is_capture() { self.fn_capture(arena, body, FnCapturePosition::RightHandSideOfPipe) } else { self.expr(arena, expression) }; if nest_pipe { expression.nest(arena, INDENT) } else { expression } } fn fn_capture( &mut self, arena: &'doc DocumentArena<'a, 'doc>, call: &'a [UntypedStatement], position: FnCapturePosition, ) -> Document<'a, 'doc> { // The body of a capture being multiple statements shouldn't be possible... if call.len() != 1 { panic!("Function capture found not to have a single statement call"); } let Some(Statement::Expression(UntypedExpr::Call { fun, arguments, location, .. })) = call.first() else { // The body of a capture being not a fn shouldn't be possible... panic!("Function capture body found not to be a call in the formatter") }; match (position, arguments.as_slice()) { // The capture has a single unlabelled hole: // // wibble |> wobble(_) // list.map([], wobble(_)) // // We want these to become: // // wibble |> wobble // list.map([], wobble) // ( FnCapturePosition::RightHandSideOfPipe | FnCapturePosition::EverywhereElse, [argument], ) if argument.is_capture_hole() && argument.label.is_none() => self.expr(arena, fun), // The capture is on the right hand side of a pipe and its first // argument it an unlabelled capture hole: // // wibble |> wobble(_, woo) // // We want it to become: // // wibble |> wobble(woo) // (FnCapturePosition::RightHandSideOfPipe, [argument, rest @ ..]) if argument.is_capture_hole() && argument.label.is_none() => { let expression = self.expr(arena, fun); let arity = rest.len(); self.append_inlinable_wrapped_arguments( arena, expression, rest, location, |argument| is_breakable_argument(&argument.value, rest.len()), |self_, argument| self_.call_arg(arena, argument, arity), ) } // In all other cases we print it like a regular function call // without changing it. // ( FnCapturePosition::RightHandSideOfPipe | FnCapturePosition::EverywhereElse, arguments, ) => { let expression = self.expr(arena, fun); let arity = arguments.len(); self.append_inlinable_wrapped_arguments( arena, expression, arguments, location, |argument| is_breakable_argument(&argument.value, arguments.len()), |self_, argument| self_.call_arg(arena, argument, arity), ) } } } pub fn record_constructor( &mut self, arena: &'doc DocumentArena<'a, 'doc>, constructor: &'a RecordConstructor, ) -> Document<'a, 'doc> { let comments = self.pop_comments(constructor.location.start); let doc_comments = self.doc_comments(arena, constructor.location.start); let attributes = AttributesPrinter::new() .set_deprecation(&constructor.deprecation) .to_doc(arena); let doc = if constructor.arguments.is_empty() { if self.any_comments(constructor.location.end) { attributes .append(arena, constructor.name.as_str().to_doc(arena)) .append( arena, self.wrap_arguments(arena, vec![], constructor.location.end), ) .group(arena) } else { attributes.append(arena, constructor.name.as_str().to_doc(arena)) } } else { let arguments = constructor .arguments .iter() .map( |RecordConstructorArg { label, ast, location, .. }| { let arg_comments = self.pop_comments(location.start); let argument = match label { Some((_, label)) => label .to_doc(arena) .append(arena, COLON_SPACE_DOCUMENT) .append(arena, self.type_ast(arena, ast)), None => self.type_ast(arena, ast), }; commented( arena, self.doc_comments(arena, location.start) .append(arena, argument) .group(arena), arg_comments, ) }, ) .collect_vec(); attributes .append(arena, constructor.name.as_str().to_doc(arena)) .append( arena, self.wrap_arguments(arena, arguments, constructor.location.end) .group(arena), ) }; commented( arena, doc_comments.append(arena, doc).group(arena), comments, ) } pub fn custom_type( &mut self, arena: &'doc DocumentArena<'a, 'doc>, type_: &'a CustomType, ) -> Document<'a, 'doc> { let CustomType { location, end_position, name, name_location: _, publicity, constructors, documentation: _, deprecation, opaque, parameters, typed_parameters: _, external_erlang, external_javascript, } = type_; let _ = self.pop_empty_lines(location.end); let attributes = AttributesPrinter::new() .set_deprecation(deprecation) .set_internal(*publicity) .set_external_erlang(external_erlang) .set_external_javascript(external_javascript) .to_doc(arena); let doc = attributes .append(arena, pub_(*publicity)) .append( arena, if *opaque { OPAQUE_TYPE_SPACE_DOCUMENT } else { TYPE_SPACE_DOCUMENT }, ) .append( arena, if parameters.is_empty() { name.clone().to_doc(arena) } else { let arguments = type_ .parameters .iter() .map(|(_, e)| e.to_doc(arena)) .collect_vec(); type_ .name .clone() .to_doc(arena) .append(arena, self.wrap_arguments(arena, arguments, location.end)) .group(arena) }, ); if constructors.is_empty() { return doc; } let doc = doc.append(arena, SPACE_OPEN_CURLY_DOCUMENT); let inner = arena.concat(constructors.iter().map(|c| { if self.pop_empty_lines(c.location.start) { TWO_LINES_DOCUMENT } else { LINE_DOCUMENT } .append(arena, self.record_constructor(arena, c)) })); // Add any trailing comments let inner = match printed_comments(arena, self.pop_comments(*end_position), false) { Some(comments) => inner.append(arena, LINE_DOCUMENT).append(arena, comments), None => inner, } .nest(arena, INDENT) .group(arena); doc.append(arena, inner) .append(arena, LINE_DOCUMENT) .append(arena, CLOSE_CURLY_DOCUMENT) } fn call_arg( &mut self, arena: &'doc DocumentArena<'a, 'doc>, argument: &'a CallArg, arity: usize, ) -> Document<'a, 'doc> { self.format_call_arg(arena, argument, expr_call_arg_formatting, |this, value| { this.comma_separated_item(arena, value, arity) }) } fn format_call_arg( &mut self, arena: &'doc DocumentArena<'a, 'doc>, argument: &'a CallArg, figure_formatting: F, format_value: G, ) -> Document<'a, 'doc> where F: Fn(&'a CallArg) -> CallArgFormatting<'a, A>, G: Fn(&mut Self, &'a A) -> Document<'a, 'doc>, { match figure_formatting(argument) { CallArgFormatting::Unlabelled(value) => format_value(self, value), CallArgFormatting::ShorthandLabelled(label) => { let comments = self.pop_comments(argument.location.start); let label = label.as_ref().to_doc(arena).append(arena, COLON_DOCUMENT); commented(arena, label, comments) } CallArgFormatting::Labelled(label, value) => { let comments = self.pop_comments(argument.location.start); let label = label .as_ref() .to_doc(arena) .append(arena, COLON_SPACE_DOCUMENT); let value = format_value(self, value); commented(arena, label, comments).append(arena, value) } } } fn record_update_arg( &mut self, arena: &'doc DocumentArena<'a, 'doc>, argument: &'a UntypedRecordUpdateArg, ) -> Document<'a, 'doc> { let comments = self.pop_comments(argument.location.start); match argument { // Argument supplied with a label shorthand. _ if argument.uses_label_shorthand() => commented( arena, argument .label .as_str() .to_doc(arena) .append(arena, COLON_DOCUMENT), comments, ), // Labelled argument. _ => { let doc = argument .label .as_str() .to_doc(arena) .append(arena, COLON_SPACE_DOCUMENT) .append(arena, self.expr(arena, &argument.value)) .group(arena); if argument.value.is_binop() || argument.value.is_pipeline() { commented(arena, doc, comments).nest(arena, INDENT) } else { commented(arena, doc, comments) } } } } fn tuple_index( &mut self, arena: &'doc DocumentArena<'a, 'doc>, tuple: &'a UntypedExpr, index: u64, ) -> Document<'a, 'doc> { // In case we have a block with a single variable tuple access we // remove that redundant wrapper: // // {tuple.1}.0 becomes // tuple.1.0 // if let UntypedExpr::Block { statements, .. } = tuple { match statements.as_slice() { [Statement::Expression(tuple @ UntypedExpr::TupleIndex { tuple: inner, .. })] // We can't apply this change if the inner thing is a // literal tuple because the compiler cannot currently parse // it: `#(1, #(2, 3)).1.0` is a syntax error at the moment. if !inner.is_tuple() => { self.expr(arena,tuple) } _ => self.expr(arena,tuple), } } else { self.expr(arena, tuple) } .append(arena, DOT_DOCUMENT) .append(arena, index) } fn case_clause_value( &mut self, arena: &'doc DocumentArena<'a, 'doc>, expression: &'a UntypedExpr, ) -> Document<'a, 'doc> { match expression { UntypedExpr::Fn { .. } | UntypedExpr::List { .. } | UntypedExpr::Tuple { .. } | UntypedExpr::BitArray { .. } => { let expression_comments = self.pop_comments(expression.location().start); let expression_doc = self.expr(arena, expression); match printed_comments(arena, expression_comments, true) { Some(comments) => LINE_DOCUMENT .append(arena, comments) .append(arena, expression_doc) .nest(arena, INDENT), None => SPACE_DOCUMENT.append(arena, expression_doc), } } UntypedExpr::Case { .. } => LINE_DOCUMENT .append(arena, self.expr(arena, expression)) .nest(arena, INDENT), UntypedExpr::Block { statements, location, .. } => SPACE_DOCUMENT.append(arena, self.block(arena, location, statements, true)), UntypedExpr::Int { .. } | UntypedExpr::Float { .. } | UntypedExpr::String { .. } | UntypedExpr::Var { .. } | UntypedExpr::Call { .. } | UntypedExpr::BinOp { .. } | UntypedExpr::PipeLine { .. } | UntypedExpr::FieldAccess { .. } | UntypedExpr::TupleIndex { .. } | UntypedExpr::Todo { .. } | UntypedExpr::Panic { .. } | UntypedExpr::Echo { .. } | UntypedExpr::RecordUpdate { .. } | UntypedExpr::NegateBool { .. } | UntypedExpr::NegateInt { .. } => BREAKABLE_SPACE_DOCUMENT .append(arena, self.expr(arena, expression).group(arena)) .nest(arena, INDENT), } .next_break_fits(arena, NextBreakFitsMode::Disabled) .group(arena) } fn assigned_value( &mut self, arena: &'doc DocumentArena<'a, 'doc>, expression: &'a UntypedExpr, ) -> Document<'a, 'doc> { match expression { UntypedExpr::Case { .. } => SPACE_DOCUMENT .append(arena, self.expr(arena, expression)) .group(arena), UntypedExpr::Int { .. } | UntypedExpr::Float { .. } | UntypedExpr::String { .. } | UntypedExpr::Block { .. } | UntypedExpr::Var { .. } | UntypedExpr::Fn { .. } | UntypedExpr::List { .. } | UntypedExpr::Call { .. } | UntypedExpr::BinOp { .. } | UntypedExpr::PipeLine { .. } | UntypedExpr::FieldAccess { .. } | UntypedExpr::Tuple { .. } | UntypedExpr::TupleIndex { .. } | UntypedExpr::Todo { .. } | UntypedExpr::Panic { .. } | UntypedExpr::Echo { .. } | UntypedExpr::BitArray { .. } | UntypedExpr::RecordUpdate { .. } | UntypedExpr::NegateBool { .. } | UntypedExpr::NegateInt { .. } => self.case_clause_value(arena, expression), } } fn clause( &mut self, arena: &'doc DocumentArena<'a, 'doc>, clause: &'a UntypedClause, index: u32, ) -> Document<'a, 'doc> { let space_before = self.pop_empty_lines(clause.location.start); let comments = self.pop_comments(clause.location.start); let clause_doc = match &clause.guard { None => self.alternative_patterns(arena, clause), Some(guard) => self .alternative_patterns(arena, clause) .append(arena, BREAKABLE_SPACE_DOCUMENT.nest(arena, INDENT)) .append(arena, IF_SPACE_DOCUMENT) .append( arena, self.clause_guard(arena, guard) .group(arena) .nest(arena, INDENT), ), }; // In case there's a guard or multiple subjects, if we decide to break // the patterns on multiple lines we also want the arrow to end up on // its own line to improve legibility. // // This looks like this: // ```gleam // case wibble, wobble { // Wibble(_), // pretend this goes over the line limit // Wobble(_) // -> todo // // Notice how the arrow is broken on its own line, the same goes // // for patterns with `if` guards. // } // ``` let has_guard = clause.guard.is_some(); let has_multiple_subjects = clause.pattern.len() > 1; let arrow_break = if has_guard || has_multiple_subjects { BREAKABLE_SPACE_DOCUMENT } else { SPACE_DOCUMENT }; let clause_doc = docvec![arena, clause_doc, arrow_break, RIGHT_ARROW_DOCUMENT] .group(arena) .append(arena, self.case_clause_value(arena, &clause.then)) .group(arena); let clause_doc = match printed_comments(arena, comments, false) { Some(comments) => comments .append(arena, LINE_DOCUMENT) .append(arena, clause_doc), None => clause_doc, }; if index == 0 { clause_doc } else if space_before { TWO_LINES_DOCUMENT.append(arena, clause_doc) } else { LINE_DOCUMENT.append(arena, clause_doc) } } fn alternative_patterns( &mut self, arena: &'doc DocumentArena<'a, 'doc>, clause: &'a UntypedClause, ) -> Document<'a, 'doc> { let has_guard = clause.guard.is_some(); let has_multiple_subjects = clause.pattern.len() > 1; // In case there's an `if` guard but no multiple subjects we want to add // additional indentation before the vartical bar separating alternative // patterns `|`. // We're not adding the indentation if there's multiple subjects as that // would make things harder to read, aligning the vertical bar with the // different subjects: // ``` // case wibble, wobble { // Wibble, // Wobble // | Wibble, // <- we don't want this indentation! // Wobble -> todo // } // ``` let alternatives_separator = if has_guard && !has_multiple_subjects { BREAKABLE_SPACE_DOCUMENT .nest(arena, INDENT) .append(arena, VERTICAL_BAR_SPACE_DOCUMENT) } else { BREAKABLE_SPACE_DOCUMENT.append(arena, VERTICAL_BAR_SPACE_DOCUMENT) }; let alternative_patterns = std::iter::once(&clause.pattern) .chain(&clause.alternative_patterns) .enumerate() .map(|(alternative_index, patterns)| { // Here `p` is a single pattern that can be comprised of // multiple subjects. // ```gleam // case wibble, wobble { // True, False // //^^^^^^^^^^^ This is a single pattern with multiple subjects // | _, _ -> todo // } // ``` let is_first_alternative = alternative_index == 0; let pattern_docs = patterns.iter().enumerate().map(|(pattern_index, pattern)| { // There's a small catch in turning each subject into a document. // Sadly we can't simply call `self.pattern` on each subject and // then nest each one in case it gets broken. // The first ever pattern that appears in a case clause (that is // the first subject of the first alternative) must not be nested // further; otherwise, when broken, it would have 2 extra spaces // of indentation: https://github.com/gleam-lang/gleam/issues/2940. let is_first_pattern = pattern_index == 0; let is_first_pattern_of_clause = is_first_pattern && is_first_alternative; let pattern_doc = self.pattern(arena, pattern); if is_first_pattern_of_clause { pattern_doc } else { pattern_doc.nest(arena, INDENT) } }); // We join all subjects with a breakable comma (that's also // going to be nested) and make the subjects into a group to // make sure the formatter tries to keep them on a single line. arena .join(pattern_docs, COMMA_BREAK_DOCUMENT.nest(arena, INDENT)) .group(arena) }); arena.join(alternative_patterns, alternatives_separator) } fn list( &mut self, arena: &'doc DocumentArena<'a, 'doc>, elements: &'a [UntypedExpr], tail: Option<&'a UntypedExpr>, location: &SrcSpan, ) -> Document<'a, 'doc> { if elements.is_empty() { return match tail { Some(tail) => self.expr(arena, tail), // We take all comments that come _before_ the end of the list, // that is all comments that are inside "[" and "]", if there's // any comment we want to put it inside the empty list! None => { let comments = self.pop_comments(location.end); match printed_comments(arena, comments, false) { None => OPEN_CLOSE_SQUARE_DOCUMENT, Some(comments) => OPEN_SQUARE_DOCUMENT .append(arena, EMPTY_BREAK_DOCUMENT.nest(arena, INDENT)) .append(arena, comments) .append(arena, EMPTY_BREAK_DOCUMENT) .append(arena, CLOSE_SQUARE_DOCUMENT) // vvv We want to make sure the comments are on a separate // line from the opening and closing brackets so we // force the breaks to be split on newlines. .force_break(arena), } } }; } let list_packing = self.items_sequence_packing( elements, tail, UntypedExpr::can_have_multiple_per_line, *location, ); let comma = match list_packing { ItemsPacking::FitMultiplePerLine => FLEX_COMMA_DOCUMENT, ItemsPacking::FitOnePerLine | ItemsPacking::BreakOnePerLine => COMMA_BREAK_DOCUMENT, }; let list_size = elements.len() + match tail { Some(_) => 1, None => 0, }; let mut is_empty = true; let mut elements_doc = EMPTY_DOCUMENT; for element in elements { let empty_lines = self.pop_empty_lines(element.location().start); let element_doc = self.comma_separated_item(arena, element, list_size); elements_doc = if is_empty { is_empty = false; element_doc } else if empty_lines { // If there's empty lines before the list item we want to add an // empty line here. Notice how we're making sure no nesting is // added after the comma, otherwise we would be adding needless // whitespace in the empty line! docvec![ arena, elements_doc, comma.set_nesting(arena, 0), LINE_DOCUMENT, element_doc ] } else { docvec![arena, elements_doc, comma, element_doc] }; } elements_doc = elements_doc.next_break_fits(arena, NextBreakFitsMode::Disabled); let doc = OPEN_SQUARE_BREAK_DOCUMENT.append(arena, elements_doc); // We need to keep the last break aside and do not add it immediately // because in case there's a final comment before the closing square // bracket we want to add indentation (to just that break). Otherwise, // the final comment would be less indented than list's elements. let (doc, last_break) = match tail { None => (doc.nest(arena, INDENT), TRAILING_COMMA_BREAK_DOCUMENT), Some(tail) => { let comments = self.pop_comments(tail.location().start); let tail = commented( arena, docvec![arena, DOT_DOT_DOCUMENT, self.expr(arena, tail)], comments, ); ( doc.append(arena, COMMA_BREAK_DOCUMENT) .append(arena, tail) .nest(arena, INDENT), EMPTY_BREAK_DOCUMENT, ) } }; // We get all remaining comments that come before the list's closing // square bracket. // If there's any we add those before the closing square bracket instead // of moving those out of the list. // Otherwise those would be moved out of the list. let comments = self.pop_comments(location.end); let doc = match printed_comments(arena, comments, false) { None => doc .append(arena, last_break) .append(arena, CLOSE_SQUARE_DOCUMENT), Some(comment) => doc .append(arena, last_break.nest(arena, INDENT)) // ^ See how here we're adding the missing indentation to the // final break so that the final comment is as indented as the // list's items. .append(arena, comment) .append(arena, LINE_DOCUMENT) .append(arena, CLOSE_SQUARE_DOCUMENT) .force_break(arena), }; match list_packing { ItemsPacking::FitOnePerLine | ItemsPacking::FitMultiplePerLine => doc.group(arena), ItemsPacking::BreakOnePerLine => doc.force_break(arena), } } fn items_sequence_packing( &self, items: &'a [T], tail: Option<&'a T>, can_have_multiple_per_line: impl Fn(&'a T) -> bool, list_location: SrcSpan, ) -> ItemsPacking { let ends_with_trailing_comma = tail .map(|tail| tail.location().end) .or_else(|| items.last().map(|last| last.location().end)) .is_some_and(|last_element_end| { self.has_trailing_comma(last_element_end, list_location.end) }); let has_multiple_elements_per_line = self.has_items_on_the_same_line(items.iter().chain(tail)); let has_empty_lines_between_elements = match (items.first(), items.last().or(tail)) { (Some(first), Some(last)) => self.empty_lines.first().is_some_and(|empty_line| { *empty_line >= first.location().end && *empty_line < last.location().start }), _ => false, }; if has_empty_lines_between_elements { // If there's any empty line between elements we want to force each // item onto its own line to preserve the empty lines that were // intentionally added. ItemsPacking::BreakOnePerLine } else if !ends_with_trailing_comma { // If the list doesn't end with a trailing comma we try and pack it in // a single line; if we can't we'll put one item per line, no matter // the content of the list. ItemsPacking::FitOnePerLine } else if tail.is_none() && items.iter().all(can_have_multiple_per_line) && has_multiple_elements_per_line && self.spans_multiple_lines(list_location.start, list_location.end) { // If there's a trailing comma, we can have multiple items per line, // and there's already multiple items per line, we try and pack as // many items as possible on each line. // // Note how we only ever try and pack lists where all items are // unbreakable primitives. To pack a list we need to use put // `flex_break`s between each item. // If the items themselves had breaks we could end up in a situation // where an item gets broken making it span multiple lines and the // spaces are not, for example: // // ```gleam // [Constructor("wibble", "lorem ipsum dolor sit amet something something"), Other(1)] // ``` // // If we used flex breaks here the list would be formatted as: // // ```gleam // [ // Constructor( // "wibble", // "lorem ipsum dolor sit amet something something", // ), Other(1) // ] // ``` // // The first item is broken, meaning that once we get to the flex // space separating it from the following one the formatter is not // going to break it since there's enough space in the current line! ItemsPacking::FitMultiplePerLine } else { // If it ends with a trailing comma we will force the list on // multiple lines, with one item per line. ItemsPacking::BreakOnePerLine } } fn has_items_on_the_same_line>( &self, items: T, ) -> bool { let mut previous: Option = None; for item in items { let item_location = item.location(); // A list has multiple items on the same line if two consecutive // ones do not span multiple lines. if let Some(previous) = previous && !self.spans_multiple_lines(previous.end, item_location.start) { return true; } previous = Some(item_location); } false } /// Pretty prints an expression to be used in a comma separated list; for /// example as a list item, a tuple item or as an argument of a function call. fn comma_separated_item( &mut self, arena: &'doc DocumentArena<'a, 'doc>, expression: &'a UntypedExpr, siblings: usize, ) -> Document<'a, 'doc> { // If there's more than one item in the comma separated list and there's a // pipeline or long binary chain, we want to indent those to make it // easier to tell where one item ends and the other starts. // Othewise we just print the expression as a normal expr. match expression { UntypedExpr::BinOp { operator, left, right, .. } if siblings > 1 => { let comments = self.pop_comments(expression.start_byte_index()); let doc = self.bin_op(arena, operator, left, right, true).group(arena); commented(arena, doc, comments) } UntypedExpr::PipeLine { expressions } if siblings > 1 => { let comments = self.pop_comments(expression.start_byte_index()); let doc = self.pipeline(arena, expressions, true).group(arena); commented(arena, doc, comments) } UntypedExpr::Int { .. } | UntypedExpr::Float { .. } | UntypedExpr::String { .. } | UntypedExpr::Block { .. } | UntypedExpr::Var { .. } | UntypedExpr::Fn { .. } | UntypedExpr::List { .. } | UntypedExpr::Call { .. } | UntypedExpr::BinOp { .. } | UntypedExpr::PipeLine { .. } | UntypedExpr::Case { .. } | UntypedExpr::FieldAccess { .. } | UntypedExpr::Tuple { .. } | UntypedExpr::TupleIndex { .. } | UntypedExpr::Todo { .. } | UntypedExpr::Panic { .. } | UntypedExpr::Echo { .. } | UntypedExpr::BitArray { .. } | UntypedExpr::RecordUpdate { .. } | UntypedExpr::NegateBool { .. } | UntypedExpr::NegateInt { .. } => self.expr(arena, expression).group(arena), } } fn pattern( &mut self, arena: &'doc DocumentArena<'a, 'doc>, pattern: &'a UntypedPattern, ) -> Document<'a, 'doc> { let comments = self.pop_comments(pattern.location().start); let doc = match pattern { Pattern::Int { value, .. } => self.int(arena, value), Pattern::Float { value, .. } => self.float(arena, value), Pattern::String { value, .. } => self.string(arena, value), Pattern::Variable { name, .. } => name.to_doc(arena), Pattern::BitArraySize(size) => self.bit_array_size(arena, size), Pattern::Assign { name, pattern, .. } => { if pattern.is_discard() { name.to_doc(arena) } else { self.pattern(arena, pattern) .append(arena, SPACE_AS_SPACE_DOCUMENT) .append(arena, name.as_str()) } } Pattern::Discard { name, .. } => name.to_doc(arena), Pattern::List { elements, tail, .. } => self.list_pattern(arena, elements, tail), Pattern::Constructor { name, arguments, module, spread, location, .. } => self.pattern_constructor(arena, name, arguments, module, *spread, location), Pattern::Tuple { elements, location, .. } => { let arguments = elements .iter() .map(|element| self.pattern(arena, element)) .collect_vec(); "#".to_doc(arena) .append(arena, self.wrap_arguments(arena, arguments, location.end)) .group(arena) } Pattern::BitArray { segments, location, .. } => { let segment_docs = segments .iter() .map(|segment| { bit_array_segment(arena, segment, |pattern| self.pattern(arena, pattern)) }) .collect_vec(); self.bit_array(arena, segment_docs, ItemsPacking::FitOnePerLine, location) } Pattern::StringPrefix { left_side_string: left, right_side_assignment: right, left_side_assignment: left_assign, .. } => { let left = self.string(arena, left); let right = match right { AssignName::Variable(name) => name.to_doc(arena), AssignName::Discard(name) => name.to_doc(arena), }; match left_assign { Some((name, _)) => { docvec![ arena, left, SPACE_AS_SPACE_DOCUMENT, name, SPACE_CONCAT_SPACE_DOCUMENT, right ] } None => docvec![arena, left, SPACE_CONCAT_SPACE_DOCUMENT, right], } } Pattern::Invalid { .. } => panic!("invalid patterns can not be in an untyped ast"), }; commented(arena, doc, comments) } fn bit_array_size( &mut self, arena: &'doc DocumentArena<'a, 'doc>, size: &'a BitArraySize<()>, ) -> Document<'a, 'doc> { match size { BitArraySize::Int { value, .. } => self.int(arena, value), BitArraySize::Variable { name, .. } => name.to_doc(arena), BitArraySize::BinaryOperator { left, right, operator, .. } => { let operator = match operator { IntOperator::Add => SPACE_PLUS_SPACE_DOCUMENT, IntOperator::Subtract => SPACE_MINUS_SPACE_DOCUMENT, IntOperator::Multiply => SPACE_TIMES_SPACE_DOCUMENT, IntOperator::Divide => SPACE_SLASH_SPACE_DOCUMENT, IntOperator::Remainder => SPACE_MODULE_SPACE_DOCUMENT, }; docvec![ arena, self.bit_array_size(arena, left), operator, self.bit_array_size(arena, right) ] } BitArraySize::Block { inner, .. } => self.bit_array_size(arena, inner).surround( arena, OPEN_CURLY_SPACE_DOCUMENT, SPACE_CLOSE_CURLY_DOCUMENT, ), } } fn list_pattern( &mut self, arena: &'doc DocumentArena<'a, 'doc>, elements: &'a [UntypedPattern], tail: &'a Option>, ) -> Document<'a, 'doc> { if elements.is_empty() { return match tail { Some(tail) => self.pattern(arena, &tail.pattern), None => OPEN_CLOSE_SQUARE_DOCUMENT, }; } let elements = arena.join( elements.iter().map(|element| self.pattern(arena, element)), COMMA_BREAK_DOCUMENT, ); let doc = OPEN_SQUARE_BREAK_DOCUMENT.append(arena, elements); match tail { None => doc .nest(arena, INDENT) .append(arena, TRAILING_COMMA_BREAK_DOCUMENT), Some(tail) => { let tail = &tail.pattern; let comments = self.pop_comments(tail.location().start); let tail = if tail.is_discard() { DOT_DOT_DOCUMENT } else { docvec![arena, DOT_DOT_DOCUMENT, self.pattern(arena, tail)] }; let tail = commented(arena, tail, comments); doc.append(arena, COMMA_BREAK_DOCUMENT) .append(arena, tail) .nest(arena, INDENT) .append(arena, EMPTY_BREAK_DOCUMENT) } } .append(arena, "]") .group(arena) } fn pattern_call_arg( &mut self, arena: &'doc DocumentArena<'a, 'doc>, argument: &'a CallArg, ) -> Document<'a, 'doc> { self.format_call_arg( arena, argument, pattern_call_arg_formatting, |this, value| this.pattern(arena, value), ) } pub fn clause_guard_bin_op( &mut self, arena: &'doc DocumentArena<'a, 'doc>, name: &'a BinOp, left: &'a UntypedClauseGuard, right: &'a UntypedClauseGuard, ) -> Document<'a, 'doc> { self.clause_guard_bin_op_side(arena, name, left, left.precedence()) .append(arena, BREAKABLE_SPACE_DOCUMENT) .append(arena, binop(*name)) .append(arena, SPACE_DOCUMENT) .append( arena, self.clause_guard_bin_op_side(arena, name, right, right.precedence() - 1), ) } fn clause_guard_bin_op_side( &mut self, arena: &'doc DocumentArena<'a, 'doc>, name: &BinOp, side: &'a UntypedClauseGuard, // As opposed to `bin_op_side`, here we take the side precedence as an // argument instead of computing it ourselves. That's because // `clause_guard_bin_op` will reduce the precedence of any right side to // make sure the formatter doesn't remove any needed curly bracket. side_precedence: u8, ) -> Document<'a, 'doc> { let side_doc = self.clause_guard(arena, side); match side.bin_op_name() { // In case the other side is a binary operation as well and it can // be grouped together with the current binary operation, the two // docs are simply concatenated, so that they will end up in the // same group and the formatter will try to keep those on a single // line. Some(side_name) if side_name.can_be_grouped_with(name) => { self.operator_side(arena, side_doc, name.precedence(), side_precedence) } // In case the binary operations cannot be grouped together the // other side is treated as a group on its own so that it can be // broken independently of other pieces of the binary operations // chain. _ => self.operator_side( arena, side_doc.group(arena), name.precedence(), side_precedence, ), } } fn clause_guard( &mut self, arena: &'doc DocumentArena<'a, 'doc>, clause_guard: &'a UntypedClauseGuard, ) -> Document<'a, 'doc> { match clause_guard { ClauseGuard::Invalid { .. } => unreachable!("invalid guard made it to formatting"), ClauseGuard::BinaryOperator { operator, left, right, .. } => self.clause_guard_bin_op(arena, operator, left, right), ClauseGuard::Var { name, .. } => name.to_doc(arena), ClauseGuard::TupleIndex { tuple, index, .. } => self .clause_guard(arena, tuple) .append(arena, DOT_DOCUMENT) .append(arena, *index) .to_doc(arena), ClauseGuard::FieldAccess { container, label, .. } => self .clause_guard(arena, container) .append(arena, DOT_DOCUMENT) .append(arena, label) .to_doc(arena), ClauseGuard::ModuleSelect { module_name, label, .. } => module_name .to_doc(arena) .append(arena, DOT_DOCUMENT) .append(arena, label) .to_doc(arena), ClauseGuard::Constant(constant) => self.const_expr(arena, constant), ClauseGuard::Not { expression, .. } => { docvec![ arena, EXCLAMATION_MARK_DOCUMENT, self.clause_guard(arena, expression) ] } ClauseGuard::Block { value, .. } => { wrap_block(arena, self.clause_guard(arena, value)).group(arena) } } } fn constant_call_arg( &mut self, arena: &'doc DocumentArena<'a, 'doc>, argument: &'a CallArg>, ) -> Document<'a, 'doc> { self.format_call_arg( arena, argument, constant_call_arg_formatting, |this, value| this.const_expr(arena, value), ) } fn negate_bool( &mut self, arena: &'doc DocumentArena<'a, 'doc>, expression: &'a UntypedExpr, ) -> Document<'a, 'doc> { match expression { UntypedExpr::NegateBool { value, .. } => self.expr(arena, value), UntypedExpr::BinOp { .. } => { let doc = self.expr(arena, expression); EXCLAMATION_MARK_DOCUMENT.append(arena, wrap_block(arena, doc)) } UntypedExpr::Int { .. } | UntypedExpr::Float { .. } | UntypedExpr::String { .. } | UntypedExpr::Block { .. } | UntypedExpr::Var { .. } | UntypedExpr::Fn { .. } | UntypedExpr::List { .. } | UntypedExpr::Call { .. } | UntypedExpr::PipeLine { .. } | UntypedExpr::Case { .. } | UntypedExpr::FieldAccess { .. } | UntypedExpr::Tuple { .. } | UntypedExpr::TupleIndex { .. } | UntypedExpr::Todo { .. } | UntypedExpr::Panic { .. } | UntypedExpr::Echo { .. } | UntypedExpr::BitArray { .. } | UntypedExpr::RecordUpdate { .. } | UntypedExpr::NegateInt { .. } => { docvec![ arena, EXCLAMATION_MARK_DOCUMENT, self.expr(arena, expression) ] } } } fn negate_int( &mut self, arena: &'doc DocumentArena<'a, 'doc>, expression: &'a UntypedExpr, ) -> Document<'a, 'doc> { match expression { UntypedExpr::NegateInt { value, .. } => self.expr(arena, value), UntypedExpr::Int { value, .. } if value.starts_with('-') => { self.int(arena, &value[1..]) } UntypedExpr::BinOp { .. } => { MINUS_SPACE_DOCUMENT.append(arena, self.expr(arena, expression)) } UntypedExpr::Int { .. } | UntypedExpr::Float { .. } | UntypedExpr::String { .. } | UntypedExpr::Block { .. } | UntypedExpr::Var { .. } | UntypedExpr::Fn { .. } | UntypedExpr::List { .. } | UntypedExpr::Call { .. } | UntypedExpr::PipeLine { .. } | UntypedExpr::Case { .. } | UntypedExpr::FieldAccess { .. } | UntypedExpr::Tuple { .. } | UntypedExpr::TupleIndex { .. } | UntypedExpr::Todo { .. } | UntypedExpr::Panic { .. } | UntypedExpr::Echo { .. } | UntypedExpr::BitArray { .. } | UntypedExpr::RecordUpdate { .. } | UntypedExpr::NegateBool { .. } => { docvec![arena, SUB_INT_DOCUMENT, self.expr(arena, expression)] } } } fn use_( &mut self, arena: &'doc DocumentArena<'a, 'doc>, use_: &'a UntypedUse, ) -> Document<'a, 'doc> { let comments = self.pop_comments(use_.location.start); let call = if use_.call.is_call() { docvec![arena, SPACE_DOCUMENT, self.expr(arena, &use_.call)] } else { docvec![ arena, BREAKABLE_SPACE_DOCUMENT, self.expr(arena, &use_.call) ] .nest(arena, INDENT) } .group(arena); let doc = if use_.assignments.is_empty() { docvec![arena, USE_AND_ARROW_DOCUMENT, call] } else { let assignments = use_.assignments.iter().map(|use_assignment| { let pattern = self.pattern(arena, &use_assignment.pattern); let annotation = use_assignment .annotation .as_ref() .map(|annotation| { COLON_SPACE_DOCUMENT.append(arena, self.type_ast(arena, annotation)) }) .unwrap_or(EMPTY_DOCUMENT); pattern.append(arena, annotation).group(arena) }); let assignments = Itertools::intersperse(assignments, COMMA_BREAK_DOCUMENT); let left = [USE_DOCUMENT, BREAKABLE_SPACE_DOCUMENT] .into_iter() .chain(assignments); let left = arena .concat(left) .nest(arena, INDENT) .append(arena, BREAKABLE_SPACE_DOCUMENT) .group(arena); docvec![arena, left, LEFT_ARROW_DOCUMENT, call].group(arena) }; commented(arena, doc, comments) } fn assert( &mut self, arena: &'doc DocumentArena<'a, 'doc>, assert: &'a UntypedAssert, ) -> Document<'a, 'doc> { let comments = self.pop_comments(assert.location.start); let expression = if assert.value.is_binop() || assert.value.is_pipeline() { self.expr(arena, &assert.value).nest(arena, INDENT) } else { self.expr(arena, &assert.value) }; let doc = self.append_as_message_expression( arena, expression, PrecedingAs::Expression, assert.message.as_ref(), ); commented(arena, docvec![arena, ASSERT_SPACE_DOCUMENT, doc], comments) } fn bit_array( &mut self, arena: &'doc DocumentArena<'a, 'doc>, segments: Vec>, packing: ItemsPacking, location: &SrcSpan, ) -> Document<'a, 'doc> { let comments = self.pop_comments(location.end); let comments_doc = printed_comments(arena, comments, false); // Avoid adding illegal comma in empty bit array by explicitly handling it if segments.is_empty() { // We take all comments that come _before_ the end of the bit array, // that is all comments that are inside "<<" and ">>", if there's // any comment we want to put it inside the empty bit array! // Refer to the `list` function for a similar procedure. return match comments_doc { None => EMPTY_BIT_ARRAY_DOCUMENT, Some(comments) => OPEN_BIT_ARRAY_DOCUMENT .append(arena, EMPTY_BREAK_DOCUMENT.nest(arena, INDENT)) .append(arena, comments) .append(arena, EMPTY_BREAK_DOCUMENT) .append(arena, CLOSE_BIT_ARRAY_DOCUMENT) // vvv We want to make sure the comments are on a separate // line from the opening and closing angle brackets so // we force the breaks to be split on newlines. .force_break(arena), }; } let comma = match packing { ItemsPacking::FitMultiplePerLine => FLEX_COMMA_DOCUMENT, ItemsPacking::FitOnePerLine | ItemsPacking::BreakOnePerLine => COMMA_BREAK_DOCUMENT, }; let last_break = TRAILING_COMMA_BREAK_DOCUMENT; let doc = OPEN_BIT_ARRAY_BREAK_DOCUMENT .append(arena, arena.join(segments, comma)) .nest(arena, INDENT); let doc = match comments_doc { None => doc .append(arena, last_break) .append(arena, CLOSE_BIT_ARRAY_DOCUMENT), Some(comments) => doc .append(arena, last_break.nest(arena, INDENT)) // ^ Notice how in this case we nest the final break before // adding it: this way the comments are going to be as // indented as the bit array items. .append(arena, comments.nest(arena, INDENT)) .append(arena, LINE_DOCUMENT) .append(arena, CLOSE_BIT_ARRAY_DOCUMENT) .force_break(arena), }; match packing { ItemsPacking::FitOnePerLine | ItemsPacking::FitMultiplePerLine => doc.group(arena), ItemsPacking::BreakOnePerLine => doc.force_break(arena), } } fn bit_array_segment_expr( &mut self, arena: &'doc DocumentArena<'a, 'doc>, expression: &'a UntypedExpr, ) -> Document<'a, 'doc> { match expression { UntypedExpr::BinOp { .. } => wrap_block(arena, self.expr(arena, expression)), UntypedExpr::Int { .. } | UntypedExpr::Float { .. } | UntypedExpr::String { .. } | UntypedExpr::Var { .. } | UntypedExpr::Fn { .. } | UntypedExpr::List { .. } | UntypedExpr::Call { .. } | UntypedExpr::PipeLine { .. } | UntypedExpr::Case { .. } | UntypedExpr::FieldAccess { .. } | UntypedExpr::Tuple { .. } | UntypedExpr::TupleIndex { .. } | UntypedExpr::Todo { .. } | UntypedExpr::Panic { .. } | UntypedExpr::Echo { .. } | UntypedExpr::BitArray { .. } | UntypedExpr::RecordUpdate { .. } | UntypedExpr::NegateBool { .. } | UntypedExpr::NegateInt { .. } | UntypedExpr::Block { .. } => self.expr(arena, expression), } } fn statement( &mut self, arena: &'doc DocumentArena<'a, 'doc>, statement: &'a UntypedStatement, ) -> Document<'a, 'doc> { match statement { Statement::Expression(expression) => self.expr(arena, expression), Statement::Assignment(assignment) => self.assignment(arena, assignment), Statement::Use(use_) => self.use_(arena, use_), Statement::Assert(assert) => self.assert(arena, assert), } } fn block( &mut self, arena: &'doc DocumentArena<'a, 'doc>, location: &SrcSpan, statements: &'a Vec1, force_breaks: bool, ) -> Document<'a, 'doc> { let statements_doc = docvec![ arena, BREAKABLE_SPACE_DOCUMENT, self.statements(arena, statements.as_vec()) ] .nest(arena, INDENT); let trailing_comments = self.pop_comments(location.end); let trailing_comments = printed_comments(arena, trailing_comments, false); let block_doc = match trailing_comments { Some(trailing_comments_doc) => docvec![ arena, OPEN_CURLY_DOCUMENT, statements_doc, LINE_DOCUMENT.nest(arena, INDENT), trailing_comments_doc.nest(arena, INDENT), LINE_DOCUMENT, CLOSE_CURLY_DOCUMENT ] .force_break(arena), None => docvec![ arena, OPEN_CURLY_DOCUMENT, statements_doc, BREAKABLE_SPACE_DOCUMENT, CLOSE_CURLY_DOCUMENT ], }; if force_breaks { block_doc.force_break(arena).group(arena) } else { block_doc.group(arena) } } pub fn wrap_function_call_arguments( &mut self, arena: &'doc DocumentArena<'a, 'doc>, arguments: I, location: &SrcSpan, ) -> Document<'a, 'doc> where I: IntoIterator>, { let mut arguments = arguments.into_iter().peekable(); if arguments.peek().is_none() { return OPEN_CLOSE_PAREN_DOCUMENT; } let arguments_doc = EMPTY_BREAK_DOCUMENT .append(arena, arena.join(arguments, COMMA_BREAK_DOCUMENT)) .nest_if_broken(arena, INDENT); // We get all remaining comments that come before the call's closing // parenthesis. // If there's any we add those before the closing parenthesis instead // of moving those out of the call. // Otherwise those would be moved out of the call. let comments = self.pop_comments(location.end); let closing_parens = match printed_comments(arena, comments, false) { None => docvec![arena, TRAILING_COMMA_BREAK_DOCUMENT, CLOSE_PAREN_DOCUMENT], Some(comment) => docvec![ arena, TRAILING_COMMA_BREAK_DOCUMENT.nest(arena, INDENT), comment, LINE_DOCUMENT, CLOSE_PAREN_DOCUMENT ] .force_break(arena), }; OPEN_PAREN_DOCUMENT .append(arena, arguments_doc) .append(arena, closing_parens) .group(arena) } pub fn wrap_arguments( &mut self, arena: &'doc DocumentArena<'a, 'doc>, arguments: I, comments_limit: u32, ) -> Document<'a, 'doc> where I: IntoIterator>, { let mut arguments = arguments.into_iter().peekable(); if arguments.peek().is_none() { let comments = self.pop_comments(comments_limit); return match printed_comments(arena, comments, false) { Some(comments) => OPEN_PAREN_DOCUMENT .to_doc(arena) .append(arena, EMPTY_BREAK_DOCUMENT) .append(arena, comments) .nest_if_broken(arena, INDENT) .force_break(arena) .append(arena, EMPTY_BREAK_DOCUMENT) .append(arena, CLOSE_PAREN_DOCUMENT), None => OPEN_CLOSE_PAREN_DOCUMENT, }; } let doc = OPEN_PAREN_BREAK_DOCUMENT.append(arena, arena.join(arguments, COMMA_BREAK_DOCUMENT)); // Include trailing comments if there are any let comments = self.pop_comments(comments_limit); match printed_comments(arena, comments, false) { Some(comments) => doc .append(arena, TRAILING_COMMA_BREAK_DOCUMENT) .append(arena, comments) .nest_if_broken(arena, INDENT) .force_break(arena) .append(arena, EMPTY_BREAK_DOCUMENT) .append(arena, CLOSE_PAREN_DOCUMENT), None => doc .nest_if_broken(arena, INDENT) .append(arena, TRAILING_COMMA_BREAK_DOCUMENT) .append(arena, CLOSE_PAREN_DOCUMENT), } } pub fn wrap_arguments_with_spread( &mut self, arena: &'doc DocumentArena<'a, 'doc>, arguments: I, comments_limit: u32, ) -> Document<'a, 'doc> where I: IntoIterator>, { let mut arguments = arguments.into_iter().peekable(); if arguments.peek().is_none() { return self.wrap_arguments(arena, arguments, comments_limit); } let doc = OPEN_PAREN_BREAK_DOCUMENT .append(arena, arena.join(arguments, COMMA_BREAK_DOCUMENT)) .append(arena, COMMA_BREAK_DOCUMENT) .append(arena, DOT_DOT_DOCUMENT); // Include trailing comments if there are any let comments = self.pop_comments(comments_limit); match printed_comments(arena, comments, false) { Some(comments) => doc .append(arena, TRAILING_COMMA_BREAK_DOCUMENT) .append(arena, comments) .nest_if_broken(arena, INDENT) .force_break(arena) .append(arena, EMPTY_BREAK_DOCUMENT) .append(arena, CLOSE_PAREN_DOCUMENT), None => doc .nest_if_broken(arena, INDENT) .append(arena, TRAILING_COMMA_BREAK_DOCUMENT) .append(arena, CLOSE_PAREN_DOCUMENT), } } /// Given some regular comments it pretty prints those with any respective /// doc comment that might be preceding those. /// For example: /// /// ```gleam /// /// Doc /// // comment /// /// /// Doc /// pub fn wibble() {} /// ``` /// /// We don't want the first doc comment to be merged together with /// `wibble`'s doc comment, so when we run into comments like `// comment` /// we need to first print all documentation comments that come before it. /// fn printed_documented_comments<'b>( &mut self, arena: &'doc DocumentArena<'a, 'doc>, comments: impl IntoIterator)>, ) -> Option> { let mut comments = comments.into_iter().peekable(); let _ = comments.peek()?; let mut doc = Vec::new(); while let Some(comment) = comments.next() { let (is_doc_commented, comment) = match comment { (comment_start, Some(c)) => { let doc_comment = self.doc_comments(arena, comment_start); let is_doc_commented = !doc_comment.is_empty(); doc.push(doc_comment); (is_doc_commented, c) } (_, None) => continue, }; doc.push(COMMENT_DOCUMENT.append(arena, EcoString::from(comment))); match comments.peek() { // Next line is a comment Some((_, Some(_))) => doc.push(LINE_DOCUMENT), // Next line is empty Some((_, None)) => { let _ = comments.next(); doc.push(TWO_LINES_DOCUMENT); } // We've reached the end, there are no more lines None => { if is_doc_commented { doc.push(TWO_LINES_DOCUMENT); } else { doc.push(LINE_DOCUMENT); } } } } let doc = arena.concat(doc); Some(doc.force_break(arena)) } fn append_as_message_expression( &mut self, arena: &'doc DocumentArena<'a, 'doc>, doc: Document<'a, 'doc>, preceding_as: PrecedingAs, message: Option<&'a UntypedExpr>, ) -> Document<'a, 'doc> { let Some(message) = message else { return doc }; let comments = self.pop_comments(message.location().start); let comments = printed_comments(arena, comments, false); let as_ = match preceding_as { PrecedingAs::Keyword => SPACE_AS_DOCUMENT, PrecedingAs::Expression => { docvec![arena, BREAKABLE_SPACE_DOCUMENT, AS_DOCUMENT].nest(arena, INDENT) } }; let doc = match comments { // If there's comments between the document and the message we want // the `as` bit to be on the same line as the original document and // go on a new indented line with the message and comments: // ```gleam // todo as // // comment! // "wibble" // ``` Some(comments) => docvec![ arena, doc.group(arena), as_, docvec![ arena, LINE_DOCUMENT, comments, LINE_DOCUMENT, self.expr(arena, message).group(arena) ] .nest(arena, INDENT) ], None => { let message = match (preceding_as, message) { // If we have `as` preceded by a keyword (like with `panic` and `todo`) // and the message is a block, we don't want to nest it any further. That is, // we want it to look like this: // ```gleam // panic as { // wibble wobble // } // ``` // instead of this: // ```gleam // panic as { // wibble wobble // } // ``` (PrecedingAs::Keyword, UntypedExpr::Block { .. }) => { self.expr(arena, message).group(arena) } _ => self.expr(arena, message).group(arena).nest(arena, INDENT), }; docvec![arena, doc.group(arena), as_, SPACE_DOCUMENT, message] } }; doc.group(arena) } fn append_as_message_constant( &mut self, arena: &'doc DocumentArena<'a, 'doc>, doc: Document<'a, 'doc>, message: Option<&'a Constant>, ) -> Document<'a, 'doc> { let Some(message) = message else { return doc }; let comments = self.pop_comments(message.location().start); let comments = printed_comments(arena, comments, false); let doc = match comments { // If there's comments between the document and the message we want // the `as` bit to be on the same line as the original document and // go on a new indented line with the message and comments: // ```gleam // todo as // // comment! // "wibble" // ``` Some(comments) => docvec![ arena, doc.group(arena), SPACE_AS_DOCUMENT, docvec![ arena, LINE_DOCUMENT, comments, LINE_DOCUMENT, self.const_expr(arena, message).group(arena) ] .nest(arena, INDENT) ], None => { let message = self .const_expr(arena, message) .group(arena) .nest(arena, INDENT); docvec![arena, doc.group(arena), SPACE_AS_SPACE_DOCUMENT, message] } }; doc.group(arena) } fn echo( &mut self, arena: &'doc DocumentArena<'a, 'doc>, expression: &'a Option>, message: &'a Option>, ) -> Document<'a, 'doc> { let Some(expression) = expression else { return self.append_as_message_expression( arena, ECHO_DOCUMENT, PrecedingAs::Keyword, message.as_deref(), ); }; // When a binary expression gets broken on multiple lines we don't want // it to be on the same line as echo, or it would look confusing; // instead it's nested onto a new line: // // ```gleam // echo first // |> wobble // |> wibble // ``` // // So it's easier to see echo is printing the whole thing. Otherwise, // it would look like echo is printing just the first item: // // ```gleam // echo first // |> wobble // |> wibble // ``` // let doc = self.expr(arena, expression); if expression.is_binop() || expression.is_pipeline() { let doc = self.append_as_message_expression( arena, doc.nest(arena, INDENT), PrecedingAs::Expression, message.as_deref(), ); docvec![arena, ECHO_SPACE_DOCUMENT, doc] } else { docvec![ arena, ECHO_SPACE_DOCUMENT, self.append_as_message_expression( arena, doc, PrecedingAs::Expression, message.as_deref() ) ] } } } /// This is used to describe the kind of things that might preceding an `as` /// message that can be added to various places: `panic`, `echo`, `let assert`, /// `assert`, `todo`. /// /// It might be preceded by a keyword, like with `echo` and `panic`, or by /// an expression, like in `assert` or `let assert`. /// enum PrecedingAs { /// An expression is preceding the `as` message: /// ```gleam /// echo 1 as "message" /// assert 1 == 2 as "message" /// let assert Ok(_) = result as "message" /// ``` /// Expression, /// A keyword is preceding the `as` message: /// ```gleam /// 1 |> echo as "message" /// panic as "message" /// todo as "message" /// ``` /// Keyword, } fn init_and_last(vec: &[T]) -> Option<(&[T], &T)> { match vec { [] => None, _ => match vec.split_at(vec.len() - 1) { (init, [last]) => Some((init, last)), _ => panic!("unreachable"), }, } } impl<'a, 'doc> Documentable<'a, 'doc> for &'a ArgNames { fn to_doc(self, arena: &'doc DocumentArena<'a, 'doc>) -> Document<'a, 'doc> { match self { ArgNames::Named { name, .. } | ArgNames::Discard { name, .. } => name.to_doc(arena), ArgNames::LabelledDiscard { label, name, .. } | ArgNames::NamedLabelled { label, name, .. } => { docvec![arena, label, " ", name] } } } } impl<'a, 'doc> Documentable<'a, 'doc> for &'a UnqualifiedImport { fn to_doc(self, arena: &'doc DocumentArena<'a, 'doc>) -> Document<'a, 'doc> { self.name.as_str().to_doc(arena).append( arena, match &self.as_name { None => EMPTY_DOCUMENT, Some(s) => " as ".to_doc(arena).append(arena, s.as_str()), }, ) } } fn binop<'a, 'doc>(binop: BinOp) -> Document<'a, 'doc> { match binop { BinOp::And => AND_DOCUMENT, BinOp::Or => OR_DOCUMENT, BinOp::LtInt => LT_INT_DOCUMENT, BinOp::LtEqInt => LT_EQ_INT_DOCUMENT, BinOp::LtFloat => LT_FLOAT_DOCUMENT, BinOp::LtEqFloat => LT_EQ_FLOAT_DOCUMENT, BinOp::Eq => EQ_DOCUMENT, BinOp::NotEq => NOT_EQ_DOCUMENT, BinOp::GtEqInt => GT_EQ_INT_DOCUMENT, BinOp::GtInt => GT_INT_DOCUMENT, BinOp::GtEqFloat => GT_EQ_FLOAT_DOCUMENT, BinOp::GtFloat => GT_FLOAT_DOCUMENT, BinOp::AddInt => ADD_INT_DOCUMENT, BinOp::AddFloat => ADD_FLOAT_DOCUMENT, BinOp::SubInt => SUB_INT_DOCUMENT, BinOp::SubFloat => SUB_FLOAT_DOCUMENT, BinOp::MultInt => MULT_INT_DOCUMENT, BinOp::MultFloat => MULT_FLOAT_DOCUMENT, BinOp::DivInt => DIV_INT_DOCUMENT, BinOp::DivFloat => DIV_FLOAT_DOCUMENT, BinOp::RemainderInt => REMAINDER_INT_DOCUMENT, BinOp::Concatenate => CONCAT_DOCUMENT, } } #[allow(clippy::enum_variant_names)] #[derive(Debug)] /// This is used to determine how to fit the items of a list, or the segments of /// a bit array in a line. /// enum ItemsPacking { /// Try and fit everything on a single line; if the items don't fit, break /// the list putting each item into its own line. /// /// ```gleam /// // unbroken /// [1, 2, 3] /// /// // broken /// [ /// 1, /// 2, /// 3, /// ] /// ``` /// FitOnePerLine, /// Try and fit everything on a single line; if the items don't fit, break /// the list putting as many items as possible in a single line. /// /// ```gleam /// // unbroken /// [1, 2, 3] /// /// // broken /// [ /// 1, 2, 3, ... /// 4, 100, /// ] /// ``` /// FitMultiplePerLine, /// Always break the list, putting each item into its own line: /// /// ```gleam /// [ /// 1, /// 2, /// 3, /// ] /// ``` /// BreakOnePerLine, } pub fn comments_before<'a>( comments: &'a [Comment<'a>], empty_lines: &'a [u32], limit: u32, retain_empty_lines: bool, ) -> ( impl Iterator)>, &'a [Comment<'a>], &'a [u32], ) { let end_comments = comments .iter() .position(|comment| comment.start > limit) .unwrap_or(comments.len()); let end_empty_lines = empty_lines .iter() .position(|empty_line| *empty_line > limit) .unwrap_or(empty_lines.len()); let popped_comments = comments .get(0..end_comments) .expect("0..end_comments is guaranteed to be in bounds") .iter() .map(|comment| (comment.start, Some(comment.content))); let popped_empty_lines = if retain_empty_lines { empty_lines } else { &[] } .get(0..end_empty_lines) .unwrap_or(&[]) .iter() .map(|i| (i, i)) // compact consecutive empty lines into a single line .coalesce(|(a_start, a_end), (b_start, b_end)| { if *a_end + 1 == *b_start { Ok((a_start, b_end)) } else { Err(((a_start, a_end), (b_start, b_end))) } }) .map(|l| (*l.0, None)); let popped = popped_comments .merge_by(popped_empty_lines, |(a, _), (b, _)| a < b) .skip_while(|(_, comment_or_line)| comment_or_line.is_none()); ( popped, comments.get(end_comments..).expect("in bounds"), empty_lines.get(end_empty_lines..).expect("in bounds"), ) } fn is_breakable_argument(expression: &UntypedExpr, arity: usize) -> bool { match expression { // A call is only breakable if it is the only argument UntypedExpr::Call { .. } => arity == 1, UntypedExpr::Fn { .. } | UntypedExpr::Block { .. } | UntypedExpr::Case { .. } | UntypedExpr::List { .. } | UntypedExpr::Tuple { .. } | UntypedExpr::BitArray { .. } => true, UntypedExpr::Int { .. } | UntypedExpr::Float { .. } | UntypedExpr::String { .. } | UntypedExpr::Var { .. } | UntypedExpr::BinOp { .. } | UntypedExpr::PipeLine { .. } | UntypedExpr::FieldAccess { .. } | UntypedExpr::TupleIndex { .. } | UntypedExpr::Todo { .. } | UntypedExpr::Panic { .. } | UntypedExpr::Echo { .. } | UntypedExpr::RecordUpdate { .. } | UntypedExpr::NegateBool { .. } | UntypedExpr::NegateInt { .. } => false, } } enum CallArgFormatting<'a, A> { ShorthandLabelled(&'a EcoString), Unlabelled(&'a A), Labelled(&'a EcoString, &'a A), } fn expr_call_arg_formatting(argument: &CallArg) -> CallArgFormatting<'_, UntypedExpr> { match argument { // An argument supplied using label shorthand syntax. _ if argument.uses_label_shorthand() => CallArgFormatting::ShorthandLabelled( argument .label .as_ref() .expect("label shorthand with no label"), ), // A labelled argument. CallArg { label: Some(label), value, .. } => CallArgFormatting::Labelled(label, value), // An unlabelled argument. CallArg { value, .. } => CallArgFormatting::Unlabelled(value), } } fn pattern_call_arg_formatting( argument: &CallArg, ) -> CallArgFormatting<'_, UntypedPattern> { match argument { // An argument supplied using label shorthand syntax. _ if argument.uses_label_shorthand() => CallArgFormatting::ShorthandLabelled( argument .label .as_ref() .expect("label shorthand with no label"), ), // A labelled argument. CallArg { label: Some(label), value, .. } => CallArgFormatting::Labelled(label, value), // An unlabelled argument. CallArg { value, .. } => CallArgFormatting::Unlabelled(value), } } fn constant_call_arg_formatting( argument: &CallArg>, ) -> CallArgFormatting<'_, Constant> { match argument { // An argument supplied using label shorthand syntax. _ if argument.uses_label_shorthand() => CallArgFormatting::ShorthandLabelled( argument .label .as_ref() .expect("label shorthand with no label"), ), // A labelled argument. CallArg { label: Some(label), value, .. } => CallArgFormatting::Labelled(label, value), // An unlabelled argument. CallArg { value, .. } => CallArgFormatting::Unlabelled(value), } } struct AttributesPrinter<'a> { external_erlang: &'a Option<(EcoString, EcoString, SrcSpan)>, external_javascript: &'a Option<(EcoString, EcoString, SrcSpan)>, deprecation: &'a Deprecation, internal: bool, } impl<'a> AttributesPrinter<'a> { pub fn new() -> Self { Self { external_erlang: &None, external_javascript: &None, deprecation: &Deprecation::NotDeprecated, internal: false, } } pub fn set_external_erlang( mut self, external: &'a Option<(EcoString, EcoString, SrcSpan)>, ) -> Self { self.external_erlang = external; self } pub fn set_external_javascript( mut self, external: &'a Option<(EcoString, EcoString, SrcSpan)>, ) -> Self { self.external_javascript = external; self } pub fn set_internal(mut self, publicity: Publicity) -> Self { self.internal = publicity.is_internal(); self } pub fn set_deprecation(mut self, deprecation: &'a Deprecation) -> Self { self.deprecation = deprecation; self } } impl<'a, 'doc> AttributesPrinter<'a> { fn to_doc(&self, arena: &'doc DocumentArena<'a, 'doc>) -> Document<'a, 'doc> { let mut attributes = vec![]; // @deprecated attribute if let Deprecation::Deprecated { message } = self.deprecation { attributes.push(docvec![ arena, DEPRECATED_ATTRIBUTE_QUOTE_DOCUMENT, message, QUOTE_CLOSE_PAREN_DOCUMENT ]) }; // @external attributes if let Some((module, function, _)) = self.external_erlang { attributes.push(docvec![ arena, EXTERNAL_ERLANG_QUOTE_DOCUMENT, module, QUOTE_COMMA_SPACE_QUOTE_DOCUMENT, function, QUOTE_CLOSE_PAREN_DOCUMENT, ]) }; if let Some((module, function, _)) = self.external_javascript { attributes.push(docvec![ arena, EXTERNAL_JAVASCRIPT_QUOTE_DOCUMENT, module, QUOTE_COMMA_SPACE_QUOTE_DOCUMENT, function, QUOTE_CLOSE_PAREN_DOCUMENT ]) }; // @internal attribute if self.internal { attributes.push(INTERNAL_ATTRIBUTE_DOCUMENT); }; if attributes.is_empty() { EMPTY_DOCUMENT } else { arena .join(attributes, arena.line()) .append(arena, arena.line()) } } } pub fn break_block<'a, 'doc>( arena: &'doc DocumentArena<'a, 'doc>, doc: Document<'a, 'doc>, ) -> Document<'a, 'doc> { OPEN_CURLY_DOCUMENT .append(arena, LINE_DOCUMENT.append(arena, doc).nest(arena, INDENT)) .append(arena, LINE_DOCUMENT) .append(arena, CLOSE_CURLY_DOCUMENT) .force_break(arena) } pub fn wrap_block<'a, 'doc>( arena: &'doc DocumentArena<'a, 'doc>, doc: Document<'a, 'doc>, ) -> Document<'a, 'doc> { OPEN_CURLY_BREAK_DOCUMENT .append(arena, doc) .nest(arena, INDENT) .append(arena, BREAKABLE_SPACE_DOCUMENT) .append(arena, CLOSE_CURLY_DOCUMENT) } fn printed_comments<'a, 'doc>( arena: &'doc DocumentArena<'a, 'doc>, comments: impl IntoIterator>, trailing_newline: bool, ) -> Option> { let mut comments = comments.into_iter().peekable(); let _ = comments.peek()?; let mut doc = Vec::new(); while let Some(c) = comments.next() { let c = match c { Some(c) => c, None => continue, }; doc.push("//".to_doc(arena).append(arena, EcoString::from(c))); match comments.peek() { // Next line is a comment Some(Some(_)) => doc.push(LINE_DOCUMENT), // Next line is empty Some(None) => { let _ = comments.next(); match comments.peek() { Some(_) => doc.push(TWO_LINES_DOCUMENT), None => { if trailing_newline { doc.push(TWO_LINES_DOCUMENT); } } } } // We've reached the end, there are no more lines None => { if trailing_newline { doc.push(LINE_DOCUMENT); } } } } let doc = arena.concat(doc); if trailing_newline { Some(doc.force_break(arena)) } else { Some(doc) } } fn commented<'a, 'doc>( arena: &'doc DocumentArena<'a, 'doc>, doc: Document<'a, 'doc>, comments: impl IntoIterator>, ) -> Document<'a, 'doc> { match printed_comments(arena, comments, true) { Some(comments) => comments.append(arena, doc.group(arena)), None => doc, } } fn bit_array_segment<'a, 'doc, Value, Type, ToDoc>( arena: &'doc DocumentArena<'a, 'doc>, segment: &'a BitArraySegment, mut to_doc: ToDoc, ) -> Document<'a, 'doc> where ToDoc: FnMut(&'a Value) -> Document<'a, 'doc>, { match segment { BitArraySegment { value, options, .. } if options.is_empty() => to_doc(value), BitArraySegment { value, options, .. } => { to_doc(value).append(arena, COLON_DOCUMENT).append( arena, arena.join( options .iter() .map(|option| segment_option(arena, option, &mut to_doc)), SUB_INT_DOCUMENT, ), ) } } } fn segment_option<'a, 'doc, ToDoc, Value>( arena: &'doc DocumentArena<'a, 'doc>, option: &'a BitArrayOption, mut to_doc: ToDoc, ) -> Document<'a, 'doc> where ToDoc: FnMut(&'a Value) -> Document<'a, 'doc>, { match option { BitArrayOption::Bytes { .. } => BYTES_DOCUMENT, BitArrayOption::Bits { .. } => BITS_DOCUMENT, BitArrayOption::Int { .. } => INT_DOCUMENT, BitArrayOption::Float { .. } => FLOAT_DOCUMENT, BitArrayOption::Utf8 { .. } => UTF8_DOCUMENT, BitArrayOption::Utf16 { .. } => UTF16_DOCUMENT, BitArrayOption::Utf32 { .. } => UTF32_DOCUMENT, BitArrayOption::Utf8Codepoint { .. } => UTF8_CODEPOINT_DOCUMENT, BitArrayOption::Utf16Codepoint { .. } => UTF16_CODEPOINT_DOCUMENT, BitArrayOption::Utf32Codepoint { .. } => UTF32_CODEPOINT_DOCUMENT, BitArrayOption::Signed { .. } => SIGNED_DOCUMENT, BitArrayOption::Unsigned { .. } => UNSIGNED_DOCUMENT, BitArrayOption::Big { .. } => BIG_DOCUMENT, BitArrayOption::Little { .. } => LITTLE_DOCUMENT, BitArrayOption::Native { .. } => NATIVE_DOCUMENT, BitArrayOption::Size { value, short_form: false, .. } => SIZE_DOCUMENT .append(arena, OPEN_PAREN_DOCUMENT) .append(arena, to_doc(value)) .append(arena, CLOSE_PAREN_DOCUMENT), BitArrayOption::Size { value, short_form: true, .. } => to_doc(value), BitArrayOption::Unit { value, .. } => UNIT_DOCUMENT .append(arena, OPEN_PAREN_DOCUMENT) .append(arena, eco_format!("{value}")) .append(arena, CLOSE_PAREN_DOCUMENT), } } fn pub_<'a, 'doc>(publicity: Publicity) -> Document<'a, 'doc> { match publicity { Publicity::Public | Publicity::Internal { .. } => PUB_SPACE_DOCUMENT, Publicity::Private => EMPTY_DOCUMENT, } }