Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/configuration/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,18 @@ impl ConfigurationBuilder {
self.insert("arrayExpression.preferHanging", value.to_string().into())
}

/// The maximum width for array elements before wrapping to a new line.
/// When set, arrays will fit as many elements as possible on each line
/// within this width constraint.
///
/// Default: `None` (disabled)
pub fn array_expression_max_width(&mut self, value: Option<u32>) -> &mut Self {
match value {
Some(width) => self.insert("arrayExpression.maxWidth", (width as i32).into()),
None => self.insert("arrayExpression.maxWidth", ConfigKeyValue::Null),
}
}

pub fn array_pattern_prefer_hanging(&mut self, value: bool) -> &mut Self {
self.insert("arrayPattern.preferHanging", value.into())
}
Expand Down
1 change: 1 addition & 0 deletions src/configuration/resolve_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ pub fn resolve_config(config: ConfigKeyMap, global_config: &GlobalConfiguration)
/* prefer hanging */
arguments_prefer_hanging: get_value(&mut config, "arguments.preferHanging", prefer_hanging_granular, &mut diagnostics),
array_expression_prefer_hanging: get_value(&mut config, "arrayExpression.preferHanging", prefer_hanging_granular, &mut diagnostics),
array_expression_max_width: get_nullable_value(&mut config, "arrayExpression.maxWidth", &mut diagnostics),
array_pattern_prefer_hanging: get_value(&mut config, "arrayPattern.preferHanging", prefer_hanging, &mut diagnostics),
do_while_statement_prefer_hanging: get_value(&mut config, "doWhileStatement.preferHanging", prefer_hanging, &mut diagnostics),
export_declaration_prefer_hanging: get_value(&mut config, "exportDeclaration.preferHanging", prefer_hanging, &mut diagnostics),
Expand Down
2 changes: 2 additions & 0 deletions src/configuration/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,8 @@ pub struct Configuration {
pub arguments_prefer_hanging: PreferHanging,
#[serde(rename = "arrayExpression.preferHanging")]
pub array_expression_prefer_hanging: PreferHanging,
#[serde(rename = "arrayExpression.maxWidth")]
pub array_expression_max_width: Option<u32>,
#[serde(rename = "arrayPattern.preferHanging")]
pub array_pattern_prefer_hanging: bool,
#[serde(rename = "doWhileStatement.preferHanging")]
Expand Down
183 changes: 166 additions & 17 deletions src/generation/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1602,6 +1602,10 @@ fn gen_named_import_or_export_specifiers<'a>(opts: GenNamedImportOrExportSpecifi
/* expressions */

fn gen_array_expr<'a>(node: &ArrayLit<'a>, context: &mut Context<'a>) -> PrintItems {
if let Some(max_width) = context.config.array_expression_max_width {
return gen_array_expr_with_max_width(node, max_width, context);
}

let prefer_hanging = match context.config.array_expression_prefer_hanging {
PreferHanging::Never => false,
PreferHanging::OnlySingleItem => node.elems.len() == 1,
Expand All @@ -1620,6 +1624,152 @@ fn gen_array_expr<'a>(node: &ArrayLit<'a>, context: &mut Context<'a>) -> PrintIt
)
}

fn generate_element_text<'a>(node: Node<'a>, context: &mut Context<'a>) -> String {
node.text_fast(context.program).to_string()
}

fn generate_width_constrained_elements(
element_texts: &[String],
max_width: u32,
context: &mut Context<'_>,
) -> PrintItems {
let mut items = PrintItems::new();

items.push_signal(Signal::StartIndent);
items.push_signal(Signal::NewLine);

let mut current_line_items = Vec::new();
let mut current_line_width = context.config.indent_width as usize;

for (i, text) in element_texts.iter().enumerate() {
let is_last_element_globally = i == element_texts.len() - 1;
let element_base_width = text.len();

let width_with_element = current_line_width + element_base_width;
let width_with_separator = if current_line_items.is_empty() {
width_with_element
} else {
width_with_element + 2
};

if !current_line_items.is_empty() && width_with_separator > max_width as usize {
generate_array_line(&mut items, &current_line_items, false);
items.push_signal(Signal::NewLine);
current_line_items.clear();
current_line_width = context.config.indent_width as usize;
}

current_line_items.push((text.clone(), !is_last_element_globally));

if current_line_items.len() == 1 {
current_line_width += element_base_width;
} else {
current_line_width += 2 + element_base_width;
}
}

if !current_line_items.is_empty() {
let should_add_trailing_comma = match context.config.array_expression_trailing_commas {
TrailingCommas::Always => true,
TrailingCommas::OnlyMultiLine => true,
TrailingCommas::Never => false,
};
generate_array_line(&mut items, &current_line_items, should_add_trailing_comma);
}

items.push_signal(Signal::NewLine);
items.push_signal(Signal::FinishIndent);
items
}

