X-Git-Url: https://git.lizzy.rs/?a=blobdiff_plain;f=src%2Fcomment.rs;h=8b8112618b4a8c948ae616846cd74cdc5c2a57b0;hb=16184d3e165018fa5b78f5f99cfc844e5af33bd6;hp=123df4927f41e14efbad75ac6e230c12df9e2329;hpb=0a42648678b83f0af19116250dfc0bd4a2e1ac4c;p=rust.git diff --git a/src/comment.rs b/src/comment.rs index 123df4927f4..8b8112618b4 100644 --- a/src/comment.rs +++ b/src/comment.rs @@ -14,25 +14,23 @@ use syntax::codemap::Span; -use {Indent, Shape}; use config::Config; use rewrite::RewriteContext; -use string::{StringFormat, rewrite_string}; -use utils::wrap_str; +use shape::{Indent, Shape}; +use string::{rewrite_string, StringFormat}; +use utils::{first_line_width, last_line_width}; fn is_custom_comment(comment: &str) -> bool { if !comment.starts_with("//") { false + } else if let Some(c) = comment.chars().nth(2) { + !c.is_alphanumeric() && !c.is_whitespace() } else { - if let Some(c) = comment.chars().nth(2) { - !c.is_alphanumeric() && !c.is_whitespace() - } else { - false - } + false } } -#[derive(PartialEq, Eq)] +#[derive(Copy, Clone, PartialEq, Eq)] pub enum CommentStyle<'a> { DoubleSlash, TripleSlash, @@ -66,10 +64,10 @@ pub fn opener(&self) -> &'a str { pub fn closer(&self) -> &'a str { match *self { - CommentStyle::DoubleSlash | - CommentStyle::TripleSlash | - CommentStyle::Custom(..) | - CommentStyle::Doc => "", + CommentStyle::DoubleSlash + | CommentStyle::TripleSlash + | CommentStyle::Custom(..) + | CommentStyle::Doc => "", CommentStyle::DoubleBullet => " **/", CommentStyle::SingleBullet | CommentStyle::Exclamation => " */", } @@ -93,13 +91,13 @@ pub fn to_str_tuplet(&self) -> (&'a str, &'a str, &'a str) { pub fn line_with_same_comment_style(&self, line: &str, normalize_comments: bool) -> bool { match *self { CommentStyle::DoubleSlash | CommentStyle::TripleSlash | CommentStyle::Doc => { - line.trim_left().starts_with(self.line_start().trim_left()) || - comment_style(line, normalize_comments) == *self + line.trim_left().starts_with(self.line_start().trim_left()) + || comment_style(line, normalize_comments) == *self } CommentStyle::DoubleBullet | CommentStyle::SingleBullet | CommentStyle::Exclamation => { - line.trim_left().starts_with(self.closer().trim_left()) || - line.trim_left().starts_with(self.line_start().trim_left()) || - comment_style(line, normalize_comments) == *self + line.trim_left().starts_with(self.closer().trim_left()) + || line.trim_left().starts_with(self.line_start().trim_left()) + || comment_style(line, normalize_comments) == *self } CommentStyle::Custom(opener) => line.trim_left().starts_with(opener.trim_right()), } @@ -123,8 +121,8 @@ fn comment_style(orig: &str, normalize_comments: bool) -> CommentStyle { } else { CommentStyle::DoubleSlash } - } else if (orig.starts_with("///") && orig.chars().nth(3).map_or(true, |c| c != '/')) || - (orig.starts_with("/**") && !orig.starts_with("/**/")) + } else if (orig.starts_with("///") && orig.chars().nth(3).map_or(true, |c| c != '/')) + || (orig.starts_with("/**") && !orig.starts_with("/**/")) { CommentStyle::TripleSlash } else if orig.starts_with("//!") || orig.starts_with("/*!") { @@ -136,6 +134,81 @@ fn comment_style(orig: &str, normalize_comments: bool) -> CommentStyle { } } +pub fn combine_strs_with_missing_comments( + context: &RewriteContext, + prev_str: &str, + next_str: &str, + span: Span, + shape: Shape, + allow_extend: bool, +) -> Option { + let mut allow_one_line = !prev_str.contains('\n') && !next_str.contains('\n'); + let first_sep = if prev_str.is_empty() || next_str.is_empty() { + "" + } else { + " " + }; + let mut one_line_width = + last_line_width(prev_str) + first_line_width(next_str) + first_sep.len(); + + let indent_str = shape.indent.to_string(context.config); + let missing_comment = rewrite_missing_comment(span, shape, context)?; + + if missing_comment.is_empty() { + if allow_extend && prev_str.len() + first_sep.len() + next_str.len() <= shape.width { + return Some(format!("{}{}{}", prev_str, first_sep, next_str)); + } else { + let sep = if prev_str.is_empty() { + String::new() + } else { + String::from("\n") + &indent_str + }; + return Some(format!("{}{}{}", prev_str, sep, next_str)); + } + } + + // We have a missing comment between the first expression and the second expression. + + // Peek the the original source code and find out whether there is a newline between the first + // 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 == '/') { + !original_snippet[..pos].contains('\n') + } else { + !original_snippet.contains('\n') + }; + + one_line_width -= first_sep.len(); + let first_sep = if prev_str.is_empty() || missing_comment.is_empty() { + String::new() + } else { + let one_line_width = last_line_width(prev_str) + first_line_width(&missing_comment) + 1; + if prefer_same_line && one_line_width <= shape.width { + String::from(" ") + } else { + format!("\n{}", indent_str) + } + }; + let second_sep = if missing_comment.is_empty() || next_str.is_empty() { + String::new() + } else if missing_comment.starts_with("//") { + format!("\n{}", indent_str) + } else { + one_line_width += missing_comment.len() + first_sep.len() + 1; + allow_one_line &= !missing_comment.starts_with("//") && !missing_comment.contains('\n'); + if prefer_same_line && allow_one_line && one_line_width <= shape.width { + String::from(" ") + } else { + format!("\n{}", indent_str) + } + }; + Some(format!( + "{}{}{}{}{}", + prev_str, first_sep, missing_comment, second_sep, next_str, + )) +} + pub fn rewrite_comment( orig: &str, block_style: bool, @@ -147,9 +220,7 @@ pub fn rewrite_comment( // we should stop now. let num_bare_lines = orig.lines() .map(|line| line.trim()) - .filter(|l| { - !(l.starts_with('*') || l.starts_with("//") || l.starts_with("/*")) - }) + .filter(|l| !(l.starts_with('*') || l.starts_with("//") || l.starts_with("/*"))) .count(); if num_bare_lines > 0 && !config.normalize_comments() { return Some(orig.to_owned()); @@ -177,13 +248,7 @@ fn identify_comment( .collect::>() .join("\n"); - let first_group_str = try_opt!(rewrite_comment_inner( - &first_group, - block_style, - style, - shape, - config, - )); + let first_group_str = rewrite_comment_inner(&first_group, block_style, style, shape, config)?; if rest.is_empty() { Some(first_group_str) } else { @@ -216,12 +281,13 @@ fn rewrite_comment_inner( .checked_sub(closer.len() + opener.len()) .unwrap_or(1); let indent_str = shape.indent.to_string(config); - let fmt = StringFormat { + let fmt_indent = shape.indent + (opener.len() - line_start.len()); + let mut fmt = StringFormat { opener: "", closer: "", line_start: line_start, line_end: "", - shape: Shape::legacy(max_chars, shape.indent + (opener.len() - line_start.len())), + shape: Shape::legacy(max_chars, fmt_indent), trim_end: true, config: config, }; @@ -233,39 +299,91 @@ fn rewrite_comment_inner( line = line.trim(); // Drop old closer. if i == line_breaks && line.ends_with("*/") && !line.starts_with("//") { - line = &line[..(line.len() - 2)].trim_right(); + line = line[..(line.len() - 2)].trim_right(); } line }) .map(|s| left_trim_comment_line(s, &style)) - .map(|line| if orig.starts_with("/*") && line_breaks == 0 { - line.trim_left() - } else { - line + .map(|line| { + if orig.starts_with("/*") && line_breaks == 0 { + line.trim_left() + } else { + line + } }); let mut result = opener.to_owned(); + let mut is_prev_line_multi_line = false; + let mut inside_code_block = false; + let comment_line_separator = format!("\n{}{}", indent_str, line_start); for line in lines { if result == opener { if line.is_empty() { continue; } + } else if is_prev_line_multi_line && !line.is_empty() { + result.push(' ') } else { - result.push('\n'); - result.push_str(&indent_str); - result.push_str(line_start); + result.push_str(&comment_line_separator); } - if config.wrap_comments() && line.len() > max_chars { - let rewrite = rewrite_string(line, &fmt).unwrap_or(line.to_owned()); - result.push_str(&rewrite); + if line.starts_with("```") { + inside_code_block = !inside_code_block; + } + if inside_code_block { + result.push_str(line); + continue; + } + + if config.wrap_comments() && line.len() > fmt.shape.width && !has_url(line) { + match rewrite_string(line, &fmt, Some(max_chars)) { + Some(ref s) => { + is_prev_line_multi_line = s.contains('\n'); + result.push_str(s); + } + None if 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, Some(max_chars)) { + Some(ref s) => { + is_prev_line_multi_line = s.contains('\n'); + result.push_str(s); + } + None => { + is_prev_line_multi_line = false; + result.push_str(line); + } + } + } + None => { + is_prev_line_multi_line = false; + result.push_str(line); + } + } + + fmt.shape = if is_prev_line_multi_line { + // 1 = " " + let offset = 1 + last_line_width(&result) - line_start.len(); + Shape { + width: max_chars.checked_sub(offset).unwrap_or(0), + indent: fmt_indent, + offset: fmt.shape.offset + offset, + } + } else { + Shape::legacy(max_chars, fmt_indent) + }; } else { if line.is_empty() && result.ends_with(' ') { // Remove space if this is an empty comment or a doc comment. result.pop(); } result.push_str(line); + fmt.shape = Shape::legacy(max_chars, fmt_indent); + is_prev_line_multi_line = false; } } @@ -278,6 +396,56 @@ fn rewrite_comment_inner( Some(result) } +/// 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. + s.contains("https://") || s.contains("http://") || s.contains("ftp://") || s.contains("file://") +} + +/// Given the span, rewrite the missing comment inside it if available. +/// Note that the given span must only include comments (or leading/trailing whitespaces). +pub fn rewrite_missing_comment( + span: Span, + shape: Shape, + context: &RewriteContext, +) -> Option { + let missing_snippet = context.snippet(span); + let trimmed_snippet = missing_snippet.trim(); + if !trimmed_snippet.is_empty() { + rewrite_comment(trimmed_snippet, false, shape, context.config) + } else { + Some(String::new()) + } +} + +/// Recover the missing comments in the specified span, if available. +/// The layout of the comments will be preserved as long as it does not break the code +/// and its total width does not exceed the max width. +pub fn recover_missing_comment_in_span( + span: Span, + shape: Shape, + context: &RewriteContext, + used_width: usize, +) -> Option { + let missing_comment = rewrite_missing_comment(span, shape, context)?; + if missing_comment.is_empty() { + Some(String::new()) + } else { + let missing_snippet = context.snippet(span); + let pos = missing_snippet.chars().position(|c| c == '/').unwrap_or(0); + // 1 = ` ` + let total_width = missing_comment.len() + used_width + 1; + let force_new_line_before_comment = + missing_snippet[..pos].contains('\n') || total_width > context.config.max_width(); + let sep = if force_new_line_before_comment { + format!("\n{}", shape.indent.to_string(context.config)) + } else { + String::from(" ") + }; + Some(format!("{}{}", sep, missing_comment)) + } +} + /// Trims whitespace and aligns to indent, but otherwise does not change comments. fn light_rewrite_comment(orig: &str, offset: Indent, config: &Config) -> Option { let lines: Vec<&str> = orig.lines() @@ -287,7 +455,7 @@ fn light_rewrite_comment(orig: &str, offset: Indent, config: &Config) -> Option< // `*` in `/*`. let first_non_whitespace = l.find(|c| !char::is_whitespace(c)); if let Some(fnw) = first_non_whitespace { - if l.as_bytes()[fnw] == '*' as u8 && fnw > 0 { + if l.as_bytes()[fnw] == b'*' && fnw > 0 { &l[fnw - 1..] } else { &l[fnw..] @@ -303,24 +471,24 @@ fn light_rewrite_comment(orig: &str, offset: Indent, config: &Config) -> Option< /// Trims comment characters and possibly a single space from the left of a string. /// Does not trim all whitespace. fn left_trim_comment_line<'a>(line: &'a str, style: &CommentStyle) -> &'a str { - if line.starts_with("//! ") || line.starts_with("/// ") || line.starts_with("/*! ") || - line.starts_with("/** ") + if line.starts_with("//! ") || line.starts_with("/// ") || line.starts_with("/*! ") + || line.starts_with("/** ") { &line[4..] - } else if let &CommentStyle::Custom(opener) = style { + } else if let CommentStyle::Custom(opener) = *style { if line.starts_with(opener) { &line[opener.len()..] } else { &line[opener.trim_right().len()..] } - } 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("/**/")) + } 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..] - } else if 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[2..] } else if line.starts_with('*') { @@ -342,14 +510,12 @@ fn find_uncommented(&self, pat: &str) -> Option { None => { return Some(i - pat.len()); } - Some(c) => { - match kind { - FullCodeCharKind::Normal if b == c => {} - _ => { - needle_iter = pat.chars(); - } + Some(c) => match kind { + FullCodeCharKind::Normal | FullCodeCharKind::InString if b == c => {} + _ => { + needle_iter = pat.chars(); } - } + }, } } @@ -368,7 +534,7 @@ fn find_uncommented(&self, pat: &str) -> Option { pub fn find_comment_end(s: &str) -> Option { let mut iter = CharClasses::new(s.char_indices()); for (kind, (i, _c)) in &mut iter { - if kind == FullCodeCharKind::Normal { + if kind == FullCodeCharKind::Normal || kind == FullCodeCharKind::InString { return Some(i); } } @@ -386,6 +552,35 @@ 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 +} + struct CharClasses where T: Iterator, @@ -449,15 +644,17 @@ enum FullCodeCharKind { InComment, /// Last character of a comment, '\n' for a line comment, '/' for a block comment. EndComment, + /// Inside a string. + InString, } impl FullCodeCharKind { fn is_comment(&self) -> bool { match *self { - FullCodeCharKind::Normal => false, - FullCodeCharKind::StartComment | - FullCodeCharKind::InComment | - FullCodeCharKind::EndComment => true, + FullCodeCharKind::StartComment + | FullCodeCharKind::InComment + | FullCodeCharKind::EndComment => true, + _ => false, } } @@ -491,47 +688,52 @@ impl Iterator for CharClasses type Item = (FullCodeCharKind, T::Item); fn next(&mut self) -> Option<(FullCodeCharKind, T::Item)> { - let item = try_opt!(self.base.next()); + let item = self.base.next()?; let chr = item.get_char(); + let mut char_kind = FullCodeCharKind::Normal; self.status = match self.status { - CharClassesStatus::LitString => { - match chr { - '"' => CharClassesStatus::Normal, - '\\' => CharClassesStatus::LitStringEscape, - _ => CharClassesStatus::LitString, + CharClassesStatus::LitString => match chr { + '"' => CharClassesStatus::Normal, + '\\' => { + char_kind = FullCodeCharKind::InString; + CharClassesStatus::LitStringEscape } - } - CharClassesStatus::LitStringEscape => CharClassesStatus::LitString, - CharClassesStatus::LitChar => { - match chr { - '\\' => CharClassesStatus::LitCharEscape, - '\'' => CharClassesStatus::Normal, - _ => CharClassesStatus::LitChar, + _ => { + char_kind = FullCodeCharKind::InString; + CharClassesStatus::LitString } + }, + CharClassesStatus::LitStringEscape => { + char_kind = FullCodeCharKind::InString; + CharClassesStatus::LitString } + CharClassesStatus::LitChar => match chr { + '\\' => CharClassesStatus::LitCharEscape, + '\'' => CharClassesStatus::Normal, + _ => CharClassesStatus::LitChar, + }, CharClassesStatus::LitCharEscape => CharClassesStatus::LitChar, - CharClassesStatus::Normal => { - match chr { - '"' => CharClassesStatus::LitString, - '\'' => CharClassesStatus::LitChar, - '/' => { - match self.base.peek() { - Some(next) if next.get_char() == '*' => { - self.status = CharClassesStatus::BlockCommentOpening(1); - return Some((FullCodeCharKind::StartComment, item)); - } - Some(next) if next.get_char() == '/' => { - self.status = CharClassesStatus::LineComment; - return Some((FullCodeCharKind::StartComment, item)); - } - _ => CharClassesStatus::Normal, - } + CharClassesStatus::Normal => match chr { + '"' => { + char_kind = FullCodeCharKind::InString; + CharClassesStatus::LitString + } + '\'' => CharClassesStatus::LitChar, + '/' => match self.base.peek() { + Some(next) if next.get_char() == '*' => { + self.status = CharClassesStatus::BlockCommentOpening(1); + return Some((FullCodeCharKind::StartComment, item)); + } + Some(next) if next.get_char() == '/' => { + self.status = CharClassesStatus::LineComment; + return Some((FullCodeCharKind::StartComment, item)); } _ => CharClassesStatus::Normal, - } - } + }, + _ => CharClassesStatus::Normal, + }, CharClassesStatus::BlockComment(deepness) => { - assert!(deepness != 0); + assert_ne!(deepness, 0); self.status = match self.base.peek() { Some(next) if next.get_char() == '/' && chr == '*' => { CharClassesStatus::BlockCommentClosing(deepness - 1) @@ -558,20 +760,18 @@ fn next(&mut self) -> Option<(FullCodeCharKind, T::Item)> { return Some((FullCodeCharKind::InComment, item)); } } - CharClassesStatus::LineComment => { - match chr { - '\n' => { - self.status = CharClassesStatus::Normal; - return Some((FullCodeCharKind::EndComment, item)); - } - _ => { - self.status = CharClassesStatus::LineComment; - return Some((FullCodeCharKind::InComment, item)); - } + CharClassesStatus::LineComment => match chr { + '\n' => { + self.status = CharClassesStatus::Normal; + return Some((FullCodeCharKind::EndComment, item)); } - } + _ => { + self.status = CharClassesStatus::LineComment; + return Some((FullCodeCharKind::InComment, item)); + } + }, }; - Some((FullCodeCharKind::Normal, item)) + Some((char_kind, item)) } } @@ -596,11 +796,14 @@ impl<'a> Iterator for UngroupedCommentCodeSlices<'a> { type Item = (CodeCharKind, usize, &'a str); fn next(&mut self) -> Option { - let (kind, (start_idx, _)) = try_opt!(self.iter.next()); + let (kind, (start_idx, _)) = self.iter.next()?; match kind { - FullCodeCharKind::Normal => { + FullCodeCharKind::Normal | FullCodeCharKind::InString => { // Consume all the Normal code - while let Some(&(FullCodeCharKind::Normal, (_, _))) = self.iter.peek() { + while let Some(&(char_kind, _)) = self.iter.peek() { + if char_kind.is_comment() { + break; + } let _ = self.iter.next(); } } @@ -662,9 +865,9 @@ fn next(&mut self) -> Option { let mut iter = CharClasses::new(subslice.char_indices()); for (kind, (i, c)) in &mut iter { - let is_comment_connector = self.last_slice_kind == CodeCharKind::Normal && - &subslice[..2] == "//" && - [' ', '\t'].contains(&c); + let is_comment_connector = self.last_slice_kind == CodeCharKind::Normal + && &subslice[..2] == "//" + && [' ', '\t'].contains(&c); if is_comment_connector && first_whitespace.is_none() { first_whitespace = Some(i); @@ -714,13 +917,11 @@ pub fn recover_comment_removed( new: String, span: Span, context: &RewriteContext, - shape: Shape, ) -> Option { let snippet = context.snippet(span); - if changed_comment_content(&snippet, &new) { - // We missed some comments - // Keep previous formatting if it satisfies the constrains - wrap_str(snippet, context.config.max_width(), shape) + if snippet != new && changed_comment_content(&snippet, &new) { + // We missed some comments. Keep the original text. + Some(snippet) } else { Some(new) } @@ -779,19 +980,17 @@ impl<'a> Iterator for CommentReducer<'a> { type Item = char; fn next(&mut self) -> Option { loop { - let mut c = try_opt!(self.iter.next()); + let mut c = self.iter.next()?; if self.is_block && self.at_start_line { while c.is_whitespace() { - c = try_opt!(self.iter.next()); + c = self.iter.next()?; } // Ignore leading '*' if c == '*' { - c = try_opt!(self.iter.next()); - } - } else { - if c == '\n' { - self.at_start_line = true; + c = self.iter.next()?; } + } else if c == '\n' { + self.at_start_line = true; } if !c.is_whitespace() { return Some(c); @@ -806,8 +1005,8 @@ fn remove_comment_header(comment: &str) -> &str { &comment[3..] } else if comment.starts_with("//") { &comment[2..] - } else if (comment.starts_with("/**") && !comment.starts_with("/**/")) || - comment.starts_with("/*!") + } else if (comment.starts_with("/**") && !comment.starts_with("/**/")) + || comment.starts_with("/*!") { &comment[3..comment.len() - 2] } else { @@ -821,9 +1020,9 @@ fn remove_comment_header(comment: &str) -> &str { #[cfg(test)] mod test { - use super::{CharClasses, CodeCharKind, FullCodeCharKind, contains_comment, rewrite_comment, - FindUncommented, CommentCodeSlices}; - use {Indent, Shape}; + use super::{contains_comment, rewrite_comment, CharClasses, CodeCharKind, CommentCodeSlices, + FindUncommented, FullCodeCharKind}; + use shape::{Indent, Shape}; #[test] fn char_classes() { @@ -927,7 +1126,7 @@ fn format_comments() { fn uncommented(text: &str) -> String { CharClasses::new(text.chars()) .filter_map(|(s, c)| match s { - FullCodeCharKind::Normal => Some(c), + FullCodeCharKind::Normal | FullCodeCharKind::InString => Some(c), _ => None, }) .collect()