mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
feat(prettier): trailing comment (wip) (#1538)
This commit is contained in:
parent
9a5bb008e7
commit
cc382835ef
6 changed files with 212 additions and 89 deletions
|
|
@ -8,6 +8,7 @@ use oxc_ast::CommentKind;
|
|||
use oxc_span::Span;
|
||||
|
||||
use crate::{
|
||||
array,
|
||||
doc::{Doc, DocBuilder, Separator},
|
||||
hardline, indent, line, ss, Prettier,
|
||||
};
|
||||
|
|
@ -40,6 +41,30 @@ impl DanglingCommentsPrintOptions {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Comment {
|
||||
start: u32,
|
||||
end: u32,
|
||||
is_block: bool,
|
||||
has_line_suffix: bool,
|
||||
}
|
||||
|
||||
impl Comment {
|
||||
fn new(start: u32, end: u32, kind: CommentKind) -> Self {
|
||||
// The comment span is for the comment value
|
||||
// -2 for `//` and `/*`
|
||||
let start = start - 2;
|
||||
// +2 for `/*`
|
||||
let end = if kind.is_multi_line() { end + 2 } else { end };
|
||||
Self { start, end, is_block: kind.is_multi_line(), has_line_suffix: false }
|
||||
}
|
||||
|
||||
fn with_line_suffix(mut self, yes: bool) -> Self {
|
||||
self.has_line_suffix = yes;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Prettier<'a> {
|
||||
#[must_use]
|
||||
pub(crate) fn print_comments(
|
||||
|
|
@ -71,34 +96,11 @@ impl<'a> Prettier<'a> {
|
|||
pub(crate) fn print_leading_comments(&mut self, range: Span) -> Option<Doc<'a>> {
|
||||
let mut parts = self.vec();
|
||||
while let Some((start, end, kind)) = self.trivias.peek().copied() {
|
||||
let comment = Comment::new(start, end, kind);
|
||||
// Comment before the span
|
||||
if end <= range.start {
|
||||
self.trivias.next();
|
||||
|
||||
parts.push(self.print_comment(start, end, kind));
|
||||
if kind.is_multi_line() {
|
||||
let line_break = if self.has_newline(end) {
|
||||
if self.has_newline(start) {
|
||||
hardline!()
|
||||
} else {
|
||||
line!()
|
||||
}
|
||||
} else {
|
||||
ss!(" ")
|
||||
};
|
||||
parts.push(line_break);
|
||||
} else {
|
||||
parts.push(hardline!());
|
||||
}
|
||||
|
||||
if self
|
||||
.get_comment_end(kind, end)
|
||||
.map(|end| self.skip_spaces(end))
|
||||
.and_then(|idx| self.skip_newline(idx))
|
||||
.is_some_and(|i| self.has_newline(i))
|
||||
{
|
||||
parts.push(hardline!());
|
||||
}
|
||||
self.print_leading_comment(&mut parts, comment);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
|
@ -109,20 +111,109 @@ impl<'a> Prettier<'a> {
|
|||
Some(Doc::Array(parts))
|
||||
}
|
||||
|
||||
fn print_leading_comment(&mut self, parts: &mut Vec<'a, Doc<'a>>, comment: Comment) {
|
||||
let printed = self.print_comment(comment);
|
||||
parts.push(printed);
|
||||
|
||||
if comment.is_block {
|
||||
let line_break = if self.has_newline(comment.end, /* backwards */ false) {
|
||||
if self.has_newline(comment.start, /* backwards */ true) {
|
||||
hardline!()
|
||||
} else {
|
||||
line!()
|
||||
}
|
||||
} else {
|
||||
ss!(" ")
|
||||
};
|
||||
parts.push(line_break);
|
||||
} else {
|
||||
parts.push(hardline!());
|
||||
}
|
||||
|
||||
if self
|
||||
.skip_spaces(comment.end, false)
|
||||
.and_then(|idx| self.skip_newline(Some(idx), false))
|
||||
.is_some_and(|i| self.has_newline(i, /* backwards */ false))
|
||||
{
|
||||
parts.push(hardline!());
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[allow(clippy::unused_self)]
|
||||
pub(crate) fn print_trailing_comments(&mut self, _range: Span) -> Option<Doc<'a>> {
|
||||
None
|
||||
pub(crate) fn print_trailing_comments(&mut self, range: Span) -> Option<Doc<'a>> {
|
||||
let mut parts = self.vec();
|
||||
let mut previous_comment: Option<Comment> = None;
|
||||
while let Some((start, end, kind)) = self.trivias.peek().copied() {
|
||||
let comment = Comment::new(start, end, kind);
|
||||
// Trailing comment if there is nothing in between.
|
||||
if range.end < comment.start
|
||||
&& self.source_text[range.end as usize..comment.start as usize]
|
||||
.chars()
|
||||
.all(|c| c == ' ')
|
||||
{
|
||||
self.trivias.next();
|
||||
let previous = self.print_trailing_comment(&mut parts, comment, previous_comment);
|
||||
previous_comment = Some(previous);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if parts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(Doc::Array(parts))
|
||||
}
|
||||
|
||||
fn print_trailing_comment(
|
||||
&mut self,
|
||||
parts: &mut Vec<'a, Doc<'a>>,
|
||||
comment: Comment,
|
||||
previous: Option<Comment>,
|
||||
) -> Comment {
|
||||
let printed = self.print_comment(comment);
|
||||
|
||||
if previous.is_some_and(|c| c.has_line_suffix && !c.is_block)
|
||||
|| self.has_newline(comment.start, /* backwards */ true)
|
||||
{
|
||||
parts.push(printed);
|
||||
let suffix = {
|
||||
let mut parts = self.vec();
|
||||
parts.push(hardline!());
|
||||
if self.is_previous_line_empty(comment.start) {
|
||||
parts.push(hardline!());
|
||||
}
|
||||
parts
|
||||
};
|
||||
parts.push(Doc::LineSuffix(suffix));
|
||||
return comment.with_line_suffix(true);
|
||||
}
|
||||
|
||||
if !comment.is_block || previous.is_some_and(|c| c.has_line_suffix) {
|
||||
let suffix = {
|
||||
let mut parts = self.vec();
|
||||
parts.push(ss!(" "));
|
||||
parts.push(printed);
|
||||
parts
|
||||
};
|
||||
let doc = array![self, Doc::LineSuffix(suffix), Doc::BreakParent];
|
||||
parts.push(doc);
|
||||
return comment.with_line_suffix(true);
|
||||
}
|
||||
|
||||
let doc = array![self, ss!(" "), printed];
|
||||
parts.push(doc);
|
||||
comment.with_line_suffix(false)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn print_inner_comment(&mut self, range: Span) -> Vec<'a, Doc<'a>> {
|
||||
let mut parts = self.vec();
|
||||
while let Some((start, end, kind)) = self.trivias.peek().copied() {
|
||||
let comment = Comment::new(start, end, kind);
|
||||
// Comment within the span
|
||||
if start >= range.start && end <= range.end {
|
||||
if comment.start >= range.start && comment.end <= range.end {
|
||||
self.trivias.next();
|
||||
parts.push(self.print_comment(start, end, kind));
|
||||
parts.push(self.print_comment(comment));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
|
@ -139,9 +230,10 @@ impl<'a> Prettier<'a> {
|
|||
) -> Option<Doc<'a>> {
|
||||
let mut parts = vec![];
|
||||
while let Some((start, end, kind)) = self.trivias.peek().copied() {
|
||||
let comment = Comment::new(start, end, kind);
|
||||
// Comment within the span
|
||||
if end <= range.end {
|
||||
parts.push(self.print_comment(start, end, kind));
|
||||
if comment.end <= range.end {
|
||||
parts.push(self.print_comment(comment));
|
||||
self.trivias.next();
|
||||
} else {
|
||||
break;
|
||||
|
|
@ -157,18 +249,7 @@ impl<'a> Prettier<'a> {
|
|||
}
|
||||
|
||||
#[must_use]
|
||||
fn print_comment(&self, start: u32, end: u32, kind: CommentKind) -> Doc<'a> {
|
||||
let end_offset = if kind.is_multi_line() { 2 } else { 0 };
|
||||
let comment = Span::new(start - 2, end + end_offset).source_text(self.source_text);
|
||||
Doc::Str(comment)
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn get_comment_end(&self, kind: CommentKind, end: u32) -> Option<u32> {
|
||||
if kind.is_single_line() {
|
||||
self.source_text[end as usize..].chars().next().map(|c| end + c.len_utf8() as u32)
|
||||
} else {
|
||||
Some(end)
|
||||
}
|
||||
fn print_comment(&self, comment: Comment) -> Doc<'a> {
|
||||
Doc::Str(Span::new(comment.start, comment.end).source_text(self.source_text))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ pub enum Doc<'a> {
|
|||
/// it's going to add a break whenever the next element doesn't fit in the line anymore.
|
||||
/// The difference with `group` is that it's not going to break all the separators, just the ones that are at the end of lines.
|
||||
Fill(Fill<'a>),
|
||||
/// Include this anywhere to force all parent groups to break.
|
||||
BreakParent,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -225,6 +227,9 @@ fn print_doc_to_debug(doc: &Doc<'_>) -> std::string::String {
|
|||
}
|
||||
string.push(')');
|
||||
}
|
||||
Doc::BreakParent => {
|
||||
string.push_str("BreakParent");
|
||||
}
|
||||
}
|
||||
|
||||
string
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ use std::borrow::Cow;
|
|||
|
||||
use oxc_allocator::{Box, Vec};
|
||||
use oxc_ast::{ast::*, AstKind};
|
||||
use oxc_span::{GetSpan, Span};
|
||||
use oxc_span::GetSpan;
|
||||
|
||||
use crate::{
|
||||
array,
|
||||
|
|
@ -150,13 +150,14 @@ impl<'a> Format<'a> for IfStatement<'a> {
|
|||
wrap!(p, self, IfStatement, {
|
||||
let mut parts = p.vec();
|
||||
|
||||
let test_doc = format!(p, self.test);
|
||||
let consequent = format!(p, self.consequent);
|
||||
let consequent = misc::adjust_clause(p, &self.consequent, consequent, false);
|
||||
|
||||
let opening = group![
|
||||
p,
|
||||
ss!("if ("),
|
||||
group!(p, indent!(p, softline!(), format!(p, self.test)), softline!()),
|
||||
group!(p, indent!(p, softline!(), test_doc), softline!()),
|
||||
ss!(")"),
|
||||
consequent
|
||||
];
|
||||
|
|
@ -1490,14 +1491,6 @@ impl<'a> Format<'a> for ObjectProperty<'a> {
|
|||
}
|
||||
} else {
|
||||
parts.push(format!(p, self.key));
|
||||
let comments = p.print_inner_comment(Span::new(
|
||||
self.key.span().end,
|
||||
self.value.span().start,
|
||||
));
|
||||
if !comments.is_empty() {
|
||||
parts.push(ss!(" "));
|
||||
parts.extend(comments);
|
||||
}
|
||||
parts.push(ss!(": "));
|
||||
parts.push(format!(p, self.value));
|
||||
}
|
||||
|
|
@ -1509,25 +1502,26 @@ impl<'a> Format<'a> for ObjectProperty<'a> {
|
|||
|
||||
impl<'a> Format<'a> for PropertyKey<'a> {
|
||||
fn format(&self, p: &mut Prettier<'a>) -> Doc<'a> {
|
||||
match self {
|
||||
PropertyKey::Identifier(ident) => ident.format(p),
|
||||
PropertyKey::PrivateIdentifier(ident) => ident.format(p),
|
||||
PropertyKey::Expression(expr) => match expr {
|
||||
Expression::StringLiteral(literal) => {
|
||||
let expr = format!(p, literal);
|
||||
let value = literal.value.as_bytes();
|
||||
if !&value[0].is_ascii_digit() && !value.contains(&b'_') {
|
||||
p.str(&literal.value)
|
||||
} else {
|
||||
literal.format(p)
|
||||
wrap!(p, self, PropertyKey, {
|
||||
match self {
|
||||
PropertyKey::Identifier(ident) => ident.format(p),
|
||||
PropertyKey::PrivateIdentifier(ident) => ident.format(p),
|
||||
PropertyKey::Expression(expr) => match expr {
|
||||
Expression::StringLiteral(literal) => {
|
||||
let value = literal.value.as_bytes();
|
||||
if !&value[0].is_ascii_digit() && !value.contains(&b'_') {
|
||||
p.str(&literal.value)
|
||||
} else {
|
||||
literal.format(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
Expression::Identifier(ident) => {
|
||||
array!(p, ss!("["), ident.format(p), ss!("]"))
|
||||
}
|
||||
_ => expr.format(p),
|
||||
},
|
||||
}
|
||||
Expression::Identifier(ident) => {
|
||||
array!(p, ss!("["), ident.format(p), ss!("]"))
|
||||
}
|
||||
_ => expr.format(p),
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -124,26 +124,58 @@ impl<'a> Prettier<'a> {
|
|||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn skip_newline(&self, start_index: u32) -> Option<u32> {
|
||||
let c = self.source_text[start_index as usize..].chars().next()?;
|
||||
is_line_terminator(c).then(|| start_index + c.len_utf8() as u32)
|
||||
fn skip_newline(&self, start_index: Option<u32>, backwards: bool) -> Option<u32> {
|
||||
let start_index = start_index?;
|
||||
let c = if backwards {
|
||||
self.source_text[..=start_index as usize].chars().next_back()
|
||||
} else {
|
||||
self.source_text[start_index as usize..].chars().next()
|
||||
}?;
|
||||
if is_line_terminator(c) {
|
||||
let len = c.len_utf8() as u32;
|
||||
return Some(if backwards { start_index - len } else { start_index + len });
|
||||
}
|
||||
Some(start_index)
|
||||
}
|
||||
|
||||
fn skip_spaces(&self, start_index: u32) -> u32 {
|
||||
fn skip_spaces(&self, start_index: u32, backwards: bool) -> Option<u32> {
|
||||
let mut index = start_index;
|
||||
for c in self.source_text[start_index as usize..].chars() {
|
||||
if matches!(c, ' ' | '\t') {
|
||||
index += 1;
|
||||
} else {
|
||||
break;
|
||||
if backwards {
|
||||
for c in self.source_text[..=start_index as usize].chars().rev() {
|
||||
if !matches!(c, ' ' | '\t') {
|
||||
return Some(index);
|
||||
}
|
||||
index -= 1_u32;
|
||||
}
|
||||
} else {
|
||||
for c in self.source_text[start_index as usize..].chars() {
|
||||
if !matches!(c, ' ' | '\t') {
|
||||
return Some(index);
|
||||
}
|
||||
index += 1_u32;
|
||||
}
|
||||
}
|
||||
index
|
||||
None
|
||||
}
|
||||
|
||||
fn has_newline(&self, start_index: u32) -> bool {
|
||||
let idx = self.skip_spaces(start_index);
|
||||
let idx2 = self.skip_newline(idx);
|
||||
Some(idx) != idx2
|
||||
fn has_newline(&self, start_index: u32, backwards: bool) -> bool {
|
||||
if (backwards && start_index == 0)
|
||||
|| (!backwards && start_index as usize == self.source_text.len())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let start_index = if backwards { start_index - 1 } else { start_index };
|
||||
let idx = self.skip_spaces(start_index, backwards);
|
||||
let idx2 = self.skip_newline(idx, backwards);
|
||||
idx != idx2
|
||||
}
|
||||
|
||||
fn is_previous_line_empty(&self, start_index: u32) -> bool {
|
||||
let idx = start_index - 1;
|
||||
let idx = self.skip_spaces(idx, true);
|
||||
let Some(idx) = self.skip_newline(idx, true) else { return false };
|
||||
let idx = self.skip_spaces(idx, true);
|
||||
let idx2 = self.skip_newline(idx, true);
|
||||
idx != idx2
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,10 +105,11 @@ impl<'a> Printer<'a> {
|
|||
Doc::IndentIfBreak(docs) => self.handle_indent_if_break(indent, mode, docs),
|
||||
Doc::Line => self.handle_line(indent, mode),
|
||||
Doc::Softline => self.handle_softline(indent, mode),
|
||||
Doc::Hardline => self.handle_hardline(indent),
|
||||
Doc::Hardline => self.handle_line(indent, Mode::Break),
|
||||
Doc::LineSuffix(docs) => self.handle_line_suffix(indent, mode, docs),
|
||||
Doc::IfBreak(doc) => self.handle_if_break(doc.unbox(), indent, mode),
|
||||
Doc::Fill(fill) => self.handle_fill(indent, mode, fill),
|
||||
Doc::BreakParent => {} // No op
|
||||
}
|
||||
|
||||
if self.cmds.is_empty() && !self.line_suffix.is_empty() {
|
||||
|
|
@ -192,6 +193,11 @@ impl<'a> Printer<'a> {
|
|||
|
||||
fn handle_line(&mut self, indent: Indent, mode: Mode) {
|
||||
if mode.is_break() {
|
||||
if !self.line_suffix.is_empty() {
|
||||
self.cmds.extend(self.line_suffix.drain(..).rev());
|
||||
return;
|
||||
}
|
||||
|
||||
self.handle_hardline(indent);
|
||||
} else {
|
||||
self.out.push(b' ');
|
||||
|
|
@ -201,7 +207,7 @@ impl<'a> Printer<'a> {
|
|||
|
||||
fn handle_softline(&mut self, indent: Indent, mode: Mode) {
|
||||
if mode.is_break() {
|
||||
self.handle_hardline(indent);
|
||||
self.handle_line(indent, Mode::Break);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -360,6 +366,7 @@ impl<'a> Printer<'a> {
|
|||
Doc::LineSuffix(_) => {
|
||||
break;
|
||||
}
|
||||
Doc::BreakParent => {}
|
||||
}
|
||||
|
||||
if remaining_width < 0 {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
<<<<<<< Updated upstream
|
||||
Compatibility: 171/591 (28.93%)
|
||||
=======
|
||||
Compatibility: 168/591 (28.43%)
|
||||
>>>>>>> Stashed changes
|
||||
|
||||
# Failed
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue