Skip to main content

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;