nova_diagnostics/line_index.rs
1//! Byte offsets to LSP positions and back (spec
2//! `docs/superpowers/specs/2026-10-08-phase-3-2-lsp-core-design.md` ยง5).
3//!
4//! The compiler's spans are byte offsets. The Language Server Protocol
5//! counts a position as a zero-based line and a column in UTF-16 code units,
6//! so a character above U+FFFF is two columns, and a Thai letter, three bytes
7//! of UTF-8, is one.
8
9/// The start of every line in a text.
10#[derive(Debug, Clone)]
11pub struct LineIndex {
12 text: String,
13 /// The byte offset of each line's first character; `starts[0]` is 0.
14 starts: Vec<u32>,
15}
16
17impl LineIndex {
18 /// Index `text`. `\n` ends a line, and so does `\r\n`, whose `\r` is
19 /// part of the line's end.
20 pub fn new(text: &str) -> LineIndex {
21 let mut starts = vec![0];
22 for (i, b) in text.bytes().enumerate() {
23 if b == b'\n' {
24 starts.push(i as u32 + 1);
25 }
26 }
27 LineIndex {
28 text: text.to_owned(),
29 starts,
30 }
31 }
32
33 /// The text's length in bytes.
34 pub fn len(&self) -> u32 {
35 self.text.len() as u32
36 }
37
38 /// Whether the text is empty.
39 pub fn is_empty(&self) -> bool {
40 self.text.is_empty()
41 }
42
43 /// The zero-based line and UTF-16 column of byte `offset`. An offset
44 /// past the end is the end of the text.
45 pub fn position(&self, offset: u32) -> (u32, u32) {
46 let offset = offset.min(self.len());
47 let line = match self.starts.binary_search(&offset) {
48 Ok(line) => line,
49 Err(next) => next - 1,
50 };
51 let start = self.starts[line] as usize;
52 let mut column = 0u32;
53 for (i, c) in self.text[start..].char_indices() {
54 if start + i >= offset as usize {
55 break;
56 }
57 column += c.len_utf16() as u32;
58 }
59 (line as u32, column)
60 }
61
62 /// The byte offset of zero-based `line` and UTF-16 `column`.
63 ///
64 /// Out-of-range positions clamp:
65 /// - a column past the line's end goes to the line's end, before its
66 /// `\r\n` or `\n`;
67 /// - a line past the last goes to the end of the text;
68 /// - a column inside a surrogate pair goes to its character's start.
69 pub fn offset(&self, line: u32, column: u32) -> u32 {
70 let Some(&start) = self.starts.get(line as usize) else {
71 return self.len();
72 };
73 let end = self.line_end(line as usize);
74 let mut units = 0u32;
75 for (i, c) in self.text[start as usize..end].char_indices() {
76 let next = units + c.len_utf16() as u32;
77 if next > column {
78 return start + i as u32;
79 }
80 units = next;
81 }
82 end as u32
83 }
84
85 /// The byte offset where `line`'s text ends, before its line break.
86 fn line_end(&self, line: usize) -> usize {
87 let start = self.starts[line] as usize;
88 let mut end = self
89 .starts
90 .get(line + 1)
91 .map_or(self.text.len(), |&s| s as usize);
92 let bytes = self.text.as_bytes();
93 if end > start && bytes[end - 1] == b'\n' {
94 end -= 1;
95 }
96 if end > start && bytes[end - 1] == b'\r' {
97 end -= 1;
98 }
99 end
100 }
101}