use brink_syntax::SyntaxNode;
use crate::line_index::LineIndex;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[expect(
clippy::struct_excessive_bools,
reason = "a per-line fact record — each flag is an independent trivia \
classification, not a state machine to encode as an enum"
)]
pub struct LineTrivia {
pub comment: bool,
pub block_comment: bool,
pub tag: bool,
pub todo: bool,
}
#[must_use]
pub fn line_trivia(source: &str, root: &SyntaxNode, line_count: usize) -> Vec<LineTrivia> {
let mut trivia = line_trivia_from_source(source, line_count);
let idx = LineIndex::new(source);
for token in root.descendants_with_tokens() {
if let Some(token) = token.as_token()
&& token.kind() == brink_syntax::SyntaxKind::BLOCK_COMMENT
{
mark_block_comment(&mut trivia, &idx, token.text_range());
}
}
for node in root.descendants() {
if node.kind() == brink_syntax::SyntaxKind::AUTHOR_WARNING {
let line = idx.line_col(node.text_range().start()).0 as usize;
if let Some(t) = trivia.get_mut(line) {
t.todo = true;
}
}
}
trivia
}
#[must_use]
pub fn line_trivia_native(
source: &str,
root: &brink_syntax_native::SyntaxNode,
line_count: usize,
) -> Vec<LineTrivia> {
let mut trivia = line_trivia_from_source(source, line_count);
let idx = LineIndex::new(source);
for token in root.descendants_with_tokens() {
if let Some(token) = token.as_token()
&& token.kind() == brink_syntax_native::SyntaxKind::BLOCK_COMMENT
{
mark_block_comment(&mut trivia, &idx, token.text_range());
}
}
trivia
}
fn line_trivia_from_source(source: &str, line_count: usize) -> Vec<LineTrivia> {
let mut trivia = vec![LineTrivia::default(); line_count];
for (i, line) in source.lines().enumerate() {
if i >= trivia.len() {
break;
}
let trimmed = line.trim_start();
if trimmed.starts_with("//") {
trivia[i].comment = true;
} else if trimmed.starts_with('#') && !trimmed.is_empty() {
trivia[i].tag = true;
}
}
trivia
}
fn mark_block_comment(trivia: &mut [LineTrivia], idx: &LineIndex, range: rowan::TextRange) {
let start_line = idx.line_col(range.start()).0 as usize;
let end_line = idx.line_col(range.end()).0 as usize;
for line in start_line..=end_line {
if let Some(t) = trivia.get_mut(line) {
t.comment = true;
t.block_comment = true;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn trivia_for(source: &str) -> Vec<LineTrivia> {
let parse = brink_syntax::parse(source);
line_trivia(source, &parse.syntax(), source.lines().count().max(1))
}
#[test]
fn line_comment_and_tag_line() {
let t = trivia_for("// comment\n# a tag\nplain\n");
assert!(t[0].comment && !t[0].tag && !t[0].block_comment);
assert!(t[1].tag && !t[1].comment);
assert_eq!(t[2], LineTrivia::default());
}
#[test]
fn block_comment_spans_lines_and_wins_over_tag_sigil() {
let t = trivia_for("/* one\n# looks like a tag\ntwo */\nafter\n");
for (line, lt) in t.iter().enumerate().take(3) {
assert!(lt.comment && lt.block_comment, "line {line}");
}
assert!(!t[3].comment && !t[3].block_comment);
}
fn native_trivia_for(source: &str) -> Vec<LineTrivia> {
let parse = brink_syntax_native::parse(source);
line_trivia_native(source, &parse.syntax(), source.lines().count().max(1))
}
#[test]
fn native_line_comment_and_tag_line() {
let t = native_trivia_for("// comment\n# a tag\nplain\n");
assert!(t[0].comment && !t[0].tag && !t[0].block_comment);
assert!(t[1].tag && !t[1].comment);
assert_eq!(t[2], LineTrivia::default());
}
#[test]
fn native_block_comment_spans_lines() {
let src = "flow main() {\n/* one\ntwo */\nafter\n}\n";
let t = native_trivia_for(src);
assert!(t[1].comment && t[1].block_comment, "{t:?}");
assert!(t[2].comment && t[2].block_comment, "{t:?}");
assert!(!t[3].comment && !t[3].block_comment, "{t:?}");
}
}