]> git.lizzy.rs Git - rust.git/blob - src/tools/rustfmt/src/vertical.rs
Auto merge of #91469 - matthiaskrgr:rollup-xom3j55, r=matthiaskrgr
[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     let result =
164         rewrite_aligned_items_inner(context, init, init_span, shape.indent, one_line_width)?;
165     if rest.is_empty() {
166         Some(result + spaces)
167     } else {
168         let rest_span = mk_sp(init_last_pos, span.hi());
169         let rest_str = rewrite_with_alignment(rest, context, shape, rest_span, one_line_width)?;
170         Some(format!(
171             "{}{}\n{}{}",
172             result,
173             spaces,
174             &shape.indent.to_string(context.config),
175             &rest_str
176         ))
177     }
178 }
179
180 fn struct_field_prefix_max_min_width<T: AlignedItem>(
181     context: &RewriteContext<'_>,
182     fields: &[T],
183     shape: Shape,
184 ) -> (usize, usize) {
185     fields
186         .iter()
187         .map(|field| {
188             field
189                 .rewrite_prefix(context, shape)
190                 .map(|field_str| trimmed_last_line_width(&field_str))
191         })
192         .fold_options((0, ::std::usize::MAX), |(max_len, min_len), len| {
193             (cmp::max(max_len, len), cmp::min(min_len, len))
194         })
195         .unwrap_or((0, 0))
196 }
197
198 fn rewrite_aligned_items_inner<T: AlignedItem>(
199     context: &RewriteContext<'_>,
200     fields: &[T],
201     span: Span,
202     offset: Indent,
203     one_line_width: usize,
204 ) -> Option<String> {
205     // 1 = ","
206     let item_shape = Shape::indented(offset, context.config).sub_width(1)?;
207     let (mut field_prefix_max_width, field_prefix_min_width) =
208         struct_field_prefix_max_min_width(context, fields, item_shape);
209     let max_diff = field_prefix_max_width.saturating_sub(field_prefix_min_width);
210     if max_diff > context.config.struct_field_align_threshold() {
211         field_prefix_max_width = 0;
212     }
213
214     let mut items = itemize_list(
215         context.snippet_provider,
216         fields.iter(),
217         "}",
218         ",",
219         |field| field.get_span().lo(),
220         |field| field.get_span().hi(),
221         |field| field.rewrite_aligned_item(context, item_shape, field_prefix_max_width),
222         span.lo(),
223         span.hi(),
224         false,
225     )
226     .collect::<Vec<_>>();
227
228     let tactic = definitive_tactic(
229         &items,
230         ListTactic::HorizontalVertical,
231         Separator::Comma,
232         one_line_width,
233     );
234
235     if tactic == DefinitiveListTactic::Horizontal {
236         // since the items fits on a line, there is no need to align them
237         let do_rewrite =
238             |field: &T| -> Option<String> { field.rewrite_aligned_item(context, item_shape, 0) };
239         fields
240             .iter()
241             .zip(items.iter_mut())
242             .for_each(|(field, list_item): (&T, &mut ListItem)| {
243                 if list_item.item.is_some() {
244                     list_item.item = do_rewrite(field);
245                 }
246             });
247     }
248
249     let fmt = ListFormatting::new(item_shape, context.config)
250         .tactic(tactic)
251         .trailing_separator(context.config.trailing_comma())
252         .preserve_newline(true);
253     write_list(&items, &fmt)
254 }
255
256 /// Returns the index in `fields` up to which a field belongs to the current group.
257 /// The returned string is the group separator to use when rewriting the fields.
258 /// Groups are defined by blank lines.
259 fn group_aligned_items<T: AlignedItem>(
260     context: &RewriteContext<'_>,
261     fields: &[T],
262 ) -> (&'static str, usize) {
263     let mut index = 0;
264     for i in 0..fields.len() - 1 {
265         if fields[i].skip() {
266             return ("", index);
267         }
268         let span = mk_sp(fields[i].get_span().hi(), fields[i + 1].get_span().lo());
269         let snippet = context
270             .snippet(span)
271             .lines()
272             .skip(1)
273             .collect::<Vec<_>>()
274             .join("\n");
275         let has_blank_line = snippet
276             .lines()
277             .dropping_back(1)
278             .any(|l| l.trim().is_empty());
279         if has_blank_line {
280             return ("\n", index);
281         }
282         index += 1;
283     }
284     ("", index)
285 }