1use crate::text_buffer::TextBuffer;
6
7#[derive(Debug, Clone)]
9pub struct LineMetadata {
10 pub is_pure_ascii: bool,
12 pub byte_length: usize,
14 pub char_count: usize,
16}
17
18impl LineMetadata {
19 pub fn new() -> Self {
21 Self {
22 is_pure_ascii: true,
23 byte_length: 0,
24 char_count: 0,
25 }
26 }
27
28 pub fn from_text(text: &str) -> Self {
30 let is_pure_ascii = text.is_ascii();
31 Self {
32 is_pure_ascii,
33 byte_length: text.len(),
34 char_count: text.chars().count(),
35 }
36 }
37}
38
39impl Default for LineMetadata {
40 fn default() -> Self {
41 Self::new()
42 }
43}
44
45#[derive(Clone)]
49pub struct LineIndex {
50 text_buffer: TextBuffer,
52}
53
54impl LineIndex {
55 pub fn new() -> Self {
57 Self {
58 text_buffer: TextBuffer::new(),
59 }
60 }
61
62 pub fn from_text(text: &str) -> Self {
68 Self {
69 text_buffer: TextBuffer::from_text(text),
70 }
71 }
72
73 pub(crate) fn text_buffer(&self) -> &TextBuffer {
75 &self.text_buffer
76 }
77
78 pub fn delete_line(&mut self, line_number: usize) {
80 if line_number >= self.text_buffer.line_count() {
81 return;
82 }
83
84 let start_char = self.text_buffer.line_to_char(line_number);
85 let end_char = if line_number + 1 < self.text_buffer.line_count() {
86 self.text_buffer.line_to_char(line_number + 1)
87 } else {
88 self.text_buffer.len_chars()
89 };
90
91 self.text_buffer.delete(start_char, end_char - start_char);
92 }
93
94 pub fn get_line(&self, line_number: usize) -> Option<LineMetadata> {
96 let text = self.text_buffer.get_line_text(line_number)?;
97 Some(LineMetadata::from_text(&text))
98 }
99
100 fn legacy_line_to_content_byte_offset(&self, line_number: usize) -> usize {
101 if line_number == 0 {
102 return 0;
103 }
104
105 if line_number >= self.text_buffer.line_count() {
106 let newline_count = self.text_buffer.line_count().saturating_sub(1);
108 return self.text_buffer.len_bytes().saturating_sub(newline_count);
109 }
110
111 self.text_buffer
114 .line_to_byte(line_number)
115 .saturating_sub(line_number)
116 }
117
118 #[deprecated(
126 note = "legacy byte offset excludes previous LF separators; use position_to_char_offset plus char_offset_to_byte_offset"
127 )]
128 pub fn line_to_offset(&self, line_number: usize) -> usize {
129 self.legacy_line_to_content_byte_offset(line_number)
130 }
131
132 #[deprecated(
138 note = "legacy byte offset excludes previous LF separators; use byte_offset_to_char_offset plus char_offset_to_position"
139 )]
140 pub fn offset_to_line(&self, offset: usize) -> usize {
141 if offset == 0 {
142 return 0;
143 }
144
145 let mut low = 0;
148 let mut high = self.text_buffer.line_count();
149
150 while low < high {
151 let mid = (low + high) / 2;
152 let mid_offset = self.legacy_line_to_content_byte_offset(mid);
153
154 if mid_offset < offset {
155 low = mid + 1;
156 } else if mid_offset > offset {
157 high = mid;
158 } else {
159 return mid;
160 }
161 }
162
163 low.saturating_sub(1)
164 .min(self.text_buffer.line_count().saturating_sub(1))
165 }
166
167 pub fn char_offset_to_position(&self, char_offset: usize) -> (usize, usize) {
169 self.text_buffer.char_offset_to_position(char_offset)
170 }
171
172 pub fn position_to_char_offset(&self, line: usize, column: usize) -> usize {
174 self.text_buffer.position_to_char_offset(line, column)
175 }
176
177 pub fn line_count(&self) -> usize {
179 self.text_buffer.line_count()
180 }
181
182 pub fn byte_count(&self) -> usize {
184 self.text_buffer.len_bytes()
185 }
186
187 pub fn char_count(&self) -> usize {
189 self.text_buffer.len_chars()
190 }
191
192 pub fn char_at(&self, char_offset: usize) -> Option<char> {
196 self.text_buffer.char_at(char_offset)
197 }
198
199 pub fn char_offset_to_byte_offset(&self, char_offset: usize) -> usize {
203 self.text_buffer.char_offset_to_byte_offset(char_offset)
204 }
205
206 pub fn byte_offset_to_char_offset(&self, byte_offset: usize) -> usize {
210 self.text_buffer.byte_offset_to_char_offset(byte_offset)
211 }
212
213 pub fn char_offset_to_line_byte_column(&self, char_offset: usize) -> (usize, usize) {
215 let char_offset = char_offset.min(self.text_buffer.len_chars());
216 let line = self.text_buffer.char_to_line(char_offset);
217 let line_start_char = self.text_buffer.line_to_char(line);
218
219 let line_start_byte = self.text_buffer.char_offset_to_byte_offset(line_start_char);
220 let byte_offset = self.text_buffer.char_offset_to_byte_offset(char_offset);
221 (line, byte_offset.saturating_sub(line_start_byte))
222 }
223
224 pub fn insert(&mut self, char_offset: usize, text: &str) {
226 self.text_buffer.insert(char_offset, text);
227 }
228
229 pub fn delete(&mut self, start_char: usize, len_chars: usize) {
231 self.text_buffer.delete(start_char, len_chars);
232 }
233
234 pub fn get_text(&self) -> String {
236 self.text_buffer.get_text()
237 }
238
239 pub fn get_range(&self, start_char: usize, len_chars: usize) -> String {
241 self.text_buffer.get_range(start_char, len_chars)
242 }
243
244 pub fn get_line_text(&self, line_number: usize) -> Option<String> {
246 self.text_buffer.get_line_text(line_number)
247 }
248}
249
250impl Default for LineIndex {
251 fn default() -> Self {
252 Self::new()
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn test_new_line_index() {
262 let index = LineIndex::new();
263 assert_eq!(index.line_count(), 1); assert_eq!(index.byte_count(), 0);
265 assert_eq!(index.char_count(), 0);
266 }
267
268 #[test]
269 fn test_from_text() {
270 let text = "Line 1\nLine 2\nLine 3";
271 let index = LineIndex::from_text(text);
272
273 assert_eq!(index.line_count(), 3);
274 assert_eq!(index.byte_count(), text.len());
275 assert_eq!(index.char_count(), text.chars().count());
276 }
277
278 #[test]
279 #[allow(deprecated)]
280 fn test_line_to_offset() {
281 let text = "First line\nSecond line\nThird line";
282 let index = LineIndex::from_text(text);
283
284 assert_eq!(index.line_to_offset(0), 0);
285 assert_eq!(index.line_to_offset(1), 10); assert_eq!(index.line_to_offset(2), 21); }
288
289 #[test]
290 #[allow(deprecated)]
291 fn test_offset_to_line() {
292 let text = "First line\nSecond line\nThird line";
293 let index = LineIndex::from_text(text);
294
295 assert_eq!(index.offset_to_line(0), 0);
296 assert_eq!(index.offset_to_line(5), 0);
297 assert_eq!(index.offset_to_line(11), 1);
298 assert_eq!(index.offset_to_line(23), 2);
299 }
300
301 #[test]
302 fn test_char_offset_to_position() {
303 let text = "ABC\nDEF\nGHI";
304 let index = LineIndex::from_text(text);
305
306 assert_eq!(index.char_offset_to_position(0), (0, 0)); assert_eq!(index.char_offset_to_position(2), (0, 2)); assert_eq!(index.char_offset_to_position(4), (1, 0)); assert_eq!(index.char_offset_to_position(8), (2, 0)); }
311
312 #[test]
313 fn test_position_to_char_offset() {
314 let text = "ABC\nDEF\nGHI";
315 let index = LineIndex::from_text(text);
316
317 assert_eq!(index.position_to_char_offset(0, 0), 0); assert_eq!(index.position_to_char_offset(0, 2), 2); assert_eq!(index.position_to_char_offset(1, 0), 4); assert_eq!(index.position_to_char_offset(2, 0), 8); }
322
323 #[test]
324 fn test_utf8_cjk() {
325 let text = "你好\n世界";
326 let index = LineIndex::from_text(text);
327
328 assert_eq!(index.line_count(), 2);
329 assert_eq!(index.byte_count(), text.len());
330 assert_eq!(index.char_count(), 5); assert_eq!(index.char_offset_to_position(0), (0, 0));
334 assert_eq!(index.char_offset_to_position(1), (0, 1));
335 assert_eq!(index.char_offset_to_position(3), (1, 0));
337 }
338
339 #[test]
340 fn test_get_line() {
341 let text = "Line 1\nLine 2\nLine 3";
342 let index = LineIndex::from_text(text);
343
344 let line0 = index.get_line(0);
345 assert!(line0.is_some());
346 let meta = line0.unwrap();
347 assert!(meta.is_pure_ascii);
348
349 let line_none = index.get_line(10);
350 assert!(line_none.is_none());
351 }
352
353 #[test]
354 fn test_insert_delete_lines() {
355 let mut index = LineIndex::from_text("Line 1\nLine 2");
356 assert_eq!(index.line_count(), 2);
357
358 index.delete_line(0);
359 assert_eq!(index.line_count(), 1);
360 }
361
362 #[test]
363 fn test_mixed_ascii_cjk() {
364 let text = "Hello 你好\nWorld 世界";
365 let index = LineIndex::from_text(text);
366
367 assert_eq!(index.line_count(), 2);
368 assert!(index.byte_count() > index.char_count());
369 }
370
371 #[test]
372 fn test_large_document() {
373 let mut lines = Vec::new();
374 for i in 0..10000 {
375 lines.push(format!("Line {}", i));
376 }
377 let text = lines.join("\n");
378
379 let index = LineIndex::from_text(&text);
380 assert_eq!(index.line_count(), 10000);
381
382 let line_5000 = index.get_line(5000);
384 assert!(line_5000.is_some());
385 }
386
387 #[test]
388 fn test_insert_text() {
389 let mut index = LineIndex::from_text("Hello World");
390
391 index.insert(6, "Beautiful ");
392 assert_eq!(index.get_text(), "Hello Beautiful World");
393 }
394
395 #[test]
396 fn test_delete_text() {
397 let mut index = LineIndex::from_text("Hello Beautiful World");
398
399 index.delete(6, 10); assert_eq!(index.get_text(), "Hello World");
401 }
402
403 #[test]
404 fn test_char_byte_offset_roundtrip() {
405 let text = "a你好\n🌍b";
406 let index = LineIndex::from_text(text);
407
408 for char_offset in 0..=index.char_count() {
409 let byte_offset = index.char_offset_to_byte_offset(char_offset);
410 let recovered = index.byte_offset_to_char_offset(byte_offset);
411 assert_eq!(recovered, char_offset);
412
413 let (line, byte_col) = index.char_offset_to_line_byte_column(char_offset);
414 let line_start_char = index.position_to_char_offset(line, 0);
415 let line_start_byte = index.char_offset_to_byte_offset(line_start_char);
416 assert_eq!(line_start_byte + byte_col, byte_offset);
417 }
418 }
419}