Skip to main content

perl_incremental_parsing/incremental/
incremental_edit.rs

1//! Enhanced edit structure for incremental parsing with text content
2//!
3//! This module provides an extended Edit type that includes the new text
4//! being inserted, enabling efficient incremental parsing with subtree reuse.
5
6use crate::position::Position;
7
8/// Enhanced edit with text content for incremental parsing
9#[derive(Debug, Clone, PartialEq)]
10pub struct IncrementalEdit {
11    /// Start byte offset of the edit
12    pub start_byte: usize,
13    /// End byte offset of the text being replaced (in old source)
14    pub old_end_byte: usize,
15    /// The new text being inserted
16    pub new_text: String,
17    /// Start position (line/column)
18    pub start_position: Position,
19    /// Old end position before edit
20    pub old_end_position: Position,
21}
22
23impl IncrementalEdit {
24    /// Create a new incremental edit
25    pub fn new(start_byte: usize, old_end_byte: usize, new_text: String) -> Self {
26        IncrementalEdit {
27            start_byte,
28            old_end_byte,
29            new_text,
30            start_position: Position::new(start_byte, 0, 0),
31            old_end_position: Position::new(old_end_byte, 0, 0),
32        }
33    }
34
35    /// Create with position information
36    pub fn with_positions(
37        start_byte: usize,
38        old_end_byte: usize,
39        new_text: String,
40        start_position: Position,
41        old_end_position: Position,
42    ) -> Self {
43        IncrementalEdit { start_byte, old_end_byte, new_text, start_position, old_end_position }
44    }
45
46    /// Get the new end byte after applying this edit
47    pub fn new_end_byte(&self) -> usize {
48        self.start_byte + self.new_text.len()
49    }
50
51    /// Calculate the byte shift caused by this edit
52    pub fn byte_shift(&self) -> isize {
53        self.new_text.len() as isize - (self.old_end_byte - self.start_byte) as isize
54    }
55
56    /// Check if this edit overlaps with a byte range
57    pub fn overlaps(&self, start: usize, end: usize) -> bool {
58        self.start_byte < end && self.old_end_byte > start
59    }
60
61    /// Check if this edit is entirely before a position
62    pub fn is_before(&self, pos: usize) -> bool {
63        self.old_end_byte <= pos
64    }
65
66    /// Check if this edit is entirely after a position
67    pub fn is_after(&self, pos: usize) -> bool {
68        self.start_byte >= pos
69    }
70}
71
72/// Collection of incremental edits
73#[derive(Debug, Clone, Default)]
74pub struct IncrementalEditSet {
75    pub edits: Vec<IncrementalEdit>,
76}
77
78impl IncrementalEditSet {
79    /// Create a new empty edit set
80    pub fn new() -> Self {
81        IncrementalEditSet { edits: Vec::new() }
82    }
83
84    /// Add an edit to the set
85    pub fn add(&mut self, edit: IncrementalEdit) {
86        self.edits.push(edit);
87    }
88
89    /// Sort edits by position (for correct application order)
90    pub fn sort(&mut self) {
91        self.edits.sort_by_key(|e| e.start_byte);
92    }
93
94    /// Sort edits in reverse order (for applying from end to start)
95    pub fn sort_reverse(&mut self) {
96        self.edits.sort_by_key(|e| std::cmp::Reverse(e.start_byte));
97    }
98
99    /// Check if the edit set is empty
100    pub fn is_empty(&self) -> bool {
101        self.edits.is_empty()
102    }
103
104    /// Get the total byte shift for all edits
105    pub fn total_byte_shift(&self) -> isize {
106        self.edits.iter().map(|e| e.byte_shift()).sum()
107    }
108
109    /// Apply edits to a string
110    pub fn apply_to_string(&self, source: &str) -> String {
111        if self.edits.is_empty() {
112            return source.to_string();
113        }
114
115        // Sort edits in reverse order to apply from end to start
116        let mut sorted_edits = self.edits.clone();
117        sorted_edits.sort_by_key(|e| std::cmp::Reverse(e.start_byte));
118
119        let mut result = source.to_string();
120        for edit in &sorted_edits {
121            result.replace_range(edit.start_byte..edit.old_end_byte, &edit.new_text);
122        }
123
124        result
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_incremental_edit_basic() {
134        let edit = IncrementalEdit::new(5, 10, "hello".to_string());
135        assert_eq!(edit.new_end_byte(), 10);
136        assert_eq!(edit.byte_shift(), 0);
137    }
138
139    #[test]
140    fn test_incremental_edit_insertion() {
141        let edit = IncrementalEdit::new(5, 5, "inserted".to_string());
142        assert_eq!(edit.new_end_byte(), 13);
143        assert_eq!(edit.byte_shift(), 8);
144    }
145
146    #[test]
147    fn test_incremental_edit_deletion() {
148        let edit = IncrementalEdit::new(5, 15, "".to_string());
149        assert_eq!(edit.new_end_byte(), 5);
150        assert_eq!(edit.byte_shift(), -10);
151    }
152
153    #[test]
154    fn test_incremental_edit_replacement() {
155        let edit = IncrementalEdit::new(5, 10, "replaced".to_string());
156        assert_eq!(edit.new_end_byte(), 13);
157        assert_eq!(edit.byte_shift(), 3);
158    }
159
160    #[test]
161    fn test_edit_set_apply() {
162        let mut edits = IncrementalEditSet::new();
163        edits.add(IncrementalEdit::new(0, 5, "Hello".to_string()));
164        edits.add(IncrementalEdit::new(6, 11, "Perl".to_string()));
165
166        let source = "hello world";
167        let result = edits.apply_to_string(source);
168        assert_eq!(result, "Hello Perl");
169    }
170}