Skip to main content

editor_core/
line_index.rs

1//! Stage 2: Logical Line Index
2//!
3//! Provides efficient line indexing using Rope data structure, supporting O(log N) access and editing.
4
5use crate::storage::Piece;
6use ropey::Rope;
7
8/// Metadata for a logical line
9#[derive(Debug, Clone)]
10pub struct LineMetadata {
11    /// List of Pieces referenced by this line (fragments may span multiple pieces)
12    pub pieces: Vec<Piece>,
13    /// Fast path flag: whether this is pure ASCII
14    pub is_pure_ascii: bool,
15    /// Byte length of this line
16    pub byte_length: usize,
17    /// Character count of this line
18    pub char_count: usize,
19}
20
21impl LineMetadata {
22    /// Create an empty line metadata record.
23    pub fn new() -> Self {
24        Self {
25            pieces: Vec::new(),
26            is_pure_ascii: true,
27            byte_length: 0,
28            char_count: 0,
29        }
30    }
31
32    /// Build line metadata for a single logical line (no trailing `\n`).
33    pub fn from_text(text: &str) -> Self {
34        let is_pure_ascii = text.is_ascii();
35        Self {
36            pieces: Vec::new(),
37            is_pure_ascii,
38            byte_length: text.len(),
39            char_count: text.chars().count(),
40        }
41    }
42}
43
44impl Default for LineMetadata {
45    fn default() -> Self {
46        Self::new()
47    }
48}
49
50/// Logical line index - implemented using Rope data structure
51///
52/// Rope provides O(log N) line access, insertion, and deletion performance, suitable for large file editing
53pub struct LineIndex {
54    /// Rope data structure that automatically manages line indexing
55    rope: Rope,
56}
57
58impl LineIndex {
59    /// Create a new line index
60    pub fn new() -> Self {
61        Self { rope: Rope::new() }
62    }
63
64    /// Build line index from text
65    pub fn from_text(text: &str) -> Self {
66        Self {
67            rope: Rope::from_str(text),
68        }
69    }
70
71    /// Append a new line
72    pub fn append_line(&mut self, line: LineMetadata) {
73        // Reconstruct text from LineMetadata and append
74        let current_len = self.rope.len_chars();
75
76        // If not the first line, add a newline first
77        if current_len > 0 {
78            self.rope.insert(current_len, "\n");
79        }
80
81        // Add line content (LineMetadata doesn't store actual text, using placeholder here)
82        // Note: This is for backward compatibility, in actual use should call insert() directly
83        let placeholder = "x".repeat(line.char_count);
84        self.rope.insert(self.rope.len_chars(), &placeholder);
85    }
86
87    /// Insert a line at the specified position
88    pub fn insert_line(&mut self, line_number: usize, line: LineMetadata) {
89        if line_number >= self.rope.len_lines() {
90            self.append_line(line);
91            return;
92        }
93
94        // Get character offset at insertion position
95        let insert_pos = self.rope.line_to_char(line_number);
96
97        // Insert new line content
98        let placeholder = "x".repeat(line.char_count);
99        self.rope.insert(insert_pos, &placeholder);
100        self.rope.insert(insert_pos + line.char_count, "\n");
101    }
102
103    /// Delete the specified line
104    pub fn delete_line(&mut self, line_number: usize) {
105        if line_number >= self.rope.len_lines() {
106            return;
107        }
108
109        let start_char = self.rope.line_to_char(line_number);
110        let end_char = if line_number + 1 < self.rope.len_lines() {
111            self.rope.line_to_char(line_number + 1)
112        } else {
113            self.rope.len_chars()
114        };
115
116        self.rope.remove(start_char..end_char);
117    }
118
119    /// Get metadata for the specified line number (simulated)
120    pub fn get_line(&self, line_number: usize) -> Option<LineMetadata> {
121        if line_number >= self.rope.len_lines() {
122            return None;
123        }
124
125        let line = self.rope.line(line_number);
126        let mut text = line.to_string();
127
128        // Remove trailing newline (Rope's line() includes newline)
129        if text.ends_with('\n') {
130            text.pop();
131        }
132        if text.ends_with('\r') {
133            text.pop();
134        }
135
136        Some(LineMetadata::from_text(&text))
137    }
138
139    /// Get mutable reference (Rope doesn't need this method, kept for compatibility)
140    pub fn get_line_mut(&mut self, line_number: usize) -> Option<&mut LineMetadata> {
141        // Rope doesn't support getting mutable references directly, return None
142        let _line_num = line_number;
143        None
144    }
145
146    /// Get byte offset for line number (excluding newlines of previous lines)
147    pub fn line_to_offset(&self, line_number: usize) -> usize {
148        if line_number == 0 {
149            return 0;
150        }
151
152        if line_number >= self.rope.len_lines() {
153            // Return total bytes minus newline count
154            let newline_count = self.rope.len_lines().saturating_sub(1);
155            return self.rope.len_bytes().saturating_sub(newline_count);
156        }
157
158        // Rope's line_to_byte includes all newlines from previous lines
159        // Subtract line_number newlines to match old behavior
160        self.rope
161            .line_to_byte(line_number)
162            .saturating_sub(line_number)
163    }
164
165    /// Get line number from byte offset (offset excludes newlines)
166    pub fn offset_to_line(&self, offset: usize) -> usize {
167        if offset == 0 {
168            return 0;
169        }
170
171        // Need to add back newline count to get actual Rope byte offset
172        // Binary search to find the correct line
173        let mut low = 0;
174        let mut high = self.rope.len_lines();
175
176        while low < high {
177            let mid = (low + high) / 2;
178            let mid_offset = self.line_to_offset(mid);
179
180            if mid_offset < offset {
181                low = mid + 1;
182            } else if mid_offset > offset {
183                high = mid;
184            } else {
185                return mid;
186            }
187        }
188
189        low.saturating_sub(1)
190            .min(self.rope.len_lines().saturating_sub(1))
191    }
192
193    /// Get line number and offset within line from character offset
194    pub fn char_offset_to_position(&self, char_offset: usize) -> (usize, usize) {
195        let char_offset = char_offset.min(self.rope.len_chars());
196
197        let line_idx = self.rope.char_to_line(char_offset);
198        let line_start_char = self.rope.line_to_char(line_idx);
199        let char_in_line = char_offset - line_start_char;
200
201        (line_idx, char_in_line)
202    }
203
204    /// Get character offset from line number and column number
205    pub fn position_to_char_offset(&self, line: usize, column: usize) -> usize {
206        if line >= self.rope.len_lines() {
207            return self.rope.len_chars();
208        }
209
210        let line_start_char = self.rope.line_to_char(line);
211        let line_len = if line + 1 < self.rope.len_lines() {
212            self.rope.line_to_char(line + 1) - line_start_char - 1 // -1 for newline
213        } else {
214            self.rope.len_chars() - line_start_char
215        };
216
217        line_start_char + column.min(line_len)
218    }
219
220    /// Get total line count
221    pub fn line_count(&self) -> usize {
222        self.rope.len_lines()
223    }
224
225    /// Get total byte count
226    pub fn byte_count(&self) -> usize {
227        self.rope.len_bytes()
228    }
229
230    /// Get total character count
231    pub fn char_count(&self) -> usize {
232        self.rope.len_chars()
233    }
234
235    /// Insert text (at specified character offset)
236    pub fn insert(&mut self, char_offset: usize, text: &str) {
237        let char_offset = char_offset.min(self.rope.len_chars());
238        self.rope.insert(char_offset, text);
239    }
240
241    /// Delete text range (character offset)
242    pub fn delete(&mut self, start_char: usize, len_chars: usize) {
243        let start_char = start_char.min(self.rope.len_chars());
244        let end_char = (start_char + len_chars).min(self.rope.len_chars());
245
246        if start_char < end_char {
247            self.rope.remove(start_char..end_char);
248        }
249    }
250
251    /// Get complete text
252    pub fn get_text(&self) -> String {
253        self.rope.to_string()
254    }
255
256    /// Get text of the specified line (excluding newline)
257    pub fn get_line_text(&self, line_number: usize) -> Option<String> {
258        if line_number >= self.rope.len_lines() {
259            return None;
260        }
261
262        let mut text = self.rope.line(line_number).to_string();
263
264        // Remove trailing newline
265        if text.ends_with('\n') {
266            text.pop();
267        }
268        if text.ends_with('\r') {
269            text.pop();
270        }
271
272        Some(text)
273    }
274}
275
276impl Default for LineIndex {
277    fn default() -> Self {
278        Self::new()
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    #[test]
287    fn test_new_line_index() {
288        let index = LineIndex::new();
289        assert_eq!(index.line_count(), 1); // Rope empty document has 1 line
290        assert_eq!(index.byte_count(), 0);
291        assert_eq!(index.char_count(), 0);
292    }
293
294    #[test]
295    fn test_from_text() {
296        let text = "Line 1\nLine 2\nLine 3";
297        let index = LineIndex::from_text(text);
298
299        assert_eq!(index.line_count(), 3);
300        assert_eq!(index.byte_count(), text.len());
301        assert_eq!(index.char_count(), text.chars().count());
302    }
303
304    #[test]
305    fn test_line_to_offset() {
306        let text = "First line\nSecond line\nThird line";
307        let index = LineIndex::from_text(text);
308
309        assert_eq!(index.line_to_offset(0), 0);
310        assert_eq!(index.line_to_offset(1), 10); // "First line" (excluding \n)
311        assert_eq!(index.line_to_offset(2), 21); // "First line" (10) + "Second line" (11) = 21
312    }
313
314    #[test]
315    fn test_offset_to_line() {
316        let text = "First line\nSecond line\nThird line";
317        let index = LineIndex::from_text(text);
318
319        assert_eq!(index.offset_to_line(0), 0);
320        assert_eq!(index.offset_to_line(5), 0);
321        assert_eq!(index.offset_to_line(11), 1);
322        assert_eq!(index.offset_to_line(23), 2);
323    }
324
325    #[test]
326    fn test_char_offset_to_position() {
327        let text = "ABC\nDEF\nGHI";
328        let index = LineIndex::from_text(text);
329
330        assert_eq!(index.char_offset_to_position(0), (0, 0)); // A
331        assert_eq!(index.char_offset_to_position(2), (0, 2)); // C
332        assert_eq!(index.char_offset_to_position(4), (1, 0)); // D
333        assert_eq!(index.char_offset_to_position(8), (2, 0)); // G
334    }
335
336    #[test]
337    fn test_position_to_char_offset() {
338        let text = "ABC\nDEF\nGHI";
339        let index = LineIndex::from_text(text);
340
341        assert_eq!(index.position_to_char_offset(0, 0), 0); // A
342        assert_eq!(index.position_to_char_offset(0, 2), 2); // C
343        assert_eq!(index.position_to_char_offset(1, 0), 4); // D
344        assert_eq!(index.position_to_char_offset(2, 0), 8); // G
345    }
346
347    #[test]
348    fn test_utf8_cjk() {
349        let text = "你好\n世界";
350        let index = LineIndex::from_text(text);
351
352        assert_eq!(index.line_count(), 2);
353        assert_eq!(index.byte_count(), text.len());
354        assert_eq!(index.char_count(), 5); // 5 characters (你好\n世界)
355
356        // First line: "你好"
357        assert_eq!(index.char_offset_to_position(0), (0, 0));
358        assert_eq!(index.char_offset_to_position(1), (0, 1));
359        // Second line: "世界" (newline at character offset 2)
360        assert_eq!(index.char_offset_to_position(3), (1, 0));
361    }
362
363    #[test]
364    fn test_get_line() {
365        let text = "Line 1\nLine 2\nLine 3";
366        let index = LineIndex::from_text(text);
367
368        let line0 = index.get_line(0);
369        assert!(line0.is_some());
370        let meta = line0.unwrap();
371        assert!(meta.is_pure_ascii);
372
373        let line_none = index.get_line(10);
374        assert!(line_none.is_none());
375    }
376
377    #[test]
378    fn test_insert_delete_lines() {
379        let mut index = LineIndex::from_text("Line 1\nLine 2");
380        assert_eq!(index.line_count(), 2);
381
382        index.delete_line(0);
383        assert_eq!(index.line_count(), 1);
384    }
385
386    #[test]
387    fn test_mixed_ascii_cjk() {
388        let text = "Hello 你好\nWorld 世界";
389        let index = LineIndex::from_text(text);
390
391        assert_eq!(index.line_count(), 2);
392        assert!(index.byte_count() > index.char_count());
393    }
394
395    #[test]
396    fn test_large_document() {
397        let mut lines = Vec::new();
398        for i in 0..10000 {
399            lines.push(format!("Line {}", i));
400        }
401        let text = lines.join("\n");
402
403        let index = LineIndex::from_text(&text);
404        assert_eq!(index.line_count(), 10000);
405
406        // Test accessing middle line
407        let line_5000 = index.get_line(5000);
408        assert!(line_5000.is_some());
409    }
410
411    #[test]
412    fn test_insert_text() {
413        let mut index = LineIndex::from_text("Hello World");
414
415        index.insert(6, "Beautiful ");
416        assert_eq!(index.get_text(), "Hello Beautiful World");
417    }
418
419    #[test]
420    fn test_delete_text() {
421        let mut index = LineIndex::from_text("Hello Beautiful World");
422
423        index.delete(6, 10); // Delete "Beautiful "
424        assert_eq!(index.get_text(), "Hello World");
425    }
426}