1use crate::{
2 error::{EditError, ErrorCode},
3 limits::LineIndexLimits,
4 model::{Position, PositionEncoding},
5};
6
7use core::mem::size_of;
8
9#[derive(Clone, Debug)]
11pub struct LineIndex<'text> {
12 text: &'text str,
13 starts: Vec<usize>,
14}
15
16impl<'text> LineIndex<'text> {
17 #[must_use]
28 pub fn new(text: &'text str) -> Self {
29 Self::try_new(
30 text,
31 LineIndexLimits {
32 max_lines: usize::MAX,
33 max_index_bytes: usize::MAX,
34 },
35 )
36 .unwrap_or_else(|error| panic!("failed to build line index: {error}"))
37 }
38
39 pub fn try_new(text: &'text str, limits: LineIndexLimits) -> Result<Self, EditError> {
46 let line_count = count_lines(text, limits.max_lines)?;
47 check_index_bytes::<usize>(line_count, limits.max_index_bytes)?;
48
49 let mut starts = Vec::new();
50 starts
51 .try_reserve_exact(line_count)
52 .map_err(|_| index_allocation_error())?;
53 starts.push(0);
54 starts.extend(text.match_indices('\n').map(|(offset, _)| offset + 1));
55 Ok(Self { text, starts })
56 }
57
58 pub fn byte_offset(&self, position: Position) -> Result<usize, EditError> {
64 self.byte_offset_with_encoding(position, PositionEncoding::Utf16)
65 }
66
67 pub fn byte_offset_with_encoding(
69 &self,
70 position: Position,
71 encoding: PositionEncoding,
72 ) -> Result<usize, EditError> {
73 if position.line == 0 {
74 return Err(position_error(position, "line numbers are 1-based"));
75 }
76 let line_index = usize::try_from(position.line - 1)
77 .map_err(|_| position_error(position, "line number is too large"))?;
78 let Some(&start) = self.starts.get(line_index) else {
79 return Err(position_error(position, "line exceeds file line count"));
80 };
81 let end = self
82 .starts
83 .get(line_index + 1)
84 .map_or(self.text.len(), |next| next - 1);
85 resolve_character(&self.text[start..end], start, position, encoding)
86 }
87
88 pub fn position_at(
90 &self,
91 byte_offset: usize,
92 encoding: PositionEncoding,
93 ) -> Result<Position, EditError> {
94 if byte_offset > self.text.len() || !self.text.is_char_boundary(byte_offset) {
95 return Err(EditError::new(
96 ErrorCode::PositionOutOfRange,
97 "byte offset is outside the text or splits a Unicode scalar value",
98 ));
99 }
100 let line_index = self.starts.partition_point(|start| *start <= byte_offset) - 1;
101 let start = self.starts[line_index];
102 let units = count_units(&self.text[start..byte_offset], encoding);
103 Ok(Position::new(
104 u32::try_from(line_index + 1).map_err(|_| {
105 EditError::new(ErrorCode::PositionOutOfRange, "line number exceeds u32")
106 })?,
107 u32::try_from(units).map_err(|_| {
108 EditError::new(
109 ErrorCode::PositionOutOfRange,
110 "character offset exceeds u32",
111 )
112 })?,
113 ))
114 }
115
116 #[must_use]
117 pub fn line_count(&self) -> usize {
118 self.starts.len()
119 }
120}
121
122#[derive(Clone, Copy, Debug)]
123pub(crate) struct SparseLine {
124 line: u32,
125 start: usize,
126 end: usize,
127 found: bool,
128}
129
130pub(crate) struct SparseLineIndex<'text> {
133 text: &'text str,
134 lines: Vec<SparseLine>,
135}
136
137impl<'text> SparseLineIndex<'text> {
138 pub(crate) fn try_for_line_pairs(
139 text: &'text str,
140 requested_line_pairs: impl ExactSizeIterator<Item = (u32, u32)>,
141 ) -> Result<Self, EditError> {
142 let requested_capacity = requested_line_pairs
143 .len()
144 .checked_mul(2)
145 .ok_or_else(index_byte_limit_error)?;
146 let max_index_bytes = requested_capacity
147 .checked_mul(size_of::<SparseLine>())
148 .ok_or_else(index_byte_limit_error)?;
149 check_index_bytes::<SparseLine>(requested_capacity, max_index_bytes)?;
150 let max_lines = text.len().checked_add(1).ok_or_else(line_limit_error)?;
151
152 let mut lines = Vec::new();
153 lines
154 .try_reserve_exact(requested_capacity)
155 .map_err(|_| index_allocation_error())?;
156 for (start_line, end_line) in requested_line_pairs {
157 for line in [start_line, end_line] {
158 lines.push(SparseLine {
159 line,
160 start: 0,
161 end: 0,
162 found: false,
163 });
164 }
165 }
166 lines.sort_unstable_by_key(|entry| entry.line);
167 lines.dedup_by_key(|entry| entry.line);
168
169 let mut current_line = 1_usize;
170 if current_line > max_lines {
171 return Err(line_limit_error());
172 }
173 let mut line_start = 0_usize;
174 let mut requested = 0_usize;
175
176 for (offset, byte) in text.bytes().enumerate() {
177 if byte != b'\n' {
178 continue;
179 }
180 fill_sparse_line(&mut lines, &mut requested, current_line, line_start, offset);
181 current_line = current_line.checked_add(1).ok_or_else(line_limit_error)?;
182 if current_line > max_lines {
183 return Err(line_limit_error());
184 }
185 line_start = offset + 1;
186 }
187 fill_sparse_line(
188 &mut lines,
189 &mut requested,
190 current_line,
191 line_start,
192 text.len(),
193 );
194
195 Ok(Self { text, lines })
196 }
197
198 pub(crate) fn byte_offset_with_encoding(
199 &self,
200 position: Position,
201 encoding: PositionEncoding,
202 ) -> Result<usize, EditError> {
203 if position.line == 0 {
204 return Err(position_error(position, "line numbers are 1-based"));
205 }
206 let Ok(index) = self
207 .lines
208 .binary_search_by_key(&position.line, |entry| entry.line)
209 else {
210 return Err(position_error(position, "line was not indexed"));
211 };
212 let line = self.lines[index];
213 if !line.found {
214 return Err(position_error(position, "line exceeds file line count"));
215 }
216 resolve_character(
217 &self.text[line.start..line.end],
218 line.start,
219 position,
220 encoding,
221 )
222 }
223}
224
225fn fill_sparse_line(
226 lines: &mut [SparseLine],
227 requested: &mut usize,
228 current_line: usize,
229 start: usize,
230 end: usize,
231) {
232 while let Some(entry) = lines.get_mut(*requested) {
233 let Ok(requested_line) = usize::try_from(entry.line) else {
234 break;
235 };
236 if requested_line > current_line {
237 break;
238 }
239 if requested_line == current_line {
240 entry.start = start;
241 entry.end = end;
242 entry.found = true;
243 }
244 *requested += 1;
245 }
246}
247
248fn count_lines(text: &str, max_lines: usize) -> Result<usize, EditError> {
249 let mut lines = 1_usize;
250 if lines > max_lines {
251 return Err(line_limit_error());
252 }
253 for byte in text.bytes() {
254 if byte == b'\n' {
255 lines = lines.checked_add(1).ok_or_else(line_limit_error)?;
256 if lines > max_lines {
257 return Err(line_limit_error());
258 }
259 }
260 }
261 Ok(lines)
262}
263
264fn check_index_bytes<Entry>(entries: usize, max_index_bytes: usize) -> Result<(), EditError> {
265 let Some(index_bytes) = entries.checked_mul(size_of::<Entry>()) else {
266 return Err(index_byte_limit_error());
267 };
268 if index_bytes > max_index_bytes {
269 return Err(index_byte_limit_error());
270 }
271 Ok(())
272}
273
274fn line_limit_error() -> EditError {
275 EditError::new(ErrorCode::PlanTooLarge, "line count exceeds index limits")
276}
277
278fn index_byte_limit_error() -> EditError {
279 EditError::new(ErrorCode::PlanTooLarge, "line index exceeds its byte limit")
280}
281
282fn index_allocation_error() -> EditError {
283 EditError::new(
284 ErrorCode::PlanTooLarge,
285 "line index allocation could not be reserved",
286 )
287}
288
289fn resolve_character(
290 line: &str,
291 absolute_start: usize,
292 position: Position,
293 encoding: PositionEncoding,
294) -> Result<usize, EditError> {
295 let target = usize::try_from(position.character)
296 .map_err(|_| position_error(position, "character is too large"))?;
297 if line.is_ascii() || encoding == PositionEncoding::Utf8 {
298 if encoding == PositionEncoding::Utf8 && !line.is_char_boundary(target) {
299 return Err(position_error(
300 position,
301 "UTF-8 character offset splits a Unicode scalar value",
302 ));
303 }
304 return (target <= line.len())
305 .then_some(absolute_start + target)
306 .ok_or_else(|| position_error(position, "character exceeds line length"));
307 }
308
309 let mut utf16_offset = 0_usize;
310 for (byte_offset, character) in line.char_indices() {
311 if utf16_offset == target {
312 return Ok(absolute_start + byte_offset);
313 }
314 let next = utf16_offset
315 + match encoding {
316 PositionEncoding::Utf8 => character.len_utf8(),
317 PositionEncoding::Utf16 => character.len_utf16(),
318 PositionEncoding::Utf32 => 1,
319 };
320 if target < next {
321 return Err(position_error(
322 position,
323 "character splits a Unicode scalar value",
324 ));
325 }
326 utf16_offset = next;
327 }
328 if utf16_offset == target {
329 Ok(absolute_start + line.len())
330 } else {
331 Err(position_error(position, "character exceeds line length"))
332 }
333}
334
335fn count_units(text: &str, encoding: PositionEncoding) -> usize {
336 match encoding {
337 PositionEncoding::Utf8 => text.len(),
338 PositionEncoding::Utf16 => text.encode_utf16().count(),
339 PositionEncoding::Utf32 => text.chars().count(),
340 }
341}
342
343fn position_error(position: Position, message: &str) -> EditError {
344 EditError::new(
345 ErrorCode::PositionOutOfRange,
346 format!("{message} at {}:{}", position.line, position.character),
347 )
348}
349
350#[cfg(test)]
351mod tests {
352 use crate::{error::ErrorCode, model::Position};
353
354 #[test]
355 fn indexes_lf_crlf_and_final_empty_line() {
356 let index = super::LineIndex::new("a\r\nemoji 😀\n");
357 assert_eq!(index.line_count(), 3);
358 assert_eq!(index.byte_offset(Position::new(1, 2)).unwrap(), 2);
359 assert_eq!(index.byte_offset(Position::new(2, 8)).unwrap(), 13);
360 assert_eq!(index.byte_offset(Position::new(3, 0)).unwrap(), 14);
361 }
362
363 #[test]
364 fn rejects_one_past_lf_and_split_surrogate() {
365 let index = super::LineIndex::new("a\nb😀");
366 assert_eq!(
367 index.byte_offset(Position::new(1, 2)).unwrap_err().code(),
368 ErrorCode::PositionOutOfRange
369 );
370 assert_eq!(
371 index.byte_offset(Position::new(2, 2)).unwrap_err().code(),
372 ErrorCode::PositionOutOfRange
373 );
374 }
375}