1use crate::lsp_types::Position;
29use std::ops::Range;
30
31#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
35pub enum PositionEncoding {
36 #[default]
38 Utf16,
39 Utf8,
41}
42
43#[derive(Clone, Debug, Default)]
49pub struct LineIndex {
50 text: String,
51 line_starts: Vec<usize>,
54}
55
56impl LineIndex {
57 pub fn new(text: impl Into<String>) -> Self {
59 let text = text.into();
60 let mut line_starts = vec![0];
61 line_starts.extend(
62 text.bytes()
63 .enumerate()
64 .filter(|&(_, b)| b == b'\n')
65 .map(|(i, _)| i + 1),
66 );
67 Self { text, line_starts }
68 }
69
70 #[inline]
72 pub fn as_str(&self) -> &str {
73 &self.text
74 }
75
76 #[inline]
78 pub fn len_bytes(&self) -> usize {
79 self.text.len()
80 }
81
82 #[inline]
84 pub fn len_lines(&self) -> usize {
85 self.line_starts.len()
86 }
87
88 #[inline]
91 pub fn line_start(&self, line: usize) -> Option<usize> {
92 self.line_starts.get(line).copied()
93 }
94
95 pub fn line_byte_range(&self, line: usize) -> Option<Range<usize>> {
97 let start = self.line_start(line)?;
98 let end = self.line_starts.get(line + 1).copied().unwrap_or(self.text.len());
99 Some(start..end)
100 }
101
102 pub fn line_str(&self, line: usize) -> Option<&str> {
104 let r = self.line_byte_range(line)?;
105 self.text.get(r)
106 }
107
108 #[inline]
111 pub fn byte_slice(&self, range: Range<usize>) -> Option<&str> {
112 self.text.get(range)
113 }
114
115 pub fn char_at_byte(&self, byte: usize) -> Option<char> {
118 self.text.get(byte..)?.chars().next()
119 }
120
121 pub fn byte_to_position(&self, byte: usize, encoding: PositionEncoding) -> Option<Position> {
126 if byte > self.text.len() {
127 return None;
128 }
129 let line = match self.line_starts.binary_search(&byte) {
130 Ok(line) => line,
131 Err(next) => next - 1, };
133 let line_start = self.line_starts[line];
134 let segment = self.text.get(line_start..byte)?; let character = match encoding {
136 PositionEncoding::Utf16 => segment.encode_utf16().count(),
137 PositionEncoding::Utf8 => segment.len(),
138 };
139 Some(Position::new(line as u32, character as u32))
140 }
141
142 pub fn position_to_byte(&self, position: Position, encoding: PositionEncoding) -> Option<usize> {
148 let line = position.line as usize;
149 let line_range = self.line_byte_range(line)?;
150 let line_text = self.text.get(line_range.clone())?;
151 let target = position.character as usize;
152
153 let col_bytes = match encoding {
154 PositionEncoding::Utf8 => target.min(line_text.len()),
155 PositionEncoding::Utf16 => {
156 let mut utf16 = 0usize;
157 let mut found = None;
158 for (byte_off, ch) in line_text.char_indices() {
159 if utf16 >= target {
160 found = Some(byte_off);
161 break;
162 }
163 utf16 += ch.len_utf16();
164 }
165 found.unwrap_or(line_text.len())
166 }
167 };
168 Some(line_range.start + col_bytes)
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 const U16: PositionEncoding = PositionEncoding::Utf16;
177 const U8: PositionEncoding = PositionEncoding::Utf8;
178
179 fn pos(l: u32, c: u32) -> Position {
180 Position::new(l, c)
181 }
182
183 #[test]
184 fn line_counting_matches_ropey_conventions() {
185 assert_eq!(LineIndex::new("").len_lines(), 1);
186 assert_eq!(LineIndex::new("abc").len_lines(), 1);
187 assert_eq!(LineIndex::new("abc\n").len_lines(), 2); assert_eq!(LineIndex::new("a\nb\nc").len_lines(), 3);
189 }
190
191 #[test]
192 fn ascii_byte_to_position() {
193 let idx = LineIndex::new("ab\ncde");
194 assert_eq!(idx.byte_to_position(0, U16), Some(pos(0, 0)));
195 assert_eq!(idx.byte_to_position(2, U16), Some(pos(0, 2))); assert_eq!(idx.byte_to_position(3, U16), Some(pos(1, 0))); assert_eq!(idx.byte_to_position(6, U16), Some(pos(1, 3))); assert_eq!(idx.byte_to_position(7, U16), None); }
200
201 #[test]
202 fn two_byte_char_column_differs_by_encoding() {
203 let idx = LineIndex::new("xéy");
205 let y_byte = "xé".len(); assert_eq!(idx.byte_to_position(y_byte, U16), Some(pos(0, 2))); assert_eq!(idx.byte_to_position(y_byte, U8), Some(pos(0, 3))); }
209
210 #[test]
211 fn astral_char_is_two_utf16_units_one_char() {
212 let idx = LineIndex::new("😀z");
214 let z_byte = "😀".len(); assert_eq!(idx.byte_to_position(z_byte, U16), Some(pos(0, 2))); assert_eq!(idx.byte_to_position(z_byte, U8), Some(pos(0, 4)));
217 }
218
219 #[test]
220 fn position_to_byte_roundtrips_utf16() {
221 for text in ["", "abc", "ab\ncde", "xéy\nfoo", "😀z\n€uro", "a\r\nb"] {
222 let idx = LineIndex::new(text);
223 for (byte, _) in text.char_indices().chain(std::iter::once((text.len(), ' '))) {
225 let p = idx.byte_to_position(byte, U16).unwrap();
226 assert_eq!(
227 idx.position_to_byte(p, U16),
228 Some(byte),
229 "roundtrip failed for {text:?} at byte {byte} (pos {p:?})"
230 );
231 }
232 }
233 }
234
235 #[test]
236 fn position_to_byte_clamps_past_end_of_line() {
237 let idx = LineIndex::new("ab\ncd");
238 assert_eq!(idx.position_to_byte(pos(0, 99), U16), Some(3)); assert_eq!(idx.position_to_byte(pos(1, 99), U16), Some(5)); assert_eq!(idx.position_to_byte(pos(9, 0), U16), None); }
243
244 #[test]
245 fn crlf_line_starts() {
246 let idx = LineIndex::new("a\r\nb");
247 assert_eq!(idx.len_lines(), 2);
248 assert_eq!(idx.byte_to_position(3, U16), Some(pos(1, 0)));
250 assert_eq!(idx.line_str(0), Some("a\r\n"));
251 assert_eq!(idx.line_str(1), Some("b"));
252 }
253
254 #[test]
255 fn byte_slice_and_char_at_byte() {
256 let idx = LineIndex::new("xéy");
257 assert_eq!(idx.byte_slice(0..1), Some("x"));
258 assert_eq!(idx.byte_slice(0..2), None); assert_eq!(idx.char_at_byte(1), Some('é'));
260 assert_eq!(idx.char_at_byte(2), None); assert_eq!(idx.char_at_byte(3), Some('y'));
262 }
263}