Skip to content
Merged
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
5 changes: 4 additions & 1 deletion packages/core/syntax/src/sexpr/formatter/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
};
Expand Down
50 changes: 48 additions & 2 deletions packages/core/syntax/src/sexpr/formatter/lists/definitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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,
Expand Down
59 changes: 52 additions & 7 deletions packages/core/syntax/src/sexpr/formatter/lists/general.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<usize> = None;
let mut head_line_count: Option<usize> = 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());
}

Expand Down
8 changes: 4 additions & 4 deletions packages/core/syntax/src/sexpr/formatter/lists/loops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
77 changes: 72 additions & 5 deletions packages/core/syntax/src/sexpr/formatter/styles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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",
Expand All @@ -103,6 +109,7 @@ pub(super) fn style_from_name(name: &str) -> Option<ListStyle> {
"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),
Expand All @@ -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),
}
}
Expand All @@ -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 {
Expand All @@ -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;
}
Expand Down Expand Up @@ -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"
Expand All @@ -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,
}
}
Expand Down
Loading