rustfmt_nightly/
vertical.rs

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