diff --git a/packages/core/syntax/src/sexpr/formatter/core.rs b/packages/core/syntax/src/sexpr/formatter/core.rs index 1ea81994..e413f366 100644 --- a/packages/core/syntax/src/sexpr/formatter/core.rs +++ b/packages/core/syntax/src/sexpr/formatter/core.rs @@ -921,6 +921,9 @@ impl Formatter { ListStyle::If => { self.format_prefix_body(tree, node_id, depth, 2, output); } + ListStyle::IfAligned => { + self.format_if_aligned(tree, node_id, depth, output); + } ListStyle::ClojureDefinition => { self.format_clojure_definition(tree, node_id, depth, output); } @@ -1141,7 +1144,7 @@ impl Formatter { /// resolves every style generically, rather than special-casing /// `General`, so a future phase that teaches another style to compact /// gets a working width profile for free. - fn effective_max_width(&self, tree: &SyntaxTree, node_id: NodeId) -> usize { + pub(super) fn effective_max_width(&self, tree: &SyntaxTree, node_id: NodeId) -> usize { let Some(head) = self.head_text(tree, node_id) else { return self.max_width; }; diff --git a/packages/core/syntax/src/sexpr/formatter/lists/definitions.rs b/packages/core/syntax/src/sexpr/formatter/lists/definitions.rs index b76f4f18..82b1578a 100644 --- a/packages/core/syntax/src/sexpr/formatter/lists/definitions.rs +++ b/packages/core/syntax/src/sexpr/formatter/lists/definitions.rs @@ -177,15 +177,29 @@ impl Formatter { ) { let node = tree.node(node_id); let delimiter = self.list_delimiter(node); - let body_column = self.add_indent(Self::last_line_width(output)); + let base_column = Self::last_line_width(output); + let body_column = self.add_indent(base_column); + let distinguished_column = self.add_indent(body_column); output.push(delimiter.open()); for (position, child) in node.children.iter().enumerate() { - if position <= prefix_len { + if position < prefix_len { if position > 0 { output.push(' '); } self.format_inline_or_node(tree, *child, depth + 1, output); + } else if position == prefix_len { + // The distinguished argument: stays on the head line + // when it fits, breaks to distinguished_column (+4) when + // it does not. + output.push(' '); + let col = Self::last_line_width(output); + if let Some(inline) = self.compact_node(tree, *child, col) { + output.push_str(&inline); + } else { + Self::break_to_column(distinguished_column, output); + self.format_node(tree, *child, depth + 1, output); + } } else { Self::break_to_column(body_column, output); self.format_node(tree, *child, depth + 1, output); @@ -195,6 +209,38 @@ impl Formatter { output.push(delimiter.close()); } + /// `if` in Common Lisp: the test shares the head line and all branches + /// (then, else) align at the same distinguished column — two indent + /// steps from the form's opening delimiter. + pub(in crate::sexpr::formatter) fn format_if_aligned( + &self, + tree: &SyntaxTree, + node_id: NodeId, + depth: usize, + output: &mut String, + ) { + let node = tree.node(node_id); + let delimiter = self.list_delimiter(node); + let base_column = Self::last_line_width(output); + let body_column = self.add_indent(base_column); + let branch_column = self.add_indent(body_column); + output.push(delimiter.open()); + for (position, child) in node.children.iter().enumerate() { + match position { + 0 => self.format_node(tree, *child, depth + 1, output), + 1 => { + output.push(' '); + self.format_inline_or_node(tree, *child, depth + 1, output); + } + _ => { + Self::break_to_column(branch_column, output); + self.format_node(tree, *child, depth + 1, output); + } + } + } + output.push(delimiter.close()); + } + pub(in crate::sexpr::formatter) fn format_head_body( &self, tree: &SyntaxTree, diff --git a/packages/core/syntax/src/sexpr/formatter/lists/general.rs b/packages/core/syntax/src/sexpr/formatter/lists/general.rs index 1bfcd9f2..15348113 100644 --- a/packages/core/syntax/src/sexpr/formatter/lists/general.rs +++ b/packages/core/syntax/src/sexpr/formatter/lists/general.rs @@ -14,17 +14,62 @@ impl Formatter { ) { let node = tree.node(node_id); let delimiter = self.list_delimiter(node); - // Every element lines up under the first one, which lands one column - // past the opening delimiter. Indenting the rest by `indent` instead - // put them `indent - 1` columns right of their own first sibling. - let element_column = Self::last_line_width(output).saturating_add(1); + let base_column = Self::last_line_width(output); output.push(delimiter.open()); + + // When the first argument shares the head's line, subsequent + // arguments align under it (Emacs `lisp-indent-function` default). + // Otherwise every argument lands one column past the opening + // delimiter — the same fallback `reindent.rs` uses. + let mut first_arg_column: Option = None; + let mut head_line_count: Option = None; + for (position, child) in node.children.iter().enumerate() { - if position > 0 { - Self::break_to_column(element_column, output); + match position { + 0 => { + self.format_node(tree, *child, depth + 1, output); + head_line_count = Some(output.lines().count()); + } + 1 => { + // If the first argument fits inline on the head line, + // align subsequent siblings under it. Otherwise break + // to one column past the opening delimiter. + let child_start = Self::last_line_width(output).saturating_add(1); + // When this argument is also the last child, the list's + // own closing delimiter lands right after it on the same + // line, so it must be charged against the budget too — + // `compact_node` only measures the argument's own text. + let is_last_child = position + 1 == node.children.len(); + let max_width = self.effective_max_width(tree, node_id); + let inline = self + .compact_node(tree, *child, child_start) + .filter(|inline| { + !is_last_child + || child_start + .saturating_add(UnicodeWidthStr::width(inline.as_str())) + < max_width + }); + if let Some(inline) = inline { + output.push(' '); + let col = Self::last_line_width(output); + output.push_str(&inline); + if head_line_count.is_some_and(|hl| output.lines().count() == hl) { + first_arg_column = Some(col); + } + } else { + let element_column = base_column.saturating_add(1); + Self::break_to_column(element_column, output); + self.format_node(tree, *child, depth + 1, output); + } + } + _ => { + let element_column = first_arg_column.unwrap_or(base_column.saturating_add(1)); + Self::break_to_column(element_column, output); + self.format_node(tree, *child, depth + 1, output); + } } - self.format_node(tree, *child, depth + 1, output); } + output.push(delimiter.close()); } diff --git a/packages/core/syntax/src/sexpr/formatter/lists/loops.rs b/packages/core/syntax/src/sexpr/formatter/lists/loops.rs index 2f5984e5..6c6c8167 100644 --- a/packages/core/syntax/src/sexpr/formatter/lists/loops.rs +++ b/packages/core/syntax/src/sexpr/formatter/lists/loops.rs @@ -12,12 +12,12 @@ impl Formatter { ) { let node = tree.node(node_id); let delimiter = self.list_delimiter(node); + let base_column = Self::last_line_width(output); output.push(delimiter.open()); self.format_node(tree, node.children[0], depth + 1, output); - // Clauses line up under the first one, one column past the head as - // actually written — not past its byte length, which is not a column - // count for a non-ASCII head. - let continuation_column = Self::last_line_width(output).saturating_add(1); + // Emacs default `lisp-loop-keyword-indentation`: keywords and + // body forms at six columns from the opening delimiter. + let continuation_column = base_column.saturating_add(6); let mut position = 1; let mut conditional_clause_open = false; diff --git a/packages/core/syntax/src/sexpr/formatter/styles.rs b/packages/core/syntax/src/sexpr/formatter/styles.rs index f9fe4c46..286a730d 100644 --- a/packages/core/syntax/src/sexpr/formatter/styles.rs +++ b/packages/core/syntax/src/sexpr/formatter/styles.rs @@ -25,6 +25,11 @@ pub(super) enum ListStyle { Loop, HeadBody, If, + /// `if` in Common Lisp: the test is on the head line and all branches + /// (then, else) align at the same distinguished column (two indent + /// steps from the form's opening delimiter — the equivalent of Emacs + /// `common-lisp-indent-function`'s `(&rest nil)` for `if`). + IfAligned, /// `(defn name [params]` with the body indented below, falling back to only /// the name on the head line when a docstring, attribute map, or multi-arity /// clause list follows it. @@ -86,6 +91,7 @@ pub const STYLE_NAMES: &[&str] = &[ "one-argument-body", "two-argument-body", "if-then-else", + "if-aligned", "cond-clauses", "case-clauses", "head-body", @@ -103,6 +109,7 @@ pub(super) fn style_from_name(name: &str) -> Option { "one-argument-body" => Some(ListStyle::OneArgumentBody), "two-argument-body" => Some(ListStyle::TwoArgumentBody), "if-then-else" => Some(ListStyle::If), + "if-aligned" => Some(ListStyle::IfAligned), "cond-clauses" => Some(ListStyle::CondClauses), "case-clauses" => Some(ListStyle::CaseClauses), "head-body" => Some(ListStyle::HeadBody), @@ -129,6 +136,7 @@ impl Formatter { } match self.dialect { Dialect::Clojure => Self::clojure_style_for_head(head), + Dialect::EmacsLisp => Self::elisp_style_for_head(head), _ => Self::common_lisp_style_for_head(head), } } @@ -155,6 +163,38 @@ impl Formatter { } } + /// Emacs Lisp dialect routing: recognizes Elisp-specific operators that + /// differ from Common Lisp conventions. Falls back to the CL table for + /// everything else. + fn elisp_style_for_head(head: &str) -> ListStyle { + let normalized_head = normalize_common_lisp_operator_head(head); + match normalized_head.to_ascii_lowercase().as_str() { + // def* forms with 'defun indent → name on head line, body indented + "defvar" + | "defconst" + | "defcustom" + | "defgroup" + | "defalias" + | "defvaralias" + | "define-derived-mode" + | "define-minor-mode" => ListStyle::DefinitionNameBody, + // progn-like (indent 0) → all children at body-indent + "save-excursion" | "save-restriction" | "save-current-buffer" | "track-mouse" => { + ListStyle::HeadBody + } + // indent 1 → one arg on head line, body indented + "while" => ListStyle::OneArgumentBody, + // indent 2 → two-component special (then at +4, else at +2) + "condition-case" => ListStyle::TwoArgumentBody, + // if in Elisp: test at +4 distinguished, then/else at body + "if" => ListStyle::If, + // Elisp-specific clause forms + "pcase" => ListStyle::CaseClauses, + "cl-loop" => ListStyle::Loop, + _ => Self::common_lisp_style_for_head(head), + } + } + fn common_lisp_style_for_head(head: &str) -> ListStyle { if let Some(operator) = CommonLispOperator::from_head(head) { match operator { @@ -163,6 +203,22 @@ impl Formatter { } CommonLispOperator::Lambda => return ListStyle::Lambda, CommonLispOperator::DefineSymbolMacro => return ListStyle::DefinitionNameBody, + CommonLispOperator::Defvar + | CommonLispOperator::Defconstant + | CommonLispOperator::Defparameter + | CommonLispOperator::Defglobal + | CommonLispOperator::Defstruct + | CommonLispOperator::DefineCondition => { + return ListStyle::DefinitionNameBody; + } + CommonLispOperator::Defpackage + | CommonLispOperator::InPackage + | CommonLispOperator::Provide + | CommonLispOperator::Require + | CommonLispOperator::UsePackage + | CommonLispOperator::Import => { + return ListStyle::General; + } operator if operator.is_asdf_system_definition() => { return ListStyle::SystemDefinition; } @@ -190,7 +246,7 @@ impl Formatter { let normalized_head = normalize_common_lisp_operator_head(head); match normalized_head.to_ascii_lowercase().as_str() { "named-lambda" => ListStyle::NamedLambda, - "if" => ListStyle::If, + "if" => ListStyle::IfAligned, "when" | "unless" | "with-open-file" @@ -207,11 +263,22 @@ impl Formatter { "case" | "ccase" | "ecase" | "typecase" | "ctypecase" | "etypecase" => { ListStyle::CaseClauses } - "progn" | "prog1" | "prog2" | "tagbody" | "defpackage" | "locally" => { - ListStyle::HeadBody - } + "progn" | "prog1" | "prog2" | "tagbody" | "locally" => ListStyle::HeadBody, "declare" | "declaim" | "proclaim" => ListStyle::Declaration, - "setq" | "psetq" | "setf" | "psetf" => ListStyle::PairAssignment, + "setq" | "psetq" | "setf" | "psetf" | "multiple-value-setq" | "multiple-value-setf" => { + ListStyle::PairAssignment + } + "multiple-value-call" + | "multiple-value-prog1" + | "pprint-logical-block" + | "with-compilation-unit" + | "with-standard-io-syntax" + | "return-from" + | "throw" => ListStyle::OneArgumentBody, + "progv" | "with-condition-restarts" | "print-unreadable-object" => { + ListStyle::TwoArgumentBody + } + "generic-flet" | "generic-labels" => ListStyle::LocalFunctions, _ => ListStyle::General, } } diff --git a/packages/core/syntax/src/sexpr/reindent.rs b/packages/core/syntax/src/sexpr/reindent.rs index 86b1d7d1..a9683ec5 100644 --- a/packages/core/syntax/src/sexpr/reindent.rs +++ b/packages/core/syntax/src/sexpr/reindent.rs @@ -31,37 +31,87 @@ use super::types::{ByteOffset, ByteSpan, NodeId}; /// The count is what `lisp-indent-function` calls the form's "number of /// distinguished arguments": `defun` has two (name and lambda list), `when` has /// one (the test), `progn` has none. -const BODY_FORMS: [(&str, usize); 30] = [ +const BODY_FORMS: [(&str, usize); 70] = [ + // ── Definition forms ── ("defun", 2), ("defmacro", 2), ("defmethod", 2), ("defgeneric", 2), ("defclass", 3), ("define-condition", 3), + // ── Lambda ── ("lambda", 1), + ("named-lambda", 2), + // ── Binding forms ── ("let", 1), ("let*", 1), ("flet", 1), ("labels", 1), + ("generic-flet", 1), + ("generic-labels", 1), ("macrolet", 1), + ("compiler-macrolet", 1), ("symbol-macrolet", 1), + ("handler-bind", 1), + ("restart-bind", 1), + // ── Control flow ── ("when", 1), ("unless", 1), + ("block", 1), + ("catch", 1), + ("unwind-protect", 1), + ("eval-when", 1), + ("locally", 1), + // ── Iteration ── ("dolist", 1), ("dotimes", 1), ("do", 2), ("do*", 2), + ("prog", 2), + ("prog*", 2), + // ── Clause forms ── ("case", 1), ("ecase", 1), + ("ccase", 1), ("typecase", 1), ("etypecase", 1), + ("ctypecase", 1), + ("handler-case", 1), + // ── progn-like ── + ("progn", 0), + ("prog1", 1), + ("prog2", 2), + // ── with-* macros ── ("with-open-file", 1), + ("with-open-stream", 1), + ("with-input-from-string", 1), + ("with-output-to-string", 1), + ("with-hash-table-iterator", 1), + ("with-package-iterator", 1), ("with-slots", 2), ("with-accessors", 2), + ("with-compilation-unit", 1), + ("with-standard-io-syntax", 1), + ("with-condition-restarts", 2), + // ── Value binding ── ("multiple-value-bind", 2), ("destructuring-bind", 2), - ("handler-case", 1), - ("progn", 0), + ("multiple-value-call", 1), + ("multiple-value-prog1", 1), + ("progv", 2), + // ── Emacs Lisp ── + ("defvar", 2), + ("defconst", 2), + ("defcustom", 2), + ("defgroup", 2), + ("defalias", 2), + ("defvaralias", 2), + ("save-excursion", 0), + ("save-restriction", 0), + ("save-current-buffer", 0), + ("track-mouse", 0), + ("while", 1), + ("condition-case", 2), ]; /// How many distinguished arguments `head` takes before its body, or `None` diff --git a/packages/core/syntax/src/sexpr/tests/formatter.rs b/packages/core/syntax/src/sexpr/tests/formatter.rs index cfad1217..06d97393 100644 --- a/packages/core/syntax/src/sexpr/tests/formatter.rs +++ b/packages/core/syntax/src/sexpr/tests/formatter.rs @@ -765,7 +765,10 @@ fn preserves_string_that_contains_a_semicolon() { let tree = SyntaxTree::parse(input).expect("valid"); assert_eq!( Formatter::new(2).format(&tree), - "(defvar path \";not-a-comment\")\n" + // `defvar` uses `DefinitionNameBody` (name on head line, value at + // body). The semicolon inside the string is preserved — it is never + // mistaken for a comment. + "(defvar path\n \";not-a-comment\")\n" ); } @@ -1372,15 +1375,14 @@ fn clojure_layouts_do_not_leak_into_other_dialects() { "(defn greet [first-name last-name] (log first-name) (str first-name \" \" last-name))"; let tree = SyntaxTree::parse_with_dialect(input, Dialect::Clojure).expect("valid Clojure"); - // Outside Clojure `defn` has no layout of its own, so this is the plain - // list layout: every element lines up under `defn`, one column past the - // opening delimiter. + // Outside Clojure `defn` has no layout of its own, so the plain list + // layout applies: `greet` stays on the head line because it fits, and + // subsequent elements align under it (Emacs convention for general lists). let common_lisp_layout = concat!( - "(defn\n", - " greet\n", - " [first-name last-name]\n", - " (log first-name)\n", - " (str first-name \" \" last-name))\n" + "(defn greet\n", + " [first-name last-name]\n", + " (log first-name)\n", + " (str first-name \" \" last-name))\n" ); assert_eq!(Formatter::new(2).format(&tree), common_lisp_layout); assert_eq!( @@ -2133,14 +2135,9 @@ fn trim_trailing_whitespace_false_keeps_a_comments_trailing_spaces() { assert!(untrimmed.contains("space \n")); } -/// A form hugged onto its operator's line, then forced to break internally, -/// indents its body from the column its own opening delimiter landed on. -/// -/// Nothing in the existing suite exercised this: every other hugged form is -/// short enough to stay inline, so the body indentation derived from nesting -/// depth was never compared against a form that did not start its own line. -/// Derived from depth, `(unless p ...)` landed at column 6 — three columns -/// *left* of the `(multiple-value-bind` that contains it. +/// Common Lisp `if` aligns all branches at the same distinguished column +/// (two indent steps from the form's opening delimiter), matching Emacs +/// `common-lisp-indent-function`'s `(&rest nil)` convention for `if`. #[test] fn indents_a_hugged_form_that_breaks_from_the_column_it_landed_on() { let input = "(if (consp value) (multiple-value-bind (copy p) (gethash value copies) (unless p (setf copy value))) value)"; @@ -2148,12 +2145,11 @@ fn indents_a_hugged_form_that_breaks_from_the_column_it_landed_on() { assert_eq!( Formatter::new(2).format(&tree), concat!( - "(if (consp value) (multiple-value-bind (copy p) (gethash value copies)\n", - // ^ column 18: the `multiple-value-bind` list opens here, - // so its body belongs at column 20. - " (unless p\n", - " (setf copy value)))\n", - " value)\n", + "(if (consp value)\n", + " (multiple-value-bind (copy p) (gethash value copies)\n", + " (unless p\n", + " (setf copy value)))\n", + " value)\n", ) ); } @@ -2176,8 +2172,8 @@ fn aligns_defclass_slots_in_one_column() { ); } -/// The same alignment for `define-condition`, whose slot list has the same -/// shape. +/// `define-condition` uses `DefinitionNameBody`: the name is on the head +/// line and everything else (supers, slot list, options) is at body-indent. #[test] fn aligns_define_condition_slots_in_one_column() { let input = "(define-condition parse-failure (error) ((offset :initarg :offset :reader parse-failure-offset) (message :initarg :message :reader parse-failure-message)) (:report report-parse-failure))"; @@ -2185,7 +2181,8 @@ fn aligns_define_condition_slots_in_one_column() { assert_eq!( Formatter::new(2).format(&tree), concat!( - "(define-condition parse-failure (error)\n", + "(define-condition parse-failure\n", + " (error)\n", " ((offset :initarg :offset :reader parse-failure-offset)\n", " (message :initarg :message :reader parse-failure-message))\n", " (:report report-parse-failure))\n", @@ -2250,10 +2247,10 @@ fn binding_continuation_columns_count_display_width_not_bytes() { ); } -/// The same for a form hugged onto a line that already carries wide -/// characters: `(setf` opens at display column 14, so its second pair lines -/// up under the first at column 20. Counting the preceding `(適合 値)` in -/// bytes would have overshot by four columns. +/// Common Lisp `if` branches at the same distinguished column inside a CJK +/// context. `setf`'s second pair (`別変数`) aligns under the first (`変数`) +/// by display column — counting bytes would overshoot because `適合 値` is +/// 6 display columns but 9 UTF-8 bytes. #[test] fn a_hugged_form_starts_at_its_display_column_not_its_byte_offset() { let input = "(if (適合 値) (setf 変数 (compute-first alpha) 別変数 (compute-second beta)) nil)"; @@ -2261,9 +2258,10 @@ fn a_hugged_form_starts_at_its_display_column_not_its_byte_offset() { assert_eq!( Formatter::new(2).format(&tree), concat!( - "(if (適合 値) (setf 変数 (compute-first alpha)\n", - " 別変数 (compute-second beta))\n", - " nil)\n", + "(if (適合 値)\n", + " (setf 変数 (compute-first alpha)\n", + " 別変数 (compute-second beta))\n", + " nil)\n", ) ); } diff --git a/packages/feature/migrate/recipes/nil-conditionals.lisp b/packages/feature/migrate/recipes/nil-conditionals.lisp index 47f44b86..994ea75c 100644 --- a/packages/feature/migrate/recipes/nil-conditionals.lisp +++ b/packages/feature/migrate/recipes/nil-conditionals.lisp @@ -19,24 +19,25 @@ ;;; `(when test a b)' is equivalent and shorter, but splicing a `progn' ;;; body out is a restructuring rather than a rename. `paredit refactor ;;; flatten-progn' does it, with its own review. -(defmigration - nil-conditionals - :description - "one-armed `if' with a nil else-branch to `when' and `unless'" - :dialects - (common-lisp emacs-lisp) - :steps - ((:query - (if (not ?test) ?then - nil) - :rewrite - (unless ?test - ?then) - :note - "first, so the general step below cannot claim a negated test") - (:query - (if ?test ?then - nil) - :rewrite - (when ?test - ?then)))) +(defmigration nil-conditionals + :description + "one-armed `if' with a nil else-branch to `when' and `unless'" + :dialects + (common-lisp emacs-lisp) + :steps + ((:query + (if (not ?test) + ?then + nil) + :rewrite + (unless ?test + ?then) + :note + "first, so the general step below cannot claim a negated test") + (:query + (if ?test + ?then + nil) + :rewrite + (when ?test + ?then)))) diff --git a/packages/feature/remove-unused/src/remove_unused_binding/domain/tests/common_lisp/variables.rs b/packages/feature/remove-unused/src/remove_unused_binding/domain/tests/common_lisp/variables.rs index c0564ac3..28c46588 100644 --- a/packages/feature/remove-unused/src/remove_unused_binding/domain/tests/common_lisp/variables.rs +++ b/packages/feature/remove-unused/src/remove_unused_binding/domain/tests/common_lisp/variables.rs @@ -71,7 +71,7 @@ fn plans_unused_binding_ignoring_shadowed_lambda_parameter() { assert_eq!(plan.reference_count, Some(0)); assert_eq!( plan.replacement, - "(let ((used 2))\n (list\n used\n (lambda (x)\n x)))" + "(let ((used 2))\n (list used\n (lambda (x)\n x)))" ); } @@ -105,7 +105,7 @@ fn plans_unused_binding_ignoring_shadowed_dolist_variable() { assert_eq!(plan.reference_count, Some(0)); assert_eq!( plan.replacement, - "(let ((used 2))\n (list\n used\n (dolist (value items value)\n value)))" + "(let ((used 2))\n (list used\n (dolist (value items value)\n value)))" ); } @@ -118,7 +118,7 @@ fn plans_unused_binding_ignoring_shadowed_with_slots_variable() { assert_eq!(plan.reference_count, Some(0)); assert_eq!( plan.replacement, - "(let ((used 2))\n (list\n used\n (with-slots (value)\n object\n value)))" + "(let ((used 2))\n (list used\n (with-slots (value)\n object\n value)))" ); } diff --git a/packages/feature/remove-unused/src/remove_unused_binding/domain/tests/pbt.rs b/packages/feature/remove-unused/src/remove_unused_binding/domain/tests/pbt.rs index dfe04268..56167ebd 100644 --- a/packages/feature/remove-unused/src/remove_unused_binding/domain/tests/pbt.rs +++ b/packages/feature/remove-unused/src/remove_unused_binding/domain/tests/pbt.rs @@ -76,7 +76,7 @@ proptest! { prop_assert_eq!(plan.reference_count, Some(0)); prop_assert!(plan.changed); - let expected_lambda = format!("(lambda ({name})\n {name})"); + let expected_lambda = format!("(lambda ({name})\n {name})"); prop_assert!(plan.rewritten.contains(&expected_lambda)); SyntaxTree::parse(&plan.rewritten) .map_err(|error| TestCaseError::fail(error.to_string()))?; diff --git a/tests/cli/format/indent_table_and_quote_style.rs b/tests/cli/format/indent_table_and_quote_style.rs index 72b6d835..e40e76ac 100644 --- a/tests/cli/format/indent_table_and_quote_style.rs +++ b/tests/cli/format/indent_table_and_quote_style.rs @@ -20,8 +20,8 @@ fn cli_format_indent_table_flag_retargets_a_symbol_onto_a_style() { let file = dir.join(Path::new("source.lisp")); fs::write(&file, "(if a b c)\n").expect("write fixture"); - // `if` never inlines by default (`ListStyle::If` always shows its - // structure), so this is past the compiled-in width regardless. + // `if` never inlines by default (`ListStyle::IfAligned` always shows + // its structure with all branches at the same distinguished column). paredit() .arg("edit") .arg("format") @@ -29,7 +29,7 @@ fn cli_format_indent_table_flag_retargets_a_symbol_onto_a_style() { .arg(&file) .assert() .success() - .stdout(predicate::eq("(if a b\n c)\n")); + .stdout(predicate::eq("(if a\n b\n c)\n")); // `--indent-table if=general` retargets `if` onto the plain-call layout, // so a short `if` form inlines like any other call. @@ -58,7 +58,7 @@ fn cli_format_indent_table_config_key_reaches_the_command() { .arg("source.lisp") .assert() .success() - .stdout(predicate::eq("(if a b\n c)\n")); + .stdout(predicate::eq("(if a\n b\n c)\n")); fs::write( root.join("paredit.toml"), diff --git a/tests/cli/format/numeric_case_alignment_and_editorconfig.rs b/tests/cli/format/numeric_case_alignment_and_editorconfig.rs index c5b9c76d..6dddcead 100644 --- a/tests/cli/format/numeric_case_alignment_and_editorconfig.rs +++ b/tests/cli/format/numeric_case_alignment_and_editorconfig.rs @@ -254,7 +254,7 @@ fn cli_format_editorconfig_indent_size_reaches_the_command() { .arg("source.lisp") .assert() .success() - .stdout(predicate::eq("(if a b\n c)\n")); + .stdout(predicate::eq("(if a\n b\n c)\n")); } #[test] @@ -298,7 +298,9 @@ fn cli_format_paredit_toml_wins_over_editorconfig_on_the_same_key() { .arg("source.lisp") .assert() .success() - .stdout(predicate::eq("(if a b\n c)\n")); + .stdout(predicate::eq( + "(if a\n b\n c)\n", + )); } #[test] @@ -314,7 +316,7 @@ fn cli_format_with_no_editorconfig_behaves_exactly_as_before() { .arg("source.lisp") .assert() .success() - .stdout(predicate::eq("(if a b\n c)\n")); + .stdout(predicate::eq("(if a\n b\n c)\n")); } // --- FR-015: `format.insert-final-newline` / `format.trim-trailing-whitespace` --- diff --git a/tests/cli/remove_unused_binding/scope/variables.rs b/tests/cli/remove_unused_binding/scope/variables.rs index a50c2b30..c6a8598a 100644 --- a/tests/cli/remove_unused_binding/scope/variables.rs +++ b/tests/cli/remove_unused_binding/scope/variables.rs @@ -20,10 +20,10 @@ fn cli_plans_remove_unused_binding_ignoring_shadowed_lambda_parameter() { .success() .stdout(predicate::str::contains("\"binding_name\": \"x\"")) .stdout(predicate::str::contains("\"reference_count\": 0")) - // `used` and `(lambda ...)` are `list`'s siblings, so they share the - // column `list` itself starts at, one past the opening delimiter. + // `used` fits on the head line with `list`, so `(lambda ...)`, its + // sibling, aligns under `used`'s column rather than one past `list`. .stdout(predicate::str::contains( - "(let ((used 2))\\n (list\\n used\\n (lambda (x)\\n x)))", + "(let ((used 2))\\n (list used\\n (lambda (x)\\n x)))", )); }