use std::ops::Range;
use oxc_allocator::{Allocator, ArenaVec};
use crate::block::IrSeg;
use crate::pos::{Segment, Span};
pub struct Input {
pub text: String,
lines: Vec<Line>,
}
#[derive(Clone, Copy)]
struct Line {
joined: usize,
padding: u8,
source: u32,
}
impl Input {
pub fn new(source: &str, segments: &[IrSeg]) -> Self {
let len = segments
.iter()
.map(|s| (s.seg.span.end - s.seg.span.start) as usize + usize::from(s.seg.padding) + 1)
.sum();
let mut text = String::with_capacity(len);
let mut lines = Vec::with_capacity(segments.len());
for (i, s) in segments.iter().enumerate() {
if i > 0 {
text.push('\n');
}
lines.push(Line {
joined: text.len(),
padding: s.seg.padding,
source: s.seg.span.start,
});
for _ in 0..s.seg.padding {
text.push(' ');
}
text.push_str(s.seg.span.slice(source));
}
Self { text, lines }
}
fn line_of(&self, joined: usize) -> usize {
self.lines.partition_point(|line| line.joined <= joined).saturating_sub(1)
}
fn line_end(&self, i: usize) -> usize {
self.lines.get(i + 1).map_or(self.text.len(), |next| next.joined - 1)
}
#[expect(clippy::cast_possible_truncation)] pub fn offset(&self, joined: usize) -> u32 {
let i = self.line_of(joined);
if let Some(next) = self.lines.get(i + 1)
&& joined + 1 >= next.joined
{
return next.source;
}
let line = self.lines[i];
let content = line.joined + usize::from(line.padding);
line.source + joined.saturating_sub(content) as u32
}
pub fn span(&self, r: Range<usize>) -> Span {
if r.is_empty() {
return Span::empty(self.offset(r.start));
}
Span::new(self.offset(r.start), self.offset(r.end - 1) + 1)
}
pub fn pieces_in<'a>(
&self,
r: Range<usize>,
allocator: &'a Allocator,
) -> ArenaVec<'a, Segment> {
ArenaVec::from_iter_in(self.pieces_iter(r), &allocator)
}
pub fn pieces(&self, r: Range<usize>) -> Vec<Segment> {
self.pieces_iter(r).collect()
}
fn pieces_iter(&self, r: Range<usize>) -> impl Iterator<Item = Segment> {
(self.line_of(r.start)..=self.line_of(r.end)).map(move |i| self.piece(i, r.clone()))
}
pub fn lines_before(&self, joined: usize) -> usize {
self.lines.partition_point(|line| line.joined < joined)
}
#[expect(clippy::cast_possible_truncation)] fn piece(&self, i: usize, r: Range<usize>) -> Segment {
let line = self.lines[i];
let start = r.start.max(line.joined);
let end = r.end.min(self.line_end(i)).max(start);
let content = line.joined + usize::from(line.padding);
let padding = content.saturating_sub(start).min(end - start);
let source = self.offset(start + padding);
Segment::new(Span::new(source, source + (end - start - padding) as u32), padding as u8)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn seg(start: u32, end: u32, padding: u8) -> IrSeg {
IrSeg { seg: Segment::new(Span::new(start, end), padding), lazy: false }
}
fn sample() -> Input {
Input::new("> ab\n>\tcd\n", &[seg(2, 4, 0), seg(7, 9, 2)])
}
#[test]
fn maps_back_to_source() {
let input = sample();
assert_eq!(input.text, "ab\n cd");
assert_eq!(input.span(0..2), Span::new(2, 4));
assert_eq!(input.span(5..7), Span::new(7, 9));
assert_eq!(input.offset(2), 7);
assert_eq!(input.offset(3), 7);
assert_eq!(input.span(1..1), Span::empty(3));
assert_eq!(input.lines_before(0), 0);
assert_eq!(input.lines_before(3), 1);
}
#[test]
fn pieces_split_on_separators_and_keep_padding() {
let input = sample();
assert_eq!(
input.pieces(0..7),
vec![Segment::new(Span::new(2, 4), 0), Segment::new(Span::new(7, 9), 2)]
);
assert_eq!(input.pieces(4..7), vec![Segment::new(Span::new(7, 9), 1)]);
assert_eq!(input.pieces(1..2), vec![Segment::new(Span::new(3, 4), 0)]);
assert_eq!(
input.pieces(0..3),
vec![Segment::new(Span::new(2, 4), 0), Segment::new(Span::empty(7), 0)]
);
assert_eq!(
input.pieces(2..7),
vec![Segment::new(Span::empty(7), 0), Segment::new(Span::new(7, 9), 2)]
);
}
}