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