use std::collections::HashMap;
use sql_dialect_fmt_syntax::{SyntaxKind::*, SyntaxNode, SyntaxToken};
pub(super) struct CommentInfo {
pub(super) text: String,
pub(super) is_line: bool,
pub(super) is_directive: bool,
}
#[derive(Default)]
pub(super) struct Comments {
leading: HashMap<u32, Vec<CommentInfo>>,
trailing: HashMap<u32, Vec<CommentInfo>>,
}
impl Comments {
pub(super) fn build(stmt: &SyntaxNode) -> Self {
let mut comments = Comments::default();
let mut last_significant: Option<u32> = None;
let mut newline_since = true; let mut pending_leading: Vec<CommentInfo> = Vec::new();
for token in stmt
.descendants_with_tokens()
.filter_map(|el| el.into_token())
{
let kind = token.kind();
if kind == NEWLINE {
newline_since = true;
continue;
}
if kind == WHITESPACE {
continue;
}
if kind.is_comment() {
let info = CommentInfo {
text: token.text().trim_end().to_string(),
is_line: kind == COMMENT,
is_directive: is_directive_comment(token.text()),
};
match last_significant {
Some(anchor) if !newline_since => {
comments.trailing.entry(anchor).or_default().push(info);
}
_ => pending_leading.push(info),
}
newline_since = false;
continue;
}
if kind == COMMA {
newline_since = false;
continue;
}
if matches!(kind, R_PAREN | R_BRACKET | R_BRACE) && !pending_leading.is_empty() {
if let Some(anchor) = last_significant {
comments
.trailing
.entry(anchor)
.or_default()
.append(&mut pending_leading);
}
}
let start = offset(&token);
if !pending_leading.is_empty() {
comments
.leading
.entry(start)
.or_default()
.append(&mut pending_leading);
}
last_significant = Some(start);
newline_since = false;
}
if !pending_leading.is_empty() {
if let Some(anchor) = last_significant {
comments
.trailing
.entry(anchor)
.or_default()
.append(&mut pending_leading);
}
}
comments
}
pub(super) fn all_placed(&self) -> bool {
self.leading.is_empty() && self.trailing.is_empty()
}
pub(super) fn take_leading(&mut self, token: &SyntaxToken) -> Vec<CommentInfo> {
self.leading.remove(&offset(token)).unwrap_or_default()
}
pub(super) fn take_trailing(&mut self, token: &SyntaxToken) -> Vec<CommentInfo> {
self.trailing.remove(&offset(token)).unwrap_or_default()
}
pub(super) fn take_statement_end_comments(&mut self, stmt: &SyntaxNode) -> Vec<CommentInfo> {
let last = stmt
.descendants_with_tokens()
.filter_map(|el| el.into_token())
.filter(|t| !t.kind().is_trivia() && t.kind() != COMMA)
.last();
last.and_then(|token| self.trailing.remove(&offset(&token)))
.unwrap_or_default()
}
}
pub(super) fn directive_comment_same_line_after_stmt(
last_stmt_end: Option<usize>,
token: &SyntaxToken,
) -> bool {
is_directive_comment(token.text())
&& last_stmt_end.is_some_and(|end| {
let mut previous = token.prev_sibling_or_token();
while let Some(element) = previous {
let element_end: usize = element.text_range().end().into();
if element_end <= end {
return true;
}
if element
.as_token()
.is_some_and(|token| token.text().contains(['\n', '\r']))
{
return false;
}
previous = element.prev_sibling_or_token();
}
false
})
}
fn offset(token: &SyntaxToken) -> u32 {
token.text_range().start().into()
}
fn is_directive_comment(text: &str) -> bool {
let Some(body) = text
.trim_start()
.strip_prefix("--")
.or_else(|| text.trim_start().strip_prefix("//"))
else {
return false;
};
let lower = body.trim_start().to_ascii_lowercase();
lower.starts_with("noqa")
|| lower.starts_with("sql-dialect-fmt:")
|| lower.starts_with("snowfmt:")
|| lower.starts_with("fmt:")
}