1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
use ra_ap_syntax::{
AstNode, AstToken, NodeOrToken, SyntaxKind, SyntaxNode,
ast::{self, HasAttrs, HasDocComments, HasName, HasVisibility},
};
use crate::formatter::node::common::{comments, fields, header};
use crate::formatter::printer::Printer;
/// Information about a variant for formatting
struct VariantInfo {
variant: ast::Variant,
leading_comments: Vec<String>,
trailing_comment: Option<(String, String)>, // (whitespace, comment)
has_blank_line_before: bool,
}
/// Pre-scan all variants to correctly assign trailing comments.
///
/// Due to how rust-analyzer parses, a trailing comment like:
/// `Foo, // comment`
/// is actually attached as a leading child of the NEXT variant, not as a
/// sibling after the current variant. We detect this by checking if the
/// first token in a variant is a comment with no preceding newline.
fn collect_variant_info(variants: &ast::VariantList) -> Vec<VariantInfo> {
let mut result: Vec<VariantInfo> = Vec::new();
let variant_list: Vec<_> = variants.variants().collect();
for (idx, variant) in variant_list.iter().enumerate() {
let mut leading_comments = Vec::new();
let mut trailing_comment_for_prev: Option<String> = None;
let mut seen_newline = false;
// Check if there's a newline before this variant - if so, any comment
// at the start is a leading comment, not trailing for previous
let newline_before_variant = idx > 0 && comments::has_newline_before_node(variant.syntax());
// Collect comments from inside the variant node (before the name)
for child in variant.syntax().children_with_tokens() {
match child {
NodeOrToken::Token(t) => {
if t.kind() == SyntaxKind::COMMENT {
let text = t.text().to_string();
// Skip doc comments (handled by HasDocComments)
if !text.starts_with("///") && !text.starts_with("//!") {
if !seen_newline
&& !newline_before_variant
&& trailing_comment_for_prev.is_none()
{
// First comment before any newline AND no newline before variant - trailing for previous
trailing_comment_for_prev = Some(text);
} else {
leading_comments.push(text);
}
}
} else if t.kind() == SyntaxKind::WHITESPACE && t.text().contains('\n') {
seen_newline = true;
// Don't move trailing_comment_for_prev to leading - it stays as trailing for previous variant
}
}
NodeOrToken::Node(n) => {
if n.kind() == SyntaxKind::NAME {
break;
}
}
}
}
// If we still have a trailing_comment_for_prev and there's a previous variant,
// attach it to that variant
if let Some(comment) = trailing_comment_for_prev {
if !result.is_empty() {
result.last_mut().unwrap().trailing_comment = Some((" ".to_string(), comment));
} else {
// No previous variant, treat as leading comment
leading_comments.insert(0, comment);
}
}
// Check for blank line before
let has_blank_line_before = if idx > 0 {
has_blank_line_before_variant(variant.syntax())
} else {
false
};
// Check for trailing comment as sibling (after comma) - this handles
// comments that ARE siblings, like the last variant's trailing comment
let trailing_from_sibling = comments::get_trailing_comment_sibling(variant.syntax());
result.push(VariantInfo {
variant: variant.clone(),
leading_comments,
trailing_comment: trailing_from_sibling,
has_blank_line_before,
});
}
result
}
/// Check if there's a blank line before this variant node
fn has_blank_line_before_variant(node: &SyntaxNode) -> bool {
let mut current = node.prev_sibling_or_token();
while let Some(item) = current {
match &item {
NodeOrToken::Token(t) => {
if t.kind() == SyntaxKind::WHITESPACE {
if t.text().matches('\n').count() >= 2 {
return true;
}
} else if t.kind() == SyntaxKind::COMMENT || t.kind() == SyntaxKind::COMMA {
// Continue past comments and commas
} else {
return false;
}
current = t.prev_sibling_or_token();
}
NodeOrToken::Node(_) => return false,
}
}
false
}
pub fn format_enum(node: &SyntaxNode, buf: &mut String, indent: usize) {
let enum_ = match ast::Enum::cast(node.clone()) {
Some(e) => e,
None => return,
};
// Header: docs, attrs, visibility, "enum", name, generics
header::format_item_header(&enum_, "enum", buf, indent);
if let Some(variants) = enum_.variant_list() {
buf.open_brace();
// Pre-scan to correctly assign trailing comments
let variant_infos = collect_variant_info(&variants);
for (idx, info) in variant_infos.iter().enumerate() {
let variant = &info.variant;
// Check for blank line before this variant (to preserve spacing)
if idx > 0 && info.has_blank_line_before {
buf.blank();
}
// Output leading comments
for comment in &info.leading_comments {
buf.line(indent + 4, comment);
}
// Variant doc comments (///)
for doc_comment in variant.doc_comments() {
buf.line(indent + 4, doc_comment.text().trim());
}
// Variant attributes
for attr in variant.attrs() {
buf.line(indent + 4, &attr.syntax().text().to_string());
}
buf.indent(indent + 4);
if let Some(name) = variant.name() {
buf.push_str(&name.text());
}
if let Some(field_list) = variant.field_list() {
match field_list {
ast::FieldList::RecordFieldList(record_fields) => {
buf.open_brace();
fields::format_record_fields(&record_fields, buf, indent + 8);
buf.close_brace(indent + 4);
}
ast::FieldList::TupleFieldList(fields_tuple) => {
buf.push('(');
for (i, field) in fields_tuple.fields().enumerate() {
if i > 0 {
buf.push_str(", ");
}
if let Some(vis) = field.visibility() {
buf.push_str(&vis.syntax().text().to_string());
buf.push(' ');
}
if let Some(ty) = field.ty() {
buf.push_str(&ty.syntax().text().to_string());
}
}
buf.push(')');
}
}
}
if let Some(expr) = variant.expr() {
buf.push_str(" = ");
buf.push_str(&expr.syntax().text().to_string());
}
// Check for trailing comment on same line
if let Some((ref whitespace, ref comment)) = info.trailing_comment {
buf.push(',');
buf.push_str(whitespace);
buf.newline(comment);
} else {
buf.newline(",");
}
}
buf.close_brace(indent);
} else {
buf.push_str(" {}");
}
buf.push('\n');
}