1 // Format with vertical alignment.
2
3 use std::cmp;
4
5 use itertools::Itertools;
6 use rustc_ast::ast;
7 use rustc_span::{BytePos, Span};
8
9 use crate::comment::combine_strs_with_missing_comments;
10 use crate::config::lists::*;
11 use crate::expr::rewrite_field;
12 use crate::items::{rewrite_struct_field, rewrite_struct_field_prefix};
13 use crate::lists::{
14 definitive_tactic, itemize_list, write_list, ListFormatting, ListItem, Separator,
15 };
16 use crate::rewrite::{Rewrite, RewriteContext};
17 use crate::shape::{Indent, Shape};
18 use crate::source_map::SpanUtils;
19 use crate::spanned::Spanned;
20 use crate::utils::{
21 contains_skip, is_attributes_extendable, mk_sp, rewrite_ident, trimmed_last_line_width,
22 };
23
24 pub(crate) trait AlignedItem {
skip(&self) -> bool25 fn skip(&self) -> bool;
get_span(&self) -> Span26 fn get_span(&self) -> Span;
rewrite_prefix(&self, context: &RewriteContext<'_>, shape: Shape) -> Option<String>27 fn rewrite_prefix(&self, context: &RewriteContext<'_>, shape: Shape) -> Option<String>;
rewrite_aligned_item( &self, context: &RewriteContext<'_>, shape: Shape, prefix_max_width: usize, ) -> Option<String>28 fn rewrite_aligned_item(
29 &self,
30 context: &RewriteContext<'_>,
31 shape: Shape,
32 prefix_max_width: usize,
33 ) -> Option<String>;
34 }
35
36 impl AlignedItem for ast::FieldDef {
skip(&self) -> bool37 fn skip(&self) -> bool {
38 contains_skip(&self.attrs)
39 }
40
get_span(&self) -> Span41 fn get_span(&self) -> Span {
42 self.span()
43 }
44
rewrite_prefix(&self, context: &RewriteContext<'_>, shape: Shape) -> Option<String>45 fn rewrite_prefix(&self, context: &RewriteContext<'_>, shape: Shape) -> Option<String> {
46 let attrs_str = self.attrs.rewrite(context, shape)?;
47 let missing_span = if self.attrs.is_empty() {
48 mk_sp(self.span.lo(), self.span.lo())
49 } else {
50 mk_sp(self.attrs.last().unwrap().span.hi(), self.span.lo())
51 };
52 let attrs_extendable = self.ident.is_none() && is_attributes_extendable(&attrs_str);
53 rewrite_struct_field_prefix(context, self).and_then(|field_str| {
54 combine_strs_with_missing_comments(
55 context,
56 &attrs_str,
57 &field_str,
58 missing_span,
59 shape,
60 attrs_extendable,
61 )
62 })
63 }
64
rewrite_aligned_item( &self, context: &RewriteContext<'_>, shape: Shape, prefix_max_width: usize, ) -> Option<String>65 fn rewrite_aligned_item(
66 &self,
67 context: &RewriteContext<'_>,
68 shape: Shape,
69 prefix_max_width: usize,
70 ) -> Option<String> {
71 rewrite_struct_field(context, self, shape, prefix_max_width)
72 }
73 }
74
75 impl AlignedItem for ast::ExprField {
skip(&self) -> bool76 fn skip(&self) -> bool {
77 contains_skip(&self.attrs)
78 }
79
get_span(&self) -> Span80 fn get_span(&self) -> Span {
81 self.span()
82 }
83
rewrite_prefix(&self, context: &RewriteContext<'_>, shape: Shape) -> Option<String>84 fn rewrite_prefix(&self, context: &RewriteContext<'_>, shape: Shape) -> Option<String> {
85 let attrs_str = self.attrs.rewrite(context, shape)?;
86 let name = rewrite_ident(context, self.ident);
87 let missing_span = if self.attrs.is_empty() {
88 mk_sp(self.span.lo(), self.span.lo())
89 } else {
90 mk_sp(self.attrs.last().unwrap().span.hi(), self.span.lo())
91 };
92 combine_strs_with_missing_comments(
93 context,
94 &attrs_str,
95 name,
96 missing_span,
97 shape,
98 is_attributes_extendable(&attrs_str),
99 )
100 }
101
rewrite_aligned_item( &self, context: &RewriteContext<'_>, shape: Shape, prefix_max_width: usize, ) -> Option<String>102 fn rewrite_aligned_item(
103 &self,
104 context: &RewriteContext<'_>,
105 shape: Shape,
106 prefix_max_width: usize,
107 ) -> Option<String> {
108 rewrite_field(context, self, shape, prefix_max_width)
109 }
110 }
111
rewrite_with_alignment<T: AlignedItem>( fields: &[T], context: &RewriteContext<'_>, shape: Shape, span: Span, one_line_width: usize, ) -> Option<String>112 pub(crate) fn rewrite_with_alignment<T: AlignedItem>(
113 fields: &[T],
114 context: &RewriteContext<'_>,
115 shape: Shape,
116 span: Span,
117 one_line_width: usize,
118 ) -> Option<String> {
119 let (spaces, group_index) = if context.config.struct_field_align_threshold() > 0 {
120 group_aligned_items(context, fields)
121 } else {
122 ("", fields.len() - 1)
123 };
124 let init = &fields[0..=group_index];
125 let rest = &fields[group_index + 1..];
126 let init_last_pos = if rest.is_empty() {
127 span.hi()
128 } else {
129 // Decide whether the missing comments should stick to init or rest.
130 let init_hi = init[init.len() - 1].get_span().hi();
131 let rest_lo = rest[0].get_span().lo();
132 let missing_span = mk_sp(init_hi, rest_lo);
133 let missing_span = mk_sp(
134 context.snippet_provider.span_after(missing_span, ","),
135 missing_span.hi(),
136 );
137
138 let snippet = context.snippet(missing_span);
139 if snippet.trim_start().starts_with("//") {
140 let offset = snippet.lines().next().map_or(0, str::len);
141 // 2 = "," + "\n"
142 init_hi + BytePos(offset as u32 + 2)
143 } else if snippet.trim_start().starts_with("/*") {
144 let comment_lines = snippet
145 .lines()
146 .position(|line| line.trim_end().ends_with("*/"))
147 .unwrap_or(0);
148
149 let offset = snippet
150 .lines()
151 .take(comment_lines + 1)
152 .collect::<Vec<_>>()
153 .join("\n")
154 .len();
155
156 init_hi + BytePos(offset as u32 + 2)
157 } else {
158 missing_span.lo()
159 }
160 };
161 let init_span = mk_sp(span.lo(), init_last_pos);
162 let one_line_width = if rest.is_empty() { one_line_width } else { 0 };
163
164 // if another group follows, we must force a separator
165 let force_separator = !rest.is_empty();
166
167 let result = rewrite_aligned_items_inner(
168 context,
169 init,
170 init_span,
171 shape.indent,
172 one_line_width,
173 force_separator,
174 )?;
175 if rest.is_empty() {
176 Some(result + spaces)
177 } else {
178 let rest_span = mk_sp(init_last_pos, span.hi());
179 let rest_str = rewrite_with_alignment(rest, context, shape, rest_span, one_line_width)?;
180 Some(format!(
181 "{}{}\n{}{}",
182 result,
183 spaces,
184 &shape.indent.to_string(context.config),
185 &rest_str
186 ))
187 }
188 }
189
struct_field_prefix_max_min_width<T: AlignedItem>( context: &RewriteContext<'_>, fields: &[T], shape: Shape, ) -> (usize, usize)190 fn struct_field_prefix_max_min_width<T: AlignedItem>(
191 context: &RewriteContext<'_>,
192 fields: &[T],
193 shape: Shape,
194 ) -> (usize, usize) {
195 fields
196 .iter()
197 .map(|field| {
198 field
199 .rewrite_prefix(context, shape)
200 .map(|field_str| trimmed_last_line_width(&field_str))
201 })
202 .fold_options((0, ::std::usize::MAX), |(max_len, min_len), len| {
203 (cmp::max(max_len, len), cmp::min(min_len, len))
204 })
205 .unwrap_or((0, 0))
206 }
207
rewrite_aligned_items_inner<T: AlignedItem>( context: &RewriteContext<'_>, fields: &[T], span: Span, offset: Indent, one_line_width: usize, force_trailing_separator: bool, ) -> Option<String>208 fn rewrite_aligned_items_inner<T: AlignedItem>(
209 context: &RewriteContext<'_>,
210 fields: &[T],
211 span: Span,
212 offset: Indent,
213 one_line_width: usize,
214 force_trailing_separator: bool,
215 ) -> Option<String> {
216 // 1 = ","
217 let item_shape = Shape::indented(offset, context.config).sub_width(1)?;
218 let (mut field_prefix_max_width, field_prefix_min_width) =
219 struct_field_prefix_max_min_width(context, fields, item_shape);
220 let max_diff = field_prefix_max_width.saturating_sub(field_prefix_min_width);
221 if max_diff > context.config.struct_field_align_threshold() {
222 field_prefix_max_width = 0;
223 }
224
225 let mut items = itemize_list(
226 context.snippet_provider,
227 fields.iter(),
228 "}",
229 ",",
230 |field| field.get_span().lo(),
231 |field| field.get_span().hi(),
232 |field| field.rewrite_aligned_item(context, item_shape, field_prefix_max_width),
233 span.lo(),
234 span.hi(),
235 false,
236 )
237 .collect::<Vec<_>>();
238
239 let tactic = definitive_tactic(
240 &items,
241 ListTactic::HorizontalVertical,
242 Separator::Comma,
243 one_line_width,
244 );
245
246 if tactic == DefinitiveListTactic::Horizontal {
247 // since the items fits on a line, there is no need to align them
248 let do_rewrite =
249 |field: &T| -> Option<String> { field.rewrite_aligned_item(context, item_shape, 0) };
250 fields
251 .iter()
252 .zip(items.iter_mut())
253 .for_each(|(field, list_item): (&T, &mut ListItem)| {
254 if list_item.item.is_some() {
255 list_item.item = do_rewrite(field);
256 }
257 });
258 }
259
260 let separator_tactic = if force_trailing_separator {
261 SeparatorTactic::Always
262 } else {
263 context.config.trailing_comma()
264 };
265
266 let fmt = ListFormatting::new(item_shape, context.config)
267 .tactic(tactic)
268 .trailing_separator(separator_tactic)
269 .preserve_newline(true);
270 write_list(&items, &fmt)
271 }
272
273 /// Returns the index in `fields` up to which a field belongs to the current group.
274 /// The returned string is the group separator to use when rewriting the fields.
275 /// Groups are defined by blank lines.
group_aligned_items<T: AlignedItem>( context: &RewriteContext<'_>, fields: &[T], ) -> (&'static str, usize)276 fn group_aligned_items<T: AlignedItem>(
277 context: &RewriteContext<'_>,
278 fields: &[T],
279 ) -> (&'static str, usize) {
280 let mut index = 0;
281 for i in 0..fields.len() - 1 {
282 if fields[i].skip() {
283 return ("", index);
284 }
285 let span = mk_sp(fields[i].get_span().hi(), fields[i + 1].get_span().lo());
286 let snippet = context
287 .snippet(span)
288 .lines()
289 .skip(1)
290 .collect::<Vec<_>>()
291 .join("\n");
292 let has_blank_line = snippet
293 .lines()
294 .dropping_back(1)
295 .any(|l| l.trim().is_empty());
296 if has_blank_line {
297 return ("\n", index);
298 }
299 index += 1;
300 }
301 ("", index)
302 }
303