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