use crate::GcRef;
use crate::text::{TextPayload, text_bytes, text_root};
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub(crate) struct Cursor(usize);
impl Cursor {
#[inline]
pub(crate) fn offset(self) -> usize {
self.0
}
#[inline]
pub(crate) fn advance(self, n: usize) -> Cursor {
Cursor(self.0.saturating_add(n))
}
#[inline]
pub(crate) fn delta_from(self, earlier: Cursor) -> usize {
self.0.saturating_sub(earlier.0)
}
}
#[derive(Clone, Copy)]
pub(crate) struct Input<'a> {
owner: GcRef,
base: usize,
text: &'a str,
}
impl<'a> Input<'a> {
pub(crate) unsafe fn new(owner: GcRef) -> Option<Input<'a>> {
let bytes = unsafe { text_bytes(owner.payload::<TextPayload>() as *const TextPayload) };
let text = std::str::from_utf8(bytes).ok()?;
let (root, base) = unsafe { text_root(owner) };
Some(Input {
owner: root,
base,
text,
})
}
#[inline]
pub(crate) fn owner(&self) -> GcRef {
self.owner
}
#[inline]
pub(crate) fn owner_offset(&self, at: usize) -> usize {
self.base.saturating_add(at)
}
#[inline]
pub(crate) fn whole(&self) -> ByteRegion {
ByteRegion {
start: Cursor(0),
end: Cursor(self.text.len()),
}
}
#[inline]
fn text(&self) -> &'a str {
self.text
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) struct ByteRegion {
start: Cursor,
end: Cursor,
}
impl ByteRegion {
#[inline]
pub(crate) fn start(self) -> Cursor {
self.start
}
#[inline]
pub(crate) fn end(self) -> Cursor {
self.end
}
#[inline]
pub(crate) fn len(self) -> usize {
self.end.delta_from(self.start)
}
#[inline]
pub(crate) fn is_empty(self) -> bool {
self.end == self.start
}
#[inline]
pub(crate) fn bytes<'a>(self, i: &Input<'a>) -> &'a [u8] {
i.text()
.as_bytes()
.get(self.start.offset()..self.end.offset())
.unwrap_or(&[])
}
#[inline]
pub(crate) fn str<'a>(self, i: &Input<'a>) -> Option<&'a str> {
i.text().get(self.start.offset()..self.end.offset())
}
#[inline]
pub(crate) fn is_all_whitespace(self, i: &Input<'_>) -> bool {
match self.str(i) {
Some(s) => s.chars().all(char::is_whitespace),
None => false,
}
}
#[inline]
pub(crate) fn subregion(self, start: Cursor, end: Cursor) -> ByteRegion {
debug_assert!(
self.start <= start && start <= end && end <= self.end,
"a subregion can only narrow: {:?}..{:?} is not inside {:?}..{:?}",
start,
end,
self.start,
self.end
);
let start = start.max(self.start).min(self.end);
let end = end.max(start).min(self.end);
ByteRegion { start, end }
}
#[inline]
pub(crate) fn from(self, at: Cursor) -> ByteRegion {
self.subregion(at, self.end)
}
pub(crate) fn next_scalar(self, i: &Input<'_>, at: Cursor) -> Option<Cursor> {
if at >= self.end {
return None;
}
let rest = i.text().get(at.offset()..self.end.offset())?;
let ch = rest.chars().next()?;
Some(at.advance(ch.len_utf8()))
}
}
#[derive(Clone, Copy)]
pub(crate) struct Walked {
pub(crate) value: GcRef,
pub(crate) next: Cursor,
}
pub(crate) fn split_lines(i: &Input<'_>, region: ByteRegion) -> Vec<ByteRegion> {
let bytes = region.bytes(i);
let base = region.start();
let mut out = Vec::new();
let mut pos = 0usize;
while pos < bytes.len() {
let start = pos;
while pos < bytes.len() && bytes[pos] != b'\n' {
pos += 1;
}
let mut end = pos;
if end > start && bytes[end - 1] == b'\r' {
end -= 1;
}
if pos < bytes.len() {
pos += 1; }
out.push(region.subregion(base.advance(start), base.advance(end)));
}
while out.last().is_some_and(|l| l.is_empty()) {
out.pop();
}
out
}
pub(crate) fn line_window_end(
i: &Input<'_>,
region: ByteRegion,
at: Cursor,
extra: usize,
) -> Cursor {
let bytes = region.from(at).bytes(i);
let mut pos = 0usize;
for _ in 0..extra {
match bytes[pos..].iter().position(|b| *b == b'\n') {
Some(n) => pos += n + 1,
None => return region.end(),
}
}
let mut end = match bytes[pos..].iter().position(|b| *b == b'\n') {
Some(n) => pos + n,
None => return region.end(),
};
if end > pos && bytes[end - 1] == b'\r' {
end -= 1;
}
at.advance(end)
}
pub(crate) fn trailing_blank_run(i: &Input<'_>, lines: &[ByteRegion]) -> usize {
let mut start = lines.len();
while start > 0 && lines[start - 1].is_all_whitespace(i) {
start -= 1;
}
start
}
pub(crate) fn split_sections(i: &Input<'_>, region: ByteRegion) -> Vec<ByteRegion> {
let mut out = Vec::new();
let mut current: Option<(Cursor, Cursor)> = None;
for line in split_lines(i, region) {
if line.is_all_whitespace(i) {
if let Some((start, end)) = current.take() {
out.push(region.subregion(start, end));
}
} else {
current = Some(match current {
Some((start, _)) => (start, line.end()),
None => (line.start(), line.end()),
});
}
}
if let Some((start, end)) = current {
out.push(region.subregion(start, end));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn input_over(text: &str) -> (crate::Runtime, GcRef) {
let rt = crate::Runtime::new();
let owner = rt.alloc_text(text);
(rt, owner)
}
#[test]
fn a_subregion_can_only_narrow() {
let (_rt, owner) = input_over("hello world");
let i = unsafe { Input::new(owner) }.expect("a Text is UTF-8");
let whole = i.whole();
let inner = whole.subregion(whole.start().advance(6), whole.end());
assert_eq!(inner.str(&i), Some("world"));
let innermost = inner.subregion(inner.start(), inner.start().advance(3));
assert_eq!(innermost.str(&i), Some("wor"));
}
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "a subregion can only narrow")]
fn a_subregion_that_would_widen_is_a_bug_and_says_so() {
let (_rt, owner) = input_over("hello world");
let i = unsafe { Input::new(owner) }.expect("a Text is UTF-8");
let whole = i.whole();
let inner = whole.subregion(whole.start().advance(6), whole.end());
let _ = inner.subregion(whole.start(), inner.end());
}
#[test]
fn next_scalar_steps_one_unicode_scalar() {
let (_rt, owner) = input_over("aéb");
let i = unsafe { Input::new(owner) }.expect("a Text is UTF-8");
let r = i.whole();
let mut at = r.start();
let mut seen = Vec::new();
while let Some(next) = r.next_scalar(&i, at) {
seen.push(r.subregion(at, next).str(&i).expect("a scalar is a str"));
at = next;
}
assert_eq!(seen, vec!["a", "é", "b"], "three scalars, not four bytes");
assert_eq!(at, r.end());
}
#[test]
fn a_section_region_excludes_its_trailing_line_ending() {
let (_rt, owner) = input_over("first\n\nsecond");
let i = unsafe { Input::new(owner) }.expect("a Text is UTF-8");
let sections: Vec<&str> = split_sections(&i, i.whole())
.into_iter()
.map(|s| s.str(&i).expect("a section is a str"))
.collect();
assert_eq!(
sections,
vec!["first", "second"],
"a section that keeps its own newline cannot be consumed exactly by `word`"
);
}
#[test]
fn split_lines_and_split_sections_agree_on_a_single_line_region() {
let (_rt, owner) = input_over("only\n");
let i = unsafe { Input::new(owner) }.expect("a Text is UTF-8");
let lines = split_lines(&i, i.whole());
let sections = split_sections(&i, i.whole());
assert_eq!(lines.len(), 1);
assert_eq!(sections.len(), 1);
assert_eq!(lines[0], sections[0]);
assert_eq!(lines[0].str(&i), Some("only"));
}
#[test]
fn split_lines_strips_crlf_and_drops_a_trailing_empty_line() {
let (_rt, owner) = input_over("abc\r\ndef\nghi");
let i = unsafe { Input::new(owner) }.expect("a Text is UTF-8");
let lines: Vec<&str> = split_lines(&i, i.whole())
.into_iter()
.map(|l| l.str(&i).expect("a line is a str"))
.collect();
assert_eq!(lines, vec!["abc", "def", "ghi"]);
let (_rt2, owner2) = input_over("a\nb\n");
let i2 = unsafe { Input::new(owner2) }.expect("a Text is UTF-8");
let lines2: Vec<&str> = split_lines(&i2, i2.whole())
.into_iter()
.map(|l| l.str(&i2).expect("a line is a str"))
.collect();
assert_eq!(lines2, vec!["a", "b"]);
}
#[test]
fn line_window_end_is_split_lines_asked_forwards_from_a_cursor() {
for (text, cursor, extra, want) in [
("a\nb\nc\n", 0usize, 0usize, "a"),
("a\nb\nc\n", 2, 0, "b"),
("a\r\nb\r\n", 0, 0, "a"),
("a\r\nb\r\n", 3, 0, "b"),
("a\nb\nc\n", 0, 1, "a\nb"),
("a\r\nb\r\nc\r\n", 0, 1, "a\r\nb"),
("a\nb\nc\n", 0, 2, "a\nb\nc"),
("a\nb", 2, 0, "b"),
("abc", 0, 0, "abc"),
("a\nb\n", 0, 5, "a\nb\n"),
] {
let (_rt, owner) = input_over(text);
let i = unsafe { Input::new(owner) }.expect("a Text is UTF-8");
let region = i.whole();
let at = region.start().advance(cursor);
let end = line_window_end(&i, region, at, extra);
assert_eq!(
region.subregion(at, end).str(&i),
Some(want),
"window of {text:?} from {cursor} over {extra} extra terminators"
);
}
}
#[test]
fn a_region_does_not_end_in_empty_lines_however_many_there_are() {
for (text, want) in [
("1\n2\n", vec!["1", "2"]),
("1\n2", vec!["1", "2"]),
("1\r\n2\r\n", vec!["1", "2"]),
("1\n2\n\n", vec!["1", "2"]),
("1\n2\n\n\n\n", vec!["1", "2"]),
("1\r\n2\r\n\r\n", vec!["1", "2"]),
("1\n2\n \n", vec!["1", "2", " "]),
("1\n2\n\t\n \n", vec!["1", "2", "\t", " "]),
("1\n2\n \n\n", vec!["1", "2", " "]),
("1 \n2 \n", vec!["1 ", "2 "]),
("1\n\n2\n", vec!["1", "", "2"]),
("1\n \n2\n", vec!["1", " ", "2"]),
("\n\n", vec![]),
(" \n \n", vec![" ", " "]),
("", vec![]),
] {
let (_rt, owner) = input_over(text);
let i = unsafe { Input::new(owner) }.expect("a Text is UTF-8");
let lines: Vec<&str> = split_lines(&i, i.whole())
.into_iter()
.map(|l| l.str(&i).expect("a line is a str"))
.collect();
assert_eq!(lines, want, "lines of {text:?}");
}
}
#[test]
fn the_trailing_blank_run_is_the_only_run_a_constructor_may_drop() {
for (text, want) in [
("1\n2\n", 2),
("1\n2\n \n", 2),
("1\n2\n \n \t\n", 2),
("1\n\n2\n", 3),
("1\n \n2\n", 3),
("1\n \n2\n \n", 3),
(" \n \n", 0),
("", 0),
] {
let (_rt, owner) = input_over(text);
let i = unsafe { Input::new(owner) }.expect("a Text is UTF-8");
let lines = split_lines(&i, i.whole());
assert_eq!(trailing_blank_run(&i, &lines), want, "run of {text:?}");
}
}
#[test]
fn the_root_region_is_the_whole_buffer_and_no_terminator_is_trimmed() {
for text in ["1 2 3\n", "a -> b\r\n", "^v<>\n", "no terminator", "\n", ""] {
let (_rt, owner) = input_over(text);
let i = unsafe { Input::new(owner) }.expect("a Text is UTF-8");
assert_eq!(i.whole().str(&i), Some(text), "root region of {text:?}");
}
}
#[test]
fn split_sections_on_blank_lines() {
let (_rt, owner) = input_over("a\nb\n\nc\nd");
let i = unsafe { Input::new(owner) }.expect("a Text is UTF-8");
let sections: Vec<&str> = split_sections(&i, i.whole())
.into_iter()
.map(|s| s.str(&i).expect("a section is a str"))
.collect();
assert_eq!(sections, vec!["a\nb", "c\nd"]);
}
#[test]
fn a_blank_line_of_spaces_separates_sections() {
let (_rt, owner) = input_over("first\n \nsecond\n\n");
let i = unsafe { Input::new(owner) }.expect("a Text is UTF-8");
let sections: Vec<&str> = split_sections(&i, i.whole())
.into_iter()
.map(|s| s.str(&i).expect("a section is a str"))
.collect();
assert_eq!(sections, vec!["first", "second"]);
}
#[test]
fn a_line_region_of_a_section_names_the_inputs_own_bytes() {
let (_rt, owner) = input_over("alpha\n\nbeta\n");
let i = unsafe { Input::new(owner) }.expect("a Text is UTF-8");
let sections = split_sections(&i, i.whole());
assert_eq!(sections.len(), 2);
let second = split_lines(&i, sections[1]);
assert_eq!(second.len(), 1);
assert_eq!(second[0].start().offset(), 7, "\"beta\" begins at byte 7");
assert_eq!(second[0].str(&i), Some("beta"));
}
}