]> git.lizzy.rs Git - rust.git/blobdiff - src/comment.rs
0.99.7
[rust.git] / src / comment.rs
index 561989a1a11bba8265053e41e97f089b18d97d77..4ba95bda053867caaacee25a799a5fd2901f7790 100644 (file)
@@ -19,7 +19,7 @@
 use rewrite::RewriteContext;
 use shape::{Indent, Shape};
 use string::{rewrite_string, StringFormat};
-use utils::{count_newlines, first_line_width, last_line_width};
+use utils::{count_newlines, first_line_width, last_line_width, trim_left_preserve_layout};
 use {ErrorKind, FormattingError};
 
 fn is_custom_comment(comment: &str) -> bool {
@@ -332,12 +332,12 @@ fn consume_same_line_comments(
     let (first_group, rest) = orig.split_at(first_group_ending);
     let rewritten_first_group =
         if !config.normalize_comments() && has_bare_lines && style.is_block_comment() {
-            light_rewrite_block_comment_with_bare_lines(first_group, shape, config)?
+            trim_left_preserve_layout(first_group, shape.indent, config)?
         } else if !config.normalize_comments()
             && !config.wrap_comments()
             && !config.format_doc_comments()
         {
-            light_rewrite_comment(first_group, shape.indent, config, is_doc_comment)?
+            light_rewrite_comment(first_group, shape.indent, config, is_doc_comment)
         } else {
             rewrite_comment_inner(
                 first_group,
@@ -370,47 +370,6 @@ fn consume_same_line_comments(
     }
 }
 
-/// Trims a minimum of leading whitespaces so that the content layout is kept and aligns to indent.
-fn light_rewrite_block_comment_with_bare_lines(
-    orig: &str,
-    shape: Shape,
-    config: &Config,
-) -> Option<String> {
-    let prefix_whitespace_min = orig
-        .lines()
-        // skip the line with the starting sigil since the leading whitespace is removed
-        // otherwise, the minimum would always be zero
-        .skip(1)
-        .filter(|line| !line.is_empty())
-        .map(|line| {
-            let mut width = 0;
-            for c in line.chars() {
-                match c {
-                    ' ' => width += 1,
-                    '\t' => width += config.tab_spaces(),
-                    _ => break,
-                }
-            }
-            width
-        })
-        .min()?;
-
-    let indent_str = shape.indent.to_string(config);
-    let mut lines = orig.lines();
-    let first_line = lines.next()?;
-    let rest = lines
-        .map(|line| {
-            if line.is_empty() {
-                line
-            } else {
-                &line[prefix_whitespace_min..]
-            }
-        })
-        .collect::<Vec<&str>>()
-        .join(&format!("\n{}", indent_str));
-    Some(format!("{}\n{}{}", first_line, indent_str, rest))
-}
-
 /// Attributes for code blocks in rustdoc.
 /// See https://doc.rust-lang.org/rustdoc/print.html#attributes
 enum CodeBlockAttribute {
@@ -439,7 +398,7 @@ fn new(attribute: &str) -> CodeBlockAttribute {
 /// Block that is formatted as an item.
 ///
 /// An item starts with either a star `*` or a dash `-`. Different level of indentation are
-/// handled.
+/// handled by shrinking the shape accordingly.
 struct ItemizedBlock {
     /// the number of whitespaces up to the item sigil
     indent: usize,
@@ -488,70 +447,75 @@ fn in_block(&self, line: &str) -> bool {
     }
 }
 
-fn rewrite_comment_inner(
-    orig: &str,
-    block_style: bool,
-    style: CommentStyle,
-    shape: Shape,
-    config: &Config,
-    is_doc_comment: bool,
-) -> Option<String> {
-    let (opener, closer, line_start) = if block_style {
-        CommentStyle::SingleBullet.to_str_tuplet()
-    } else {
-        comment_style(orig, config.normalize_comments()).to_str_tuplet()
-    };
+struct CommentRewrite<'a> {
+    result: String,
+    code_block_buffer: String,
+    is_prev_line_multi_line: bool,
+    code_block_attr: Option<CodeBlockAttribute>,
+    item_block_buffer: String,
+    item_block: Option<ItemizedBlock>,
+    comment_line_separator: String,
+    indent_str: String,
+    max_chars: usize,
+    fmt_indent: Indent,
+    fmt: StringFormat<'a>,
 
-    let max_chars = shape
-        .width
-        .checked_sub(closer.len() + opener.len())
-        .unwrap_or(1);
-    let indent_str = shape.indent.to_string_with_newline(config);
-    let fmt_indent = shape.indent + (opener.len() - line_start.len());
-    let mut fmt = StringFormat {
-        opener: "",
-        closer: "",
-        line_start,
-        line_end: "",
-        shape: Shape::legacy(max_chars, fmt_indent),
-        trim_end: true,
-        config,
-    };
+    opener: String,
+    closer: String,
+    line_start: String,
+}
 
-    let line_breaks = count_newlines(orig.trim_right());
-    let lines = orig
-        .lines()
-        .enumerate()
-        .map(|(i, mut line)| {
-            line = trim_right_unless_two_whitespaces(line.trim_left(), is_doc_comment);
-            // Drop old closer.
-            if i == line_breaks && line.ends_with("*/") && !line.starts_with("//") {
-                line = line[..(line.len() - 2)].trim_right();
-            }
+impl<'a> CommentRewrite<'a> {
+    fn new(
+        orig: &'a str,
+        block_style: bool,
+        shape: Shape,
+        config: &'a Config,
+    ) -> CommentRewrite<'a> {
+        let (opener, closer, line_start) = if block_style {
+            CommentStyle::SingleBullet.to_str_tuplet()
+        } else {
+            comment_style(orig, config.normalize_comments()).to_str_tuplet()
+        };
 
-            line
-        })
-        .map(|s| left_trim_comment_line(s, &style))
-        .map(|(line, has_leading_whitespace)| {
-            if orig.starts_with("/*") && line_breaks == 0 {
-                (
-                    line.trim_left(),
-                    has_leading_whitespace || config.normalize_comments(),
-                )
-            } else {
-                (line, has_leading_whitespace || config.normalize_comments())
-            }
-        });
+        let max_chars = shape
+            .width
+            .checked_sub(closer.len() + opener.len())
+            .unwrap_or(1);
+        let indent_str = shape.indent.to_string_with_newline(config).to_string();
+        let fmt_indent = shape.indent + (opener.len() - line_start.len());
+
+        let mut cr = CommentRewrite {
+            result: String::with_capacity(orig.len() * 2),
+            code_block_buffer: String::with_capacity(128),
+            is_prev_line_multi_line: false,
+            code_block_attr: None,
+            item_block_buffer: String::with_capacity(128),
+            item_block: None,
+            comment_line_separator: format!("{}{}", indent_str, line_start),
+            max_chars,
+            indent_str,
+            fmt_indent,
+
+            fmt: StringFormat {
+                opener: "",
+                closer: "",
+                line_start,
+                line_end: "",
+                shape: Shape::legacy(max_chars, fmt_indent),
+                trim_end: true,
+                config,
+            },
 
-    let mut result = String::with_capacity(orig.len() * 2);
-    result.push_str(opener);
-    let mut code_block_buffer = String::with_capacity(128);
-    let mut is_prev_line_multi_line = false;
-    let mut code_block_attr = None;
-    let mut item_block_buffer = String::with_capacity(128);
-    let mut item_block: Option<ItemizedBlock> = None;
-    let comment_line_separator = format!("{}{}", indent_str, line_start);
-    let join_block = |s: &str, sep: &str| {
+            opener: opener.to_owned(),
+            closer: closer.to_owned(),
+            line_start: line_start.to_owned(),
+        };
+        cr.result.push_str(opener);
+        cr
+    }
+
+    fn join_block(s: &str, sep: &str) -> String {
         let mut result = String::with_capacity(s.len() + 128);
         let mut iter = s.lines().peekable();
         while let Some(line) = iter.next() {
@@ -563,186 +527,252 @@ fn rewrite_comment_inner(
             });
         }
         result
-    };
+    }
 
-    for (i, (line, has_leading_whitespace)) in lines.enumerate() {
+    fn finish(mut self) -> String {
+        if !self.code_block_buffer.is_empty() {
+            // There is a code block that is not properly enclosed by backticks.
+            // We will leave them untouched.
+            self.result.push_str(&self.comment_line_separator);
+            self.result.push_str(&Self::join_block(
+                &trim_custom_comment_prefix(&self.code_block_buffer),
+                &self.comment_line_separator,
+            ));
+        }
+
+        if !self.item_block_buffer.is_empty() {
+            // the last few lines are part of an itemized block
+            self.fmt.shape = Shape::legacy(self.max_chars, self.fmt_indent);
+            let mut ib = None;
+            ::std::mem::swap(&mut ib, &mut self.item_block);
+            let ib = ib.unwrap();
+            let item_fmt = ib.create_string_format(&self.fmt);
+            self.result.push_str(&self.comment_line_separator);
+            self.result.push_str(&ib.opener);
+            match rewrite_string(
+                &self.item_block_buffer.replace("\n", " "),
+                &item_fmt,
+                self.max_chars.saturating_sub(ib.indent),
+            ) {
+                Some(s) => self.result.push_str(&Self::join_block(
+                    &s,
+                    &format!("{}{}", &self.comment_line_separator, ib.line_start),
+                )),
+                None => self.result.push_str(&Self::join_block(
+                    &self.item_block_buffer,
+                    &self.comment_line_separator,
+                )),
+            };
+        }
+
+        self.result.push_str(&self.closer);
+        if self.result.ends_with(&self.opener) && self.opener.ends_with(' ') {
+            // Trailing space.
+            self.result.pop();
+        }
+
+        self.result
+    }
+
+    fn handle_line(
+        &mut self,
+        orig: &'a str,
+        i: usize,
+        line: &'a str,
+        has_leading_whitespace: bool,
+    ) -> bool {
         let is_last = i == count_newlines(orig);
 
-        if let Some(ref ib) = item_block {
+        if let Some(ref ib) = self.item_block {
             if ib.in_block(&line) {
-                item_block_buffer.push_str(&line);
-                item_block_buffer.push('\n');
-                continue;
+                self.item_block_buffer.push_str(line.trim_start());
+                self.item_block_buffer.push('\n');
+                return false;
             }
-            is_prev_line_multi_line = false;
-            fmt.shape = Shape::legacy(max_chars, fmt_indent);
-            let item_fmt = ib.create_string_format(&fmt);
-            result.push_str(&comment_line_separator);
-            result.push_str(&ib.opener);
+            self.is_prev_line_multi_line = false;
+            self.fmt.shape = Shape::legacy(self.max_chars, self.fmt_indent);
+            let item_fmt = ib.create_string_format(&self.fmt);
+            self.result.push_str(&self.comment_line_separator);
+            self.result.push_str(&ib.opener);
             match rewrite_string(
-                &item_block_buffer.replace("\n", " "),
+                &self.item_block_buffer.replace("\n", " "),
                 &item_fmt,
-                max_chars.saturating_sub(ib.indent),
+                self.max_chars.saturating_sub(ib.indent),
             ) {
-                Some(s) => result.push_str(&join_block(
+                Some(s) => self.result.push_str(&Self::join_block(
                     &s,
-                    &format!("{}{}", &comment_line_separator, ib.line_start),
+                    &format!("{}{}", &self.comment_line_separator, ib.line_start),
+                )),
+                None => self.result.push_str(&Self::join_block(
+                    &self.item_block_buffer,
+                    &self.comment_line_separator,
                 )),
-                None => result.push_str(&join_block(&item_block_buffer, &comment_line_separator)),
             };
-            item_block_buffer.clear();
-        } else if let Some(ref attr) = code_block_attr {
+            self.item_block_buffer.clear();
+        } else if self.code_block_attr.is_some() {
             if line.starts_with("```") {
-                let code_block = match attr {
+                let code_block = match self.code_block_attr.as_ref().unwrap() {
                     CodeBlockAttribute::Ignore | CodeBlockAttribute::Text => {
-                        trim_custom_comment_prefix(&code_block_buffer)
+                        trim_custom_comment_prefix(&self.code_block_buffer)
                     }
-                    _ if code_block_buffer.is_empty() => String::new(),
+                    _ if self.code_block_buffer.is_empty() => String::new(),
                     _ => {
-                        let mut config = config.clone();
+                        let mut config = self.fmt.config.clone();
                         config.set().format_doc_comments(false);
-                        match ::format_code_block(&code_block_buffer, &config) {
-                            Some(ref s) => trim_custom_comment_prefix(s),
-                            None => trim_custom_comment_prefix(&code_block_buffer),
+                        match ::format_code_block(&self.code_block_buffer, &config) {
+                            Some(ref s) => trim_custom_comment_prefix(&s.snippet),
+                            None => trim_custom_comment_prefix(&self.code_block_buffer),
                         }
                     }
                 };
                 if !code_block.is_empty() {
-                    result.push_str(&comment_line_separator);
-                    result.push_str(&join_block(&code_block, &comment_line_separator));
+                    self.result.push_str(&self.comment_line_separator);
+                    self.result
+                        .push_str(&Self::join_block(&code_block, &self.comment_line_separator));
                 }
-                code_block_buffer.clear();
-                result.push_str(&comment_line_separator);
-                result.push_str(line);
-                code_block_attr = None;
+                self.code_block_buffer.clear();
+                self.result.push_str(&self.comment_line_separator);
+                self.result.push_str(line);
+                self.code_block_attr = None;
             } else {
-                code_block_buffer.push_str(&hide_sharp_behind_comment(line));
-                code_block_buffer.push('\n');
+                self.code_block_buffer
+                    .push_str(&hide_sharp_behind_comment(line));
+                self.code_block_buffer.push('\n');
             }
-            continue;
+            return false;
         }
 
-        code_block_attr = None;
-        item_block = None;
+        self.code_block_attr = None;
+        self.item_block = None;
         if line.starts_with("```") {
-            code_block_attr = Some(CodeBlockAttribute::new(&line[3..]))
-        } else if config.wrap_comments() && ItemizedBlock::is_itemized_line(&line) {
+            self.code_block_attr = Some(CodeBlockAttribute::new(&line[3..]))
+        } else if self.fmt.config.wrap_comments() && ItemizedBlock::is_itemized_line(&line) {
             let ib = ItemizedBlock::new(&line);
-            item_block_buffer.push_str(&line[ib.indent..]);
-            item_block_buffer.push('\n');
-            item_block = Some(ib);
-            continue;
+            self.item_block_buffer.push_str(&line[ib.indent..]);
+            self.item_block_buffer.push('\n');
+            self.item_block = Some(ib);
+            return false;
         }
 
-        if result == opener {
-            let force_leading_whitespace = opener == "/* " && count_newlines(orig) == 0;
-            if !has_leading_whitespace && !force_leading_whitespace && result.ends_with(' ') {
-                result.pop();
+        if self.result == self.opener {
+            let force_leading_whitespace = &self.opener == "/* " && count_newlines(orig) == 0;
+            if !has_leading_whitespace && !force_leading_whitespace && self.result.ends_with(' ') {
+                self.result.pop();
             }
             if line.is_empty() {
-                continue;
+                return false;
             }
-        } else if is_prev_line_multi_line && !line.is_empty() {
-            result.push(' ')
+        } else if self.is_prev_line_multi_line && !line.is_empty() {
+            self.result.push(' ')
         } else if is_last && line.is_empty() {
             // trailing blank lines are unwanted
-            if !closer.is_empty() {
-                result.push_str(&indent_str);
+            if !self.closer.is_empty() {
+                self.result.push_str(&self.indent_str);
             }
-            break;
+            return true;
         } else {
-            result.push_str(&comment_line_separator);
-            if !has_leading_whitespace && result.ends_with(' ') {
-                result.pop();
+            self.result.push_str(&self.comment_line_separator);
+            if !has_leading_whitespace && self.result.ends_with(' ') {
+                self.result.pop();
             }
         }
 
-        if config.wrap_comments() && line.len() > fmt.shape.width && !has_url(line) {
-            match rewrite_string(line, &fmt, max_chars) {
+        if self.fmt.config.wrap_comments() && line.len() > self.fmt.shape.width && !has_url(line) {
+            match rewrite_string(line, &self.fmt, self.max_chars) {
                 Some(ref s) => {
-                    is_prev_line_multi_line = s.contains('\n');
-                    result.push_str(s);
+                    self.is_prev_line_multi_line = s.contains('\n');
+                    self.result.push_str(s);
                 }
-                None if is_prev_line_multi_line => {
+                None if self.is_prev_line_multi_line => {
                     // We failed to put the current `line` next to the previous `line`.
                     // Remove the trailing space, then start rewrite on the next line.
-                    result.pop();
-                    result.push_str(&comment_line_separator);
-                    fmt.shape = Shape::legacy(max_chars, fmt_indent);
-                    match rewrite_string(line, &fmt, max_chars) {
+                    self.result.pop();
+                    self.result.push_str(&self.comment_line_separator);
+                    self.fmt.shape = Shape::legacy(self.max_chars, self.fmt_indent);
+                    match rewrite_string(line, &self.fmt, self.max_chars) {
                         Some(ref s) => {
-                            is_prev_line_multi_line = s.contains('\n');
-                            result.push_str(s);
+                            self.is_prev_line_multi_line = s.contains('\n');
+                            self.result.push_str(s);
                         }
                         None => {
-                            is_prev_line_multi_line = false;
-                            result.push_str(line);
+                            self.is_prev_line_multi_line = false;
+                            self.result.push_str(line);
                         }
                     }
                 }
                 None => {
-                    is_prev_line_multi_line = false;
-                    result.push_str(line);
+                    self.is_prev_line_multi_line = false;
+                    self.result.push_str(line);
                 }
             }
 
-            fmt.shape = if is_prev_line_multi_line {
+            self.fmt.shape = if self.is_prev_line_multi_line {
                 // 1 = " "
-                let offset = 1 + last_line_width(&result) - line_start.len();
+                let offset = 1 + last_line_width(&self.result) - self.line_start.len();
                 Shape {
-                    width: max_chars.saturating_sub(offset),
-                    indent: fmt_indent,
-                    offset: fmt.shape.offset + offset,
+                    width: self.max_chars.saturating_sub(offset),
+                    indent: self.fmt_indent,
+                    offset: self.fmt.shape.offset + offset,
                 }
             } else {
-                Shape::legacy(max_chars, fmt_indent)
+                Shape::legacy(self.max_chars, self.fmt_indent)
             };
         } else {
-            if line.is_empty() && result.ends_with(' ') && !is_last {
+            if line.is_empty() && self.result.ends_with(' ') && !is_last {
                 // Remove space if this is an empty comment or a doc comment.
-                result.pop();
+                self.result.pop();
             }
-            result.push_str(line);
-            fmt.shape = Shape::legacy(max_chars, fmt_indent);
-            is_prev_line_multi_line = false;
+            self.result.push_str(line);
+            self.fmt.shape = Shape::legacy(self.max_chars, self.fmt_indent);
+            self.is_prev_line_multi_line = false;
         }
+
+        false
     }
-    if !code_block_buffer.is_empty() {
-        // There is a code block that is not properly enclosed by backticks.
-        // We will leave them untouched.
-        result.push_str(&comment_line_separator);
-        result.push_str(&join_block(
-            &trim_custom_comment_prefix(&code_block_buffer),
-            &comment_line_separator,
-        ));
-    }
-    if !item_block_buffer.is_empty() {
-        // the last few lines are part of an itemized block
-        let ib = item_block.unwrap();
-        fmt.shape = Shape::legacy(max_chars, fmt_indent);
-        let item_fmt = ib.create_string_format(&fmt);
-        result.push_str(&comment_line_separator);
-        result.push_str(&ib.opener);
-        match rewrite_string(
-            &item_block_buffer.replace("\n", " "),
-            &item_fmt,
-            max_chars.saturating_sub(ib.indent),
-        ) {
-            Some(s) => result.push_str(&join_block(
-                &s,
-                &format!("{}{}", &comment_line_separator, ib.line_start),
-            )),
-            None => result.push_str(&join_block(&item_block_buffer, &comment_line_separator)),
-        };
-    }
+}
+
+fn rewrite_comment_inner(
+    orig: &str,
+    block_style: bool,
+    style: CommentStyle,
+    shape: Shape,
+    config: &Config,
+    is_doc_comment: bool,
+) -> Option<String> {
+    let mut rewriter = CommentRewrite::new(orig, block_style, shape, config);
+
+    let line_breaks = count_newlines(orig.trim_right());
+    let lines = orig
+        .lines()
+        .enumerate()
+        .map(|(i, mut line)| {
+            line = trim_right_unless_two_whitespaces(line.trim_left(), is_doc_comment);
+            // Drop old closer.
+            if i == line_breaks && line.ends_with("*/") && !line.starts_with("//") {
+                line = line[..(line.len() - 2)].trim_right();
+            }
+
+            line
+        })
+        .map(|s| left_trim_comment_line(s, &style))
+        .map(|(line, has_leading_whitespace)| {
+            if orig.starts_with("/*") && line_breaks == 0 {
+                (
+                    line.trim_left(),
+                    has_leading_whitespace || config.normalize_comments(),
+                )
+            } else {
+                (line, has_leading_whitespace || config.normalize_comments())
+            }
+        });
 
-    result.push_str(closer);
-    if result.ends_with(opener) && opener.ends_with(' ') {
-        // Trailing space.
-        result.pop();
+    for (i, (line, has_leading_whitespace)) in lines.enumerate() {
+        if rewriter.handle_line(orig, i, line, has_leading_whitespace) {
+            break;
+        }
     }
 
-    Some(result)
+    Some(rewriter.finish())
 }
 
 const RUSTFMT_CUSTOM_COMMENT_PREFIX: &str = "//#### ";
@@ -761,7 +791,7 @@ fn trim_custom_comment_prefix(s: &str) -> String {
             let left_trimmed = line.trim_left();
             if left_trimmed.starts_with(RUSTFMT_CUSTOM_COMMENT_PREFIX) {
                 let orig = left_trimmed.trim_left_matches(RUSTFMT_CUSTOM_COMMENT_PREFIX);
-                // due to comment wrapping, a line that was originaly behind `#` is split over
+                // due to comment wrapping, a line that was originally behind `#` is split over
                 // multiple lines, which needs then to be prefixed with a `#`
                 if !orig.trim_left().starts_with("# ") {
                     Cow::from(format!("# {}", orig))
@@ -841,7 +871,7 @@ fn light_rewrite_comment(
     offset: Indent,
     config: &Config,
     is_doc_comment: bool,
-) -> Option<String> {
+) -> String {
     let lines: Vec<&str> = orig
         .lines()
         .map(|l| {
@@ -862,7 +892,7 @@ fn light_rewrite_comment(
             trim_right_unless_two_whitespaces(left_trimmed, is_doc_comment)
         })
         .collect();
-    Some(lines.join(&format!("\n{}", offset.to_string(config))))
+    lines.join(&format!("\n{}", offset.to_string(config)))
 }
 
 /// Trims comment characters and possibly a single space from the left of a string.
@@ -957,35 +987,6 @@ pub fn contains_comment(text: &str) -> bool {
     CharClasses::new(text.chars()).any(|(kind, _)| kind.is_comment())
 }
 
-/// Remove trailing spaces from the specified snippet. We do not remove spaces
-/// inside strings or comments.
-pub fn remove_trailing_white_spaces(text: &str) -> String {
-    let mut buffer = String::with_capacity(text.len());
-    let mut space_buffer = String::with_capacity(128);
-    for (char_kind, c) in CharClasses::new(text.chars()) {
-        match c {
-            '\n' => {
-                if char_kind == FullCodeCharKind::InString {
-                    buffer.push_str(&space_buffer);
-                }
-                space_buffer.clear();
-                buffer.push('\n');
-            }
-            _ if c.is_whitespace() => {
-                space_buffer.push(c);
-            }
-            _ => {
-                if !space_buffer.is_empty() {
-                    buffer.push_str(&space_buffer);
-                    space_buffer.clear();
-                }
-                buffer.push(c);
-            }
-        }
-    }
-    buffer
-}
-
 pub struct CharClasses<T>
 where
     T: Iterator,
@@ -1780,12 +1781,6 @@ fn check(haystack: &str, needle: &str, expected: Option<usize>) {
         check("\"/* abc", "abc", Some(4));
     }
 
-    #[test]
-    fn test_remove_trailing_white_spaces() {
-        let s = "    r#\"\n        test\n    \"#";
-        assert_eq!(remove_trailing_white_spaces(&s), s);
-    }
-
     #[test]
     fn test_filter_normal_code() {
         let s = r#"