// Formatting and tools for comments.
-use std::{self, iter, borrow::Cow};
+use std::{self, borrow::Cow, iter};
+use itertools::{multipeek, MultiPeek};
use syntax::codemap::Span;
use config::Config;
use shape::{Indent, Shape};
use string::{rewrite_string, StringFormat};
use utils::{count_newlines, first_line_width, last_line_width};
+use {ErrorKind, FormattingError};
fn is_custom_comment(comment: &str) -> bool {
if !comment.starts_with("//") {
// expression and the second expression or the missing comment. We will preserve the original
// layout whenever possible.
let original_snippet = context.snippet(span);
- let prefer_same_line = if let Some(pos) = original_snippet.chars().position(|c| c == '/') {
+ let prefer_same_line = if let Some(pos) = original_snippet.find('/') {
!original_snippet[..pos].contains('\n')
} else {
!original_snippet.contains('\n')
// If there are lines without a starting sigil, we won't format them correctly
// so in that case we won't even re-align (if !config.normalize_comments()) and
// we should stop now.
- let num_bare_lines = orig.lines()
+ let num_bare_lines = orig
+ .lines()
.map(|line| line.trim())
.filter(|l| !(l.starts_with('*') || l.starts_with("//") || l.starts_with("/*")))
.count();
is_doc_comment: bool,
) -> Option<String> {
let style = comment_style(orig, false);
- let first_group = orig.lines()
+ let first_group = orig
+ .lines()
.take_while(|l| style.line_with_same_comment_style(l, false))
.collect::<Vec<_>>()
.join("\n");
- let rest = orig.lines()
+ let rest = orig
+ .lines()
.skip(first_group.lines().count())
.collect::<Vec<_>>()
.join("\n");
};
let line_breaks = count_newlines(orig.trim_right());
- let lines = orig.lines()
+ let lines = orig
+ .lines()
.enumerate()
.map(|(i, mut line)| {
line = trim_right_unless_two_whitespaces(line.trim_left(), is_doc_comment);
if line.starts_with("```") {
inside_code_block = false;
result.push_str(&comment_line_separator);
- let code_block = ::format_code_block(&code_block_buffer, config)
- .unwrap_or_else(|| code_block_buffer.to_owned());
+ let code_block = {
+ let mut config = config.clone();
+ config.set().wrap_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),
+ }
+ };
result.push_str(&join_code_block_with_comment_line_separator(&code_block));
code_block_buffer.clear();
result.push_str(&comment_line_separator);
result.push_str(line);
} else {
- code_block_buffer.push_str(line);
+ code_block_buffer.push_str(&hide_sharp_behind_comment(line));
code_block_buffer.push('\n');
+
+ if is_last {
+ // There is an code block that is not properly enclosed by backticks.
+ // We will leave them untouched.
+ result.push_str(&comment_line_separator);
+ result.push_str(&join_code_block_with_comment_line_separator(
+ &trim_custom_comment_prefix(&code_block_buffer),
+ ));
+ }
}
continue;
} else {
- inside_code_block = line.starts_with("```rust");
+ inside_code_block = line.starts_with("```");
if result == opener {
let force_leading_whitespace = opener == "/* " && count_newlines(orig) == 0;
}
if config.wrap_comments() && line.len() > fmt.shape.width && !has_url(line) {
- match rewrite_string(line, &fmt, Some(max_chars)) {
+ match rewrite_string(line, &fmt) {
Some(ref s) => {
is_prev_line_multi_line = s.contains('\n');
result.push_str(s);
result.pop();
result.push_str(&comment_line_separator);
fmt.shape = Shape::legacy(max_chars, fmt_indent);
- match rewrite_string(line, &fmt, Some(max_chars)) {
+ match rewrite_string(line, &fmt) {
Some(ref s) => {
is_prev_line_multi_line = s.contains('\n');
result.push_str(s);
// 1 = " "
let offset = 1 + last_line_width(&result) - line_start.len();
Shape {
- width: max_chars.checked_sub(offset).unwrap_or(0),
+ width: max_chars.saturating_sub(offset),
indent: fmt_indent,
offset: fmt.shape.offset + offset,
}
Some(result)
}
+const RUSTFMT_CUSTOM_COMMENT_PREFIX: &str = "//#### ";
+
+fn hide_sharp_behind_comment<'a>(s: &'a str) -> Cow<'a, str> {
+ if s.trim_left().starts_with('#') {
+ Cow::from(format!("{}{}", RUSTFMT_CUSTOM_COMMENT_PREFIX, s))
+ } else {
+ Cow::from(s)
+ }
+}
+
+fn trim_custom_comment_prefix(s: &str) -> String {
+ s.lines()
+ .map(|line| {
+ let left_trimmed = line.trim_left();
+ if left_trimmed.starts_with(RUSTFMT_CUSTOM_COMMENT_PREFIX) {
+ left_trimmed.trim_left_matches(RUSTFMT_CUSTOM_COMMENT_PREFIX)
+ } else {
+ line
+ }
+ })
+ .collect::<Vec<_>>()
+ .join("\n")
+}
+
/// Returns true if the given string MAY include URLs or alike.
fn has_url(s: &str) -> bool {
// This function may return false positive, but should get its job done in most cases.
Some(String::new())
} else {
let missing_snippet = context.snippet(span);
- let pos = missing_snippet.chars().position(|c| c == '/').unwrap_or(0);
+ let pos = missing_snippet.find('/').unwrap_or(0);
// 1 = ` `
let total_width = missing_comment.len() + used_width + 1;
let force_new_line_before_comment =
config: &Config,
is_doc_comment: bool,
) -> Option<String> {
- let lines: Vec<&str> = orig.lines()
+ let lines: Vec<&str> = orig
+ .lines()
.map(|l| {
// This is basically just l.trim(), but in the case that a line starts
// with `*` we want to leave one space before it, so it aligns with the
/// Does not trim all whitespace. If a single space is trimmed from the left of the string,
/// this function returns true.
fn left_trim_comment_line<'a>(line: &'a str, style: &CommentStyle) -> (&'a str, bool) {
- if line.starts_with("//! ") || line.starts_with("/// ") || line.starts_with("/*! ")
+ if line.starts_with("//! ")
+ || line.starts_with("/// ")
+ || line.starts_with("/*! ")
|| line.starts_with("/** ")
{
(&line[4..], true)
} else {
(&line[opener.trim_right().len()..], false)
}
- } else if line.starts_with("/* ") || line.starts_with("// ") || line.starts_with("//!")
- || line.starts_with("///") || line.starts_with("** ")
+ } else if line.starts_with("/* ")
+ || line.starts_with("// ")
+ || line.starts_with("//!")
+ || line.starts_with("///")
+ || line.starts_with("** ")
|| line.starts_with("/*!")
|| (line.starts_with("/**") && !line.starts_with("/**/"))
{
(&line[3..], line.chars().nth(2).unwrap() == ' ')
- } else if line.starts_with("/*") || line.starts_with("* ") || line.starts_with("//")
+ } else if line.starts_with("/*")
+ || line.starts_with("* ")
+ || line.starts_with("//")
|| line.starts_with("**")
{
(&line[2..], line.chars().nth(1).unwrap() == ' ')
T: Iterator,
T::Item: RichChar,
{
- base: iter::Peekable<T>,
+ base: MultiPeek<T>,
status: CharClassesStatus,
}
{
pub fn new(base: T) -> CharClasses<T> {
CharClasses {
- base: base.peekable(),
+ base: multipeek(base),
status: CharClassesStatus::Normal,
}
}
char_kind = FullCodeCharKind::InString;
CharClassesStatus::LitString
}
- '\'' => CharClassesStatus::LitChar,
+ '\'' => {
+ // HACK: Work around mut borrow.
+ match self.base.peek() {
+ Some(next) if next.get_char() == '\\' => {
+ self.status = CharClassesStatus::LitChar;
+ return Some((char_kind, item));
+ }
+ _ => (),
+ }
+
+ match self.base.peek() {
+ Some(next) if next.get_char() == '\'' => CharClassesStatus::LitChar,
+ _ => CharClassesStatus::Normal,
+ }
+ }
'/' => match self.base.peek() {
Some(next) if next.get_char() == '*' => {
self.status = CharClassesStatus::BlockCommentOpening(1);
}
}
+/// An iterator over the lines of a string, paired with the char kind at the
+/// end of the line.
+pub struct LineClasses<'a> {
+ base: iter::Peekable<CharClasses<std::str::Chars<'a>>>,
+ kind: FullCodeCharKind,
+}
+
+impl<'a> LineClasses<'a> {
+ pub fn new(s: &'a str) -> Self {
+ LineClasses {
+ base: CharClasses::new(s.chars()).peekable(),
+ kind: FullCodeCharKind::Normal,
+ }
+ }
+}
+
+impl<'a> Iterator for LineClasses<'a> {
+ type Item = (FullCodeCharKind, String);
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.base.peek().is_none() {
+ return None;
+ }
+
+ let mut line = String::new();
+
+ while let Some((kind, c)) = self.base.next() {
+ self.kind = kind;
+ if c == '\n' {
+ break;
+ } else {
+ line.push(c);
+ }
+ }
+
+ Some((self.kind, line))
+ }
+}
+
/// Iterator over functional and commented parts of a string. Any part of a string is either
/// functional code, either *one* block comment, either *one* line comment. Whitespace between
/// comments is functional code. Line comments contain their ending newlines.
) -> Option<String> {
let snippet = context.snippet(span);
if snippet != new && changed_comment_content(snippet, &new) {
- // We missed some comments. Keep the original text.
+ // We missed some comments. Warn and keep the original text.
+ if context.config.error_on_unformatted() {
+ context.report.append(
+ context.codemap.span_to_filename(span).into(),
+ vec![FormattingError::from_span(
+ &span,
+ &context.codemap,
+ ErrorKind::LostComment,
+ )],
+ );
+ }
Some(snippet.to_owned())
} else {
Some(new)
impl<'a> Iterator for CommentReducer<'a> {
type Item = char;
+
fn next(&mut self) -> Option<Self::Item> {
loop {
let mut c = self.iter.next()?;
#[cfg(test)]
mod test {
- use super::{contains_comment, rewrite_comment, CharClasses, CodeCharKind, CommentCodeSlices,
- FindUncommented, FullCodeCharKind};
+ use super::*;
use shape::{Indent, Shape};
#[test]
}
#[test]
- #[cfg_attr(rustfmt, rustfmt_skip)]
+ #[rustfmt::skip]
fn format_comments() {
let mut config: ::config::Config = Default::default();
config.set().wrap_comments(true);
check("\"/* abc */\"", "abc", Some(4));
check("\"/* abc", "abc", Some(4));
}
+
+ #[test]
+ fn test_remove_trailing_white_spaces() {
+ let s = format!(" r#\"\n test\n \"#");
+ assert_eq!(remove_trailing_white_spaces(&s), s);
+ }
}