]> git.lizzy.rs Git - rust.git/blob - src/tools/rustfmt/src/vertical.rs
Rollup merge of #91518 - luojia65:rustdoc-riscv-arch, r=GuillaumeGomez
[rust.git] / src / tools / rustfmt / src / vertical.rs
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 {
25     fn skip(&self) -> bool;
26     fn get_span(&self) -> Span;
27     fn rewrite_prefix(&self, context: &RewriteContext<'_>, shape: Shape) -> 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 {
37     fn skip(&self) -> bool {
38         contains_skip(&self.attrs)
39     }
40
41     fn get_span(&self) -> Span {
42         self.span()
43     }
44
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
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 {
76     fn skip(&self) -> bool {
77         contains_skip(&self.attrs)
78     }
79
80     fn get_span(&self) -> Span {
81         self.span()
82     }
83
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
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
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
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
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.
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 }