reovim_driver_syntax/edit.rs
1//! Edit types for incremental parsing.
2//!
3//! This module defines the [`SyntaxEdit`] type used to describe text
4//! modifications for efficient incremental re-parsing.
5
6use std::ops::Range;
7
8/// Edit information for incremental parsing.
9///
10/// Describes a text modification in terms that parsers can use for
11/// efficient incremental re-parsing. Mirrors tree-sitter's `InputEdit`
12/// structure but without the tree-sitter dependency.
13///
14/// # Fields
15///
16/// The edit is described in terms of both byte offsets and row/column positions:
17/// - `start_*`: Where the edit begins
18/// - `old_end_*`: Where the old (replaced) content ended
19/// - `new_end_*`: Where the new (inserted) content ends
20///
21/// # Example
22///
23/// ```
24/// use reovim_driver_syntax::SyntaxEdit;
25///
26/// // Inserting "Hello" at position 0
27/// let edit = SyntaxEdit::insert(0, 0, 0, 5, 0, 5);
28/// assert_eq!(edit.new_end_byte, 5);
29/// assert_eq!(edit.old_end_byte, 0); // Nothing was replaced
30///
31/// // Deleting 10 bytes starting at position 5
32/// let edit = SyntaxEdit::delete(5, 0, 5, 15, 0, 15);
33/// assert_eq!(edit.start_byte, 5);
34/// assert_eq!(edit.old_end_byte, 15);
35/// assert_eq!(edit.new_end_byte, 5); // Nothing was inserted
36/// ```
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct SyntaxEdit {
39 /// Byte offset where the edit starts.
40 pub start_byte: usize,
41 /// Byte offset where the old content ended.
42 pub old_end_byte: usize,
43 /// Byte offset where the new content ends.
44 pub new_end_byte: usize,
45 /// Row (line) where the edit starts (0-indexed).
46 pub start_row: u32,
47 /// Column where the edit starts (0-indexed).
48 pub start_col: u32,
49 /// Row where old content ended.
50 pub old_end_row: u32,
51 /// Column where old content ended.
52 pub old_end_col: u32,
53 /// Row where new content ends.
54 pub new_end_row: u32,
55 /// Column where new content ends.
56 pub new_end_col: u32,
57}
58
59impl SyntaxEdit {
60 /// Create a new syntax edit with all fields specified.
61 #[must_use]
62 #[allow(clippy::too_many_arguments)]
63 pub const fn new(
64 start_byte: usize,
65 old_end_byte: usize,
66 new_end_byte: usize,
67 start_row: u32,
68 start_col: u32,
69 old_end_row: u32,
70 old_end_col: u32,
71 new_end_row: u32,
72 new_end_col: u32,
73 ) -> Self {
74 Self {
75 start_byte,
76 old_end_byte,
77 new_end_byte,
78 start_row,
79 start_col,
80 old_end_row,
81 old_end_col,
82 new_end_row,
83 new_end_col,
84 }
85 }
86
87 /// Create an edit for an insertion (no content replaced).
88 ///
89 /// When inserting, `old_end` equals `start` since nothing was removed.
90 #[must_use]
91 pub const fn insert(
92 start_byte: usize,
93 start_row: u32,
94 start_col: u32,
95 new_end_byte: usize,
96 new_end_row: u32,
97 new_end_col: u32,
98 ) -> Self {
99 Self {
100 start_byte,
101 old_end_byte: start_byte,
102 new_end_byte,
103 start_row,
104 start_col,
105 old_end_row: start_row,
106 old_end_col: start_col,
107 new_end_row,
108 new_end_col,
109 }
110 }
111
112 /// Create an edit for a deletion (no content inserted).
113 ///
114 /// When deleting, `new_end` equals `start` since nothing was inserted.
115 #[must_use]
116 pub const fn delete(
117 start_byte: usize,
118 start_row: u32,
119 start_col: u32,
120 old_end_byte: usize,
121 old_end_row: u32,
122 old_end_col: u32,
123 ) -> Self {
124 Self {
125 start_byte,
126 old_end_byte,
127 new_end_byte: start_byte,
128 start_row,
129 start_col,
130 old_end_row,
131 old_end_col,
132 new_end_row: start_row,
133 new_end_col: start_col,
134 }
135 }
136
137 /// Get the byte range that was affected by this edit.
138 ///
139 /// Returns the range from start to the maximum of old and new end.
140 #[must_use]
141 pub const fn affected_range(&self) -> Range<usize> {
142 let end = if self.old_end_byte > self.new_end_byte {
143 self.old_end_byte
144 } else {
145 self.new_end_byte
146 };
147 self.start_byte..end
148 }
149
150 /// Get the number of bytes that were removed.
151 #[must_use]
152 pub const fn bytes_removed(&self) -> usize {
153 self.old_end_byte - self.start_byte
154 }
155
156 /// Get the number of bytes that were inserted.
157 #[must_use]
158 pub const fn bytes_inserted(&self) -> usize {
159 self.new_end_byte - self.start_byte
160 }
161
162 /// Get the net change in bytes (positive = growth, negative = shrink).
163 #[must_use]
164 #[allow(clippy::cast_possible_wrap)]
165 pub const fn byte_delta(&self) -> isize {
166 self.new_end_byte as isize - self.old_end_byte as isize
167 }
168
169 /// Check if this edit is an insertion (no removal).
170 #[must_use]
171 pub const fn is_insert(&self) -> bool {
172 self.old_end_byte == self.start_byte
173 }
174
175 /// Check if this edit is a deletion (no insertion).
176 #[must_use]
177 pub const fn is_delete(&self) -> bool {
178 self.new_end_byte == self.start_byte
179 }
180
181 /// Check if this edit is a replacement (both removal and insertion).
182 #[must_use]
183 pub const fn is_replace(&self) -> bool {
184 self.old_end_byte != self.start_byte && self.new_end_byte != self.start_byte
185 }
186}
187
188#[cfg(test)]
189#[path = "edit_tests.rs"]
190mod tests;