fn generate_array_line(items: &mut PrintItems, line_items: &[(String, bool)], add_trailing_comma: bool) {
for (i, (text, has_comma_between)) in line_items.iter().enumerate() {
items.push_string(text.clone());

let is_last_element = i == line_items.len() - 1;
let needs_comma = *has_comma_between || (is_last_element && add_trailing_comma);

if needs_comma {
items.push_sc(sc!(","));
if !is_last_element {
items.push_sc(sc!(" "));
}
}
}
}

fn gen_array_expr_with_max_width<'a>(node: &ArrayLit<'a>, max_width: u32, context: &mut Context<'a>) -> PrintItems {
let nodes: Vec<Option<Node<'a>>> = node.elems.iter().map(|&x| x.map(|elem| elem.into())).collect();

if nodes.is_empty() {
return gen_surrounded_by_tokens(
|_| PrintItems::new(),
|_| None,
GenSurroundedByTokensOptions {
open_token: sc!("["),
close_token: sc!("]"),
range: Some(node.range()),
first_member: None,
prefer_single_line_when_empty: true,
allow_open_token_trailing_comments: true,
single_line_space_around: context.config.array_expression_space_around,
},
context,
);
}

let element_texts: Vec<String> = nodes
.iter()
.filter_map(|node_opt| *node_opt)
.map(|node| generate_element_text(node, context))
.collect();

let single_line_text = element_texts.join(", ");
let bracket_and_space_width = 2 + if context.config.array_expression_space_around { 2 } else { 0 };
let total_single_line_width = single_line_text.len() + bracket_and_space_width;

if total_single_line_width <= max_width as usize {
return gen_surrounded_by_tokens(
|_context| {
let mut inner_items = PrintItems::new();
for (i, text) in element_texts.iter().enumerate() {
inner_items.push_string(text.clone());
if i < element_texts.len() - 1 {
inner_items.push_sc(sc!(", "));
}
}
inner_items
},
|_| None,
GenSurroundedByTokensOptions {
open_token: sc!("["),
close_token: sc!("]"),
range: Some(node.range()),
first_member: nodes.first().and_then(|n| n.as_ref()).map(|n| n.range()),
prefer_single_line_when_empty: true,
allow_open_token_trailing_comments: true,
single_line_space_around: context.config.array_expression_space_around,
},
context,
);
}

gen_surrounded_by_tokens(
|context| generate_width_constrained_elements(&element_texts, max_width, context),
|_| None,
GenSurroundedByTokensOptions {
open_token: sc!("["),
close_token: sc!("]"),
range: Some(node.range()),
first_member: nodes.first().and_then(|n| n.as_ref()).map(|n| n.range()),
prefer_single_line_when_empty: true,
allow_open_token_trailing_comments: true,
single_line_space_around: false,
},
context,
)
}

fn gen_arrow_func_expr<'a>(node: &'a ArrowExpr<'a>, context: &mut Context<'a>) -> PrintItems {
let items = gen_inner(node, context);
return if should_add_parens_around_expr(node.into(), context) {
Expand Down Expand Up @@ -8863,14 +9013,14 @@ fn gen_conditional_brace_body<'a>(opts: GenConditionalBraceBodyOptions<'a>, cont
items.push_info(end_ln);
items.push_reevaluation(open_brace_condition_reevaluation);

// return result
return GenConditionalBraceBodyResult {
GenConditionalBraceBodyResult {
generated_node: items,
open_brace_condition_ref,
close_brace_condition_ref,
};
}
}

fn get_should_use_new_line<'a>(
fn get_should_use_new_line<'a>(
body_node: Node,
body_should_be_multi_line: bool,
single_body_position: &Option<SameOrNextLinePosition>,
Expand Down Expand Up @@ -8915,7 +9065,7 @@ fn gen_conditional_brace_body<'a>(opts: GenConditionalBraceBodyOptions<'a>, cont
}
}

