use alloc::vec::Vec;
use diag_lang::{Diagnostic, Label, Severity};
use syntax_lang::{Span, Token, TokenKind};
use crate::{
codes,
kind::Kind,
scan::{LayoutRt, Scanner, StringKinds},
};
pub(crate) fn apply(
sc: &Scanner,
layout: &LayoutRt,
src: &str,
first: usize,
tokens: &mut Vec<Token<Kind>>,
diags: &mut Vec<Diagnostic>,
) {
let k = &sc.k;
let mut joins: Vec<(u16, u16)> = layout.joins.to_vec();
let mut newline_joins: Vec<(u16, u16)> = layout.newline_joins.to_vec();
for s in sc.strings.iter() {
if let StringKinds::Node {
interp_open: Some(o),
interp_close: Some(c),
..
} = &s.kinds
{
joins.push((o.index(), c.index()));
newline_joins.push((o.index(), c.index()));
}
}
let depth_change = |pairs: &[(u16, u16)], kind: u16| -> i32 {
if pairs.iter().any(|(o, _)| *o == kind) {
1
} else if pairs.iter().any(|(_, c)| *c == kind) {
-1
} else {
0
}
};
let input: Vec<Token<Kind>> = tokens.drain(first..).collect();
let out = tokens;
out.reserve(input.len() + 8);
let mut stack: Vec<(u32, u32)> = Vec::from([(0, 0)]);
let mut join_depth: i32 = 0;
let mut newline_depth: i32 = 0;
let mut at_line_start = true;
let mut line_has_tokens = false;
let mut pending: Option<usize> = None;
let mut prev_significant: Option<u16> = None;
let whitespace = |t: &Token<Kind>| Token::new(k.whitespace, t.span());
for t in input {
let kind = *t.kind();
if kind == k.newline {
let inside = if layout.indent {
join_depth > 0
} else {
newline_depth > 0
};
if inside || !line_has_tokens {
out.push(whitespace(&t));
continue;
}
let terminates = match layout.newlines {
2 if !layout.indent => prev_significant
.is_some_and(|p| layout.terminate_after.binary_search(&p).is_ok()),
0 if !layout.indent => false,
_ => true,
};
if terminates {
pending = Some(out.len());
out.push(t);
line_has_tokens = false;
at_line_start = true;
} else {
out.push(whitespace(&t));
}
continue;
}
if kind.is_trivia() {
out.push(t);
continue;
}
let index = kind.index();
if at_line_start {
if layout.newlines == 2
&& !layout.indent
&& layout.continue_before.binary_search(&index).is_ok()
{
if let Some(at) = pending.take() {
out[at] = whitespace(&out[at]);
}
}
if layout.indent && join_depth == 0 {
let (column, alt) = indentation(src, t.span().start().to_usize(), layout.tab_width);
let &(top, top_alt) = stack.last().unwrap_or(&(0, 0));
let deeper = column > top;
let continuation = deeper
&& !layout.open_after.is_empty()
&& prev_significant
.is_none_or(|p| layout.open_after.binary_search(&p).is_err());
if continuation {
if let Some(at) = pending.take() {
out[at] = whitespace(&out[at]);
}
} else {
let at = t.span().start().to_u32();
if layout.mixed_error && (column > top) != (alt > top_alt) {
diags.push(error(
codes::LEX_MIXED_INDENT,
at,
"inconsistent use of tabs and spaces in indentation",
));
}
if deeper {
stack.push((column, alt));
out.push(Token::new(k.indent, Span::empty(at)));
} else if column < top {
while stack.len() > 1 && stack.last().is_some_and(|s| s.0 > column) {
let _ = stack.pop();
out.push(Token::new(k.dedent, Span::empty(at)));
}
if stack.last().is_some_and(|s| s.0 != column) {
diags.push(error(
codes::LEX_BAD_DEDENT,
at,
"unindent does not match any outer indentation level",
));
}
}
pending = None;
}
} else {
pending = None;
}
at_line_start = false;
}
join_depth = (join_depth + depth_change(&joins, index)).max(0);
newline_depth = (newline_depth + depth_change(&newline_joins, index)).max(0);
line_has_tokens = true;
prev_significant = Some(index);
out.push(t);
}
if layout.indent {
let end = src.len() as u32;
if line_has_tokens {
out.push(Token::new(k.newline, Span::empty(end)));
}
while stack.len() > 1 {
let _ = stack.pop();
out.push(Token::new(k.dedent, Span::empty(end)));
}
}
}
fn indentation(src: &str, at: usize, tab: u32) -> (u32, u32) {
let bytes = src.as_bytes();
let mut start = at;
while start > 0 && bytes[start - 1] != b'\n' {
start -= 1;
}
let tab = tab.max(1);
let (mut column, mut alt) = (0u32, 0u32);
for &b in &bytes[start..at] {
match b {
b'\t' => {
column = (column / tab + 1) * tab;
alt += 1;
}
b' ' => {
column += 1;
alt += 1;
}
_ => {
column += 1;
alt += 1;
}
}
}
(column, alt)
}
fn error(code: diag_lang::Code, at: u32, message: &str) -> Diagnostic {
Diagnostic::new(
Severity::Error,
alloc::string::String::from(message),
Label::unlabelled(Span::empty(at)),
)
.with_code(code)
}