use std::str::{CharIndices, from_utf8};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use super::{handler::GraphicalReportHandler, span::FancySpan};
use crate::source_impls::SpanContents;
#[derive(Debug)]
pub(super) struct Line<'a> {
pub(super) number: usize,
pub(super) offset: usize,
pub(super) length: usize,
pub(super) text: &'a str,
}
impl Line<'_> {
pub(super) fn span_line_only(&self, span: &FancySpan) -> bool {
span.offset() >= self.offset && span.offset() + span.len() <= self.offset + self.length
}
pub(super) fn span_applies(&self, span: &FancySpan) -> bool {
let spanlen = if span.len() == 0 { 1 } else { span.len() };
(span.offset() >= self.offset && span.offset() < self.offset + self.length)
|| (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) || (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length)
}
pub(super) fn span_applies_gutter(&self, span: &FancySpan) -> bool {
let spanlen = if span.len() == 0 { 1 } else { span.len() };
self.span_applies(span)
&& !(
(span.offset() >= self.offset && span.offset() < self.offset + self.length)
&& (span.offset() + spanlen > self.offset
&& span.offset() + spanlen <= self.offset + self.length)
)
}
pub(super) fn span_flyby(&self, span: &FancySpan) -> bool {
span.offset() < self.offset
&& span.offset() + span.len() > self.offset + self.length
}
pub(super) fn span_starts(&self, span: &FancySpan) -> bool {
span.offset() >= self.offset
}
pub(super) fn span_ends(&self, span: &FancySpan) -> bool {
span.offset() + span.len() >= self.offset
&& span.offset() + span.len() <= self.offset + self.length
}
}
struct CharWidthIterator<'a> {
chars: CharIndices<'a>,
grapheme_boundaries: Option<Vec<(usize, usize)>>, current_grapheme_idx: usize,
column: usize,
escaped: bool,
}
impl Iterator for CharWidthIterator<'_> {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
let (byte_pos, c) = self.chars.next()?;
let width = match (self.escaped, c) {
(false, '\t') => 4 - self.column % 4,
(false, '\x1b') => {
self.escaped = true;
0
}
(false, _) => {
if let Some(ref boundaries) = self.grapheme_boundaries {
if self.current_grapheme_idx < boundaries.len()
&& boundaries[self.current_grapheme_idx].0 == byte_pos
{
let width = boundaries[self.current_grapheme_idx].1;
self.current_grapheme_idx += 1;
width
} else {
0 }
} else {
1
}
}
(true, 'm') => {
self.escaped = false;
0
}
(true, _) => 0,
};
self.column += width;
Some(width)
}
}
impl GraphicalReportHandler {
pub(super) fn line_visual_char_width(text: &str) -> impl Iterator<Item = usize> + '_ + use<'_> {
let grapheme_boundaries = if text.is_ascii() {
None
} else {
Some(
text.grapheme_indices(true)
.map(|(pos, grapheme)| (pos, grapheme.width()))
.collect(),
)
};
CharWidthIterator {
chars: text.char_indices(),
grapheme_boundaries,
current_grapheme_idx: 0,
column: 0,
escaped: false,
}
}
pub(super) fn visual_offset(line: &Line<'_>, offset: usize, start: bool) -> usize {
let line_range = line.offset..=(line.offset + line.length);
assert!(line_range.contains(&offset));
let mut text_index = offset - line.offset;
while text_index <= line.text.len() && !line.text.is_char_boundary(text_index) {
if start {
text_index -= 1;
} else {
text_index += 1;
}
}
let text = &line.text[..text_index.min(line.text.len())];
let text_width =
if text.is_ascii() && memchr::memchr2(b'\t', b'\x1b', text.as_bytes()).is_none() {
text.len()
} else {
Self::line_visual_char_width(text).sum()
};
if text_index > line.text.len() {
text_width + 1
} else {
text_width
}
}
#[expect(clippy::unused_self, reason = "kept as a renderer method for call-site consistency")]
pub(super) fn get_lines<'a>(&self, context_data: &SpanContents<'a>) -> Vec<Line<'a>> {
let context = from_utf8(context_data.data()).expect("Bad utf8 detected");
let mut line = context_data.line();
let base = context_data.span().offset() as usize;
let bytes = context.as_bytes();
let capacity =
context_data.line_count().saturating_sub(context_data.line()).max(1).min(bytes.len());
let mut lines = Vec::with_capacity(capacity);
let mut start = 0;
for newline in memchr::memchr_iter(b'\n', bytes) {
let end = newline + 1;
let text_end =
if newline > start && bytes[newline - 1] == b'\r' { newline - 1 } else { newline };
line += 1;
lines.push(Line {
number: line,
offset: base + start,
length: end - start,
text: &context[start..text_end],
});
start = end;
}
if start < bytes.len() {
if bytes.last() != Some(&b'\r') {
line += 1;
}
lines.push(Line {
number: line,
offset: base + start,
length: bytes.len() - start,
text: &context[start..],
});
}
lines
}
}
#[cfg(test)]
mod tests {
#![expect(
clippy::cast_possible_truncation,
reason = "test fixtures are much smaller than u32::MAX"
)]
use super::*;
use crate::source_impls::SpanScanner;
type ExpectedLine<'a> = (usize, usize, usize, &'a str);
#[test]
fn get_lines_preserves_line_geometry() {
const BASE: usize = 10;
let cases: &[(&str, &[ExpectedLine<'_>])] = &[
("", &[]),
("abc", &[(5, BASE, 3, "abc")]),
("a\nb", &[(5, BASE, 2, "a"), (6, BASE + 2, 1, "b")]),
("a\n", &[(5, BASE, 2, "a")]),
("\n", &[(5, BASE, 1, "")]),
("a\r\nb", &[(5, BASE, 3, "a"), (6, BASE + 3, 1, "b")]),
("a\rb", &[(5, BASE, 3, "a\rb")]),
("a\r", &[(4, BASE, 2, "a\r")]),
("é\n火", &[(5, BASE, 3, "é"), (6, BASE + 3, 3, "火")]),
];
let handler = GraphicalReportHandler::new();
for &(text, expected) in cases {
let contents = SpanContents::new(
text.as_bytes(),
(BASE as u32, text.len() as u32).into(),
4,
2,
expected.len(),
);
let actual = handler
.get_lines(&contents)
.iter()
.map(|line| (line.number, line.offset, line.length, line.text))
.collect::<Vec<_>>();
assert_eq!(actual, expected, "text={text:?}");
}
}
#[test]
fn get_lines_preallocates_the_source_window() {
let source = "before\ntarget\nafter\nrest";
let mut scanner = SpanScanner::new(source.as_bytes(), 1, 1);
let contents = scanner.read_span((7u32, 6u32).into()).unwrap();
let lines = GraphicalReportHandler::new().get_lines(&contents);
assert_eq!(lines.len(), 3);
assert_eq!(lines.capacity(), 3);
}
}