diff --git a/src/configuration/builder.rs b/src/configuration/builder.rs index aeaaa4d3..e1e6e78c 100644 --- a/src/configuration/builder.rs +++ b/src/configuration/builder.rs @@ -1077,6 +1077,67 @@ impl ConfigurationBuilder { self.insert("whileStatement.spaceAround", value.into()) } + /* alignment */ + + /// Enable all alignment options at once. + /// + /// When set to `true`, all individual alignment settings are enabled by default. + /// Individual settings can still be set to `false` to override this global setting. + /// + /// Default: `false` + pub fn alignment_enable_all(&mut self, value: bool) -> &mut Self { + self.insert("alignment.enableAll", value.into()) + } + + /// Whether to align assignments with surrounding assignments. + /// + /// Default: `false` + pub fn variable_statement_align_assignments(&mut self, value: bool) -> &mut Self { + self.insert("variableStatement.alignAssignments", value.into()) + } + + /// Whether to align object properties with surrounding properties. + /// + /// Default: `false` + pub fn object_expression_align_properties(&mut self, value: bool) -> &mut Self { + self.insert("objectExpression.alignProperties", value.into()) + } + + /// Whether to align interface properties with surrounding properties. + /// + /// Default: `false` + pub fn interface_declaration_align_properties(&mut self, value: bool) -> &mut Self { + self.insert("interfaceDeclaration.alignProperties", value.into()) + } + + /// Whether to align type literal properties with surrounding properties. + /// + /// Default: `false` + pub fn type_literal_align_properties(&mut self, value: bool) -> &mut Self { + self.insert("typeLiteral.alignProperties", value.into()) + } + + /// Whether to align class properties with surrounding properties. + /// + /// Default: `false` + pub fn class_declaration_align_properties(&mut self, value: bool) -> &mut Self { + self.insert("classDeclaration.alignProperties", value.into()) + } + + /// Whether to align enum members with surrounding members. + /// + /// Default: `false` + pub fn enum_declaration_align_members(&mut self, value: bool) -> &mut Self { + self.insert("enumDeclaration.alignMembers", value.into()) + } + + /// Whether to align module declaration properties with surrounding properties. + /// + /// Default: `false` + pub fn module_declaration_align_properties(&mut self, value: bool) -> &mut Self { + self.insert("moduleDeclaration.alignProperties", value.into()) + } + #[cfg(test)] pub(super) fn get_inner_config(&self) -> ConfigKeyMap { self.config.clone() @@ -1294,10 +1355,18 @@ mod tests { .paren_expression_space_around(true) .switch_statement_space_around(true) .tuple_type_space_around(true) - .while_statement_space_around(true); + .while_statement_space_around(true) + .alignment_enable_all(true) + .variable_statement_align_assignments(true) + .object_expression_align_properties(true) + .interface_declaration_align_properties(true) + .type_literal_align_properties(true) + .class_declaration_align_properties(true) + .enum_declaration_align_members(true) + .module_declaration_align_properties(true); let inner_config = config.get_inner_config(); - assert_eq!(inner_config.len(), 182); + assert_eq!(inner_config.len(), 190); let diagnostics = resolve_config(inner_config, &Default::default()).diagnostics; assert_eq!(diagnostics.len(), 0); } diff --git a/src/configuration/resolve_config.rs b/src/configuration/resolve_config.rs index 486b2365..b7981678 100644 --- a/src/configuration/resolve_config.rs +++ b/src/configuration/resolve_config.rs @@ -333,6 +333,32 @@ pub fn resolve_config(config: ConfigKeyMap, global_config: &GlobalConfiguration) switch_statement_space_around: get_value(&mut config, "switchStatement.spaceAround", space_around, &mut diagnostics), tuple_type_space_around: get_value(&mut config, "tupleType.spaceAround", space_around, &mut diagnostics), while_statement_space_around: get_value(&mut config, "whileStatement.spaceAround", space_around, &mut diagnostics), + /* alignment - initialize with defaults */ + alignment_enable_all: false, + variable_statement_align_assignments: false, + object_expression_align_properties: false, + interface_declaration_align_properties: false, + type_literal_align_properties: false, + class_declaration_align_properties: false, + enum_declaration_align_members: false, + module_declaration_align_properties: false, + }; + + // Get alignment.enableAll setting to use as default for individual alignment settings + let alignment_enable_all = get_value(&mut config, "alignment.enableAll", false, &mut diagnostics); + + let resolved_config = Configuration { + /* alignment */ + alignment_enable_all, + variable_statement_align_assignments: get_value(&mut config, "variableStatement.alignAssignments", alignment_enable_all, &mut diagnostics), + object_expression_align_properties: get_value(&mut config, "objectExpression.alignProperties", alignment_enable_all, &mut diagnostics), + interface_declaration_align_properties: get_value(&mut config, "interfaceDeclaration.alignProperties", alignment_enable_all, &mut diagnostics), + type_literal_align_properties: get_value(&mut config, "typeLiteral.alignProperties", alignment_enable_all, &mut diagnostics), + class_declaration_align_properties: get_value(&mut config, "classDeclaration.alignProperties", alignment_enable_all, &mut diagnostics), + enum_declaration_align_members: get_value(&mut config, "enumDeclaration.alignMembers", alignment_enable_all, &mut diagnostics), + module_declaration_align_properties: get_value(&mut config, "moduleDeclaration.alignProperties", alignment_enable_all, &mut diagnostics), + // Copy all the previous fields from resolved_config + ..resolved_config }; diagnostics.extend(get_unknown_property_diagnostics(config)); @@ -410,4 +436,63 @@ mod tests { assert_eq!(result.config.line_width, expected_config.line_width); assert_eq!(result.diagnostics.len(), 0); } + + #[test] + fn handle_global_alignment_enable_all() { + let mut config = ConfigKeyMap::new(); + config.insert(String::from("alignment.enableAll"), ConfigKeyValue::from_bool(true)); + let global_config = GlobalConfiguration::default(); + let result = resolve_config(config, &global_config); + + // When alignment.enableAll is true, all individual alignment settings should be true by default + assert!(result.config.alignment_enable_all); + assert!(result.config.variable_statement_align_assignments); + assert!(result.config.object_expression_align_properties); + assert!(result.config.interface_declaration_align_properties); + assert!(result.config.type_literal_align_properties); + assert!(result.config.class_declaration_align_properties); + assert!(result.config.enum_declaration_align_members); + assert!(result.config.module_declaration_align_properties); + assert_eq!(result.diagnostics.len(), 0); + } + + #[test] + fn handle_global_alignment_enable_all_with_individual_override() { + let mut config = ConfigKeyMap::new(); + config.insert(String::from("alignment.enableAll"), ConfigKeyValue::from_bool(true)); + // Override one individual setting to false + config.insert(String::from("variableStatement.alignAssignments"), ConfigKeyValue::from_bool(false)); + let global_config = GlobalConfiguration::default(); + let result = resolve_config(config, &global_config); + + // alignment.enableAll is true but variableStatement.alignAssignments should be false due to override + assert!(result.config.alignment_enable_all); + assert!(!result.config.variable_statement_align_assignments); // overridden to false + assert!(result.config.object_expression_align_properties); // should still be true from global setting + assert!(result.config.interface_declaration_align_properties); + assert!(result.config.type_literal_align_properties); + assert!(result.config.class_declaration_align_properties); + assert!(result.config.enum_declaration_align_members); + assert!(result.config.module_declaration_align_properties); + assert_eq!(result.diagnostics.len(), 0); + } + + #[test] + fn handle_alignment_defaults() { + let config = ConfigKeyMap::new(); + // No alignment settings provided + let global_config = GlobalConfiguration::default(); + let result = resolve_config(config, &global_config); + + // All alignment settings should be false by default + assert!(!result.config.alignment_enable_all); + assert!(!result.config.variable_statement_align_assignments); + assert!(!result.config.object_expression_align_properties); + assert!(!result.config.interface_declaration_align_properties); + assert!(!result.config.type_literal_align_properties); + assert!(!result.config.class_declaration_align_properties); + assert!(!result.config.enum_declaration_align_members); + assert!(!result.config.module_declaration_align_properties); + assert_eq!(result.diagnostics.len(), 0); + } } diff --git a/src/configuration/types.rs b/src/configuration/types.rs index 1e67d7b7..720c0341 100644 --- a/src/configuration/types.rs +++ b/src/configuration/types.rs @@ -662,4 +662,21 @@ pub struct Configuration { pub tuple_type_space_around: bool, #[serde(rename = "whileStatement.spaceAround")] pub while_statement_space_around: bool, + /* alignment */ + #[serde(rename = "alignment.enableAll")] + pub alignment_enable_all: bool, + #[serde(rename = "variableStatement.alignAssignments")] + pub variable_statement_align_assignments: bool, + #[serde(rename = "objectExpression.alignProperties")] + pub object_expression_align_properties: bool, + #[serde(rename = "interfaceDeclaration.alignProperties")] + pub interface_declaration_align_properties: bool, + #[serde(rename = "typeLiteral.alignProperties")] + pub type_literal_align_properties: bool, + #[serde(rename = "classDeclaration.alignProperties")] + pub class_declaration_align_properties: bool, + #[serde(rename = "enumDeclaration.alignMembers")] + pub enum_declaration_align_members: bool, + #[serde(rename = "moduleDeclaration.alignProperties")] + pub module_declaration_align_properties: bool, } diff --git a/src/generation/context.rs b/src/generation/context.rs index 13b23f7f..4cc0e62a 100644 --- a/src/generation/context.rs +++ b/src/generation/context.rs @@ -14,6 +14,15 @@ use dprint_core::formatting::LineStartIndentLevel; use rustc_hash::FxHashMap; use rustc_hash::FxHashSet; +/// Stores alignment information for variable statements and object expressions. +#[derive(Debug, Clone)] +pub struct AlignmentGroup { + /// Maximum width of the left-hand side for alignment calculation. + pub max_left_width: usize, +} + +use deno_ast::SourceTextProvider; +use super::swc::is_text_valid_identifier; use super::*; use crate::configuration::*; use crate::utils::Stack; @@ -72,6 +81,8 @@ pub struct Context<'a> { before_comments_start_info_stack: Stack<(SourceRange, LineNumber, IsStartOfLine)>, if_stmt_last_brace_condition_ref: Option, expr_stmt_single_line_parent_brace_ref: Option, + /// Stores alignment information for variable statements and object expressions. + pub alignment_groups: FxHashMap, /// Used for ensuring nodes are parsed in order. #[cfg(debug_assertions)] pub last_generated_node_pos: SourcePos, @@ -106,6 +117,7 @@ impl<'a> Context<'a> { before_comments_start_info_stack: Default::default(), if_stmt_last_brace_condition_ref: None, expr_stmt_single_line_parent_brace_ref: None, + alignment_groups: Default::default(), #[cfg(debug_assertions)] last_generated_node_pos: deno_ast::SourceTextInfoProvider::text_info(&program).range().start.into(), diagnostics: Vec::new(), @@ -241,4 +253,59 @@ impl<'a> Context<'a> { panic!("Debug Panic Expected text `{expected_text}`, but found `{actual_text}`") } } + + /// Measures the display width of a node's text representation + pub fn measure_node_width(&self, node: Node) -> usize { + match node { + Node::Ident(ident) => ident.sym().chars().count(), + Node::BindingIdent(binding) => binding.id.sym().chars().count(), + Node::PrivateName(name) => name.name().chars().count() + 1, // +1 for the # + Node::Str(str_lit) => { + // For string keys in objects, measure the actual key length + if self.config.quote_props == QuoteProps::AsNeeded { + // Check if quotes are needed + let value = str_lit.value(); + if is_text_valid_identifier(value) { + value.chars().count() + } else { + value.chars().count() + 2 // +2 for quotes + } + } else { + str_lit.value().chars().count() + 2 // +2 for quotes + } + } + Node::Number(num) => num.raw().as_ref().map(|r| r.chars().count()).unwrap_or(8), // fallback for numeric keys + Node::ComputedPropName(computed) => { + // For computed properties like [key]: value, measure [key] + 2 + self.measure_node_width(computed.expr.as_node()) // +2 for brackets + } + Node::ArrayPat(array_pat) => { + // For array destructuring: [a, b, c] = ... + let elems_width: usize = array_pat.elems.iter() + .filter_map(|elem| elem.as_ref()) + .map(|elem| self.measure_node_width(elem.as_node())) + .sum(); + let separators_width = if array_pat.elems.len() > 1 { (array_pat.elems.len() - 1) * 2 } else { 0 }; // ", " between elements + 2 + elems_width + separators_width // +2 for brackets + } + Node::ObjectPat(object_pat) => { + // For object destructuring: { a, b, c } = ... + let props_width: usize = object_pat.props.iter() + .map(|prop| match prop { + ObjectPatProp::Assign(assign) => assign.key.id.sym().chars().count(), + ObjectPatProp::KeyValue(kv) => self.measure_node_width(kv.key.as_node()), + ObjectPatProp::Rest(_) => 3, // "..." + }) + .sum(); + let separators_width = if object_pat.props.len() > 1 { (object_pat.props.len() - 1) * 2 } else { 0 }; // ", " between props + 4 + props_width + separators_width // +4 for "{ " and " }" + } + _ => { + // Fallback: estimate based on node text span (less accurate but works) + let range = node.range(); + let start_pos = self.program.start_pos(); + (range.end.as_byte_index(start_pos) - range.start.as_byte_index(start_pos)).min(50) // cap at reasonable width + } + } + } } diff --git a/src/generation/generate.rs b/src/generation/generate.rs index d5090dc9..78d09c3f 100644 --- a/src/generation/generate.rs +++ b/src/generation/generate.rs @@ -714,7 +714,43 @@ fn gen_class_prop_common<'a, 'b>(node: GenClassPropCommon<'a, 'b>, context: &mut items.extend(gen_type_ann_with_colon_if_exists(node.type_ann, context)); if let Some(value) = node.value { - items.extend(gen_assignment(value.into(), sc!("="), context)); + if context.config.class_declaration_align_properties { + if let Some(parent) = node.original.parent() { + if parent.is::() { + // try grouped alignment first, then class-wide alignment + let alignment_group = context.alignment_groups.get(&node.original.range()) + .or_else(|| context.alignment_groups.get(&parent.range())); + + if let Some(alignment_group) = alignment_group { + let key_width = context.measure_node_width(node.key); + let max_key_width = alignment_group.max_left_width; + + let target_width = max_key_width; + let current_width = key_width; + let padding_needed = if target_width > current_width { + target_width - current_width + } else { + 0 + }; + + for _ in 0..padding_needed { + items.push_space(); + } + + items.extend(gen_assignment(value.into(), sc!("="), context)); + } else { + items.extend(gen_assignment(value.into(), sc!("="), context)); + } + } else { + items.extend(gen_assignment(value.into(), sc!("="), context)); + } + } else { + items.extend(gen_assignment(value.into(), sc!("="), context)); + } + } else { + // Fall back to regular assignment + items.extend(gen_assignment(value.into(), sc!("="), context)); + } } let should_semi = context.config.semi_colons.is_true() @@ -952,6 +988,12 @@ fn gen_class_decl_or_expr<'a>(node: ClassDeclOrExpr<'a>, context: &mut Context<' context, )); + if context.config.class_declaration_align_properties && node.members.len() > 1 { + if let Node::Class(class_node) = node.member_node { + prepare_class_alignment(class_node, context); + } + } + items.extend(context.with_maybe_consistent_props( node.members, |members| use_consistent_quotes_for_members(members.iter().copied()), @@ -1036,6 +1078,10 @@ fn gen_enum_decl<'a>(node: &TsEnumDecl<'a>, context: &mut Context<'a>) -> PrintI items.push_sc(sc!("enum ")); items.extend(gen_node(node.id.into(), context)); + if context.config.enum_declaration_align_members && node.members.len() > 1 { + prepare_enum_alignment(node, context); + } + // body let member_spacing = context.config.enum_declaration_member_spacing; items.extend(gen_membered_body( @@ -1059,10 +1105,42 @@ fn gen_enum_decl<'a>(node: &TsEnumDecl<'a>, context: &mut Context<'a>) -> PrintI fn gen_enum_member<'a>(node: &TsEnumMember<'a>, context: &mut Context<'a>) -> PrintItems { let mut items = PrintItems::new(); + items.extend(gen_node(node.id.into(), context)); - + if let Some(init) = &node.init { - items.extend(gen_assignment(init.into(), sc!("="), context)); + if context.config.enum_declaration_align_members && context.parent().is::() { + if let Node::TsEnumDecl(parent_enum) = context.parent() { + // try grouped alignment first, then enum-wide alignment + let alignment_group = context.alignment_groups.get(&node.range()) + .or_else(|| context.alignment_groups.get(&parent_enum.range())); + + if let Some(alignment_group) = alignment_group { + let key_width = context.measure_node_width(node.id.into()); + let max_key_width = alignment_group.max_left_width; + + let target_width = max_key_width; + let current_width = key_width; + let padding_needed = if target_width > current_width { + target_width - current_width + } else { + 0 + }; + + for _ in 0..padding_needed { + items.push_space(); + } + + items.extend(gen_assignment(init.into(), sc!("="), context)); + } else { + items.extend(gen_assignment(init.into(), sc!("="), context)); + } + } else { + items.extend(gen_assignment(init.into(), sc!("="), context)); + } + } else { + items.extend(gen_assignment(init.into(), sc!("="), context)); + } } items @@ -1406,6 +1484,14 @@ fn gen_interface_decl<'a>(node: &TsInterfaceDecl<'a>, context: &mut Context<'a>) } fn gen_module_decl<'a>(node: &TsModuleDecl<'a>, context: &mut Context<'a>) -> PrintItems { + if context.config.module_declaration_align_properties { + if let Some(TsNamespaceBody::TsModuleBlock(body)) = &node.body { + if body.body.len() > 1 { + prepare_module_alignment(node, context); + } + } + } + gen_module_or_namespace_decl( ModuleOrNamespaceDecl { declare: node.declare(), @@ -1418,6 +1504,45 @@ fn gen_module_decl<'a>(node: &TsModuleDecl<'a>, context: &mut Context<'a>) -> Pr } fn gen_namespace_decl<'a>(node: &TsNamespaceDecl<'a>, context: &mut Context<'a>) -> PrintItems { + // Check if we should align properties and prepare alignment info + if context.config.module_declaration_align_properties { + if let TsNamespaceBody::TsModuleBlock(body) = &node.body { + if body.body.len() > 1 { + // use namespace range for alignment group key + let mut variable_names = Vec::new(); + + // Collect all variable declarations from export statements + for stmt in body.body { + if let ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export_decl)) = stmt { + if let Decl::Var(var_decl) = &export_decl.decl { + // Collect all variable declarations with assignments in this statement + for decl in var_decl.decls { + if decl.init.is_some() { + variable_names.push(decl.name.into()); + } + } + } + } + } + + if !variable_names.is_empty() { + let max_key_width = variable_names + .iter() + .map(|key_node| context.measure_node_width(*key_node)) + .max() + .unwrap_or(0); + + if max_key_width > 0 { + let alignment_group = super::context::AlignmentGroup { + max_left_width: max_key_width, + }; + context.alignment_groups.insert(node.range(), alignment_group); + } + } + } + } + } + gen_module_or_namespace_decl( ModuleOrNamespaceDecl { declare: node.declare(), @@ -1461,6 +1586,7 @@ fn gen_module_or_namespace_decl<'a, 'b>(node: ModuleOrNamespaceDecl<'a, 'b>, con if let Some(body) = body { match body { TsNamespaceBody::TsModuleBlock(block) => { + items.extend(gen_membered_body( GenMemberedBodyOptions { node: (*block).into(), @@ -2741,11 +2867,53 @@ fn gen_getter_prop<'a>(node: &GetterProp<'a>, context: &mut Context<'a>) -> Prin fn gen_key_value_prop<'a>(node: &KeyValueProp<'a>, context: &mut Context<'a>) -> PrintItems { let mut items = PrintItems::new(); + + let max_left_width = if context.config.object_expression_align_properties { + let parent = node.parent(); + context.alignment_groups.get(&parent.range()).map(|ag| ag.max_left_width) + } else { + None + }; + items.extend(gen_quotable_prop(node.key.into(), context)); - items.extend(gen_assignment(node.value.into(), sc!(":"), context)); + + if let Some(max_width) = max_left_width { + items.extend(gen_aligned_assignment(node.key.into(), node.value.into(), sc!(":"), max_width, context)); + } else { + items.extend(gen_assignment(node.value.into(), sc!(":"), context)); + } + items } +fn prepare_object_alignment<'a>(node: &ObjectLit<'a>, context: &mut Context<'a>) { + // calculate max width of all property keys + let max_key_width = node.props + .iter() + .filter_map(|prop| match prop { + PropOrSpread::Prop(prop_node) => Some(prop_node), + PropOrSpread::Spread(_) => None, // skip spread properties + }) + .filter_map(|prop| match prop { + Prop::KeyValue(kv) => Some(kv.key.into()), + Prop::Shorthand(_) => None, // skip shorthand properties + Prop::Assign(_) => None, // skip assign properties (invalid but handle gracefully) + Prop::Method(_) => None, // skip method properties + Prop::Getter(_) => None, // skip getter properties + Prop::Setter(_) => None, // skip setter properties + }) + .map(|key_node| context.measure_node_width(key_node)) + .max() + .unwrap_or(0); + + if max_key_width > 0 { + let alignment_group = super::context::AlignmentGroup { + max_left_width: max_key_width, + }; + context.alignment_groups.insert(node.range(), alignment_group); + } +} + fn gen_assign_prop<'a>(node: &AssignProp<'a>, context: &mut Context<'a>) -> PrintItems { // assignment properties are not valid, so turn this into a key value property let mut items = PrintItems::new(); @@ -2800,7 +2968,294 @@ fn gen_non_null_expr<'a>(node: &TsNonNullExpr<'a>, context: &mut Context<'a>) -> items } +fn prepare_variable_statement_alignment<'a>(stmts: &[Node<'a>], context: &mut Context<'a>) { + let mut i = 0; + while i < stmts.len() { + // find consecutive variable declarations + let start = i; + while i < stmts.len() { + match stmts[i] { + Node::VarDecl(var_decl) if var_decl.decls.len() == 1 => { + i += 1; + } + _ => break, + } + } + + let end = i; + if end - start > 1 { + // we have a group of consecutive variable declarations + let var_decls: Vec<&VarDecl> = stmts[start..end] + .iter() + .filter_map(|stmt| { + match stmt { + Node::VarDecl(var_decl) if var_decl.decls.len() == 1 => Some(*var_decl), + _ => None, + } + }) + .collect(); + + // calculate maximum left-hand side width across all variable declarations + let max_name_width = var_decls + .iter() + .map(|var_decl| { + let decl = &var_decl.decls[0]; + context.measure_node_width(decl.name.into()) + }) + .max() + .unwrap_or(0); + + // store alignment info + for var_decl in var_decls { + let alignment_group = super::context::AlignmentGroup { + max_left_width: max_name_width, + }; + context.alignment_groups.insert(var_decl.range(), alignment_group); + } + } + + if i == start { + i += 1; // avoid infinite loop if we didn't advance + } + } +} + + + +fn group_interface_members_by_blank_lines<'a>(node: &TsInterfaceBody<'a>, context: &Context<'a>) -> Vec>> { + let mut groups = Vec::new(); + let mut current_group = Vec::new(); + + for (i, member) in node.body.iter().enumerate() { + current_group.push(member); + + // Check if there's a blank line after this member + let has_blank_line_after = if i + 1 < node.body.len() { + let next_member = &node.body[i + 1]; + // Use the existing node_helpers function to check for separating blank line + super::node_helpers::has_separating_blank_line(&Node::from(*member), &Node::from(*next_member), context.program) + } else { + true // End of interface, finish the group + }; + + if has_blank_line_after { + if !current_group.is_empty() { + groups.push(current_group); + current_group = Vec::new(); + } + } + } + + groups +} + +fn prepare_interface_body_alignment<'a>(node: &TsInterfaceBody<'a>, context: &mut Context<'a>) { + // Group interface members by blank line separations + let member_groups = group_interface_members_by_blank_lines(node, context); + + for group in member_groups { + let max_key_width = group + .iter() + .filter_map(|member| { + match member { + TsTypeElement::TsPropertySignature(prop) => { + Some(prop.key.into()) + }, + _ => None, // skip other member types (methods, call signatures, etc.) + } + }) + .map(|key_node| context.measure_node_width(key_node)) + .max() + .unwrap_or(0); + + if max_key_width > 0 && group.len() > 1 { + for member in &group { + let alignment_group = super::context::AlignmentGroup { + max_left_width: max_key_width, + }; + context.alignment_groups.insert(member.range(), alignment_group); + } + } + } +} + +fn prepare_type_literal_alignment<'a>(node: &TsTypeLit<'a>, context: &mut Context<'a>) { + // calculate max width of all property keys + let max_key_width = node.members + .iter() + .filter_map(|member| { + match member { + TsTypeElement::TsPropertySignature(prop) => { + Some(prop.key.into()) + }, + _ => None, // skip other member types (methods, call signatures, etc.) + } + }) + .map(|key_node| context.measure_node_width(key_node)) + .max() + .unwrap_or(0); + + if max_key_width > 0 { + let alignment_group = super::context::AlignmentGroup { + max_left_width: max_key_width, + }; + context.alignment_groups.insert(node.range(), alignment_group); + } +} + +fn group_class_members_by_blank_lines<'a>(node: &Class<'a>, context: &Context<'a>) -> Vec>> { + let mut groups = Vec::new(); + let mut current_group = Vec::new(); + + for (i, member) in node.body.iter().enumerate() { + current_group.push(member); + + // Check if there's a blank line after this member + let has_blank_line_after = if i + 1 < node.body.len() { + let next_member = &node.body[i + 1]; + // Use the existing node_helpers function to check for separating blank line + super::node_helpers::has_separating_blank_line(&Node::from(*member), &Node::from(*next_member), context.program) + } else { + true // End of class, finish the group + }; + + if has_blank_line_after { + if !current_group.is_empty() { + groups.push(current_group); + current_group = Vec::new(); + } + } + } + + groups +} + +fn prepare_class_alignment<'a>(node: &Class<'a>, context: &mut Context<'a>) { + if !context.config.class_declaration_align_properties { + return; + } + + // Group class members by blank line separations + let property_groups = group_class_members_by_blank_lines(node, context); + + for group in property_groups { + let max_key_width = group + .iter() + .filter_map(|member| match member { + ClassMember::ClassProp(prop) if prop.value.is_some() => Some(prop.key.into()), + ClassMember::PrivateProp(prop) if prop.value.is_some() => Some(prop.key.into()), + ClassMember::AutoAccessor(prop) if prop.value.is_some() => Some(prop.key.into()), + _ => None, // skip methods and properties without values + }) + .map(|key_node| context.measure_node_width(key_node)) + .max() + .unwrap_or(0); + + if max_key_width > 0 && group.len() > 1 { + for member in &group { + let alignment_group = super::context::AlignmentGroup { + max_left_width: max_key_width, + }; + context.alignment_groups.insert(member.range(), alignment_group); + } + } + } +} + +fn group_enum_members_by_blank_lines<'a>(node: &TsEnumDecl<'a>, context: &Context<'a>) -> Vec>> { + let mut groups = Vec::new(); + let mut current_group = Vec::new(); + + for (i, member) in node.members.iter().enumerate() { + current_group.push(*member); + + // Check if there's a blank line after this member + let has_blank_line_after = if i + 1 < node.members.len() { + let next_member = &node.members[i + 1]; + // Use the existing node_helpers function to check for separating blank line + super::node_helpers::has_separating_blank_line(&Node::from(*member), &Node::from(*next_member), context.program) + } else { + true // End of enum, finish the group + }; + + if has_blank_line_after { + if !current_group.is_empty() { + groups.push(current_group); + current_group = Vec::new(); + } + } + } + + groups +} + +fn prepare_enum_alignment<'a>(node: &TsEnumDecl<'a>, context: &mut Context<'a>) { + // Group enum members by blank line separations + let member_groups = group_enum_members_by_blank_lines(node, context); + + for group in member_groups { + let max_key_width = group + .iter() + .filter(|member| member.init.is_some()) // only align members with values + .map(|member| context.measure_node_width(member.id.into())) + .max() + .unwrap_or(0); + + if max_key_width > 0 && group.len() > 1 { + for member in &group { + let alignment_group = super::context::AlignmentGroup { + max_left_width: max_key_width, + }; + context.alignment_groups.insert(member.range(), alignment_group); + } + } + } +} + + + +fn prepare_module_alignment<'a>(node: &TsModuleDecl<'a>, context: &mut Context<'a>) { + // For module declarations, align export variable declarations + if let Some(TsNamespaceBody::TsModuleBlock(body)) = &node.body { + let mut variable_names = Vec::new(); + + // Collect all variable declarations from export statements + for stmt in body.body { + if let ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export_decl)) = stmt { + if let Decl::Var(var_decl) = &export_decl.decl { + // Collect all variable declarations with assignments in this statement + for decl in var_decl.decls { + if decl.init.is_some() { + variable_names.push(decl.name.into()); + } + } + } + } + } + + if !variable_names.is_empty() { + let max_key_width = variable_names + .iter() + .map(|key_node| context.measure_node_width(*key_node)) + .max() + .unwrap_or(0); + + if max_key_width > 0 { + let alignment_group = super::context::AlignmentGroup { + max_left_width: max_key_width, + }; + context.alignment_groups.insert(node.range(), alignment_group); + } + } + } +} + fn gen_object_lit<'a>(node: &ObjectLit<'a>, context: &mut Context<'a>) -> PrintItems { + // Check if we should align properties and prepare alignment info + if context.config.object_expression_align_properties && node.props.len() > 1 { + prepare_object_alignment(node, context); + } + let items = context.with_maybe_consistent_props( node, |node| use_consistent_quotes_for_members(node.props.iter().map(|p| p.into())), @@ -3631,7 +4086,44 @@ fn gen_property_signature<'a>(node: &TsPropertySignature<'a>, context: &mut Cont if node.optional() { items.push_sc(sc!("?")); } - items.extend(gen_type_ann_with_colon_if_exists(node.type_ann, context)); + + // Check if we have alignment information and config is enabled for interfaces/type literals + let max_left_width = { + let parent = node.parent(); + match parent { + Node::TsInterfaceBody(interface_body) => { + if context.config.interface_declaration_align_properties { + // First try to get alignment info for this specific member (grouped alignment) + // Then fall back to interface-wide alignment (legacy behavior) + context.alignment_groups.get(&node.range()) + .or_else(|| context.alignment_groups.get(&interface_body.range())) + .map(|ag| ag.max_left_width) + } else { + None + } + }, + Node::TsTypeLit(type_lit) => { + if context.config.type_literal_align_properties { + // First try to get alignment info for this specific member (grouped alignment) + // Then fall back to type literal-wide alignment (legacy behavior) + context.alignment_groups.get(&node.range()) + .or_else(|| context.alignment_groups.get(&type_lit.range())) + .map(|ag| ag.max_left_width) + } else { + None + } + }, + _ => None, + } + }; + + if let (Some(type_ann), Some(max_width)) = (node.type_ann, max_left_width) { + // Generate aligned type annotation + items.extend(gen_aligned_assignment(node.key.into(), type_ann.type_ann.into(), sc!(":"), max_width, context)); + } else { + // Standard type annotation + items.extend(gen_type_ann_with_colon_if_exists(node.type_ann, context)); + } items } @@ -3666,6 +4158,10 @@ fn gen_quotable_prop<'a>(node: Node<'a>, context: &mut Context<'a>) -> PrintItem fn gen_interface_body<'a>(node: &TsInterfaceBody<'a>, context: &mut Context<'a>) -> PrintItems { let start_header_lsil = get_parent_lsil(node, context); + if context.config.interface_declaration_align_properties { + prepare_interface_body_alignment(node, context); + } + return context.with_maybe_consistent_props( node, |node| use_consistent_quotes_for_members(node.body.iter().map(|n| n.into())), @@ -3732,6 +4228,11 @@ fn use_consistent_quotes_for_members<'a>(mut members: impl Iterator(node: &TsTypeLit<'a>, context: &mut Context<'a>) -> PrintItems { + // Check if we should align properties and prepare alignment info + if context.config.type_literal_align_properties { + prepare_type_literal_alignment(node, context); + } + return context.with_maybe_consistent_props( node, |node| use_consistent_quotes_for_members(node.members.iter().map(|n| n.into())), @@ -5543,43 +6044,141 @@ fn gen_var_declarators<'a>(parent: Node<'a>, decls: &[&'a VarDeclarator<'a>], co // be lightweight by default gen_node(decls[0].into(), context) } else if decls_len > 1 { - let force_use_new_lines = get_use_new_lines(parent, decls, context); - gen_separated_values( - GenSeparatedValuesParams { - nodes: decls.iter().map(|&p| NodeOrSeparator::Node(p.into())).collect(), - prefer_hanging: context.config.variable_statement_prefer_hanging, - force_use_new_lines, - allow_blank_lines: false, - separator: TrailingCommas::Never.into(), - single_line_options: ir_helpers::SingleLineOptions::same_line_maybe_space_separated(), - multi_line_options: ir_helpers::MultiLineOptions::same_line_start_hanging_indent(), - force_possible_newline_at_start: false, - node_sorter: None, - }, - context, - ) + // Check if we should align assignments + if context.config.variable_statement_align_assignments && decls_len > 1 { + gen_var_declarators_with_alignment(parent, decls, context) + } else { + let force_use_new_lines = get_use_new_lines(parent, decls, context); + gen_separated_values( + GenSeparatedValuesParams { + nodes: decls.iter().map(|&p| NodeOrSeparator::Node(p.into())).collect(), + prefer_hanging: context.config.variable_statement_prefer_hanging, + force_use_new_lines, + allow_blank_lines: false, + separator: TrailingCommas::Never.into(), + single_line_options: ir_helpers::SingleLineOptions::same_line_maybe_space_separated(), + multi_line_options: ir_helpers::MultiLineOptions::same_line_start_hanging_indent(), + force_possible_newline_at_start: false, + node_sorter: None, + }, + context, + ) + } } else { PrintItems::new() } } +fn gen_var_declarators_with_alignment<'a>(parent: Node<'a>, decls: &[&'a VarDeclarator<'a>], context: &mut Context<'a>) -> PrintItems { + // Measure all left-hand sides to find maximum width + let max_name_width = decls + .iter() + .map(|decl| context.measure_node_width(decl.name.into())) + .max() + .unwrap_or(0); + + // Store alignment info for use in gen_var_declarator + let alignment_group = super::context::AlignmentGroup { + max_left_width: max_name_width, + }; + context.alignment_groups.insert(parent.range(), alignment_group); + + // Check if we should use new lines (same logic as original) + let force_use_new_lines = if get_use_new_lines_for_nodes(decls, context.config.variable_statement_prefer_single_line, context) { + true + } else { + // probably minified code + decls.len() >= 2 && is_node_definitely_above_line_width(parent.range(), context) + }; + + // Generate using the same structure as the original but with alignment + gen_separated_values( + GenSeparatedValuesParams { + nodes: decls.iter().map(|&p| NodeOrSeparator::Node(p.into())).collect(), + prefer_hanging: context.config.variable_statement_prefer_hanging, + force_use_new_lines, + allow_blank_lines: false, + separator: TrailingCommas::Never.into(), + single_line_options: ir_helpers::SingleLineOptions::same_line_maybe_space_separated(), + multi_line_options: ir_helpers::MultiLineOptions::same_line_start_hanging_indent(), + force_possible_newline_at_start: false, + node_sorter: None, + }, + context, + ) +} + fn gen_var_declarator<'a>(node: &VarDeclarator<'a>, context: &mut Context<'a>) -> PrintItems { let mut items = PrintItems::new(); + // Check if we have alignment information for this variable declaration + // First check the parent VarDecl (for within-statement alignment) + // Then check the VarDecl itself (for cross-statement alignment) + // Finally check for module alignment if we're inside a module + let max_left_width = { + let parent_range = node.parent().range(); + context.alignment_groups.get(&parent_range).map(|ag| ag.max_left_width) + .or_else(|| { + // Check if this VarDecl has cross-statement alignment info + match node.parent() { + Node::VarDecl(var_decl) => { + context.alignment_groups.get(&var_decl.range()).map(|ag| ag.max_left_width) + } + _ => None + } + }) + .or_else(|| { + // Check for module declaration alignment + if context.config.module_declaration_align_properties { + // Walk up the tree to find a TsModuleDecl + let mut current = Some(node.parent()); + while let Some(current_node) = current { + if let Node::TsModuleDecl(module_decl) = current_node { + return context.alignment_groups.get(&module_decl.range()).map(|ag| ag.max_left_width); + } + current = current_node.parent(); + } + } + None + }) + }; + items.extend(gen_node(node.name.into(), context)); if let Some(init) = &node.init { - items.extend(gen_assignment(init.into(), sc!("="), context)); + if let Some(max_width) = max_left_width { + let key_width = context.measure_node_width(node.name.into()); + let target_width = max_width; + let current_width = key_width; + let padding_needed = if target_width > current_width { + target_width - current_width + } else { + 0 + }; + + for _ in 0..padding_needed { + items.push_space(); + } + + // Add the assignment normally + items.extend(gen_assignment(init.into(), sc!("="), context)); + } else { + // Standard assignment + items.extend(gen_assignment(init.into(), sc!("="), context)); + } } // Indent the first variable declarator when there are multiple. // Not ideal, but doing this here because of the abstraction used in // `gen_var_decl`. In the future this should probably be moved away. - let parent_decls = match node.parent() { - Node::VarDecl(parent) => Some(&parent.decls), - Node::UsingDecl(parent) => Some(&parent.decls), - _ => None, + let parent_decls = { + let parent = node.parent(); + match parent { + Node::VarDecl(parent) => Some(&parent.decls), + Node::UsingDecl(parent) => Some(&parent.decls), + _ => None, + } }; if let Some(var_decls) = parent_decls { if var_decls.len() > 1 && var_decls[0].range() == node.range() { @@ -6092,6 +6691,7 @@ fn gen_lit_type<'a>(node: &TsLitType<'a>, context: &mut Context<'a>) -> PrintIte } fn gen_mapped_type<'a>(node: &TsMappedType<'a>, context: &mut Context<'a>) -> PrintItems { + let force_use_new_lines = !context.config.mapped_type_prefer_single_line && node_helpers::get_use_new_lines_for_nodes(&node.start(), node.type_param, context.program); @@ -6160,7 +6760,58 @@ fn gen_mapped_type<'a>(node: &TsMappedType<'a>, context: &mut Context<'a>) -> Pr }); } - items.extend(gen_type_ann_with_colon_if_exists_for_type(node.type_ann, context)); + // Check if we should use aligned assignment for mapped types + if let Some(type_ann) = node.type_ann { + let max_left_width = context.alignment_groups.get(&node.range()).map(|ag| ag.max_left_width); + + if let Some(max_width) = max_left_width { + // Calculate the current left width for this mapped type + let mut current_left_width = 0; + + // Add readonly prefix width if present + if let Some(readonly) = node.readonly() { + current_left_width += match readonly { + TruePlusMinus::True => "readonly ".len(), + TruePlusMinus::Plus => "+readonly ".len(), + TruePlusMinus::Minus => "-readonly ".len(), + }; + } + + // Add width of [K in keyof T] part + current_left_width += 1; // opening bracket + current_left_width += context.measure_node_width(node.type_param.as_node()); + current_left_width += 1; // closing bracket + + // Add optional suffix width if present + if let Some(optional) = node.optional() { + current_left_width += match optional { + TruePlusMinus::True => "?".len(), + TruePlusMinus::Plus => "+?".len(), + TruePlusMinus::Minus => "-?".len(), + }; + } + + // Add aligned colon and type + let padding_needed = if max_width > current_left_width { + max_width - current_left_width + } else { + 0 + }; + + // Add padding spaces before the colon + for _ in 0..padding_needed { + items.push_space(); + } + items.push_space(); // space before colon + items.push_sc(sc!(":")); + items.push_space(); // space after colon + items.extend(gen_node(type_ann.into(), context)); + } else { + // Standard type annotation without alignment + items.extend(gen_type_ann_with_colon_if_exists_for_type(Some(type_ann), context)); + } + } + items.extend(get_generated_semi_colon(context.config.semi_colons, true, &is_different_line_than_start)); items.extend(generated_semi_colon_comments); @@ -7238,6 +7889,13 @@ where } fn gen_statements<'a>(inner_range: SourceRange, stmts: Vec>, context: &mut Context<'a>) -> PrintItems { + // Prepare alignment information for variable statements + if context.config.variable_statement_align_assignments { + prepare_variable_statement_alignment(&stmts, context); + } + + + let stmt_groups = get_stmt_groups(stmts, context); let mut items = PrintItems::new(); let mut last_node: Option = None; @@ -9381,6 +10039,43 @@ fn gen_assignment<'a>(expr: Node<'a>, op: &'static StringContainer, context: &mu gen_assignment_op_to(expr, op.text, op, context) } +fn gen_aligned_assignment<'a>( + left_node: Node<'a>, + expr: Node<'a>, + op: &'static StringContainer, + max_left_width: usize, + context: &mut Context<'a> +) -> PrintItems { + let mut items = PrintItems::new(); + + // calculate current left width and add padding + let current_left_width = context.measure_node_width(left_node); + let padding_needed = if max_left_width > current_left_width { + max_left_width - current_left_width + } else { + 0 + }; + + if op.text == "=" { + // for variable assignments, add padding before the operator + for _ in 0..padding_needed { + items.push_space(); + } + // add the assignment normally + items.extend(gen_assignment(expr, op, context)); + } else { + // for object properties (:), add operator first then padding + items.push_sc(op); + for _ in 0..padding_needed { + items.push_space(); + } + items.push_space(); // standard space + items.extend(gen_node(expr, context)); + } + + items +} + #[inline] fn gen_assignment_op_to<'a>(expr: Node<'a>, _op: &'static str, op_to: &'static StringContainer, context: &mut Context<'a>) -> PrintItems { let op_token = context.token_finder.get_previous_token(&expr); diff --git a/tests/specs/declarations/class/TestClassGroups_All.txt b/tests/specs/declarations/class/TestClassGroups_All.txt new file mode 100644 index 00000000..53837955 --- /dev/null +++ b/tests/specs/declarations/class/TestClassGroups_All.txt @@ -0,0 +1,32 @@ +~~ classDeclaration.alignProperties: true, indentWidth: 2 ~~ +== should align class properties in groups == +class ServerConfig { +// Connection settings +host = "localhost"; +port = 3000; + +// Security settings +tls = true; +tlsCertificate = "/path/to/cert.pem"; +tlsPrivateKey = "/path/to/key.pem"; + +// Timeouts +readTimeout = 5000; +writeTimeoutDuration = 30000; +} + +[expect] +class ServerConfig { + // Connection settings + host = "localhost"; + port = 3000; + + // Security settings + tls = true; + tlsCertificate = "/path/to/cert.pem"; + tlsPrivateKey = "/path/to/key.pem"; + + // Timeouts + readTimeout = 5000; + writeTimeoutDuration = 30000; +} diff --git a/tests/specs/declarations/class/TestClass_All.txt b/tests/specs/declarations/class/TestClass_All.txt new file mode 100644 index 00000000..a2cd7939 --- /dev/null +++ b/tests/specs/declarations/class/TestClass_All.txt @@ -0,0 +1,22 @@ +~~ classDeclaration.alignProperties: true, indentWidth: 2 ~~ +== should align class properties == +class TestClass { +a = 1; +longPropertyName = "value"; +x = true; +veryLongMethodName() { return 42; } +b() { return "short"; } +} + +[expect] +class TestClass { + a = 1; + longPropertyName = "value"; + x = true; + veryLongMethodName() { + return 42; + } + b() { + return "short"; + } +} diff --git a/tests/specs/declarations/enum/EnumGroups_All.txt b/tests/specs/declarations/enum/EnumGroups_All.txt new file mode 100644 index 00000000..6f2571e7 --- /dev/null +++ b/tests/specs/declarations/enum/EnumGroups_All.txt @@ -0,0 +1,32 @@ +~~ enumDeclaration.alignMembers: true, indentWidth: 2 ~~ +== should align enum members in groups == +enum HttpStatus { +// Success codes +OK = 200, +CREATED = 201, + +// Client error codes +BAD_REQUEST = 400, +UNAUTHORIZED = 401, +NOT_FOUND = 404, + +// Server error codes +INTERNAL_SERVER_ERROR = 500, +BAD_GATEWAY = 502 +} + +[expect] +enum HttpStatus { + // Success codes + OK = 200, + CREATED = 201, + + // Client error codes + BAD_REQUEST = 400, + UNAUTHORIZED = 401, + NOT_FOUND = 404, + + // Server error codes + INTERNAL_SERVER_ERROR = 500, + BAD_GATEWAY = 502, +} diff --git a/tests/specs/declarations/enum/EnumMember_Alignment_All.txt b/tests/specs/declarations/enum/EnumMember_Alignment_All.txt new file mode 100644 index 00000000..9e49e9ff --- /dev/null +++ b/tests/specs/declarations/enum/EnumMember_Alignment_All.txt @@ -0,0 +1,16 @@ +~~ enumDeclaration.alignMembers: true, indentWidth: 2 ~~ +== should align enum members == +enum StatusCodes { +OK = 200, +CREATED = 201, +NOT_FOUND = 404, +INTERNAL_SERVER_ERROR = 500 +} + +[expect] +enum StatusCodes { + OK = 200, + CREATED = 201, + NOT_FOUND = 404, + INTERNAL_SERVER_ERROR = 500, +} diff --git a/tests/specs/declarations/enum/TestEnum_All.txt b/tests/specs/declarations/enum/TestEnum_All.txt new file mode 100644 index 00000000..8a854b54 --- /dev/null +++ b/tests/specs/declarations/enum/TestEnum_All.txt @@ -0,0 +1,14 @@ +~~ enumDeclaration.alignMembers: true, indentWidth: 2 ~~ +== should align enum members == +enum TestEnum { +A = 1, +longEnumMemberName = "value", +B = true +} + +[expect] +enum TestEnum { + A = 1, + longEnumMemberName = "value", + B = true, +} diff --git a/tests/specs/declarations/interface/InterfaceDeclaration_AlignProperties.txt b/tests/specs/declarations/interface/InterfaceDeclaration_AlignProperties.txt new file mode 100644 index 00000000..389c8ebd --- /dev/null +++ b/tests/specs/declarations/interface/InterfaceDeclaration_AlignProperties.txt @@ -0,0 +1,12 @@ +~~ interfaceDeclaration.alignProperties: true, indentWidth: 2 ~~ +== should align interface properties == +interface User { +name: string; +age: number; +} + +[expect] +interface User { + name: string; + age: number; +} diff --git a/tests/specs/declarations/interface/InterfaceDeclaration_AlignProperties_False.txt b/tests/specs/declarations/interface/InterfaceDeclaration_AlignProperties_False.txt new file mode 100644 index 00000000..913966be --- /dev/null +++ b/tests/specs/declarations/interface/InterfaceDeclaration_AlignProperties_False.txt @@ -0,0 +1,12 @@ +~~ interfaceDeclaration.alignProperties: false, indentWidth: 2 ~~ +== should not align when disabled == +interface User { +name: string; +age: number; +} + +[expect] +interface User { + name: string; + age: number; +} diff --git a/tests/specs/declarations/interface/InterfaceGroups_All.txt b/tests/specs/declarations/interface/InterfaceGroups_All.txt new file mode 100644 index 00000000..49b37afb --- /dev/null +++ b/tests/specs/declarations/interface/InterfaceGroups_All.txt @@ -0,0 +1,32 @@ +~~ interfaceDeclaration.alignProperties: true, indentWidth: 2 ~~ +== should align interface properties in groups == +interface ServerConfig { +// Connection settings +host: string; +port: number; + +// Security settings +tls: boolean; +tlsCertificate: string; +tlsPrivateKey: string; + +// Timeouts +readTimeout: number; +writeTimeoutDuration: number; +} + +[expect] +interface ServerConfig { + // Connection settings + host: string; + port: number; + + // Security settings + tls: boolean; + tlsCertificate: string; + tlsPrivateKey: string; + + // Timeouts + readTimeout: number; + writeTimeoutDuration: number; +} diff --git a/tests/specs/declarations/module/ModuleDeclaration_Alignment_All.txt b/tests/specs/declarations/module/ModuleDeclaration_Alignment_All.txt new file mode 100644 index 00000000..09b1ad05 --- /dev/null +++ b/tests/specs/declarations/module/ModuleDeclaration_Alignment_All.txt @@ -0,0 +1,28 @@ +~~ moduleDeclaration.alignProperties: true, indentWidth: 2 ~~ +== should align module declaration properties == +declare module "example-module" { +export const shortName: string; +export const veryLongVariableName: number; +export function func(): void; +export function aVeryLongFunctionName(): boolean; +} + +namespace MyNamespace { +export const x = 1; +export const longVariableName = 2; +export const y = 3; +} + +[expect] +declare module "example-module" { + export const shortName: string; + export const veryLongVariableName: number; + export function func(): void; + export function aVeryLongFunctionName(): boolean; +} + +namespace MyNamespace { + export const x = 1; + export const longVariableName = 2; + export const y = 3; +} diff --git a/tests/specs/expressions/ObjectExpression/ObjectExpression_AlignProperties.txt b/tests/specs/expressions/ObjectExpression/ObjectExpression_AlignProperties.txt new file mode 100644 index 00000000..0f2501a6 --- /dev/null +++ b/tests/specs/expressions/ObjectExpression/ObjectExpression_AlignProperties.txt @@ -0,0 +1,36 @@ +~~ objectExpression.alignProperties: true ~~ +== should align object properties == +const foo = { + bar: 123, + barbaz: 123 +} + +[expect] +const foo = { + bar: 123, + barbaz: 123, +}; + +== should align computed properties == +const foo = { + [key]: 123, + barbaz: 456 +} + +[expect] +const foo = { + [key]: 123, + barbaz: 456, +}; + +== should align quoted properties == +const foo = { + "short": 123, + "very long key": 456 +} + +[expect] +const foo = { + "short": 123, + "very long key": 456, +}; diff --git a/tests/specs/expressions/ObjectExpression/ObjectExpression_AlignProperties_False.txt b/tests/specs/expressions/ObjectExpression/ObjectExpression_AlignProperties_False.txt new file mode 100644 index 00000000..4b044e52 --- /dev/null +++ b/tests/specs/expressions/ObjectExpression/ObjectExpression_AlignProperties_False.txt @@ -0,0 +1,12 @@ +~~ objectExpression.alignProperties: false ~~ +== should not align when disabled == +const foo = { + bar: 123, + barbaz: 123 +} + +[expect] +const foo = { + bar: 123, + barbaz: 123, +}; diff --git a/tests/specs/statements/variableStatement/VariableStatement_AlignAssignments.txt b/tests/specs/statements/variableStatement/VariableStatement_AlignAssignments.txt new file mode 100644 index 00000000..ab9ef7ca --- /dev/null +++ b/tests/specs/statements/variableStatement/VariableStatement_AlignAssignments.txt @@ -0,0 +1,40 @@ +~~ variableStatement.alignAssignments: true ~~ +== should align variable assignments == +const foo = 12; +const bar = 21; +const foobar = 22; + +[expect] +const foo = 12; +const bar = 21; +const foobar = 22; + +== should align let assignments == +let foo = 12; +let bar = 21; +let foobar = 22; + +[expect] +let foo = 12; +let bar = 21; +let foobar = 22; + +== should align var assignments == +var foo = 12; +var bar = 21; +var foobar = 22; + +[expect] +var foo = 12; +var bar = 21; +var foobar = 22; + +== should align with destructuring == +const { a, b } = obj1; +const { c } = obj2; +const simple = obj3; + +[expect] +const { a, b } = obj1; +const { c } = obj2; +const simple = obj3; diff --git a/tests/specs/statements/variableStatement/VariableStatement_AlignAssignments_False.txt b/tests/specs/statements/variableStatement/VariableStatement_AlignAssignments_False.txt new file mode 100644 index 00000000..0c9e91a8 --- /dev/null +++ b/tests/specs/statements/variableStatement/VariableStatement_AlignAssignments_False.txt @@ -0,0 +1,10 @@ +~~ variableStatement.alignAssignments: false ~~ +== should not align when disabled == +const foo = 12; +const bar = 21; +const foobar = 22; + +[expect] +const foo = 12; +const bar = 21; +const foobar = 22; diff --git a/tests/specs/types/TypeLiteral/TypeLiteral_AlignProperties.txt b/tests/specs/types/TypeLiteral/TypeLiteral_AlignProperties.txt new file mode 100644 index 00000000..2f93ee37 --- /dev/null +++ b/tests/specs/types/TypeLiteral/TypeLiteral_AlignProperties.txt @@ -0,0 +1,36 @@ +~~ typeLiteral.alignProperties: true ~~ +== should align type literal properties == +type Config = { +host: string; +port: number; +database: string; +username: string; +password: string; +maxConnections: number; +}; + +type UserPermissions = { +read: boolean; +write: boolean; +admin: boolean; +deleteUsers: boolean; +manageRoles: boolean; +}; + +[expect] +type Config = { + host: string; + port: number; + database: string; + username: string; + password: string; + maxConnections: number; +}; + +type UserPermissions = { + read: boolean; + write: boolean; + admin: boolean; + deleteUsers: boolean; + manageRoles: boolean; +}; diff --git a/tests/specs/types/TypeLiteral/TypeLiteral_AlignProperties_False.txt b/tests/specs/types/TypeLiteral/TypeLiteral_AlignProperties_False.txt new file mode 100644 index 00000000..26257e8f --- /dev/null +++ b/tests/specs/types/TypeLiteral/TypeLiteral_AlignProperties_False.txt @@ -0,0 +1,20 @@ +~~ typeLiteral.alignProperties: false ~~ +== should not align when disabled == +type Config = { +host: string; +port: number; +database: string; +username: string; +password: string; +maxConnections: number; +}; + +[expect] +type Config = { + host: string; + port: number; + database: string; + username: string; + password: string; + maxConnections: number; +}; diff --git a/tests/specs/types/TypeLiteral_AlignProperties.txt b/tests/specs/types/TypeLiteral_AlignProperties.txt new file mode 100644 index 00000000..f2e5b575 --- /dev/null +++ b/tests/specs/types/TypeLiteral_AlignProperties.txt @@ -0,0 +1,12 @@ +~~ typeLiteral.alignProperties: true, indentWidth: 2 ~~ +== should align type literal properties == +type User = { +name: string; +age: number; +}; + +[expect] +type User = { + name: string; + age: number; +}; diff --git a/tests/specs/types/TypeLiteral_AlignProperties_False.txt b/tests/specs/types/TypeLiteral_AlignProperties_False.txt new file mode 100644 index 00000000..cdfaf028 --- /dev/null +++ b/tests/specs/types/TypeLiteral_AlignProperties_False.txt @@ -0,0 +1,12 @@ +~~ typeLiteral.alignProperties: false, indentWidth: 2 ~~ +== should not align when disabled == +type User = { +name: string; +age: number; +}; + +[expect] +type User = { + name: string; + age: number; +};