fn get_body_should_be_multi_line<'a>(body_node: Node<'a>, header_trailing_comments: &[&'a Comment], context: &mut Context<'a>) -> bool {
fn get_body_should_be_multi_line<'a>(body_node: Node<'a>, header_trailing_comments: &[&'a Comment], context: &mut Context<'a>) -> bool {
if let Node::BlockStmt(body_node) = body_node {
if body_node.stmts.len() == 1 && !has_leading_comment_on_different_line(&body_node.stmts[0].range(), header_trailing_comments, context.program) {
return false;
Expand All @@ -8928,22 +9078,22 @@ fn gen_conditional_brace_body<'a>(opts: GenConditionalBraceBodyOptions<'a>, cont
return has_leading_comment_on_different_line(&body_node.range(), header_trailing_comments, context.program);
}

fn has_leading_comment_on_different_line<'a>(node: &SourceRange, header_trailing_comments: &[&'a Comment], program: Program<'a>) -> bool {
node_helpers::has_leading_comment_on_different_line(node, /* comments to ignore */ Some(header_trailing_comments), program)
}
fn has_leading_comment_on_different_line<'a>(node: &SourceRange, header_trailing_comments: &[&'a Comment], program: Program<'a>) -> bool {
node_helpers::has_leading_comment_on_different_line(node, /* comments to ignore */ Some(header_trailing_comments), program)
}
}

fn get_force_braces(body_node: Node) -> bool {
fn get_force_braces(body_node: Node) -> bool {
if let Node::BlockStmt(body_node) = body_node {
body_node.stmts.is_empty()
|| body_node.stmts.iter().all(|s| s.kind() == NodeKind::EmptyStmt)
|| (body_node.stmts.len() == 1 && matches!(body_node.stmts[0], Stmt::Decl(_)))
} else {
false
}
}
}

fn get_header_trailing_comments<'a>(body_node: Node<'a>, context: &mut Context<'a>) -> Vec<&'a Comment> {
fn get_header_trailing_comments<'a>(body_node: Node<'a>, context: &mut Context<'a>) -> Vec<&'a Comment> {
let mut comments = Vec::new();
if let Node::BlockStmt(block_stmt) = body_node {
let comment_line = body_node.leading_comments_fast(context.program).find(|c| c.kind == CommentKind::Line);
Expand Down Expand Up @@ -8972,12 +9122,11 @@ fn gen_conditional_brace_body<'a>(opts: GenConditionalBraceBodyOptions<'a>, cont
comments
}

fn get_open_brace_token<'a>(body_node: Node<'a>, context: &mut Context<'a>) -> Option<&'a TokenAndSpan> {
if let Node::BlockStmt(block_stmt) = body_node {
context.token_finder.get_first_open_brace_token_within(block_stmt)
} else {
None
}
fn get_open_brace_token<'a>(body_node: Node<'a>, context: &mut Context<'a>) -> Option<&'a TokenAndSpan> {
if let Node::BlockStmt(block_stmt) = body_node {
context.token_finder.get_first_open_brace_token_within(block_stmt)
} else {
None
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
~~ arrayExpression.maxWidth: 50 ~~
== should format array elements with max width constraint ==
const values = [0x90, 0x94, 0x19, 0x21, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x2d, 0x69, 0x64, 0x09, 0x31, 0x09, 0x31, 0x09, 0x31, 0x30, 0x30, 0x09, 0x45, 0x55, 0x52];

[expect]
const values = [
0x90, 0x94, 0x19, 0x21, 0x72, 0x61, 0x6e, 0x64,
0x6f, 0x6d, 0x2d, 0x69, 0x64, 0x09, 0x31, 0x09,
0x31, 0x09, 0x31, 0x30, 0x30, 0x09, 0x45, 0x55,
0x52,
];

== should fit short arrays on single line ==
const short = [1, 2, 3];

[expect]
const short = [1, 2, 3];

== should handle empty arrays ==
const empty = [];

[expect]
const empty = [];

== should handle single element arrays ==
const single = [0x90];

[expect]
const single = [0x90];

== should wrap longer strings appropriately ==
const strings = ["hello", "world", "this", "is", "a", "test", "more"];

[expect]
const strings = [
"hello", "world", "this", "is", "a", "test",
"more",
];