use rowan::{TextRange, TextSize};
use crate::SyntaxKind;
use crate::lexer::lex;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SegmentKind {
Header,
Knot,
TopLevelStitch,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Segment {
pub kind: SegmentKind,
pub range: TextRange,
pub lowered_range: TextRange,
pub header_start: Option<TextSize>,
}
#[must_use]
pub fn segment_file(source: &str) -> Vec<Segment> {
let tokens = lex(source);
let mut starts: Vec<TextSize> = Vec::with_capacity(tokens.len());
let mut pos = TextSize::from(0);
for (_, text) in &tokens {
starts.push(pos);
pos += TextSize::of(*text);
}
let total = pos;
let mut boundaries: Vec<(TextSize, TextSize, SegmentKind)> = Vec::new();
let mut brace_depth: u32 = 0;
let mut at_dispatch = true;
let mut seen_knot = false;
let mut i = 0;
while i < tokens.len() {
let kind = tokens[i].0;
if at_dispatch {
match kind {
_ if kind.is_trivia() => {
i += 1;
continue;
}
SyntaxKind::NEWLINE => {
i += 1;
continue;
}
SyntaxKind::EQ_EQ => {
boundaries.push((
doc_extended_start(&tokens, &starts, i),
starts[i],
SegmentKind::Knot,
));
seen_knot = true;
at_dispatch = false;
i += 1;
continue;
}
SyntaxKind::EQ if !seen_knot && at_stitch_lookahead(&tokens, i) => {
boundaries.push((
doc_extended_start(&tokens, &starts, i),
starts[i],
SegmentKind::TopLevelStitch,
));
at_dispatch = false;
i += 1;
continue;
}
_ => at_dispatch = false,
}
}
match kind {
SyntaxKind::L_BRACE => brace_depth += 1,
SyntaxKind::R_BRACE => brace_depth = brace_depth.saturating_sub(1),
SyntaxKind::NEWLINE if brace_depth == 0 => at_dispatch = true,
_ => {}
}
i += 1;
}
let first_cut = boundaries.first().map_or(total, |b| b.0);
let first_header = boundaries.first().map_or(total, |b| b.1);
let mut segments = Vec::with_capacity(boundaries.len() + 1);
segments.push(Segment {
kind: SegmentKind::Header,
range: TextRange::new(TextSize::from(0), first_cut),
lowered_range: TextRange::new(TextSize::from(0), first_header),
header_start: None,
});
for (idx, &(cut, header_start, kind)) in boundaries.iter().enumerate() {
let end = boundaries.get(idx + 1).map_or(total, |b| b.0);
let lowered_end = boundaries.get(idx + 1).map_or(total, |b| b.1);
segments.push(Segment {
kind,
range: TextRange::new(cut, end),
lowered_range: TextRange::new(cut, lowered_end),
header_start: Some(header_start),
});
}
segments
}
fn at_stitch_lookahead(tokens: &[(SyntaxKind, &str)], eq_idx: usize) -> bool {
let mut j = eq_idx + 1;
while j < tokens.len() && tokens[j].0.is_trivia() {
j += 1;
}
!matches!(
tokens.get(j).map(|t| t.0),
Some(SyntaxKind::EQ | SyntaxKind::GT)
)
}
fn doc_extended_start(
tokens: &[(SyntaxKind, &str)],
starts: &[TextSize],
header_idx: usize,
) -> TextSize {
let mut cut = starts[header_idx];
let mut newlines = 0u32;
let mut j = header_idx;
while j > 0 {
j -= 1;
match tokens[j].0 {
SyntaxKind::WHITESPACE => {}
SyntaxKind::NEWLINE => {
newlines += 1;
if newlines >= 2 {
break;
}
}
SyntaxKind::LINE_COMMENT if tokens[j].1.starts_with("///") => {
newlines = 0;
cut = starts[j];
}
_ => break,
}
}
cut
}
#[cfg(test)]
mod tests;