]> git.lizzy.rs Git - rust.git/blob - src/string.rs
Merge pull request #3063 from otavio/add-doc-is_doc_comment
[rust.git] / src / string.rs
1 // Copyright 2015 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 string literals.
12
13 use regex::Regex;
14 use unicode_segmentation::UnicodeSegmentation;
15
16 use config::Config;
17 use shape::Shape;
18 use utils::wrap_str;
19
20 const MIN_STRING: usize = 10;
21
22 /// Describes the layout of a piece of text.
23 pub struct StringFormat<'a> {
24     /// The opening sequence of characters for the piece of text
25     pub opener: &'a str,
26     /// The closing sequence of characters for the piece of text
27     pub closer: &'a str,
28     /// The opening sequence of characters for a line
29     pub line_start: &'a str,
30     /// The closing sequence of characters for a line
31     pub line_end: &'a str,
32     /// The allocated box to fit the text into
33     pub shape: Shape,
34     /// Trim trailing whitespaces
35     pub trim_end: bool,
36     pub config: &'a Config,
37 }
38
39 impl<'a> StringFormat<'a> {
40     pub fn new(shape: Shape, config: &'a Config) -> StringFormat<'a> {
41         StringFormat {
42             opener: "\"",
43             closer: "\"",
44             line_start: " ",
45             line_end: "\\",
46             shape,
47             trim_end: false,
48             config,
49         }
50     }
51
52     /// Returns the maximum number of graphemes that is possible on a line while taking the
53     /// indentation into account.
54     ///
55     /// If we cannot put at least a single character per line, the rewrite won't succeed.
56     fn max_chars_with_indent(&self) -> Option<usize> {
57         Some(
58             self.shape
59                 .width
60                 .checked_sub(self.opener.len() + self.line_end.len() + 1)?
61                 + 1,
62         )
63     }
64
65     /// Like max_chars_with_indent but the indentation is not substracted.
66     /// This allows to fit more graphemes from the string on a line when
67     /// SnippetState::Overflow.
68     fn max_chars_without_indent(&self) -> Option<usize> {
69         Some(self.config.max_width().checked_sub(self.line_end.len())?)
70     }
71 }
72
73 pub fn rewrite_string<'a>(orig: &str, fmt: &StringFormat<'a>) -> Option<String> {
74     let max_chars_with_indent = fmt.max_chars_with_indent()?;
75     let max_chars_without_indent = fmt.max_chars_without_indent()?;
76     let indent = fmt.shape.indent.to_string_with_newline(fmt.config);
77
78     // Strip line breaks.
79     // With this regex applied, all remaining whitespaces are significant
80     let strip_line_breaks_re = Regex::new(r"([^\\](\\\\)*)\\[\n\r][[:space:]]*").unwrap();
81     let stripped_str = strip_line_breaks_re.replace_all(orig, "$1");
82
83     let graphemes = UnicodeSegmentation::graphemes(&*stripped_str, false).collect::<Vec<&str>>();
84
85     // `cur_start` is the position in `orig` of the start of the current line.
86     let mut cur_start = 0;
87     let mut result = String::with_capacity(
88         stripped_str
89             .len()
90             .checked_next_power_of_two()
91             .unwrap_or(usize::max_value()),
92     );
93     result.push_str(fmt.opener);
94
95     // Snip a line at a time from `stripped_str` until it is used up. Push the snippet
96     // onto result.
97     let mut cur_max_chars = max_chars_with_indent;
98     loop {
99         // All the input starting at cur_start fits on the current line
100         if graphemes.len() - cur_start <= cur_max_chars {
101             result.push_str(&graphemes[cur_start..].join(""));
102             break;
103         }
104
105         // The input starting at cur_start needs to be broken
106         match break_string(cur_max_chars, fmt.trim_end, &graphemes[cur_start..]) {
107             SnippetState::LineEnd(line, len) => {
108                 result.push_str(&line);
109                 result.push_str(fmt.line_end);
110                 result.push_str(&indent);
111                 result.push_str(fmt.line_start);
112                 cur_max_chars = max_chars_with_indent;
113                 cur_start += len;
114             }
115             SnippetState::Overflow(line, len) => {
116                 result.push_str(&line);
117                 cur_max_chars = max_chars_without_indent;
118                 cur_start += len;
119             }
120             SnippetState::EndOfInput(line) => {
121                 result.push_str(&line);
122                 break;
123             }
124         }
125     }
126
127     result.push_str(fmt.closer);
128     wrap_str(result, fmt.config.max_width(), fmt.shape)
129 }
130
131 /// Result of breaking a string so it fits in a line and the state it ended in.
132 /// The state informs about what to do with the snippet and how to continue the breaking process.
133 #[derive(Debug, PartialEq)]
134 enum SnippetState {
135     /// The input could not be broken and so rewriting the string is finished.
136     EndOfInput(String),
137     /// The input could be broken and the returned snippet should be ended with a
138     /// `[StringFormat::line_end]`. The next snippet needs to be indented.
139     /// The returned string is the line to print out and the number is the length that got read in
140     /// the text being rewritten. That length may be greater than the returned string if trailing
141     /// whitespaces got trimmed.
142     LineEnd(String, usize),
143     /// The input could be broken but the returned snippet should not be ended with a
144     /// `[StringFormat::line_end]` because the whitespace is significant. Therefore, the next
145     /// snippet should not be indented.
146     Overflow(String, usize),
147 }
148
149 /// Break the input string at a boundary character around the offset `max_chars`. A boundary
150 /// character is either a punctuation or a whitespace.
151 fn break_string(max_chars: usize, trim_end: bool, input: &[&str]) -> SnippetState {
152     let break_at = |index /* grapheme at index is included */| {
153         // Take in any whitespaces to the left/right of `input[index]` and
154         // check if there is a line feed, in which case whitespaces needs to be kept.
155         let mut index_minus_ws = index;
156         for (i, grapheme) in input[0..=index].iter().enumerate().rev() {
157             if !is_whitespace(grapheme) {
158                 index_minus_ws = i;
159                 break;
160             }
161         }
162         // Take into account newlines occuring in input[0..=index], i.e., the possible next new
163         // line. If there is one, then text after it could be rewritten in a way that the available
164         // space is fully used.
165         for (i, grapheme) in input[0..=index].iter().enumerate() {
166             if is_line_feed(grapheme) {
167                 if i < index_minus_ws || !trim_end {
168                     return SnippetState::Overflow(input[0..=i].join("").to_string(), i + 1);
169                 }
170                 break;
171             }
172         }
173
174         let mut index_plus_ws = index;
175         for (i, grapheme) in input[index + 1..].iter().enumerate() {
176             if !trim_end && is_line_feed(grapheme) {
177                 return SnippetState::Overflow(
178                     input[0..=index + 1 + i].join("").to_string(),
179                     index + 2 + i,
180                 );
181             } else if !is_whitespace(grapheme) {
182                 index_plus_ws = index + i;
183                 break;
184             }
185         }
186
187         if trim_end {
188             SnippetState::LineEnd(
189                 input[0..=index_minus_ws].join("").to_string(),
190                 index_plus_ws + 1,
191             )
192         } else {
193             SnippetState::LineEnd(
194                 input[0..=index_plus_ws].join("").to_string(),
195                 index_plus_ws + 1,
196             )
197         }
198     };
199
200     // Find the position in input for breaking the string
201     match input[0..max_chars]
202         .iter()
203         .rposition(|grapheme| is_whitespace(grapheme))
204     {
205         // Found a whitespace and what is on its left side is big enough.
206         Some(index) if index >= MIN_STRING => break_at(index),
207         // No whitespace found, try looking for a punctuation instead
208         _ => match input[0..max_chars]
209             .iter()
210             .rposition(|grapheme| is_punctuation(grapheme))
211         {
212             // Found a punctuation and what is on its left side is big enough.
213             Some(index) if index >= MIN_STRING => break_at(index),
214             // Either no boundary character was found to the left of `input[max_chars]`, or the line
215             // got too small. We try searching for a boundary character to the right.
216             _ => match input[max_chars..]
217                 .iter()
218                 .position(|grapheme| is_whitespace(grapheme) || is_punctuation(grapheme))
219             {
220                 // A boundary was found after the line limit
221                 Some(index) => break_at(max_chars + index),
222                 // No boundary to the right, the input cannot be broken
223                 None => SnippetState::EndOfInput(input.join("").to_string()),
224             },
225         },
226     }
227 }
228
229 fn is_line_feed(grapheme: &str) -> bool {
230     grapheme.as_bytes()[0] == b'\n'
231 }
232
233 fn is_whitespace(grapheme: &str) -> bool {
234     grapheme.chars().all(|c| c.is_whitespace())
235 }
236
237 fn is_punctuation(grapheme: &str) -> bool {
238     match grapheme.as_bytes()[0] {
239         b':' | b',' | b';' | b'.' => true,
240         _ => false,
241     }
242 }
243
244 #[cfg(test)]
245 mod test {
246     use super::{break_string, rewrite_string, SnippetState, StringFormat};
247     use config::Config;
248     use shape::{Indent, Shape};
249     use unicode_segmentation::UnicodeSegmentation;
250
251     #[test]
252     fn issue343() {
253         let config = Default::default();
254         let fmt = StringFormat::new(Shape::legacy(2, Indent::empty()), &config);
255         rewrite_string("eq_", &fmt);
256     }
257
258     #[test]
259     fn should_break_on_whitespace() {
260         let string = "Placerat felis. Mauris porta ante sagittis purus.";
261         let graphemes = UnicodeSegmentation::graphemes(&*string, false).collect::<Vec<&str>>();
262         assert_eq!(
263             break_string(20, false, &graphemes[..]),
264             SnippetState::LineEnd("Placerat felis. ".to_string(), 16)
265         );
266         assert_eq!(
267             break_string(20, true, &graphemes[..]),
268             SnippetState::LineEnd("Placerat felis.".to_string(), 16)
269         );
270     }
271
272     #[test]
273     fn should_break_on_punctuation() {
274         let string = "Placerat_felis._Mauris_porta_ante_sagittis_purus.";
275         let graphemes = UnicodeSegmentation::graphemes(&*string, false).collect::<Vec<&str>>();
276         assert_eq!(
277             break_string(20, false, &graphemes[..]),
278             SnippetState::LineEnd("Placerat_felis.".to_string(), 15)
279         );
280     }
281
282     #[test]
283     fn should_break_forward() {
284         let string = "Venenatis_tellus_vel_tellus. Aliquam aliquam dolor at justo.";
285         let graphemes = UnicodeSegmentation::graphemes(&*string, false).collect::<Vec<&str>>();
286         assert_eq!(
287             break_string(20, false, &graphemes[..]),
288             SnippetState::LineEnd("Venenatis_tellus_vel_tellus. ".to_string(), 29)
289         );
290         assert_eq!(
291             break_string(20, true, &graphemes[..]),
292             SnippetState::LineEnd("Venenatis_tellus_vel_tellus.".to_string(), 29)
293         );
294     }
295
296     #[test]
297     fn nothing_to_break() {
298         let string = "Venenatis_tellus_vel_tellus";
299         let graphemes = UnicodeSegmentation::graphemes(&*string, false).collect::<Vec<&str>>();
300         assert_eq!(
301             break_string(20, false, &graphemes[..]),
302             SnippetState::EndOfInput("Venenatis_tellus_vel_tellus".to_string())
303         );
304     }
305
306     #[test]
307     fn significant_whitespaces() {
308         let string = "Neque in sem.      \n      Pellentesque tellus augue.";
309         let graphemes = UnicodeSegmentation::graphemes(&*string, false).collect::<Vec<&str>>();
310         assert_eq!(
311             break_string(15, false, &graphemes[..]),
312             SnippetState::Overflow("Neque in sem.      \n".to_string(), 20)
313         );
314         assert_eq!(
315             break_string(25, false, &graphemes[..]),
316             SnippetState::Overflow("Neque in sem.      \n".to_string(), 20)
317         );
318         // if `StringFormat::line_end` is true, then the line feed does not matter anymore
319         assert_eq!(
320             break_string(15, true, &graphemes[..]),
321             SnippetState::LineEnd("Neque in sem.".to_string(), 26)
322         );
323         assert_eq!(
324             break_string(25, true, &graphemes[..]),
325             SnippetState::LineEnd("Neque in sem.".to_string(), 26)
326         );
327     }
328
329     #[test]
330     fn big_whitespace() {
331         let string = "Neque in sem.            Pellentesque tellus augue.";
332         let graphemes = UnicodeSegmentation::graphemes(&*string, false).collect::<Vec<&str>>();
333         assert_eq!(
334             break_string(20, false, &graphemes[..]),
335             SnippetState::LineEnd("Neque in sem.            ".to_string(), 25)
336         );
337         assert_eq!(
338             break_string(20, true, &graphemes[..]),
339             SnippetState::LineEnd("Neque in sem.".to_string(), 25)
340         );
341     }
342
343     #[test]
344     fn newline_in_candidate_line() {
345         let string = "Nulla\nconsequat erat at massa. Vivamus id mi.";
346
347         let graphemes = UnicodeSegmentation::graphemes(&*string, false).collect::<Vec<&str>>();
348         assert_eq!(
349             break_string(25, false, &graphemes[..]),
350             SnippetState::Overflow("Nulla\n".to_string(), 6)
351         );
352         assert_eq!(
353             break_string(25, true, &graphemes[..]),
354             SnippetState::Overflow("Nulla\n".to_string(), 6)
355         );
356
357         let mut config: Config = Default::default();
358         config.set().max_width(27);
359         let fmt = StringFormat::new(Shape::legacy(25, Indent::empty()), &config);
360         let rewritten_string = rewrite_string(string, &fmt);
361         assert_eq!(
362             rewritten_string,
363             Some("\"Nulla\nconsequat erat at massa. \\\n Vivamus id mi.\"".to_string())
364         );
365     }
366 }