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()
}
Comment on lines +1627 to +1629
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not an accurate way to measure the text of a node because the node could have things like spaces or newlines and bad formatting, so the width might be different once printed. Similarly, in this other PR it's not an accurate way to figure out the node width: https://github.com/dprint/dprint-plugin-typescript/pull/737/changes#diff-d83dd6f700946697666bb782a867b64343578bc2067c1904c08060c5f21009d1R258

Both of these are difficult to do properly and I'm not sure I want to maintain them. Sorry!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback! It would really help me to know if you think these issues (both this one and #737) are actually worth solving within dprint typescript plugin.

If I get clear direction on the approach you'd prefer, I'm happy to implement it, but I don't want to push for features that don't align with your vision for the module. I'd rather spend my time on things you actually find valuable for the project.


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",
];