дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
110 рядки
3.3 KiB
Rust
110 рядки
3.3 KiB
Rust
//! Byte offset <-> 0-based line / UTF-16 column, for one text snapshot.
|
|
//!
|
|
//! Only line starts are indexed (one `memchr` pass); columns are converted
|
|
//! within their line, so building the index stays well under a millisecond
|
|
//! even for 20k-line files.
|
|
|
|
use ide_core::{Pos, Range};
|
|
|
|
pub(crate) struct Lines<'a> {
|
|
text: &'a str,
|
|
starts: Vec<usize>,
|
|
}
|
|
|
|
impl<'a> Lines<'a> {
|
|
pub(crate) fn new(text: &'a str) -> Self {
|
|
let mut starts = Vec::with_capacity(text.len() / 40 + 1);
|
|
starts.push(0);
|
|
starts.extend(memchr::memchr_iter(b'\n', text.as_bytes()).map(|i| i + 1));
|
|
Self { text, starts }
|
|
}
|
|
|
|
pub(crate) fn text(&self) -> &'a str {
|
|
self.text
|
|
}
|
|
|
|
fn line_of(&self, byte: usize) -> usize {
|
|
self.starts.partition_point(|&s| s <= byte) - 1
|
|
}
|
|
|
|
fn line_end(&self, line: usize) -> usize {
|
|
self.starts.get(line + 1).map_or(self.text.len(), |&s| s - 1)
|
|
}
|
|
|
|
pub(crate) fn pos(&self, byte: usize) -> Pos {
|
|
let byte = floor_char(self.text, byte.min(self.text.len()));
|
|
let line = self.line_of(byte);
|
|
Pos::new(line as u32, utf16_len(&self.text[self.starts[line]..byte]))
|
|
}
|
|
|
|
pub(crate) fn range(&self, start: usize, end: usize) -> Range {
|
|
Range::new(self.pos(start), self.pos(end))
|
|
}
|
|
|
|
/// Byte offset of a position, clamped to its line and to the text.
|
|
pub(crate) fn byte(&self, pos: Pos) -> usize {
|
|
let Some(&start) = self.starts.get(pos.line as usize) else { return self.text.len() };
|
|
let line = &self.text[start..self.line_end(pos.line as usize)];
|
|
start + utf16_to_byte(line, pos.col as usize)
|
|
}
|
|
|
|
/// `(start, end)` bytes of a range, ordered.
|
|
pub(crate) fn bytes(&self, range: Range) -> (usize, usize) {
|
|
let (a, b) = (self.byte(range.start), self.byte(range.end));
|
|
(a.min(b), a.max(b))
|
|
}
|
|
}
|
|
|
|
pub(crate) fn utf16_len(s: &str) -> u32 {
|
|
if s.is_ascii() { s.len() as u32 } else { s.chars().map(|c| c.len_utf16() as u32).sum() }
|
|
}
|
|
|
|
fn utf16_to_byte(s: &str, offset: usize) -> usize {
|
|
if s.is_ascii() {
|
|
return offset.min(s.len());
|
|
}
|
|
let mut units = 0;
|
|
for (i, c) in s.char_indices() {
|
|
if units >= offset {
|
|
return i;
|
|
}
|
|
units += c.len_utf16();
|
|
if units > offset {
|
|
// Inside a surrogate pair: round down to the char start.
|
|
return i;
|
|
}
|
|
}
|
|
s.len()
|
|
}
|
|
|
|
fn floor_char(s: &str, mut i: usize) -> usize {
|
|
while i > 0 && !s.is_char_boundary(i) {
|
|
i -= 1;
|
|
}
|
|
i
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn converts_utf16_both_ways() {
|
|
let text = "ab\n😀x\n\nzz";
|
|
let l = Lines::new(text);
|
|
assert_eq!(l.pos(0), Pos::new(0, 0));
|
|
assert_eq!(l.pos(3), Pos::new(1, 0));
|
|
assert_eq!(l.pos(7), Pos::new(1, 2));
|
|
assert_eq!(l.pos(5), Pos::new(1, 0), "mid-char bytes round down");
|
|
assert_eq!(l.pos(10), Pos::new(3, 0));
|
|
assert_eq!(l.pos(99), Pos::new(3, 2));
|
|
assert_eq!(l.byte(Pos::new(1, 2)), 7);
|
|
assert_eq!(l.byte(Pos::new(1, 1)), 3, "inside a surrogate pair rounds down");
|
|
assert_eq!(l.byte(Pos::new(0, 99)), 2, "clamped to the line");
|
|
assert_eq!(l.byte(Pos::new(9, 0)), text.len());
|
|
for b in [0, 1, 2, 3, 7, 8, 9, 10, 11, 12] {
|
|
assert_eq!(l.byte(l.pos(b)), b, "byte {b}");
|
|
}
|
|
}
|
|
}
|