#[derive(Clone, Copy)]
pub enum LineBreaks {
TypeScript,
Lsp,
}
impl LineBreaks {
fn is_break(self, ch: char) -> bool {
matches!(ch, '\r' | '\n')
|| matches!(self, Self::TypeScript) && matches!(ch, '\u{2028}' | '\u{2029}')
}
pub fn line_starts(self, source: &str) -> impl Iterator<Item = usize> + '_ {
let bytes = source.as_bytes();
std::iter::once(0).chain(memchr::memchr3_iter(b'\r', b'\n', 0xe2, bytes).filter_map(
move |at| match bytes.get(at)? {
b'\r' if bytes.get(at + 1) == Some(&b'\n') => None,
b'\r' | b'\n' => Some(at + 1),
_ if matches!(self, Self::TypeScript)
&& bytes.get(at + 1) == Some(&0x80)
&& matches!(bytes.get(at + 2), Some(0xa8 | 0xa9)) =>
{
Some(at + 3)
}
_ => None,
},
))
}
pub fn position_to_offset(self, source: &str, line: u32, character: u32) -> Option<usize> {
let start = self.line_starts(source).nth(line as usize)?;
let text = source.get(start..)?.split(|ch| self.is_break(ch)).next()?;
super::utf16_offset(text, character).map(|offset| start + offset)
}
pub fn offset_to_position(self, source: &str, offset: usize) -> (u32, u32) {
let offset = offset.min(source.len());
let (line, start) = self
.line_starts(source)
.enumerate()
.take_while(|(_, start)| *start <= offset)
.last()
.unwrap_or((0, 0));
let tail = source.get(start..).unwrap_or_default();
let column = super::utf16::prefix_len(tail, offset - start);
(line as u32, column as u32)
}
}
#[cfg(test)]
mod tests {
use super::LineBreaks;
#[test]
fn simd_line_starts_match_scalar_rules_for_mixed_unicode() {
let pieces = [
"", "a", "\r", "\n", "\r\n", "\u{2028}", "\u{2029}", "€", "😀", "\u{0085}",
];
for mode in [LineBreaks::Lsp, LineBreaks::TypeScript] {
for left in pieces {
for right in pieces {
let source = crate::cstr!("{left}{right}");
let expected: Vec<_> = std::iter::once(0)
.chain(source.char_indices().filter_map(|(at, ch)| {
if ch == '\r' && source.as_bytes().get(at + 1) == Some(&b'\n') {
None
} else {
mode.is_break(ch).then_some(at + ch.len_utf8())
}
}))
.collect();
assert_eq!(
mode.line_starts(&source).collect::<Vec<_>>(),
expected,
"{source:?}"
);
}
}
}
}
#[test]
fn native_lines_and_utf16_columns_have_exact_boundaries() {
let mode = LineBreaks::TypeScript;
for newline in ["\r", "\n", "\r\n", "\u{2028}", "\u{2029}"] {
let source = crate::cstr!("\u{1f600}{newline}x{newline}");
let second = 4 + newline.len();
assert_eq!(
mode.line_starts(&source).collect::<Vec<_>>(),
[0, second, source.len()]
);
assert_eq!(mode.position_to_offset(&source, 0, 1), None);
assert_eq!(mode.position_to_offset(&source, 0, 2), Some(4));
assert_eq!(mode.position_to_offset(&source, 0, 3), None);
assert_eq!(mode.position_to_offset(&source, 1, 0), Some(second));
assert_eq!(mode.position_to_offset(&source, 1, 1), Some(second + 1));
assert_eq!(mode.position_to_offset(&source, 2, 0), Some(source.len()));
assert_eq!(mode.position_to_offset(&source, 3, 0), None);
assert_eq!(mode.offset_to_position(&source, second), (1, 0));
assert_eq!(mode.offset_to_position(&source, second + 1), (1, 1));
assert_eq!(mode.offset_to_position(&source, usize::MAX), (2, 0));
}
assert_eq!(
mode.line_starts("\r\r\n\n\u{2028}\u{2029}")
.collect::<Vec<_>>(),
[0, 1, 3, 4, 7, 10]
);
assert_eq!(
mode.line_starts("\u{0085}\u{000b}\u{000c}")
.collect::<Vec<_>>(),
[0]
);
}
#[test]
fn lsp_keeps_unicode_line_separators_inside_the_line() {
let source = "\u{1f600}\u{2028}\u{2029}\r\nx\ry";
let mode = LineBreaks::Lsp;
assert_eq!(mode.line_starts(source).collect::<Vec<_>>(), [0, 12, 14]);
assert_eq!(mode.position_to_offset(source, 0, 4), Some(10));
assert_eq!(mode.position_to_offset(source, 0, 5), None);
assert_eq!(mode.offset_to_position(source, 10), (0, 4));
assert_eq!(mode.position_to_offset(source, 2, 1), Some(15));
}
}